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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. package/dist/{HLSView-KOCGTP23.css → HLSView-LQNFHQLW.css} +3 -3
  2. package/dist/{HLSView-KOCGTP23.css.map → HLSView-LQNFHQLW.css.map} +1 -1
  3. package/dist/{HLSView-P7GF2RAU.js → HLSView-PTKFE7ET.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-QGNIWXTH.js} +10176 -9767
  15. package/dist/chunk-QGNIWXTH.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 +3974 -3512
  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 +2275 -1908
  24. package/dist/meta.esbuild.json +2275 -1908
  25. package/package.json +8 -7
  26. package/src/Prebuilt/components/AudioVideoToggle.tsx +14 -7
  27. package/src/Prebuilt/components/Chat/ChatBody.tsx +4 -12
  28. package/src/Prebuilt/components/Chat/ChatFooter.tsx +2 -3
  29. package/src/Prebuilt/components/Chat/utils.ts +11 -0
  30. package/src/Prebuilt/components/ConferenceScreen.tsx +13 -1
  31. package/src/Prebuilt/components/Footer/ParticipantList.tsx +1 -4
  32. package/src/Prebuilt/components/MoreSettings/MoreSettings.tsx +4 -1
  33. package/src/Prebuilt/components/MoreSettings/SplitComponents/DesktopOptions.tsx +13 -5
  34. package/src/Prebuilt/components/Notifications/HandRaisedNotifications.tsx +33 -0
  35. package/src/Prebuilt/components/PIP/PIPChat.tsx +225 -0
  36. package/src/Prebuilt/components/PIP/PIPChatOption.tsx +18 -0
  37. package/src/Prebuilt/components/PIP/PIPProvider.tsx +56 -0
  38. package/src/Prebuilt/components/PIP/PIPWindow.tsx +13 -0
  39. package/src/Prebuilt/components/PIP/context.ts +10 -0
  40. package/src/Prebuilt/components/PIP/usePIPChat.tsx +103 -0
  41. package/src/Prebuilt/components/PIP/usePIPWindow.tsx +12 -0
  42. package/src/Prebuilt/components/Preview/PreviewJoin.tsx +3 -1
  43. package/src/Prebuilt/components/Toast/ToastConfig.jsx +2 -2
  44. package/dist/chunk-DDB4BLHX.js.map +0 -7
  45. /package/dist/{HLSView-P7GF2RAU.js.map → HLSView-PTKFE7ET.js.map} +0 -0
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "prebuilt",
11
11
  "roomkit"
12
12
  ],
13
- "version": "0.3.16-alpha.0",
13
+ "version": "0.3.16-alpha.2",
14
14
  "author": "100ms",
15
15
  "license": "MIT",
16
16
  "repository": {
@@ -75,12 +75,12 @@
75
75
  "react": ">=17.0.2 <19.0.0"
76
76
  },
77
77
  "dependencies": {
78
- "@100mslive/hls-player": "0.3.16-alpha.0",
78
+ "@100mslive/hls-player": "0.3.16-alpha.2",
79
79
  "@100mslive/hms-noise-cancellation": "0.0.1",
80
- "@100mslive/hms-virtual-background": "1.13.16-alpha.0",
81
- "@100mslive/hms-whiteboard": "0.0.6-alpha.0",
82
- "@100mslive/react-icons": "0.10.16-alpha.0",
83
- "@100mslive/react-sdk": "0.10.16-alpha.0",
80
+ "@100mslive/hms-virtual-background": "1.13.16-alpha.2",
81
+ "@100mslive/hms-whiteboard": "0.0.6-alpha.2",
82
+ "@100mslive/react-icons": "0.10.16-alpha.2",
83
+ "@100mslive/react-sdk": "0.10.16-alpha.2",
84
84
  "@100mslive/types-prebuilt": "0.12.9",
85
85
  "@emoji-mart/data": "^1.0.6",
86
86
  "@emoji-mart/react": "^1.0.1",
@@ -104,6 +104,7 @@
104
104
  "eventemitter2": "^6.4.9",
105
105
  "lodash.merge": "^4.6.2",
106
106
  "qrcode.react": "^3.1.0",
107
+ "react-dom": "^18.2.0",
107
108
  "react-draggable": "^4.4.5",
108
109
  "react-intersection-observer": "^9.4.3",
109
110
  "react-swipeable": "^7.0.1",
@@ -116,5 +117,5 @@
116
117
  "uuid": "^8.3.2",
117
118
  "worker-timers": "^7.0.40"
118
119
  },
