@100mslive/roomkit-react 0.1.14 → 0.1.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. package/dist/{HLSView-662T7R7H.js → HLSView-MXBOUQBG.js} +128 -39
  2. package/dist/HLSView-MXBOUQBG.js.map +7 -0
  3. package/dist/Prebuilt/common/constants.d.ts +2 -1
  4. package/dist/Prebuilt/components/HMSVideo/HLSCaptionSelector.d.ts +5 -0
  5. package/dist/Prebuilt/components/Polls/Voting/Leaderboard.d.ts +4 -0
  6. package/dist/Prebuilt/components/Polls/Voting/LeaderboardEntry.d.ts +9 -0
  7. package/dist/Prebuilt/components/Polls/Voting/PeerParticipationSummary.d.ts +5 -0
  8. package/dist/{chunk-2B7YYNHQ.js → chunk-HEOH5H43.js} +6767 -1064
  9. package/dist/chunk-HEOH5H43.js.map +7 -0
  10. package/dist/index.cjs.js +7263 -1445
  11. package/dist/index.cjs.js.map +4 -4
  12. package/dist/index.js +1 -1
  13. package/dist/meta.cjs.json +335 -50
  14. package/dist/meta.esbuild.json +351 -65
  15. package/package.json +6 -6
  16. package/src/Prebuilt/common/PeersSorter.ts +12 -3
  17. package/src/Prebuilt/common/constants.ts +1 -0
  18. package/src/Prebuilt/common/utils.js +34 -0
  19. package/src/Prebuilt/components/Chat/Chat.jsx +3 -4
  20. package/src/Prebuilt/components/HMSVideo/HLSCaptionSelector.tsx +13 -0
  21. package/src/Prebuilt/components/HMSVideo/HMSVideo.jsx +34 -2
  22. package/src/Prebuilt/components/Notifications/Notifications.tsx +33 -2
  23. package/src/Prebuilt/components/Polls/CreatePollQuiz/PollsQuizMenu.jsx +3 -9
  24. package/src/Prebuilt/components/Polls/CreateQuestions/CreateQuestions.jsx +21 -1
  25. package/src/Prebuilt/components/Polls/CreateQuestions/QuestionForm.jsx +34 -7
  26. package/src/Prebuilt/components/Polls/CreateQuestions/SavedQuestion.jsx +2 -2
  27. package/src/Prebuilt/components/Polls/Polls.tsx +3 -0
  28. package/src/Prebuilt/components/Polls/Voting/Leaderboard.tsx +115 -0
  29. package/src/Prebuilt/components/Polls/Voting/LeaderboardEntry.tsx +63 -0
  30. package/src/Prebuilt/components/Polls/Voting/PeerParticipationSummary.tsx +38 -0
  31. package/src/Prebuilt/components/Polls/Voting/QuestionCard.jsx +28 -11
  32. package/src/Prebuilt/components/Polls/Voting/StandardVoting.jsx +7 -1
  33. package/src/Prebuilt/components/Polls/Voting/Voting.jsx +31 -13
  34. package/src/Prebuilt/components/Polls/common/MultipleChoiceOptions.jsx +33 -21
  35. package/src/Prebuilt/components/Polls/common/SingleChoiceOptions.jsx +47 -35
  36. package/src/Prebuilt/components/Polls/common/StatusIndicator.jsx +2 -22
  37. package/src/Prebuilt/components/Polls/common/VoteCount.jsx +1 -15
  38. package/src/Prebuilt/components/VideoLayouts/EqualProminence.tsx +6 -5
  39. package/src/Prebuilt/components/VideoLayouts/GridLayout.tsx +25 -6
  40. package/src/Prebuilt/components/VideoLayouts/ScreenshareLayout.tsx +0 -1
  41. package/src/Prebuilt/layouts/HLSView.jsx +51 -3
  42. package/dist/HLSView-662T7R7H.js.map +0 -7
  43. package/dist/chunk-2B7YYNHQ.js.map +0 -7
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "prebuilt",
11
11
  "roomkit"
12
12
  ],
13
- "version": "0.1.14",
13
+ "version": "0.1.15",
14
14
  "author": "100ms",
