@100mslive/roomkit-react 0.3.16-alpha.0 → 0.3.16-alpha.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. package/dist/{HLSView-KOCGTP23.css → HLSView-D62ZFGUE.css} +3 -3
  2. package/dist/{HLSView-KOCGTP23.css.map → HLSView-D62ZFGUE.css.map} +1 -1
  3. package/dist/{HLSView-P7GF2RAU.js → HLSView-QYHOAJX7.js} +2 -2
  4. package/dist/Prebuilt/components/Chat/ChatBody.d.ts +887 -0
  5. package/dist/Prebuilt/components/Chat/ChatFooter.d.ts +1 -1
  6. package/dist/Prebuilt/components/Chat/utils.d.ts +2 -0
  7. package/dist/Prebuilt/components/PIP/PIPChat.d.ts +2 -0
  8. package/dist/Prebuilt/components/PIP/PIPChatOption.d.ts +5 -0
  9. package/dist/Prebuilt/components/PIP/PIPProvider.d.ts +6 -0
  10. package/dist/Prebuilt/components/PIP/PIPWindow.d.ts +7 -0
  11. package/dist/Prebuilt/components/PIP/context.d.ts +8 -0
  12. package/dist/Prebuilt/components/PIP/usePIPChat.d.ts +5 -0
  13. package/dist/Prebuilt/components/PIP/usePIPWindow.d.ts +2 -0
  14. package/dist/{chunk-DDB4BLHX.js → chunk-NYBDQX5B.js} +10131 -9675
  15. package/dist/chunk-NYBDQX5B.js.map +7 -0
  16. package/dist/index.cjs.css +2 -2
  17. package/dist/index.cjs.css.map +1 -1
  18. package/dist/index.cjs.js +4021 -3510
  19. package/dist/index.cjs.js.map +4 -4
  20. package/dist/index.css +2 -2
  21. package/dist/index.css.map +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/meta.cjs.json +2292 -1910
  24. package/dist/meta.esbuild.json +2326 -1944
  25. package/package.json +8 -7
  26. package/src/Prebuilt/App.tsx +21 -18
  27. package/src/Prebuilt/components/AudioVideoToggle.tsx +16 -9
  28. package/src/Prebuilt/components/Chat/ChatBody.tsx +4 -12
  29. package/src/Prebuilt/components/Chat/ChatFooter.tsx +2 -3
  30. package/src/Prebuilt/components/Chat/utils.ts +11 -0
  31. package/src/Prebuilt/components/ConferenceScreen.tsx +13 -1
  32. package/src/Prebuilt/components/Footer/ParticipantList.tsx +1 -4
  33. package/src/Prebuilt/components/MoreSettings/SplitComponents/DesktopOptions.tsx +24 -6
  34. package/src/Prebuilt/components/Notifications/HandRaisedNotifications.tsx +33 -0
  35. package/src/Prebuilt/components/Notifications/Notifications.tsx +3 -1
  36. package/src/Prebuilt/components/PIP/PIPChat.tsx +273 -0
  37. package/src/Prebuilt/components/PIP/PIPChatOption.tsx +18 -0
  38. package/src/Prebuilt/components/PIP/PIPProvider.tsx +56 -0
  39. package/src/Prebuilt/components/PIP/PIPWindow.tsx +13 -0
  40. package/src/Prebuilt/components/PIP/context.ts +10 -0
  41. package/src/Prebuilt/components/PIP/usePIPChat.tsx +105 -0
  42. package/src/Prebuilt/components/PIP/usePIPWindow.tsx +12 -0
  43. package/src/Prebuilt/components/Preview/PreviewJoin.tsx +3 -1
  44. package/src/Prebuilt/components/Toast/ToastConfig.jsx +2 -2
  45. package/dist/chunk-DDB4BLHX.js.map +0 -7
  46. /package/dist/{HLSView-P7GF2RAU.js.map → HLSView-QYHOAJX7.js.map} +0 -0