119
- "gitHead": "0ba6be8700e395887440d6ab1cadaacb6a769e98"
120
+ "gitHead": "0be9feff65c2b2ae3a0def2236e41b492ac17e98"
120
121
  }
@@ -109,6 +109,9 @@ const useNoiseCancellationWithPlugin = () => {
109
109
  if (inProgress) {
110
110
  return;
111
111
  }
112
+ if (!krispPlugin.checkSupport().isSupported) {
113
+ throw Error('Krisp plugin is not supported');
114
+ }
112
115
  setInProgress(true);
113
116
  if (enabled) {
114
117
  await actions.addPluginToAudioTrack(krispPlugin);
@@ -281,13 +284,17 @@ export const AudioVideoToggle = ({ hideOptions = false }: { hideOptions?: boolea
281
284
  useEffect(() => {
282
285
  (async () => {
283
286
  if (isNoiseCancellationEnabled && !isKrispPluginAdded && !inProgress && localPeer?.audioTrack) {
284
- await setNoiseCancellationWithPlugin(true);
285
- ToastManager.addToast({
286
- title: `Noise Reduction Enabled`,
287
- variant: 'standard',
288
- duration: 2000,
289
- icon: <AudioLevelIcon />,
290
- });
287
+ try {
288
+ await setNoiseCancellationWithPlugin(true);
289
+ ToastManager.addToast({
290
+ title: `Noise Reduction Enabled`,
291
+ variant: 'standard',
292
+ duration: 2000,
293
+ icon: <AudioLevelIcon />,
294
+ });
295
+ } catch (error) {
296
+ console.error(error);
297
+ }
291
298
  }
292
299
  })();
293
300
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -28,18 +28,9 @@ import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvid
28
28
  // @ts-ignore: No implicit Any
29
29
  import { useSetSubscribedChatSelector } from '../AppData/useUISettings';
30
30
  import { usePinnedBy } from '../hooks/usePinnedBy';
31
+ import { formatTime } from './utils';
31
32
  import { CHAT_SELECTOR, SESSION_STORE_KEY } from '../../common/constants';
32
33
 
33
- const formatTime = (date: Date) => {
34
- if (!(date instanceof Date)) {
35
- return '';
36
- }
37
- const hours = date.getHours();
38
- const minutes = date.getMinutes();
39
- const suffix = hours > 11 ? 'PM' : 'AM';
40
- return `${hours < 10 ? '0' : ''}${hours}:${minutes < 10 ? '0' : ''}${minutes} ${suffix}`;
41
- };
42
-
43
34
  const rowHeights: Record<number, { size: number; id: string }> = {};
44
35
  let listInstance: VariableSizeList | null = null; //eslint-disable-line
45
36
  function getRowHeight(index: number) {
@@ -109,7 +100,7 @@ const MessageTypeContainer = ({ left, right }: { left?: string; right?: string }
109
100
  );
110
101
  };
111
102
 
112
- const MessageType = ({
103
+ export const MessageType = ({
113
104
  roles,
114
105
  hasCurrentUserSent,
115
106
  receiver,
@@ -172,7 +163,8 @@ const getMessageType = ({ roles, receiver }: { roles?: HMSRoleName[]; receiver?:
172
163
  }
173
164
  return receiver ? 'private' : '';
174
165
  };
175
- const SenderName = styled(Text, {
166
+
167
+ export const SenderName = styled(Text, {
176
168
  overflow: 'hidden',
177
169
  textOverflow: 'ellipsis',
178
170
  whiteSpace: 'nowrap',
@@ -20,10 +20,9 @@ import { useIsPeerBlacklisted } from '../hooks/useChatBlacklist';
20
20
  // @ts-ignore: No implicit any
21
21
  import { useEmojiPickerStyles } from './useEmojiPickerStyles';
22
22
  import { useDefaultChatSelection, useLandscapeHLSStream, useMobileHLSStream } from '../../common/hooks';
23
+ import { CHAT_MESSAGE_LIMIT } from './utils';
23
24
  import { CHAT_SELECTOR, SESSION_STORE_KEY } from '../../common/constants';
24
25
 
25
- const CHAT_MESSAGE_LIMIT = 2000;
26
-
27
26
  const TextArea = styled('textarea', {
28
27
  width: '100%',
29
28
  bg: 'transparent',
@@ -76,7 +75,7 @@ function EmojiPicker({ onSelect }: { onSelect: (emoji: any) => void }) {
76
75
  );
77
76
  }
78
77
 
79
- export const ChatFooter = ({ onSend, children }: { onSend: (count: number) => void; children: ReactNode }) => {
78
+ export const ChatFooter = ({ onSend, children }: { onSend: (count: number) => void; children?: ReactNode }) => {
80
79
  const hmsActions = useHMSActions();
81
80
  const inputRef = useRef<HTMLTextAreaElement>(null);
82
81
  const [draftMessage, setDraftMessage] = useChatDraftMessage();
@@ -0,0 +1,11 @@
1
+ export const formatTime = (date: Date) => {
2
+ if (!(date instanceof Date)) {
3
+ return '';
4
+ }
5
+ const hours = date.getHours();
6
+ const minutes = date.getMinutes();
7
+ const suffix = hours > 11 ? 'PM' : 'AM';
8
+ return `${hours < 10 ? '0' : ''}${hours}:${minutes < 10 ? '0' : ''}${minutes} ${suffix}`;
9
+ };
10
+
11
+ export const CHAT_MESSAGE_LIMIT = 2000;
@@ -6,6 +6,7 @@ import {
6
6
  selectAppData,
7
7
  selectIsConnectedToRoom,
8
8
  selectRoomState,
9
+ useAwayNotifications,
9
10
  useHMSActions,
10
11
  useHMSStore,
11
12
  } from '@100mslive/react-sdk';
@@ -50,6 +51,7 @@ export const ConferenceScreen = () => {
50
51
  const isMobileDevice = isAndroid || isIOS || isIPadOS;
51
52
  const dropdownListRef = useRef<string[]>();
52
53
  const [isHLSStarted] = useSetAppDataByKey(APP_DATA.hlsStarted);
54
+ const { requestPermission } = useAwayNotifications();
53
55
 
54
56
  // using it in hls stream to show action button when chat is disabled
55
57
  const showChat = !!screenProps.elements?.chat;
@@ -99,10 +101,20 @@ export const ConferenceScreen = () => {
99
101
  speakerAutoSelectionBlacklist: ['Yeti Stereo Microphone'],
100
102
  },
101
103
  })
104
+ .then(() => requestPermission())
102
105
  .catch(console.error);
103
106
  autoRoomJoined.current = true;
104
107
  }
105
- }, [authTokenInAppData, endpoints?.init, hmsActions, isConnectedToRoom, isPreviewScreenEnabled, roomState, userName]);
108
+ }, [
109
+ authTokenInAppData,
110
+ endpoints?.init,
111
+ hmsActions,
112
+ isConnectedToRoom,
113
+ isPreviewScreenEnabled,
114
+ roomState,
115
+ userName,
116
+ requestPermission,
117
+ ]);
106
118
 
107
119
  useEffect(() => {
108
120
  onJoinFunc?.();
@@ -194,7 +194,6 @@ export const Participant = ({
194
194
  <ParticipantActions
195
195
  peerId={peer.id}
196
196
  peerType={peer.type}
197
- isLocal={peer.id === localPeerId}
198
197
  role={peer.roleName}
199
198
  isHandRaisedAccordion={isHandRaisedAccordion}
200
199
  />
@@ -273,12 +272,10 @@ const ParticipantActions = React.memo(
273
272
  peerId,
274
273
  peerType,
275
274
  role,
276
- isLocal,
277
275
  isHandRaisedAccordion,
278
276
  }: {
279
277
  peerId: string;
280
278
  role: string;
281
- isLocal: boolean;
282
279
  isHandRaisedAccordion?: boolean;
283
280
  peerType: HMSPeerType;
284
281
  }) => {
@@ -331,7 +328,7 @@ const ParticipantActions = React.memo(
331
328
  </Flex>
332
329
  ) : null}
333
330
 
334
- {shouldShowMoreActions && !isLocal ? <ParticipantMoreActions peerId={peerId} role={role} /> : null}
331
+ {shouldShowMoreActions ? <ParticipantMoreActions peerId={peerId} role={role} /> : null}
335
332
  </>
336
333
  )}
337
334
  </Flex>
@@ -10,6 +10,7 @@ import { DesktopOptions } from './SplitComponents/DesktopOptions';
10
10
  // @ts-ignore: No implicit Any
11
11
  import { MwebOptions } from './SplitComponents/MwebOptions';
12
12
  import { config as cssConfig } from '../../..';
13
+ import { PIPProvider } from '../PIP/PIPProvider';
13
14
  import { useLandscapeHLSStream } from '../../common/hooks';
14
15
 
15
16
  export const MoreSettings = ({
@@ -24,6 +25,8 @@ export const MoreSettings = ({
24
25
  return isMobile || isLandscapeHLSStream ? (
25
26
  <MwebOptions elements={elements} screenType={screenType} />
26
27
  ) : (
27
- <DesktopOptions elements={elements} screenType={screenType} />
28
+ <PIPProvider>
29
+ <DesktopOptions elements={elements} screenType={screenType} />
30
+ </PIPProvider>
28
31
  );
29
32
  };
@@ -28,8 +28,11 @@ import { Checkbox, Dropdown, Flex, Switch, Text, Tooltip } from '../../../..';
28
28
  import IconButton from '../../../IconButton';
29
29
  // @ts-ignore: No implicit any
30
30
  import { PIP } from '../../PIP';
31
+ import { PIPChat } from '../../PIP/PIPChat';
31
32
  // @ts-ignore: No implicit any
33
+ import { PIPChatOption } from '../../PIP/PIPChatOption';
32
34
  import { PictureInPicture } from '../../PIP/PIPManager';
35
+ import { PIPWindow } from '../../PIP/PIPWindow';
33
36
  // @ts-ignore: No implicit any
34
37
  import { RoleChangeModal } from '../../RoleChangeModal';
35
38
  // @ts-ignore: No implicit any
@@ -48,6 +51,8 @@ import { MuteAllModal } from '../MuteAllModal';
48
51
  import { useDropdownList } from '../../hooks/useDropdownList';
49
52
  import { useMyMetadata } from '../../hooks/useMetadata';
50
53
  // @ts-ignore: No implicit any
54
+ import { usePIPChat } from '../../PIP/usePIPChat';
55
+ // @ts-ignore: No implicit any
51
56
  import { APP_DATA, isMacOS } from '../../../common/constants';
52
57
 
53
58
  const MODALS = {
@@ -79,6 +84,8 @@ export const DesktopOptions = ({
79
84
  const isBRBEnabled = !!elements?.brb;
80
85
  const isTranscriptionAllowed = useHMSStore(selectIsTranscriptionAllowedByMode(HMSTranscriptionMode.CAPTION));
81
86
  const isTranscriptionEnabled = useHMSStore(selectIsTranscriptionEnabled);
87
+ const { isSupported, pipWindow, requestPipWindow } = usePIPChat();
88
+ const showPipChatOption = !!elements?.chat && isSupported;
82
89
 
83
90
  useDropdownList({ open: openModals.size > 0, name: 'MoreSettings' });
84
91
 
@@ -98,6 +105,11 @@ export const DesktopOptions = ({
98
105
 
99
106
  return (
100
107
  <Fragment>
108
+ {isSupported && pipWindow ? (
109
+ <PIPWindow pipWindow={pipWindow}>
110
+ <PIPChat />
111
+ </PIPWindow>
112
+ ) : null}
101
113
  <Dropdown.Root
102
114
  open={openModals.has(MODALS.MORE_SETTINGS)}
103
115
  onOpenChange={value => updateState(MODALS.MORE_SETTINGS, value)}
@@ -168,13 +180,9 @@ export const DesktopOptions = ({
168
180
  </Dropdown.Item>
169
181
  ) : null}
170
182
 
183
+ <PIPChatOption showPIPChat={showPipChatOption} openChat={async () => await requestPipWindow(350, 500)} />
171
184
  <FullScreenItem />
172
- {/* {isAllowedToPublish.screen && isEmbedEnabled && (
173
- <EmbedUrl setShowOpenUrl={() => updateState(MODALS.EMBED_URL, true)} />
174
- )} */}
175
-
176
185
  <Dropdown.ItemSeparator css={{ mx: 0 }} />
177
-
178
186
  <Dropdown.Item onClick={() => updateState(MODALS.DEVICE_SETTINGS, true)} data-testid="device_settings_btn">
179
187
  <SettingsIcon />
180
188
  <Text variant="sm" css={{ ml: '$4' }}>
@@ -1,14 +1,19 @@
1
1
  import { useEffect } from 'react';
2
+ import { useDebounce } from 'react-use';
2
3
  import {
3
4
  HMSNotificationTypes,
4
5
  HMSRoomState,
6
+ selectHandRaisedPeers,
5
7
  selectHasPeerHandRaised,
8
+ selectIsLocalScreenShared,
6
9
  selectPeerByID,
7
10
  selectRoomState,
11
+ useAwayNotifications,
8
12
  useHMSNotifications,
9
13
  useHMSStore,
10
14
  useHMSVanillaStore,
11
15
  } from '@100mslive/react-sdk';
16
+ import { useRoomLayout } from '../../provider/roomLayoutProvider';
12
17
  // @ts-ignore: No implicit Any
13
18
  import { ToastBatcher } from '../Toast/ToastBatcher';
14
19
  import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvider/hooks/useRoomLayoutScreen';
@@ -22,6 +27,9 @@ export const HandRaisedNotifications = () => {
22
27
  const vanillaStore = useHMSVanillaStore();
23
28
  const { on_stage_exp } = useRoomLayoutConferencingScreen().elements || {};
24
29
  const isSubscribing = !!useSubscribedNotifications(SUBSCRIBED_NOTIFICATIONS.METADATA_UPDATED);
30
+ const amIScreenSharing = useHMSStore(selectIsLocalScreenShared);
31
+ const { showNotification } = useAwayNotifications();
32
+ const logoURL = useRoomLayout()?.logo?.url;
25
33
 
26
34
  useEffect(() => {
27
35
  if (!notification?.data) {
@@ -32,6 +40,7 @@ export const HandRaisedNotifications = () => {
32
40
  if (roomState !== HMSRoomState.Connected || notification.data.isLocal || !isSubscribing) {
33
41
  return;
34
42
  }
43
+
35
44
  const hasPeerHandRaised = vanillaStore.getState(selectHasPeerHandRaised(notification.data.id));
36
45
  const peer = vanillaStore.getState(selectPeerByID(notification.data.id));
37
46
  if (hasPeerHandRaised) {
@@ -41,5 +50,29 @@ export const HandRaisedNotifications = () => {
41
50
  }
42
51
  }, [isSubscribing, notification, on_stage_exp, roomState, vanillaStore]);
43
52
 
53
+ useDebounce(
54
+ () => {
55
+ if (!notification?.data) {
56
+ return;
57
+ }
58
+
59
+ // Don't show toast message in case of local peer.
60
+ if (roomState !== HMSRoomState.Connected || notification.data.isLocal || !isSubscribing) {
61
+ return;
62
+ }
63
+
64
+ const hasPeerHandRaised = vanillaStore.getState(selectHasPeerHandRaised(notification.data.id));
65
+ const peer = vanillaStore.getState(selectPeerByID(notification.data.id));
66
+ const handRaisedPeers = vanillaStore.getState(selectHandRaisedPeers);
67
+ if (amIScreenSharing && hasPeerHandRaised) {
68
+ const title = `${peer?.name} ${
69
+ handRaisedPeers.length > 1 ? `and ${handRaisedPeers.length - 1} others` : ''
70
+ } raised hand`;
71
+ showNotification(title, { icon: logoURL });
72
+ }
73
+ },
74
+ 1000,
75
+ [isSubscribing, notification, roomState, vanillaStore, amIScreenSharing],
76
+ );
44
77
  return null;
45
78
  };
@@ -0,0 +1,225 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ selectHMSMessages,
4
+ selectLocalPeerID,
5
+ selectSessionStore,
6
+ selectUnreadHMSMessagesCount,
7
+ useHMSStore,
8
+ } from '@100mslive/react-sdk';
9
+ import { SendIcon } from '@100mslive/react-icons';
10
+ import { Box, Flex } from '../../../Layout';
11
+ import { Text } from '../../../Text';
12
+ import { TextArea } from '../../../TextArea';
13
+ import { Tooltip } from '../../../Tooltip';
14
+ import IconButton from '../../IconButton';
15
+ import { AnnotisedMessage } from '../Chat/ChatBody';
16
+ import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvider/hooks/useRoomLayoutScreen';
17
+ import { CHAT_MESSAGE_LIMIT, formatTime } from '../Chat/utils';
18
+ import { SESSION_STORE_KEY } from '../../common/constants';
19
+
20
+ export const PIPChat = () => {
21
+ const messages = useHMSStore(selectHMSMessages);
22
+ const localPeerID = useHMSStore(selectLocalPeerID);
23
+ const count = useHMSStore(selectUnreadHMSMessagesCount);
24
+ const [unreadMessageCount, setUnreadMessageCount] = useState(0);
25
+
26
+ const getSenderName = useCallback(
27
+ (senderName: string, senderID?: string) => {
28
+ const slicedName = senderName.length > 10 ? senderName.slice(0, 10) + '...' : senderName;
29
+ return slicedName + (senderID === localPeerID ? ' (You)' : '');
30
+ },
31
+ [localPeerID],
32
+ );
33
+
34
+ useEffect(() => {
35
+ const timeoutId = setTimeout(() => {
36
+ setUnreadMessageCount(count);
37
+ }, 100);
38
+ return () => clearTimeout(timeoutId);
39
+ }, [count]);
40
+
41
+ const blacklistedMessageIDs = useHMSStore(selectSessionStore(SESSION_STORE_KEY.CHAT_MESSAGE_BLACKLIST));
42
+ const filteredMessages = useMemo(() => {
43
+ const blacklistedMessageIDSet = new Set(blacklistedMessageIDs || []);
44
+ return messages?.filter(message => message.type === 'chat' && !blacklistedMessageIDSet.has(message.id)) || [];
45
+ }, [blacklistedMessageIDs, messages]);
46
+ const { elements } = useRoomLayoutConferencingScreen();
47
+ const message_placeholder = elements?.chat?.message_placeholder || 'Send a message';
48
+ const canSendChatMessages = !!elements?.chat?.public_chat_enabled || !!elements?.chat?.roles_whitelist?.length;
49
+
50
+ return (
51
+ <div style={{ height: '100%' }}>
52
+ <Box
53
+ id="chat-container"
54
+ css={{
55
+ bg: '$surface_dim',
56
+ overflowY: 'auto',
57
+ // Subtracting height of footer
58
+ h: canSendChatMessages ? 'calc(100% - 90px)' : '100%',
59
+ position: 'relative',
60
+ }}
61
+ >
62
+ {unreadMessageCount ? (
63
+ <Box
64
+ id="new-message-notif"
65
+ style={{
66
+ position: 'fixed',
67
+ bottom: '76px',
68
+ right: '4px',
69
+ }}
70
+ >
71
+ <Text
72
+ variant="xs"
73
+ css={{ cursor: 'pointer' }}
74
+ style={{ color: 'white', background: 'gray', padding: '4px', borderRadius: '4px' }}
75
+ >
76
+ {unreadMessageCount === 1 ? 'New message' : `${unreadMessageCount} new messages`}
77
+ </Text>
78
+ </Box>
79
+ ) : (
80
+ ''
81
+ )}
82
+ {filteredMessages.length === 0 ? (
83
+ <div
84
+ style={{ display: 'flex', height: '100%', width: '100%', alignItems: 'center', justifyContent: 'center' }}
85
+ >
86
+ <Text>No messages here yet</Text>
87
+ </div>
88
+ ) : (
89
+ filteredMessages.map(message => (
90
+ <Box className="pip-message" key={message.id} id={message.id} style={{ padding: '8px 0.75rem' }}>
91
+ <Flex style={{ width: '100%', alignItems: 'center', justifyContent: 'between' }}>
92
+ <Text
93
+ style={{ display: 'flex', justifyContent: 'between', width: '100%', alignItems: 'center' }}
94
+ css={{
95
+ color: '$on_surface_high',
96
+ fontWeight: '$semiBold',
97
+ }}
98
+ >
99
+ <Flex style={{ flexGrow: 1, gap: '2px', alignItems: 'center' }}>
100
+ {message.senderName === 'You' || !message.senderName ? (
101
+ <Text as="span" variant="sub2" css={{ color: '$on_surface_high', fontWeight: '$semiBold' }}>
102
+ {message.senderName || 'Anonymous'}
103
+ </Text>
104
+ ) : (
105
+ <Tooltip title={message.senderName} side="top" align="start">
106
+ <Text as="span" variant="sub2" css={{ color: '$on_surface_high', fontWeight: '$semiBold' }}>
107
+ {getSenderName(message.senderName, message?.sender)}
108
+ </Text>
109
+ </Tooltip>
110
+ )}
111
+ {message.recipientRoles ? (
112
+ <Text as="span" variant="sub2" css={{ color: '$on_surface_high', fontWeight: '$semiBold' }}>
113
+ to {message.recipientRoles} (Group)
114
+ </Text>
115
+ ) : null}
116
+ {message.recipientPeer ? (
117
+ <Text as="span" variant="sub2" css={{ color: '$on_surface_high', fontWeight: '$semiBold' }}>
118
+ (DM)
119
+ </Text>
120
+ ) : null}
121
+ </Flex>
122
+
123
+ <Text
124
+ variant="xs"
125
+ css={{
126
+ color: '$on_surface_medium',
127
+ flexShrink: 0,
128
+ p: '$2',
129
+ whitespace: 'nowrap',
130
+ }}
131
+ >
132
+ {formatTime(message.time)}
133
+ </Text>
134
+ </Text>
135
+ </Flex>
136
+ <Text
137
+ variant="sm"
138
+ css={{
139
+ w: '100%',
140
+ mt: '$2',
141
+ wordBreak: 'break-word',
142
+ whiteSpace: 'pre-wrap',
143
+ userSelect: 'all',
144
+ color: '$on_surface_high',
145
+ }}
146
+ >
147
+ <AnnotisedMessage message={message.message} />
148
+ </Text>
149
+ </Box>
150
+ ))
151
+ )}
152
+ <div id="marker" style={{ height: filteredMessages.length ? '1px' : 0 }} />
153
+ </Box>
154
+ {canSendChatMessages && (
155
+ <Box css={{ bg: '$surface_dim' }}>
156
+ <Flex css={{ px: '$4', pb: '3px', gap: '$2', alignItems: 'center' }}>
157
+ <Text variant="caption">To:</Text>
158
+ <Flex css={{ bg: '$primary_bright', color: '$on_primary_high', r: '$2' }}>
159
+ <select
160
+ id="selector"
161
+ style={{
162
+ background: 'inherit',
163
+ color: 'inherit',
164
+ border: 'none',
165
+ outline: 'none',
166
+ borderRadius: '4px',
167
+ padding: '0 2px',
168
+ }}
169
+ defaultValue={elements.chat?.public_chat_enabled ? 'Everyone' : elements.chat?.roles_whitelist?.[0]}
170
+ >
171
+ {elements.chat?.roles_whitelist?.map(role => (
172
+ <option key={role} value={role}>
173
+ {role}
174
+ </option>
175
+ ))}
176
+ {elements.chat?.public_chat_enabled ? <option value="Everyone">Everyone</option> : ''}
177
+ </select>
178
+ </Flex>
179
+ </Flex>
180
+ <Flex
181
+ align="center"
182
+ css={{
183
+ bg: '$surface_default',
184
+ minHeight: '$16',
185
+ width: '100%',
186
+ py: '$6',
187
+ pl: '$4',
188
+ boxSizing: 'border-box',
189
+ gap: '$2',
190
+ r: '$2',
191
+ }}
192
+ >
193
+ <TextArea
194
+ id="chat-input"
195
+ maxLength={CHAT_MESSAGE_LIMIT}
196
+ style={{ border: 'none', resize: 'none' }}
197
+ css={{
198
+ w: '100%',
199
+ c: '$on_surface_high',
200
+ padding: '0.25rem !important',
201
+ }}
202
+ placeholder={message_placeholder}
203
+ required
204
+ autoComplete="off"
205
+ aria-autocomplete="none"
206
+ />
207
+
208
+ <IconButton
209
+ id="send-btn"
210
+ css={{
211
+ ml: 'auto',
212
+ height: 'max-content',
213
+ mr: '$4',
214
+ '&:hover': { c: '$on_surface_medium' },
215
+ }}
216
+ data-testid="send_msg_btn"
217
+ >
218
+ <SendIcon />
219
+ </IconButton>
220
+ </Flex>
221
+ </Box>
222
+ )}
223
+ </div>
224
+ );
225
+ };
@@ -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.overflowX = 'hidden';
12
+ return createPortal(children, pipWindow.document.body);
13
+ };