15
15
  "license": "MIT",
16
16
  "files": [
@@ -76,10 +76,10 @@
76
76
  "react": ">=17.0.2 <19.0.0"
77
77
  },
78
78
  "dependencies": {
79
- "@100mslive/hls-player": "0.1.23",
80
- "@100mslive/hms-virtual-background": "1.11.23",
81
- "@100mslive/react-icons": "0.8.23",
82
- "@100mslive/react-sdk": "0.8.23",
79
+ "@100mslive/hls-player": "0.1.24",
80
+ "@100mslive/hms-virtual-background": "1.11.24",
81
+ "@100mslive/react-icons": "0.8.24",
82
+ "@100mslive/react-sdk": "0.8.24",
83
83
  "@100mslive/types-prebuilt": "0.12.4",
84
84
  "@emoji-mart/data": "^1.0.6",
85
85
  "@emoji-mart/react": "^1.0.1",
@@ -115,5 +115,5 @@
115
115
  "uuid": "^8.3.2",
116
116
  "worker-timers": "^7.0.40"
117
117
  },
118
- "gitHead": "aed9a8922bbebf1015858ab5b4896acff5647c0a"
118
+ "gitHead": "49795c5577e2d578fc0a8f372f3290dd0a52c9b2"
119
119
  }
@@ -17,6 +17,7 @@ class PeersSorter {
17
17
  }
18
18
 