@@ -0,0 +1,273 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ selectHMSMessages,
4
+ selectLocalPeerID,
5
+ selectPeerNameByID,
6
+ selectSessionStore,
7
+ selectUnreadHMSMessagesCount,
8
+ useHMSStore,
9
+ } from '@100mslive/react-sdk';
10
+ import { SendIcon } from '@100mslive/react-icons';
11
+ import { Box, Flex } from '../../../Layout';
12
+ import { Text } from '../../../Text';
13
+ import { TextArea } from '../../../TextArea';
14
+ import { Tooltip } from '../../../Tooltip';
15
+ import IconButton from '../../IconButton';
16
+ import { AnnotisedMessage } from '../Chat/ChatBody';
17
+ import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvider/hooks/useRoomLayoutScreen';
18
+ import { useIsPeerBlacklisted } from '../hooks/useChatBlacklist';
19
+ import { CHAT_MESSAGE_LIMIT, formatTime } from '../Chat/utils';
20
+ import { SESSION_STORE_KEY } from '../../common/constants';
21
+
22
+ export const PIPChat = () => {
23
+ const messages = useHMSStore(selectHMSMessages);
24
+ const localPeerID = useHMSStore(selectLocalPeerID);
25
+ const count = useHMSStore(selectUnreadHMSMessagesCount);
26
+ const [unreadMessageCount, setUnreadMessageCount] = useState(0);
27
+
28
+ const getSenderName = useCallback(
29
+ (senderName: string, senderID?: string) => {
30
+ const slicedName = senderName.length > 10 ? senderName.slice(0, 10) + '...' : senderName;
31
+ return slicedName + (senderID === localPeerID ? ' (You)' : '');
32
+ },
33
+ [localPeerID],
34
+ );
35
+
36
+ useEffect(() => {
37
+ const timeoutId = setTimeout(() => {
38
+ setUnreadMessageCount(count);
39
+ }, 100);
40
+ return () => clearTimeout(timeoutId);
41
+ }, [count]);
42
+
43
+ const blacklistedMessageIDs = useHMSStore(selectSessionStore(SESSION_STORE_KEY.CHAT_MESSAGE_BLACKLIST));
44
+ const filteredMessages = useMemo(() => {
45
+ const blacklistedMessageIDSet = new Set(blacklistedMessageIDs || []);
46
+ return messages?.filter(message => message.type === 'chat' && !blacklistedMessageIDSet.has(message.id)) || [];
47
+ }, [blacklistedMessageIDs, messages]);
48
+ const { enabled: isChatEnabled = true, updatedBy: chatStateUpdatedBy = '' } =
49
+ useHMSStore(selectSessionStore(SESSION_STORE_KEY.CHAT_STATE)) || {};
50
+ const isLocalPeerBlacklisted = useIsPeerBlacklisted({ local: true });
51
+ const { elements } = useRoomLayoutConferencingScreen();
52
+ const message_placeholder = elements?.chat?.message_placeholder || 'Send a message';
53
+ const canSendChatMessages = !!elements?.chat?.public_chat_enabled || !!elements?.chat?.roles_whitelist?.length;
54
+
55
+ const getChatStatus = useCallback(() => {
56
+ if (isLocalPeerBlacklisted) return "You've been blocked from sending messages";
57
+ if (!isChatEnabled)
58
+ return `Chat has been paused by ${
59
+ chatStateUpdatedBy.peerId === localPeerID ? 'you' : chatStateUpdatedBy?.userName
60
+ }`;
61
+ return message_placeholder;
62
+ }, [
63
+ chatStateUpdatedBy.peerId,
64
+ chatStateUpdatedBy?.userName,
65
+ isChatEnabled,
66
+ isLocalPeerBlacklisted,
67
+ localPeerID,
68
+ message_placeholder,
69
+ ]);
70
+
71
+ return (
72
+ <div style={{ height: '100%' }}>
73
+ <Box
74
+ id="chat-container"
75
+ css={{
76
+ bg: '$surface_dim',
77
+ overflowY: 'auto',
78
+ // Subtracting height of footer
79
+ h: canSendChatMessages ? 'calc(100% - 87px)' : '100%',
80
+ position: 'relative',
81
+ }}
82
+ >
83
+ {unreadMessageCount ? (
84
+ <Box
85
+ id="new-message-notif"
86
+ style={{
87
+ position: 'fixed',
88
+ bottom: '76px',
89
+ right: '4px',
90
+ }}
91
+ >
92
+ <Text
93
+ variant="xs"
94
+ css={{ cursor: 'pointer' }}
95
+ style={{ color: 'white', background: 'gray', padding: '4px', borderRadius: '4px' }}
96
+ >
97
+ {unreadMessageCount === 1 ? 'New message' : `${unreadMessageCount} new messages`}
98
+ </Text>
99
+ </Box>
100
+ ) : (
101
+ ''
102
+ )}
103
+ {filteredMessages.length === 0 ? (
104
+ <div
105
+ style={{ display: 'flex', height: '100%', width: '100%', alignItems: 'center', justifyContent: 'center' }}
106
+ >
107
+ <Text>No messages here yet</Text>
108
+ </div>
109
+ ) : (
110
+ filteredMessages.map(message => (
111
+ <Box className="pip-message" key={message.id} id={message.id} style={{ padding: '8px 0.75rem' }}>
112
+ <Flex style={{ width: '100%', alignItems: 'center', justifyContent: 'between' }}>
113
+ <Text
114
+ style={{ display: 'flex', justifyContent: 'between', width: '100%', alignItems: 'center' }}
115
+ css={{
116
+ color: '$on_surface_high',
117
+ fontWeight: '$semiBold',
118
+ }}
119
+ >
120
+ <Flex style={{ flexGrow: 1, gap: '2px', alignItems: 'center' }}>
121
+ {message.senderName === 'You' || !message.senderName ? (
122
+ <Text as="span" variant="sub2" css={{ color: '$on_surface_high', fontWeight: '$semiBold' }}>
123
+ {message.senderName || 'Anonymous'}
124
+ </Text>
125
+ ) : (
126
+ <Tooltip title={message.senderName} side="top" align="start">
127
+ <Text as="span" variant="sub2" css={{ color: '$on_surface_high', fontWeight: '$semiBold' }}>
128
+ {getSenderName(message.senderName, message?.sender)}
129
+ </Text>
130
+ </Tooltip>
131
+ )}
132
+ <MessageTitle
133
+ localPeerID={localPeerID}
134
+ recipientPeer={message.recipientPeer}
135
+ recipientRoles={message.recipientRoles}
136
+ />
137
+ </Flex>
138
+
139
+ <Text
140
+ variant="xs"
141
+ css={{
142
+ color: '$on_surface_medium',
143
+ flexShrink: 0,
144
+ p: '$2',
145
+ whitespace: 'nowrap',
146
+ }}
147
+ >
148
+ {formatTime(message.time)}
149
+ </Text>
150
+ </Text>
151
+ </Flex>
152
+ <Text
153
+ variant="sm"
154
+ css={{
155
+ w: '100%',
156
+ mt: '$2',
157
+ wordBreak: 'break-word',
158
+ whiteSpace: 'pre-wrap',
159
+ userSelect: 'all',
160
+ color: '$on_surface_high',
161
+ }}
162
+ >
163
+ <AnnotisedMessage message={message.message} />
164
+ </Text>
165
+ </Box>
166
+ ))
167
+ )}
168
+ <div id="marker" style={{ height: filteredMessages.length ? '1px' : 0 }} />
169
+ </Box>
170
+ {canSendChatMessages && (
171
+ <Box css={{ bg: '$surface_dim' }}>
172
+ <Flex css={{ px: '$4', pb: '3px', gap: '$2', alignItems: 'center' }}>
173
+ <Text variant="caption">To:</Text>
174
+ <Flex css={{ bg: '$primary_bright', color: '$on_primary_high', r: '$2' }}>
175
+ <select
176
+ id="selector"
177
+ style={{
178
+ background: 'inherit',
179
+ color: 'inherit',
180
+ border: 'none',
181
+ outline: 'none',
182
+ borderRadius: '4px',
183
+ padding: '0 2px',
184
+ }}
185
+ defaultValue={elements.chat?.public_chat_enabled ? 'Everyone' : elements.chat?.roles_whitelist?.[0]}
186
+ >
187
+ {elements.chat?.roles_whitelist?.map(role => (
188
+ <option key={role} value={role}>
189
+ {role}
190
+ </option>
191
+ ))}
192
+ {elements.chat?.public_chat_enabled ? <option value="Everyone">Everyone</option> : ''}
193
+ </select>
194
+ </Flex>
195
+ </Flex>
196
+ <Flex
197
+ align="center"
198
+ css={{
199
+ bg: '$surface_default',
200
+ minHeight: '$16',
201
+ width: '100%',
202
+ py: '$6',
203
+ pl: '$4',
204
+ boxSizing: 'border-box',
205
+ gap: '$2',
206
+ r: '$2',
207
+ }}
208
+ >
209
+ <TextArea
210
+ id="chat-input"
211
+ maxLength={CHAT_MESSAGE_LIMIT}
212
+ disabled={!isChatEnabled || isLocalPeerBlacklisted}
213
+ rows={1}
214
+ css={{
215
+ w: '100%',
216
+ c: '$on_surface_high',
217
+ p: '0.75rem 0.75rem !important',
218
+ border: 'none',
219
+ resize: 'none',
220
+ }}
221
+ placeholder={getChatStatus()}
222
+ required
223
+ autoComplete="off"
224
+ aria-autocomplete="none"
225
+ />
226
+
227
+ <IconButton
228
+ id="send-btn"
229
+ disabled={!isChatEnabled || isLocalPeerBlacklisted}
230
+ title={getChatStatus()}
231
+ css={{
232
+ ml: 'auto',
233
+ height: 'max-content',
234
+ mr: '$4',
235
+ '&:hover': { c: '$on_surface_medium' },
236
+ }}
237
+ data-testid="send_msg_btn"
238
+ >
239
+ <SendIcon />
240
+ </IconButton>
241
+ </Flex>
242
+ </Box>
243
+ )}
244
+ </div>
245
+ );
246
+ };
247
+
248
+ const MessageTitle = ({
249
+ recipientPeer,
250
+ recipientRoles,
251
+ localPeerID,
252
+ }: {
253
+ recipientPeer?: string;
254
+ recipientRoles?: string[];
255
+ localPeerID: string;
256
+ }) => {
257
+ const peerName = useHMSStore(selectPeerNameByID(recipientPeer));
258
+
259
+ return (
260
+ <>
261
+ {recipientRoles ? (
262
+ <Text as="span" variant="sub2" css={{ color: '$on_surface_high', fontWeight: '$semiBold' }}>
263
+ to {recipientRoles} (Group)
264
+ </Text>
265
+ ) : null}
266
+ {recipientPeer ? (
267
+ <Text as="span" variant="sub2" css={{ color: '$on_surface_high', fontWeight: '$semiBold' }}>
268
+ to {recipientPeer === localPeerID ? 'You' : peerName} (DM)
269
+ </Text>
270
+ ) : null}
271
+ </>
272
+ );
273
+ };
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { ExternalLinkIcon } from '@100mslive/react-icons';
3
+ import { Dropdown } from '../../../Dropdown';
4
+ import { Text } from '../../../Text';
5
+
6
+ export const PIPChatOption = ({ openChat, showPIPChat }: { openChat: () => void; showPIPChat: boolean }) => {
7
+ if (!showPIPChat) {
8
+ return <></>;
9
+ }
10
+ return (
11
+ <Dropdown.Item onClick={openChat} data-testid="brb_btn">
12
+ <ExternalLinkIcon height={18} width={18} style={{ padding: '0 $2' }} />
13
+ <Text variant="sm" css={{ ml: '$4', color: '$on_surface_high' }}>
14
+ Pop out Chat
15
+ </Text>
16
+ </Dropdown.Item>
17
+ );
18
+ };
@@ -0,0 +1,56 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import { PIPContext } from './context';
3
+
4
+ type PIPProviderProps = {
5
+ children: React.ReactNode;
6
+ };
7
+
8
+ export const PIPProvider = ({ children }: PIPProviderProps) => {
9
+ // Detect if the feature is available.
10
+ const isSupported = 'documentPictureInPicture' in window;
11
+
12
+ // Expose pipWindow that is currently active
13
+ const [pipWindow, setPipWindow] = useState<Window | null>(null);
14
+
15
+ // Close pipWidnow programmatically
16
+ const closePipWindow = useCallback(() => {
17
+ if (pipWindow != null) {
18
+ pipWindow.close();
19
+ setPipWindow(null);
20
+ }
21
+ }, [pipWindow]);
22
+
23
+ // Open new pipWindow
24
+ const requestPipWindow = useCallback(
25
+ async (width: number, height: number) => {
26
+ // We don't want to allow multiple requests.
27
+ if (pipWindow != null) {
28
+ return;
29
+ }
30
+ // @ts-ignore for documentPIP
31
+ const pip = await window.documentPictureInPicture.requestWindow({
32
+ width,
33
+ height,
34
+ });
35
+
36
+ // Detect when window is closed by user
37
+ pip.addEventListener('pagehide', () => {
38
+ setPipWindow(null);
39
+ });
40
+
41
+ setPipWindow(pip);
42
+ },
43
+ [pipWindow],
44
+ );
45
+
46
+ const value = useMemo(() => {
47
+ return {
48
+ isSupported,
49
+ pipWindow,
50
+ requestPipWindow,
51
+ closePipWindow,
52
+ };
53
+ }, [closePipWindow, isSupported, pipWindow, requestPipWindow]);
54
+
55
+ return <PIPContext.Provider value={value}>{children}</PIPContext.Provider>;
56
+ };
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { createPortal } from 'react-dom';
3
+
4
+ type PIPWindowProps = {
5
+ pipWindow: Window;
6
+ children: React.ReactNode;
7
+ };
8
+
9
+ export const PIPWindow = ({ pipWindow, children }: PIPWindowProps) => {
10
+ pipWindow.document.body.style.margin = '0';
11
+ pipWindow.document.body.style.overflow = 'clip';
12
+ return createPortal(children, pipWindow.document.body);
13
+ };
@@ -0,0 +1,10 @@
1
+ import { createContext } from 'react';
2
+
3
+ export type PIPContextType = {
4
+ isSupported: boolean;
5
+ pipWindow: Window | null;
6
+ requestPipWindow: (width: number, height: number) => Promise<void>;
7
+ closePipWindow: () => void;
8
+ };
9
+
10
+ export const PIPContext = createContext<PIPContextType | undefined>(undefined);
@@ -0,0 +1,105 @@
1
+ import { useEffect } from 'react';
2
+ import { useHMSActions } from '@100mslive/react-sdk';
3
+ import { getCssText } from '../../../Theme';
4
+ import { usePIPWindow } from './usePIPWindow';
5
+
6
+ export const usePIPChat = () => {
7
+ const hmsActions = useHMSActions();
8
+ const { isSupported, requestPipWindow, pipWindow, closePipWindow } = usePIPWindow();
9
+
10
+ useEffect(() => {
11
+ if (document && pipWindow) {
12
+ const style = document.createElement('style');
13
+ style.id = 'stitches';
14
+ style.textContent = getCssText();
15
+ pipWindow.document.head.appendChild(style);
16
+ }
17
+ }, [pipWindow]);
18
+
19
+ // @ts-ignore
20
+ useEffect(() => {
21
+ if (pipWindow) {
22
+ const chatContainer = pipWindow.document.getElementById('chat-container');
23
+ const selector = pipWindow.document.getElementById('selector') as HTMLSelectElement;
24
+ const sendBtn = pipWindow.document.getElementById('send-btn');
25
+ const pipChatInput = pipWindow.document.getElementById('chat-input') as HTMLTextAreaElement;
26
+ const marker = pipWindow.document.getElementById('marker');
27
+
28
+ marker?.scrollIntoView({ block: 'end' });
29
+
30
+ const observer = new IntersectionObserver(
31
+ entries => {
32
+ entries.forEach(entry => {
33
+ if (entry.isIntersecting && entry.target.id) {
34
+ hmsActions.setMessageRead(true, entry.target.id);
35
+ }
36
+ });
37
+ },
38
+ {
39
+ root: chatContainer,
40
+ threshold: 0.8,
41
+ },
42
+ );
43
+
44
+ const mutationObserver = new MutationObserver(mutations => {
45
+ mutations.forEach(mutation => {
46
+ if (mutation.addedNodes.length > 0) {
47
+ const newMessages = mutation.addedNodes;
48
+ newMessages.forEach(message => {
49
+ const messageId = (message as Element)?.id;
50
+ if (messageId === 'new-message-notif') {
51
+ message.addEventListener('click', () =>
52
+ setTimeout(() => marker?.scrollIntoView({ block: 'end', behavior: 'smooth' }), 0),
53
+ );
54
+ } else if (messageId) observer.observe(message as Element);
55
+ });
56
+ }
57
+ });
58
+ });
59
+ mutationObserver.observe(chatContainer as Node, {
60
+ childList: true,
61
+ });
62
+
63
+ const sendMessage = async () => {
64
+ const selection = selector?.value || 'Everyone';
65
+ if (selection === 'Everyone') {
66
+ await hmsActions.sendBroadcastMessage(pipChatInput.value.trim());
67
+ } else {
68
+ await hmsActions.sendGroupMessage(pipChatInput.value.trim(), [selection]);
69
+ }
70
+ pipChatInput.value = '';
71
+ setTimeout(() => marker?.scrollIntoView({ block: 'end', behavior: 'smooth' }), 0);
72
+ };
73
+
74
+ if (sendBtn && hmsActions && pipChatInput) {
75
+ const pipMessages = pipWindow.document.getElementsByClassName('pip-message');
76
+ // @ts-ignore
77
+ [...pipMessages].forEach(message => {
78
+ if (message.id) {
79
+ hmsActions.setMessageRead(true, message.id);
80
+ }
81
+ });
82
+ // @ts-ignore
83
+ const sendOnEnter = e => {
84
+ if (e.key === 'Enter') sendMessage();
85
+ };
86
+ sendBtn.addEventListener('click', sendMessage);
87
+ pipChatInput.addEventListener('keypress', sendOnEnter);
88
+ return () => {
89
+ sendBtn.removeEventListener('click', sendMessage);
90
+ pipChatInput.removeEventListener('keypress', sendOnEnter);
91
+ mutationObserver.disconnect();
92
+ observer.disconnect();
93
+ };
94
+ }
95
+ }
96
+ }, [pipWindow, hmsActions]);
97
+
98
+ useEffect(() => {
99
+ return () => {
100
+ pipWindow && closePipWindow();
101
+ };
102
+ }, [closePipWindow, pipWindow]);
103
+
104
+ return { isSupported, requestPipWindow, pipWindow };
105
+ };
@@ -0,0 +1,12 @@
1
+ import { useContext } from 'react';
2
+ import { PIPContext, PIPContextType } from './context';
3
+
4
+ export const usePIPWindow = (): PIPContextType => {
5
+ const context = useContext(PIPContext);
6
+
7
+ if (context === undefined) {
8
+ throw new Error('usePIPWindow must be used within a PIPContext');
9
+ }
10
+
11
+ return context;
12
+ };
@@ -8,6 +8,7 @@ import {
8
8
  selectRoomState,
9
9
  selectVideoTrackByID,
10
10
  useAVToggle,
11
+ useAwayNotifications,
11
12
  useHMSStore,
12
13
  useParticipants,
13
14
  usePreviewJoin,
@@ -100,6 +101,7 @@ const PreviewJoin = ({
100
101
  },
101
102
  asRole,
102
103
  });
