@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.
- package/dist/{HLSView-662T7R7H.js → HLSView-MXBOUQBG.js} +128 -39
- package/dist/HLSView-MXBOUQBG.js.map +7 -0
- package/dist/Prebuilt/common/constants.d.ts +2 -1
- package/dist/Prebuilt/components/HMSVideo/HLSCaptionSelector.d.ts +5 -0
- package/dist/Prebuilt/components/Polls/Voting/Leaderboard.d.ts +4 -0
- package/dist/Prebuilt/components/Polls/Voting/LeaderboardEntry.d.ts +9 -0
- package/dist/Prebuilt/components/Polls/Voting/PeerParticipationSummary.d.ts +5 -0
- package/dist/{chunk-2B7YYNHQ.js → chunk-HEOH5H43.js} +6767 -1064
- package/dist/chunk-HEOH5H43.js.map +7 -0
- package/dist/index.cjs.js +7263 -1445
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.js +1 -1
- package/dist/meta.cjs.json +335 -50
- package/dist/meta.esbuild.json +351 -65
- package/package.json +6 -6
- package/src/Prebuilt/common/PeersSorter.ts +12 -3
- package/src/Prebuilt/common/constants.ts +1 -0
- package/src/Prebuilt/common/utils.js +34 -0
- package/src/Prebuilt/components/Chat/Chat.jsx +3 -4
- package/src/Prebuilt/components/HMSVideo/HLSCaptionSelector.tsx +13 -0
- package/src/Prebuilt/components/HMSVideo/HMSVideo.jsx +34 -2
- package/src/Prebuilt/components/Notifications/Notifications.tsx +33 -2
- package/src/Prebuilt/components/Polls/CreatePollQuiz/PollsQuizMenu.jsx +3 -9
- package/src/Prebuilt/components/Polls/CreateQuestions/CreateQuestions.jsx +21 -1
- package/src/Prebuilt/components/Polls/CreateQuestions/QuestionForm.jsx +34 -7
- package/src/Prebuilt/components/Polls/CreateQuestions/SavedQuestion.jsx +2 -2
- package/src/Prebuilt/components/Polls/Polls.tsx +3 -0
- package/src/Prebuilt/components/Polls/Voting/Leaderboard.tsx +115 -0
- package/src/Prebuilt/components/Polls/Voting/LeaderboardEntry.tsx +63 -0
- package/src/Prebuilt/components/Polls/Voting/PeerParticipationSummary.tsx +38 -0
- package/src/Prebuilt/components/Polls/Voting/QuestionCard.jsx +28 -11
- package/src/Prebuilt/components/Polls/Voting/StandardVoting.jsx +7 -1
- package/src/Prebuilt/components/Polls/Voting/Voting.jsx +31 -13
- package/src/Prebuilt/components/Polls/common/MultipleChoiceOptions.jsx +33 -21
- package/src/Prebuilt/components/Polls/common/SingleChoiceOptions.jsx +47 -35
- package/src/Prebuilt/components/Polls/common/StatusIndicator.jsx +2 -22
- package/src/Prebuilt/components/Polls/common/VoteCount.jsx +1 -15
- package/src/Prebuilt/components/VideoLayouts/EqualProminence.tsx +6 -5
- package/src/Prebuilt/components/VideoLayouts/GridLayout.tsx +25 -6
- package/src/Prebuilt/components/VideoLayouts/ScreenshareLayout.tsx +0 -1
- package/src/Prebuilt/layouts/HLSView.jsx +51 -3
- package/dist/HLSView-662T7R7H.js.map +0 -7
- 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.
|
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.
|
80
|
-
"@100mslive/hms-virtual-background": "1.11.
|
81
|
-
"@100mslive/react-icons": "0.8.
|
82
|
-
"@100mslive/react-sdk": "0.8.
|
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": "
|
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
|
-
|
72
|
-
|
73
|
-
|
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: '$
|
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
|
7
|
-
|
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
|
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}
|
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
|
-
|
186
|
-
<
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
+
};
|