19
19
  setPeersAndTilesPerPage = ({ peers, tilesPerPage }: { peers: HMSPeer[]; tilesPerPage: number }) => {
20
+ this.speaker = undefined;
20
21
  this.tilesPerPage = tilesPerPage;
21
22
  const peerIds = new Set(peers.map(peer => peer.id));
22
23
  // remove existing peers which are no longer provided
@@ -46,6 +47,8 @@ class PeersSorter {
46
47
  this.updateListeners();
47
48
  this.listeners.clear();
48
49
  this.storeUnsubscribe?.();
50
+ this.storeUnsubscribe = undefined;
51
+ this.speaker = undefined;
49
52
  };
50
53
 
51
54
  moveSpeakerToFront = (speaker?: HMSPeer) => {
@@ -68,10 +71,16 @@ class PeersSorter {
68
71
  };
69
72
 
70
73
  onDominantSpeakerChange = (speaker: HMSPeer | null) => {
71
- if (speaker && speaker.id !== this?.speaker?.id) {
72
- this.speaker = speaker;
73
- this.moveSpeakerToFront(speaker);
74
+ // no speaker or is current speaker do nothing
75
+ if (!speaker || speaker.id === this.speaker?.id) {
76
+ return;
77
+ }
78
+ // if the active speaker is not from the peers passed ignore
79
+ if (!this.peers.has(speaker.id)) {
80
+ return;
74
81
  }
82
+ this.speaker = speaker;
83
+ this.moveSpeakerToFront(speaker);
75
84
  };
76
85
 
77
86
  updateListeners = () => {
@@ -111,6 +111,7 @@ export enum SESSION_STORE_KEY {
111
111
  CHAT_PEER_BLACKLIST = 'chatPeerBlacklist',
112
112
  CHAT_MESSAGE_BLACKLIST = 'chatMessageBlacklist',
113
113
  CHAT_STATE = 'chatState',
114
+ SHARED_LEADERBOARDS = 'sharedLeaderboards',
114
115
  }
115
116
 
116
117
  export enum INTERACTION_TYPE {
@@ -1,3 +1,4 @@
1
+ import { isEqual } from 'lodash';
1
2
  import { QUESTION_TYPE } from './constants';
2
3
 
3
4
  // eslint-disable-next-line complexity
@@ -137,3 +138,36 @@ export const calculateAvatarAndAttribBoxSize = (calculatedWidth, calculatedHeigh
137
138
  };
138
139
 
139
140
  export const isMobileUserAgent = /Mobi|Android|iPhone/i.test(navigator.userAgent);
141
+
142
+ export const getPeerResponses = (questions, peerid, userid) => {
143
+ return questions.map(question =>
144
+ question.responses?.filter(
145
+ response =>
146
+ ((response && response.peer?.peerid === peerid) || response.peer?.userid === userid) && !response.skipped,
147
+ ),
148
+ );
149
+ };
150
+
151
+ export const getPeerParticipationSummary = (poll, localPeerID, localCustomerUserID) => {
152
+ let correctResponses = 0;
153
+ let score = 0;
154
+ const questions = poll.questions || [];
155
+ const peerResponses = getPeerResponses(questions, localPeerID, localCustomerUserID);
156
+ let totalResponses = peerResponses.length || 0;
157
+
158
+ peerResponses.forEach(peerResponse => {
159
+ if (!peerResponse?.[0]) {
160
+ return;
161
+ }
162
+ const submission = [peerResponse[0].option] || peerResponse[0].options;
163
+ const answer =
164
+ [questions[peerResponse[0].questionIndex - 1].answer?.option] ||
165
+ questions[peerResponse[0].questionIndex - 1].answer?.options;
166
+ const isCorrect = isEqual(submission, answer);
167
+ if (isCorrect) {
168
+ score += questions[peerResponse[0].questionIndex - 1]?.weight || 0;
169
+ correctResponses++;
170
+ }
171
+ });
172
+ return { totalResponses, correctResponses, score };
173
+ };
@@ -128,9 +128,8 @@ const NewMessageIndicator = ({ role, peerId, scrollToBottom }) => {
128
128
  }}
129
129
  icon
130
130
  css={{
131
- p: '$4',
132
- pl: '$8',
133
- pr: '$6',
131
+ p: '$3 $4',
132
+ pl: '$6',
134
133
  '& > svg': { ml: '$4' },
135
134
  borderRadius: '$round',
136
135
  position: 'relative',
@@ -141,7 +140,7 @@ const NewMessageIndicator = ({ role, peerId, scrollToBottom }) => {
141
140
  }}
142
141
  >
143
142
  New {unreadCount === 1 ? 'message' : 'messages'}
144
- <ChevronDownIcon />
143
+ <ChevronDownIcon height={16} width={16} />
145
144
  </Button>
146
145
  </Flex>
147
146
  );
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { ClosedCaptionIcon, OpenCaptionIcon } from '@100mslive/react-icons';
3
+ import { IconButton, Tooltip } from '../../../';
4
+
5
+ export function HLSCaptionSelector({ isEnabled, onClick }: { isEnabled: boolean; onClick: () => void }) {
6
+ return (
7
+ <Tooltip title="Subtitles/closed captions" side="top">
8
+ <IconButton css={{ p: '$2' }} onClick={() => onClick()}>
9
+ {isEnabled ? <ClosedCaptionIcon width="20" height="20px" /> : <OpenCaptionIcon width="20" height="20px" />}
10
+ </IconButton>
11
+ </Tooltip>
12
+ );
13
+ }
@@ -3,8 +3,40 @@ import { Flex } from '../../../';
3
3
 
4
4
  export const HMSVideo = forwardRef(({ children, ...props }, videoRef) => {
5
5
  return (
6
- <Flex data-testid="hms-video" css={{ size: '100%', position: 'relative' }} direction="column" {...props}>
7
- <video style={{ flex: '1 1 0', margin: '0 auto', minHeight: '0' }} ref={videoRef} playsInline />
6
+ <Flex
7
+ data-testid="hms-video"
8
+ css={{
9
+ size: '100%',
10
+ position: 'relative',
11
+ '& video::cue': {
12
+ color: 'white',
13
+ // textShadow: '0px 0px 4px #000',
14
+ whiteSpace: 'pre-line',
15
+ fontSize: '$lg',
16
+ fontStyle: 'normal',
17
+ fontWeight: '$semiBold',
18
+ lineHeight: '$sm',
19
+ letterSpacing: '0.5px',
20
+ },
21
+ '& video::-webkit-media-text-track-display': {
22
+ padding: '0 $4',
23
+ },
24
+ '& video::-webkit-media-text-track-container': {
25
+ fontSize: '$space$10 !important',
26
+ },
27
+ }}
28
+ direction="column"
29
+ {...props}
30
+ >
31
+ <video
32
+ style={{
33
+ flex: '1 1 0',
34
+ margin: '0 auto',
35
+ minHeight: '0',
36
+ }}
37
+ ref={videoRef}
38
+ playsInline
39
+ />
8
40
  {children}
9
41
  </Flex>
10
42
  );
@@ -28,6 +28,7 @@ import { ReconnectNotifications } from './ReconnectNotifications';
28
28
  import { TrackBulkUnmuteModal } from './TrackBulkUnmuteModal';
29
29
  import { TrackNotifications } from './TrackNotifications';
30
30
  import { TrackUnmuteModal } from './TrackUnmuteModal';
31
+ import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvider/hooks/useRoomLayoutScreen';
31
32
  // @ts-ignore: No implicit Any
32
33
  import { usePollViewToggle } from '../AppData/useSidepane';
33
34
  // @ts-ignore: No implicit Any
@@ -43,6 +44,7 @@ export function Notifications() {
43
44
  const roomState = useHMSStore(selectRoomState);
44
45
  const updateRoomLayoutForRole = useUpdateRoomLayout();
45
46
  const isNotificationDisabled = useIsNotificationDisabled();
47
+ const screenProps = useRoomLayoutConferencingScreen();
46
48
  const vanillaStore = useHMSVanillaStore();
47
49
  const togglePollView = usePollViewToggle();
48
50
 
@@ -53,7 +55,36 @@ export function Notifications() {
53
55
  });
54
56
  }, []);
55
57
 
58
+ /*
59
+ const leaderboardResultsShared = useCallback(
60
+ (stringifiedPollDetails: string) => {
61
+ const pollDetails = JSON.parse(stringifiedPollDetails);
62
+ if (pollDetails.startedBy !== localPeerID) {
63
+ const pollStartedBy = pollDetails.initiatorName;
64
+ ToastManager.addToast({
65
+ title: `${pollStartedBy} shared leaderboard for the quiz`,
66
+ action: (
67
+ <Button
68
+ onClick={() => togglePollView(pollDetails.id)}
69
+ variant="standard"
70
+ css={{
71
+ backgroundColor: '$surface_bright',
72
+ fontWeight: '$semiBold',
73
+ color: '$on_surface_high',
74
+ p: '$xs $md',
75
+ }}
76
+ >
77
+ View
78
+ </Button>
79
+ ),
80
+ });
81
+ }
82
+ },
83
+ [localPeerID, togglePollView],
84
+ ); */
85
+
56
86
  useCustomEvent({ type: ROLE_CHANGE_DECLINED, onEvent: handleRoleChangeDenied });
87
+ // useCustomEvent({ type: 'POLL_LEADERBOARD_SHARED', onEvent: leaderboardResultsShared });
57
88
 
58
89
  useEffect(() => {
59
90
  if (!notification || isNotificationDisabled) {
@@ -146,7 +177,7 @@ export function Notifications() {
146
177
  break;
147
178
 
148
179
  case HMSNotificationTypes.POLL_STARTED:
149
- if (notification.data.startedBy !== localPeerID) {
180
+ if (notification.data.startedBy !== localPeerID && screenProps.screenType !== 'hls_live_streaming') {
150
181
  const pollStartedBy = vanillaStore.getState(selectPeerNameByID(notification.data.startedBy)) || 'Participant';
151
182
  ToastManager.addToast({
152
183
  title: `${pollStartedBy} started a ${notification.data.type}: ${notification.data.title}`,
@@ -161,7 +192,7 @@ export function Notifications() {
161
192
  p: '$xs $md',
162
193
  }}
163
194
  >
164
- Vote
195
+ {notification.data.type === 'quiz' ? 'Answer' : 'Vote'}
165
196
  </Button>
166
197
  ),
167
198
  });
@@ -191,13 +191,7 @@ const PrevMenu = () => {
191
191
  </Text>
192
192
  <Flex direction="column" css={{ gap: '$10', mt: '$8' }}>
193
193
  {polls.map(poll => (
194
- <InteractionCard
195
- key={poll.id}
196
- id={poll.id}
197
- title={poll.title}
198
- isLive={poll.state === 'started'}
199
- isTimed={(poll.duration || 0) > 0}
200
- />
194
+ <InteractionCard key={poll.id} id={poll.id} title={poll.title} isLive={poll.state === 'started'} />
201
195
  ))}
202
196
  </Flex>
203
197
  </Flex>
@@ -205,7 +199,7 @@ const PrevMenu = () => {
205
199
  ) : null;
206
200
  };
207
201
 
208
- const InteractionCard = ({ id, title, isLive, isTimed }) => {
202
+ const InteractionCard = ({ id, title, isLive }) => {
209
203
  const { setPollState } = usePollViewState();
210
204
 
211
205
  const goToVote = id => {
@@ -221,7 +215,7 @@ const InteractionCard = ({ id, title, isLive, isTimed }) => {
221
215
  <Text variant="sub1" css={{ c: '$on_surface_high', fontWeight: '$semiBold' }}>
222
216
  {title}
223
217
  </Text>
224
- <StatusIndicator isLive={isLive} shouldShowTimer={isLive && isTimed} />
218
+ <StatusIndicator isLive={isLive} />
225
219
  </Flex>
226
220
  <Flex css={{ w: '100%', gap: '$4' }} justify="end">
227
221
  <Button variant="primary" onClick={() => goToVote(id)}>
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  import React, { useMemo, useState } from 'react';
3
3
  import { v4 as uuid } from 'uuid';
4
- import { selectPollByID, useHMSActions, useHMSStore } from '@100mslive/react-sdk';
4
+ import { selectPollByID, useHMSActions, useHMSStore, useRecordingStreaming } from '@100mslive/react-sdk';
5
5
  import { AddCircleIcon } from '@100mslive/react-icons';
6
6
  import { Button, Flex, Text } from '../../../../';
7
7
  import { Container, ContentHeader } from '../../Streaming/Common';
@@ -14,6 +14,7 @@ import { POLL_VIEWS } from '../../../common/constants';
14
14
  export function CreateQuestions() {
15
15
  const [questions, setQuestions] = useState([{ draftID: uuid() }]);
16
16
  const actions = useHMSActions();
17
+ const { isHLSRunning } = useRecordingStreaming();
17
18
  const togglePollView = usePollViewToggle();
18
19
  const { pollInView: id, setPollView } = usePollViewState();
19
20
  const interaction = useHMSStore(selectPollByID(id));
@@ -31,11 +32,30 @@ export function CreateQuestions() {
31
32
  type: question.type,
32
33
  options: question.options,
33
34
  skippable: question.skippable,
35
+ weight: question.weight,
34
36
  }));
35
37
  await actions.interactivityCenter.addQuestionsToPoll(id, validQuestions);
36
38
  await actions.interactivityCenter.startPoll(id);
39
+ await sendTimedMetadata(id);
37
40
  setPollView(POLL_VIEWS.VOTE);
38
41
  };
42
+
43
+ const sendTimedMetadata = async poll_id => {
44
+ // send hls timedmetadata when it is running
45
+ if (poll_id && isHLSRunning) {
46
+ try {
47
+ await actions.sendHLSTimedMetadata([
48
+ {
49
+ payload: `poll:${poll_id}`,
50
+ duration: 100,
51
+ },
52
+ ]);
53
+ } catch (e) {
54
+ console.error(e);
55
+ }
56
+ }
57
+ };
58
+
39
59
  const headingTitle = interaction?.type
40
60
  ? interaction?.type?.[0]?.toUpperCase() + interaction?.type?.slice(1)
41
61
  : 'Polls and Quizzes';
@@ -17,6 +17,7 @@ export const QuestionForm = ({ question, index, length, onSave, removeQuestion,
17
17
  const [open, setOpen] = useState(false);
18
18
  const [type, setType] = useState(question.type || QUESTION_TYPE.SINGLE_CHOICE);
19
19
  const [text, setText] = useState(question.text);
20
+ const [weight, setWeight] = useState(isQuiz ? 10 : 1);
20
21
  const [options, setOptions] = useState(
21
22
  question?.options || [
22
23
  { text: '', isCorrectAnswer: false },
@@ -28,6 +29,7 @@ export const QuestionForm = ({ question, index, length, onSave, removeQuestion,
28
29
  text,
29
30
  type,
30
31
  options,
32
+ weight,
31
33
  isQuiz,
32
34
  });
33
35
 
@@ -182,12 +184,31 @@ export const QuestionForm = ({ question, index, length, onSave, removeQuestion,
182
184
  </Flex>
183
185
  )}
184
186
  {isQuiz ? (
185
- <Flex css={{ mt: '$md', gap: '$6' }}>
186
- <Switch defaultChecked={skippable} onCheckedChange={checked => setSkippable(checked)} />
187
- <Text variant="sm" css={{ color: '$on_surface_medium' }}>
188
- Not required to answer
189
- </Text>
190
- </Flex>
187
+ <>
188
+ <Flex justify="between" align="center" css={{ mt: '$md', gap: '$6', w: '100%' }}>
189
+ <Text variant="sm" css={{ color: '$on_surface_medium' }}>
190
+ Point Weightage
191
+ </Text>
192
+ <Input
193
+ type="number"
194
+ value={weight}
195
+ min={1}
196
+ max={999}
197
+ onChange={e => setWeight(Math.min(e.target.value, 999))}
198
+ css={{
199
+ backgroundColor: '$surface_bright',
200
+ border: '1px solid $border_bright',
201
+ maxWidth: '$20',
202
+ }}
203
+ />
204
+ </Flex>
205
+ <Flex justify="between" css={{ mt: '$md', gap: '$6', w: '100%' }}>
206
+ <Text variant="sm" css={{ color: '$on_surface_medium' }}>
207
+ Allow to skip
208
+ </Text>
209
+ <Switch defaultChecked={skippable} onCheckedChange={checked => setSkippable(checked)} />
210
+ </Flex>
211
+ </>
191
212
  ) : null}
192
213
  </>
193
214
  ) : null}
@@ -222,6 +243,7 @@ export const QuestionForm = ({ question, index, length, onSave, removeQuestion,
222
243
  options,
223
244
  skippable,
224
245
  draftID: question.draftID,
246
+ weight,
225
247
  });
226
248
  }}
227
249
  >
@@ -235,7 +257,7 @@ export const QuestionForm = ({ question, index, length, onSave, removeQuestion,
235
257
  );
236
258
  };
237
259
 
238
- export const isValidQuestion = ({ text, type, options, isQuiz = false }) => {
260
+ export const isValidQuestion = ({ text, type, options, weight, isQuiz = false }) => {
239
261
  if (!isValidTextInput(text) || !type) {
240
262
  return false;
241
263
  }
@@ -251,5 +273,10 @@ export const isValidQuestion = ({ text, type, options, isQuiz = false }) => {
251
273
  return everyOptionHasText;
252
274
  }
253
275
 
276
+ // The minimum acceptable value of weight is 1
277
+ if (isQuiz && weight < 1) {
278
+ return false;
279
+ }
280
+
254
281
  return everyOptionHasText && hasCorrectAnswer;
255
282
  };
@@ -15,8 +15,8 @@ export const SavedQuestion = ({ question, index, length, convertToDraft, removeQ
15
15
  <Text variant="body2" css={{ mt: '$4', mb: '$md' }}>
16
16
  {question.text}
17
17
  </Text>
18
- {question.options.map(option => (
19
- <Flex css={{ alignItems: 'center', my: '$xs' }}>
18
+ {question.options.map((option, index) => (
19
+ <Flex key={`${option.text}-${index}`} css={{ alignItems: 'center', my: '$xs' }}>
20
20
  <Text variant="body2" css={{ c: '$on_surface_medium' }}>
21
21
  {option.text}
22
22
  </Text>
@@ -3,6 +3,7 @@ import React from 'react';
3
3
  import { PollsQuizMenu } from './CreatePollQuiz/PollsQuizMenu';
4
4
  // @ts-ignore: No implicit Any
5
5
  import { CreateQuestions } from './CreateQuestions/CreateQuestions';
6
+ import { Leaderboard } from './Voting/Leaderboard';
6
7
  // @ts-ignore: No implicit Any
7
8
  import { Voting } from './Voting/Voting';
8
9
  // @ts-ignore: No implicit Any
@@ -22,6 +23,8 @@ export const Polls = () => {
22
23
  return <CreateQuestions />;
23
24
  } else if (view === POLL_VIEWS.VOTE) {
24
25
  return <Voting toggleVoting={togglePollView} id={pollID} />;
26
+ } else if (view === POLL_VIEWS.RESULTS) {
27
+ return <Leaderboard pollID={pollID} />;
25
28
  } else {
26
29
  return null;
27
30
  }
@@ -0,0 +1,115 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { HMSPollLeaderboardResponse, selectPollByID, useHMSActions, useHMSStore } from '@100mslive/react-sdk';
3
+ import { ChevronLeftIcon, CrossIcon } from '@100mslive/react-icons';
4
+ import { Box, Flex } from '../../../../Layout';
5
+ import { Loading } from '../../../../Loading';
6
+ import { Text } from '../../../../Text';
7
+ import { LeaderboardEntry } from './LeaderboardEntry';
8
+ // @ts-ignore
9
+ import { useSidepaneToggle } from '../../AppData/useSidepane';
10
+ // @ts-ignore
11
+ import { usePollViewState } from '../../AppData/useUISettings';
12
+ // @ts-ignore
13
+ import { StatusIndicator } from '../common/StatusIndicator';
14
+ import { POLL_VIEWS } from '../../../common/constants';
15
+
16
+ export const Leaderboard = ({ pollID }: { pollID: string }) => {
17
+ const hmsActions = useHMSActions();
18
+ const poll = useHMSStore(selectPollByID(pollID));
19
+ const [pollLeaderboard, setPollLeaderboard] = useState<HMSPollLeaderboardResponse | undefined>();
20
+ const { setPollView } = usePollViewState();
21
+ const toggleSidepane = useSidepaneToggle();
22
+
23
+ /*
24
+ const sharedLeaderboardRef = useRef(false);
25
+ const sharedLeaderboards = useHMSStore(selectSessionStore(SESSION_STORE_KEY.SHARED_LEADERBOARDS));
26
+ const { sendEvent } = useCustomEvent({
27
+ type: HMSNotificationTypes.POLL_LEADERBOARD_SHARED,
28
+ onEvent: () => {
29
+ return;
30
+ },
31
+ });
32
+ */
33
+
34
+ useEffect(() => {
35
+ const fetchLeaderboardData = async () => {
36
+ if (poll) {
37
+ const leaderboardData = await hmsActions.interactivityCenter.fetchLeaderboard(poll, 0, 50);
38
+ setPollLeaderboard(leaderboardData);
39
+ }
40
+ };
41
+ fetchLeaderboardData();
42
+ }, [poll, hmsActions.interactivityCenter]);
43
+
44
+ if (!poll || !pollLeaderboard)
45
+ return (
46
+ <Flex align="center" justify="center" css={{ size: '100%' }}>
47
+ <Loading />
48
+ </Flex>
49
+ );
50
+ const maxPossibleScore = poll.questions?.reduce((total, question) => (total += question.weight || 0), 0) || 0;
51
+ const questionCount = poll.questions?.length || 0;
52
+
53
+ return (
54
+ <Flex direction="column" css={{ size: '100%' }}>
55
+ <Flex justify="between" align="center" css={{ pb: '$6', borderBottom: '1px solid $border_bright', mb: '$8' }}>
56
+ <Flex align="center" css={{ gap: '$4' }}>
57
+ <Flex
58
+ css={{ color: '$on_surface_medium', '&:hover': { color: '$on_surface_high', cursor: 'pointer' } }}
59
+ onClick={() => setPollView(POLL_VIEWS.VOTE)}
60
+ >
61
+ <ChevronLeftIcon />
62
+ </Flex>
63
+ <Text variant="lg" css={{ fontWeight: '$semiBold' }}>
64
+ {poll.title}
65
+ </Text>
66
+ <StatusIndicator isLive={false} />
67
+ </Flex>
68
+ <Flex
69
+ css={{ color: '$on_surface_medium', '&:hover': { color: '$on_surface_high', cursor: 'pointer' } }}
70
+ onClick={toggleSidepane}
71
+ >
72
+ <CrossIcon />
73
+ </Flex>
74
+ </Flex>
75
+ <Text variant="sm" css={{ fontWeight: '$semiBold' }}>
76
+ Leaderboard
77
+ </Text>
78
+ <Text variant="xs" css={{ color: '$on_surface_medium' }}>
79
+ Based on score and time taken to cast the correct answer
80
+ </Text>
81
+ <Box css={{ mt: '$8', gap: '$4', overflowY: 'auto', flex: '1 1 0', mr: '-$6', pr: '$6' }}>
82
+ {pollLeaderboard?.entries &&
83
+ pollLeaderboard.entries.map(question => (
84
+ <LeaderboardEntry
85
+ key={question.position}
86
+ position={question.position}
87
+ score={question.score}
88
+ questionCount={questionCount}
89
+ correctResponses={question.correctResponses}
90
+ userName={question.peer.username || ''}
91
+ maxPossibleScore={maxPossibleScore}
92
+ />
93
+ ))}
94
+ </Box>
95
+
96
+ {/* {!sharedLeaderboardRef.current ? (
97
+ <Button
98
+ css={{ ml: 'auto', mt: '$8' }}
99
+ onClick={() => {
100
+ const currentlySharedLeaderboards = sharedLeaderboards || [];
101
+ hmsActions.sessionStore.set(SESSION_STORE_KEY.SHARED_LEADERBOARDS, [
102
+ ...currentlySharedLeaderboards,
103
+ pollID,
104
+ ]);
105
+ const pollDetails = { initiatorName: '', startedBy: poll.startedBy, id: pollID };
106
+ sendEvent();
107
+ sharedLeaderboardRef.current = true;
108
+ }}
109
+ >
110
+ Share Results
111
+ </Button>
112
+ ) : null} */}
113
+ </Flex>
114
+ );
115
+ };
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { CheckCircleIcon, TrophyFilledIcon } from '@100mslive/react-icons';
3
+ import { Box, Flex } from '../../../../Layout';
4
+ import { Text } from '../../../../Text';
5
+
6
+ const positionColorMap: Record<number, string> = { 1: '#D69516', 2: '#3E3E3E', 3: '#583B0F' };
7
+
8
+ export const LeaderboardEntry = ({
9
+ position,
10
+ score,
11
+ questionCount,
12
+ correctResponses,
13
+ userName,
14
+ maxPossibleScore,
15
+ }: {
16
+ position: number;
17
+ score: number;
18
+ questionCount: number;
19
+ correctResponses: number;
20
+ userName: string;
21
+ maxPossibleScore: number;
22
+ }) => {
23
+ return (
24
+ <Flex align="center" justify="between">
25
+ <Flex align="center" css={{ gap: '$6' }}>
26
+ <Flex
27
+ align="center"
28
+ justify="center"
29
+ css={{
30
+ backgroundColor: positionColorMap[position] || '',
31
+ h: '$10',
32
+ w: '$10',
33
+ borderRadius: '$round',
34
+ color: position > 3 ? '$on_surface_low' : '#FFF',
35
+ fontSize: '$xs',
36
+ fontWeight: '$semiBold',
37
+ }}
38
+ >
39
+ {position}
40
+ </Flex>
41
+
42
+ <Box>
43
+ <Text variant="sm" css={{ fontWeight: '$semiBold', color: '$on_surface_high' }}>
44
+ {userName}
45
+ </Text>
46
+
47
+ <Text variant="sm">
48
+ {score}/{maxPossibleScore} points
49
+ </Text>
50
+ </Box>
51
+ </Flex>
52
+ <Flex align="center" css={{ gap: '$6', color: '$on_surface_medium' }}>
53
+ {position === 1 ? <TrophyFilledIcon /> : null}
54
+ <CheckCircleIcon height={16} width={16} />
55
+ {questionCount ? (
56
+ <Text variant="xs">
57
+ {correctResponses}/{questionCount}
58
+ </Text>
59
+ ) : null}
60
+ </Flex>
61
+ </Flex>
62
+ );
63
+ };