104
+ const { requestPermission } = useAwayNotifications();
103
105
  const roomState = useHMSStore(selectRoomState);
104
106
  const savePreferenceAndJoin = useCallback(() => {
105
107
  setPreviewPreference({
@@ -115,7 +117,7 @@ const PreviewJoin = ({
115
117
  if (skipPreview) {
116
118
  savePreferenceAndJoin();
117
119
  } else {
118
- preview();
120
+ preview().then(() => requestPermission());
119
121
  }
120
122
  }
121
123
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -111,7 +111,7 @@ export const ToastConfig = {
111
111
  const count = new Set(notifications.map(notification => notification.data?.id)).size;
112
112
  return {
113
113
  title: `${notifications[notifications.length - 1].data?.name} ${
114
- count > 1 ? `${count} and others` : ''
114
+ count > 1 ? `and ${count} others` : ''
115
115
  } raised hand`,
116
116
  icon: <HandIcon />,
117
117
  };
@@ -129,7 +129,7 @@ export const ToastConfig = {
129
129
  const count = new Set(notifications.map(notification => notification.data?.id)).size;
130
130
  return {
131
131
  title: `${notifications[notifications.length - 1].data?.name} ${
132
- count > 1 ? `${count} and others` : ''
132
+ count > 1 ? `and ${count} others` : ''
133
133
  } raised hand`,
134
134
  icon: <HandIcon />,
135
135
  action: <HandRaiseAction isSingleHandRaise={false} />,