@100mslive/roomkit-react 0.1.14 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ };