@100mslive/roomkit-react 0.1.14 → 0.1.16

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. package/dist/{HLSView-662T7R7H.js → HLSView-EMUOLCTM.js} +128 -39
  2. package/dist/HLSView-EMUOLCTM.js.map +7 -0
  3. package/dist/Prebuilt/common/PeersSorter.d.ts +1 -0
  4. package/dist/Prebuilt/common/constants.d.ts +9 -5
  5. package/dist/Prebuilt/common/hooks.d.ts +1 -0
  6. package/dist/Prebuilt/components/Footer/ParticipantList.d.ts +17 -0
  7. package/dist/Prebuilt/components/Footer/RoleAccordion.d.ts +3 -2
  8. package/dist/Prebuilt/components/Footer/WhiteboardToggle.d.ts +2 -0
  9. package/dist/Prebuilt/components/HMSVideo/HLSCaptionSelector.d.ts +5 -0
  10. package/dist/Prebuilt/components/Notifications/HandRaisedNotifications.d.ts +1 -0
  11. package/dist/Prebuilt/components/Polls/Voting/Leaderboard.d.ts +4 -0
  12. package/dist/Prebuilt/components/Polls/Voting/LeaderboardEntry.d.ts +9 -0
  13. package/dist/Prebuilt/components/Polls/Voting/PeerParticipationSummary.d.ts +5 -0
  14. package/dist/Prebuilt/components/PreviousRoleInMetadata.d.ts +1 -0
  15. package/dist/Prebuilt/components/RemoveParticipant.d.ts +5 -0
  16. package/dist/Prebuilt/components/hooks/useCloseScreenshareWhiteboard.d.ts +4 -0
  17. package/dist/Prebuilt/layouts/WhiteboardView.d.ts +2 -0
  18. package/dist/{chunk-2B7YYNHQ.js → chunk-ZYR4B4KQ.js} +2240 -1767
  19. package/dist/chunk-ZYR4B4KQ.js.map +7 -0
  20. package/dist/index.cjs.js +2805 -2172
  21. package/dist/index.cjs.js.map +4 -4
  22. package/dist/index.js +1 -1
  23. package/dist/meta.cjs.json +739 -177
  24. package/dist/meta.esbuild.json +749 -186
  25. package/package.json +7 -7
  26. package/src/Prebuilt/AppStateContext.tsx +1 -1
  27. package/src/Prebuilt/common/PeersSorter.ts +24 -8
  28. package/src/Prebuilt/common/constants.ts +6 -6
  29. package/src/Prebuilt/common/hooks.ts +16 -0
  30. package/src/Prebuilt/common/utils.js +33 -0
  31. package/src/Prebuilt/components/AppData/AppData.tsx +1 -16
  32. package/src/Prebuilt/components/Chat/Chat.jsx +10 -34
  33. package/src/Prebuilt/components/Chat/ChatBody.jsx +107 -66
  34. package/src/Prebuilt/components/Chat/ChatFooter.tsx +21 -12
  35. package/src/Prebuilt/components/Chat/ChatSelector.tsx +25 -25
  36. package/src/Prebuilt/components/Chat/ChatSelectorContainer.tsx +15 -16
  37. package/src/Prebuilt/components/Chat/PinnedMessage.tsx +7 -2
  38. package/src/Prebuilt/components/ConferenceScreen.tsx +2 -0
  39. package/src/Prebuilt/components/Footer/ChatToggle.tsx +30 -7
  40. package/src/Prebuilt/components/Footer/Footer.tsx +2 -1
  41. package/src/Prebuilt/components/Footer/PaginatedParticipants.tsx +0 -1
  42. package/src/Prebuilt/components/Footer/{ParticipantList.jsx → ParticipantList.tsx} +169 -127
  43. package/src/Prebuilt/components/Footer/RoleAccordion.tsx +23 -13
  44. package/src/Prebuilt/components/Footer/WhiteboardToggle.tsx +34 -0
  45. package/src/Prebuilt/components/HMSVideo/HLSCaptionSelector.tsx +13 -0
  46. package/src/Prebuilt/components/HMSVideo/HMSVideo.jsx +34 -2
  47. package/src/Prebuilt/components/Notifications/HandRaisedNotifications.tsx +35 -0
  48. package/src/Prebuilt/components/Notifications/Notifications.tsx +47 -14
  49. package/src/Prebuilt/components/Notifications/PeerNotifications.tsx +7 -2
  50. package/src/Prebuilt/components/Polls/CreatePollQuiz/PollsQuizMenu.jsx +3 -9
  51. package/src/Prebuilt/components/Polls/CreateQuestions/CreateQuestions.jsx +21 -1
  52. package/src/Prebuilt/components/Polls/CreateQuestions/QuestionForm.jsx +34 -7
  53. package/src/Prebuilt/components/Polls/CreateQuestions/SavedQuestion.jsx +2 -2
  54. package/src/Prebuilt/components/Polls/Polls.tsx +3 -0
  55. package/src/Prebuilt/components/Polls/Voting/Leaderboard.tsx +115 -0
  56. package/src/Prebuilt/components/Polls/Voting/LeaderboardEntry.tsx +63 -0
  57. package/src/Prebuilt/components/Polls/Voting/PeerParticipationSummary.tsx +38 -0
  58. package/src/Prebuilt/components/Polls/Voting/QuestionCard.jsx +33 -8
  59. package/src/Prebuilt/components/Polls/Voting/StandardVoting.jsx +7 -1
  60. package/src/Prebuilt/components/Polls/Voting/Voting.jsx +31 -13
  61. package/src/Prebuilt/components/Polls/common/MultipleChoiceOptions.jsx +33 -21
  62. package/src/Prebuilt/components/Polls/common/SingleChoiceOptions.jsx +47 -35
  63. package/src/Prebuilt/components/Polls/common/StatusIndicator.jsx +2 -22
  64. package/src/Prebuilt/components/Polls/common/VoteCount.jsx +1 -15
  65. package/src/Prebuilt/components/PreviousRoleInMetadata.tsx +21 -0
  66. package/src/Prebuilt/components/RemoveParticipant.tsx +35 -0
  67. package/src/Prebuilt/components/RoleChangeModal.jsx +1 -1
  68. package/src/Prebuilt/components/SidePaneTabs.tsx +0 -1
  69. package/src/Prebuilt/components/TileMenu/TileMenuContent.tsx +1 -1
  70. package/src/Prebuilt/components/Toast/ToastConfig.jsx +15 -3
  71. package/src/Prebuilt/components/VideoLayouts/EqualProminence.tsx +6 -5
  72. package/src/Prebuilt/components/VideoLayouts/GridLayout.tsx +27 -5
  73. package/src/Prebuilt/components/VideoLayouts/ScreenshareLayout.tsx +0 -1
  74. package/src/Prebuilt/components/hooks/useCloseScreenshareWhiteboard.tsx +24 -0
  75. package/src/Prebuilt/layouts/HLSView.jsx +51 -3
  76. package/src/Prebuilt/layouts/VideoStreamingSection.tsx +20 -3
  77. package/src/Prebuilt/layouts/WhiteboardView.tsx +66 -0
  78. package/dist/HLSView-662T7R7H.js.map +0 -7
  79. package/dist/chunk-2B7YYNHQ.js.map +0 -7
  80. package/src/Prebuilt/components/AppData/useAppLayout.js +0 -6
  81. package/src/Prebuilt/components/init/initUtils.js +0 -67
@@ -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
  );
@@ -0,0 +1,35 @@
1
+ import { useEffect } from 'react';
2
+ import {
3
+ HMSNotificationTypes,
4
+ HMSRoomState,
5
+ selectHasPeerHandRaised,
6
+ selectRoomState,
7
+ useHMSNotifications,
8
+ useHMSStore,
9
+ useHMSVanillaStore,
10
+ } from '@100mslive/react-sdk';
11
+ // @ts-ignore: No implicit Any
12
+ import { ToastBatcher } from '../Toast/ToastBatcher';
13
+ import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvider/hooks/useRoomLayoutScreen';
14
+
15
+ export const HandRaisedNotifications = () => {
16
+ const notification = useHMSNotifications(HMSNotificationTypes.HAND_RAISE_CHANGED);
17
+ const roomState = useHMSStore(selectRoomState);
18
+ const vanillaStore = useHMSVanillaStore();
19
+ const { on_stage_exp } = useRoomLayoutConferencingScreen().elements || {};
20
+
21
+ useEffect(() => {
22
+ if (!notification?.data) {
23
+ return;
24
+ }
25
+ if (roomState !== HMSRoomState.Connected || notification.data.isLocal || !on_stage_exp) {
26
+ return;
27
+ }
28
+ const hasPeerHandRaised = vanillaStore.getState(selectHasPeerHandRaised(notification.data.id));
29
+ if (hasPeerHandRaised) {
30
+ ToastBatcher.showToast({ notification, type: 'RAISE_HAND' });
31
+ }
32
+ }, [notification, on_stage_exp, roomState, vanillaStore]);
33
+
34
+ return null;
35
+ };
@@ -4,7 +4,6 @@ import {
4
4
  HMSNotificationTypes,
5
5
  HMSRoleChangeRequest,
6
6
  HMSRoomState,
7
- selectHasPeerHandRaised,
8
7
  selectLocalPeerID,
9
8
  selectPeerNameByID,
10
9
  selectRoomState,
@@ -13,7 +12,8 @@ import {
13
12
  useHMSStore,
14
13
  useHMSVanillaStore,
15
14
  } from '@100mslive/react-sdk';
16
- import { Button } from '../../..';
15
+ import { GroupIcon } from '@100mslive/react-icons';
16
+ import { Box, Button } from '../../..';
17
17
  import { useUpdateRoomLayout } from '../../provider/roomLayoutProvider';
18
18
  // @ts-ignore: No implicit Any
19
19
  import { ToastBatcher } from '../Toast/ToastBatcher';
@@ -21,6 +21,7 @@ import { ToastBatcher } from '../Toast/ToastBatcher';
21
21
  import { ToastManager } from '../Toast/ToastManager';
22
22
  import { AutoplayBlockedModal } from './AutoplayBlockedModal';
23
23
  import { ChatNotifications } from './ChatNotifications';
24
+ import { HandRaisedNotifications } from './HandRaisedNotifications';
24
25
  import { InitErrorModal } from './InitErrorModal';
25
26
  import { PeerNotifications } from './PeerNotifications';
26
27
  import { PermissionErrorModal } from './PermissionErrorModal';
@@ -28,6 +29,7 @@ import { ReconnectNotifications } from './ReconnectNotifications';
28
29
  import { TrackBulkUnmuteModal } from './TrackBulkUnmuteModal';
29
30
  import { TrackNotifications } from './TrackNotifications';
30
31
  import { TrackUnmuteModal } from './TrackUnmuteModal';
32
+ import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvider/hooks/useRoomLayoutScreen';
31
33
  // @ts-ignore: No implicit Any
32
34
  import { usePollViewToggle } from '../AppData/useSidepane';
33
35
  // @ts-ignore: No implicit Any
@@ -43,6 +45,7 @@ export function Notifications() {
43
45
  const roomState = useHMSStore(selectRoomState);
44
46
  const updateRoomLayoutForRole = useUpdateRoomLayout();
45
47
  const isNotificationDisabled = useIsNotificationDisabled();
48
+ const screenProps = useRoomLayoutConferencingScreen();
46
49
  const vanillaStore = useHMSVanillaStore();
47
50
  const togglePollView = usePollViewToggle();
48
51
 
@@ -53,23 +56,42 @@ export function Notifications() {
53
56
  });
54
57
  }, []);
55
58
 
59
+ /*
60
+ const leaderboardResultsShared = useCallback(
61
+ (stringifiedPollDetails: string) => {
62
+ const pollDetails = JSON.parse(stringifiedPollDetails);
63
+ if (pollDetails.startedBy !== localPeerID) {
64
+ const pollStartedBy = pollDetails.initiatorName;
65
+ ToastManager.addToast({
66
+ title: `${pollStartedBy} shared leaderboard for the quiz`,
67
+ action: (
68
+ <Button
69
+ onClick={() => togglePollView(pollDetails.id)}
70
+ variant="standard"
71
+ css={{
72
+ backgroundColor: '$surface_bright',
73
+ fontWeight: '$semiBold',
74
+ color: '$on_surface_high',
75
+ p: '$xs $md',
76
+ }}
77
+ >
78
+ View
79
+ </Button>
80
+ ),
81
+ });
82
+ }
83
+ },
84
+ [localPeerID, togglePollView],
85
+ ); */
86
+
56
87
  useCustomEvent({ type: ROLE_CHANGE_DECLINED, onEvent: handleRoleChangeDenied });
88
+ // useCustomEvent({ type: 'POLL_LEADERBOARD_SHARED', onEvent: leaderboardResultsShared });
57
89
 
58
90
  useEffect(() => {
59
91
  if (!notification || isNotificationDisabled) {
60
92
  return;
61
93
  }
62
94
  switch (notification.type) {
63
- case HMSNotificationTypes.HAND_RAISE_CHANGED: {
64
- if (roomState !== HMSRoomState.Connected || notification.data.isLocal) {
65
- return;
66
- }
67
- const hasPeerHandRaised = vanillaStore.getState(selectHasPeerHandRaised(notification.data.id));
68
- if (hasPeerHandRaised) {
69
- ToastBatcher.showToast({ notification, type: 'RAISE_HAND' });
70
- }
71
- break;
72
- }
73
95
  case HMSNotificationTypes.METADATA_UPDATED:
74
96
  if (roomState !== HMSRoomState.Connected) {
75
97
  return;
@@ -92,6 +114,16 @@ export function Notifications() {
92
114
  ToastManager.addToast({
93
115
  title: `Error: ${notification.data?.message}`,
94
116
  });
117
+ } else if (notification.data?.message === 'role limit reached') {
118
+ ToastManager.addToast({
119
+ title: 'The room is currently full, try joining later',
120
+ close: true,
121
+ icon: (
122
+ <Box css={{ color: '$alert_error_default' }}>
123
+ <GroupIcon />
124
+ </Box>
125
+ ),
126
+ });
95
127
  } else {
96
128
  ToastManager.addToast({
97
129
  title:
@@ -146,7 +178,7 @@ export function Notifications() {
146
178
  break;
147
179
 
148
180
  case HMSNotificationTypes.POLL_STARTED:
149
- if (notification.data.startedBy !== localPeerID) {
181
+ if (notification.data.startedBy !== localPeerID && screenProps.screenType !== 'hls_live_streaming') {
150
182
  const pollStartedBy = vanillaStore.getState(selectPeerNameByID(notification.data.startedBy)) || 'Participant';
151
183
  ToastManager.addToast({
152
184
  title: `${pollStartedBy} started a ${notification.data.type}: ${notification.data.title}`,
@@ -161,7 +193,7 @@ export function Notifications() {
161
193
  p: '$xs $md',
162
194
  }}
163
195
  >
164
- Vote
196
+ {notification.data.type === 'quiz' ? 'Answer' : 'Vote'}
165
197
  </Button>
166
198
  ),
167
199
  });
@@ -188,6 +220,7 @@ export function Notifications() {
188
220
  <PermissionErrorModal />
189
221
  <InitErrorModal />
190
222
  <ChatNotifications />
223
+ <HandRaisedNotifications />
191
224
  </>
192
225
  );
193
226
  }
@@ -3,9 +3,9 @@ import { HMSNotificationTypes, useHMSNotifications } from '@100mslive/react-sdk'
3
3
  // @ts-ignore: No implicit Any
4
4
  import { ToastBatcher } from '../Toast/ToastBatcher';
5
5
  // @ts-ignore: No implicit Any
6
- import { useSubscribedNotifications } from '../AppData/useUISettings';
6
+ import { useSetSubscribedChatSelector, useSubscribedNotifications } from '../AppData/useUISettings';
7
7
  // @ts-ignore: No implicit Any
8
- import { SUBSCRIBED_NOTIFICATIONS } from '../../common/constants';
8
+ import { CHAT_SELECTOR, SUBSCRIBED_NOTIFICATIONS } from '../../common/constants';
9
9
 
10
10
  const notificationTypes = [
11
11
  HMSNotificationTypes.PEER_LIST,
@@ -17,6 +17,8 @@ export const PeerNotifications = () => {
17
17
  const notification = useHMSNotifications(notificationTypes);
18
18
  const isPeerJoinSubscribed = useSubscribedNotifications(SUBSCRIBED_NOTIFICATIONS.PEER_JOINED);
19
19
  const isPeerLeftSubscribed = useSubscribedNotifications(SUBSCRIBED_NOTIFICATIONS.PEER_LEFT);
20
+ const [selectedPeer, setPeerSelector] = useSetSubscribedChatSelector(CHAT_SELECTOR.PEER);
21
+
20
22
  useEffect(() => {
21
23
  if (!notification?.data) {
22
24
  return;
@@ -35,6 +37,9 @@ export const PeerNotifications = () => {
35
37
  }
36
38
  break;
37
39
  case HMSNotificationTypes.PEER_LEFT:
40
+ if (selectedPeer.id === notification.data.id) {
41
+ setPeerSelector({});
42
+ }
38
43
  if (!isPeerLeftSubscribed) {
39
44
  return;
40
45
  }
@@ -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
+ };
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { HMSPoll, selectLocalPeer, useHMSStore } from '@100mslive/react-sdk';
3
+ import { Box } from '../../../../Layout';
4
+ import { Text } from '../../../../Text';
5
+ // @ts-ignore
6
+ import { getPeerParticipationSummary } from '../../../common/utils';
7
+
8
+ export const PeerParticipationSummary = ({ poll }: { poll: HMSPoll }) => {
9
+ const localPeer = useHMSStore(selectLocalPeer);
10
+ const { totalResponses, correctResponses, score } = getPeerParticipationSummary(
11
+ poll,
12
+ localPeer?.id,
13
+ localPeer?.customerUserId,
14
+ );
15
+
16
+ const boxes = [
17
+ { title: 'Points', value: score },
18
+ { title: 'Correct Answers', value: `${correctResponses}/${totalResponses}` },
19
+ ];
20
+ return (
21
+ <Box>
22
+ <Text css={{ fontWeight: '$semiBold', my: '$8' }}>Participation Summary</Text>
23
+ <Box css={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '$4' }}>
24
+ {boxes.map(box => (
25
+ <Box key={box.title} css={{ p: '$8', background: '$surface_default', borderRadius: '$1' }}>
26
+ <Text
27
+ variant="tiny"
28
+ css={{ textTransform: 'uppercase', color: '$on_surface_medium', fontWeight: '$semiBold', my: '$4' }}
29
+ >
30
+ {box.title}
31
+ </Text>
32
+ <Text css={{ fontWeight: '$semiBold' }}>{box.value}</Text>
33
+ </Box>
34
+ ))}
35
+ </Box>
36
+ </Box>
37
+ );
38
+ };