@whereby.com/browser-sdk 3.1.0 → 3.2.0

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.
@@ -1,6 +1,8 @@
1
1
  import * as React from 'react';
2
2
  import { LocalMediaOptions, RemoteParticipant, LocalParticipant, Screenshare, ConnectionStatus, NotificationsEventEmitter, ClientView, ChatMessage as ChatMessage$1 } from '@whereby.com/core';
3
3
  import { RoleName } from '@whereby.com/media';
4
+ import * as _radix_ui_react_popover from '@radix-ui/react-popover';
5
+ import { PopoverProps } from '@radix-ui/react-popover';
4
6
 
5
7
  interface ProviderProps {
6
8
  children: React.ReactNode;
@@ -142,41 +144,31 @@ declare function useRoomConnection(roomUrl: string, roomConnectionOptions?: UseR
142
144
 
143
145
  declare function useLocalMedia(optionsOrStream?: UseLocalMediaOptions | MediaStream): UseLocalMediaResult;
144
146
 
145
- type Origin = {
146
- top: number;
147
- left: number;
147
+ type GridCellSelfProps = {
148
+ participant: ClientView;
148
149
  };
149
- type Bounds = {
150
- width: number;
151
- height: number;
152
- };
153
- type CellView = {
154
- aspectRatio?: number;
155
- avatarSize?: number;
156
- cellPaddings?: {
157
- top: number;
158
- right: number;
159
- };
160
- client?: ClientView;
161
- clientId: string;
162
- isDraggable?: boolean;
163
- isPlaceholder?: boolean;
164
- isSubgrid?: boolean;
165
- type: string;
166
- };
167
-
150
+ type GridCellProps = GridCellSelfProps & React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
151
+ declare const GridCell: React.ForwardRefExoticComponent<Omit<GridCellProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
152
+ declare const GridVideoView: React.ForwardRefExoticComponent<Omit<VideoViewProps, "ref" | "stream"> & {
153
+ stream?: MediaStream | undefined;
154
+ } & React.RefAttributes<WherebyVideoElement>>;
168
155
  interface GridProps {
169
- renderParticipant?: ({ cell, participant, }: {
170
- cell: {
171
- clientId: string;
172
- bounds: Bounds;
173
- origin: Origin;
174
- };
175
- participant: CellView["client"];
156
+ renderParticipant?: ({ participant }: {
157
+ participant: ClientView;
176
158
  }) => React.ReactNode;
159
+ gridGap?: number;
177
160
  videoGridGap?: number;
161
+ enableSubgrid?: boolean;
178
162
  stageParticipantLimit?: number;
163
+ enableParticipantMenu?: boolean;
179
164
  }
180
- declare function Grid({ renderParticipant, stageParticipantLimit, videoGridGap }: GridProps): React.JSX.Element;
165
+ declare function Grid({ renderParticipant, stageParticipantLimit, gridGap, videoGridGap, enableSubgrid, enableParticipantMenu, }: GridProps): React.JSX.Element;
166
+
167
+ declare const ParticipantMenu: (props: PopoverProps) => React.JSX.Element;
168
+ declare const ParticipantMenuContent: React.ForwardRefExoticComponent<Omit<Omit<_radix_ui_react_popover.PopoverContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
169
+ declare const ParticipantMenuTrigger: React.ForwardRefExoticComponent<Omit<_radix_ui_react_popover.PopoverTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
170
+ declare const ParticipantMenuItem: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref"> & {
171
+ participantAction?: "maximize" | "spotlight" | undefined;
172
+ } & React.RefAttributes<HTMLButtonElement>>;
181
173
 
182
- export { type ChatMessageState as ChatMessage, type LocalParticipantState as LocalParticipant, type RemoteParticipantState as RemoteParticipant, type RoomConnectionState as RoomConnection, type ScreenshareState as Screenshare, type UseLocalMediaResult, Grid as VideoGrid, VideoView, type WaitingParticipantState as WaitingParticipant, Provider as WherebyProvider, useLocalMedia, useRoomConnection };
174
+ export { type ChatMessageState as ChatMessage, GridCell, GridVideoView, type LocalParticipantState as LocalParticipant, ParticipantMenu, ParticipantMenuContent, ParticipantMenuItem, ParticipantMenuTrigger, type RemoteParticipantState as RemoteParticipant, type RoomConnectionState as RoomConnection, type ScreenshareState as Screenshare, type UseLocalMediaResult, Grid as VideoGrid, VideoView, type WaitingParticipantState as WaitingParticipant, Provider as WherebyProvider, useLocalMedia, useRoomConnection };
@@ -3,6 +3,7 @@ import { Provider as Provider$1, useDispatch, useSelector } from 'react-redux';
3
3
  import { createServices, createStore, selectCurrentSpeakerDeviceId, debounce, doRtcReportStreamResolution, selectChatMessages, selectCloudRecordingRaw, selectLocalParticipantRaw, selectLocalMediaStream, selectRemoteParticipants, selectScreenshares, selectRoomConnectionStatus, selectStreamingRaw, selectWaitingParticipants, selectNotificationsEmitter, selectSpotlightedClientViews, doAppStop, doSendChatMessage, doKnockRoom, doSetDisplayName, toggleCameraEnabled, toggleMicrophoneEnabled, toggleLowDataModeEnabled, doSetLocalStickyReaction, doRequestAudioEnable, doAcceptWaitingParticipant, doRejectWaitingParticipant, doStartCloudRecording, doStartScreenshare, doStopCloudRecording, doStopScreenshare, doAppStart, doLockRoom, doSpotlightParticipant, doRemoveSpotlight, doKickParticipant, doEndMeeting, selectCameraDeviceError, selectCameraDevices, selectCurrentCameraDeviceId, selectCurrentMicrophoneDeviceId, selectIsSettingCameraDevice, selectIsSettingMicrophoneDevice, selectIsLocalMediaStarting, selectMicrophoneDeviceError, selectMicrophoneDevices, selectSpeakerDevices, selectLocalMediaStartError, doStartLocalMedia, doStopLocalMedia, setCurrentCameraDeviceId, setCurrentMicrophoneDeviceId, setCurrentSpeakerDeviceId, selectAllClientViews, selectNumParticipants } from '@whereby.com/core';
4
4
  import { createSelector } from '@reduxjs/toolkit';
5
5
  import runes from 'runes';
6
+ import * as PopoverPrimitive from '@radix-ui/react-popover';
6
7
 
7
8
  function Provider({ children }) {
8
9
  const services = createServices();
@@ -59,8 +60,8 @@ const VideoView = React.forwardRef((_a, ref) => {
59
60
  if (!videoEl.current) {
60
61
  return null;
61
62
  }
62
- const h = videoEl.current.clientHeight;
63
- const w = videoEl.current.clientWidth;
63
+ const h = videoEl.current.videoHeight;
64
+ const w = videoEl.current.videoWidth;
64
65
  if (w && h && w + h > 20) {
65
66
  return w / h;
66
67
  }
@@ -145,7 +146,7 @@ const selectRoomConnectionState = createSelector(selectChatMessages, selectCloud
145
146
  return state;
146
147
  });
147
148
 
148
- const browserSdkVersion = "3.1.0";
149
+ const browserSdkVersion = "3.2.0";
149
150
 
150
151
  const defaultRoomConnectionOptions = {
151
152
  localMediaOptions: {
@@ -1468,13 +1469,16 @@ function makeVideoCellView({ aspectRatio, avatarSize, cellPaddings, client = und
1468
1469
  const STAGE_PARTICIPANT_LIMIT = 12;
1469
1470
  const ACTIVE_VIDEO_SUBGRID_TRIGGER = 12;
1470
1471
 
1471
- function calculateSubgridViews({ clientViews, activeVideosSubgridTrigger, shouldShowSubgrid, spotlightedParticipants, }) {
1472
+ function calculateSubgridViews({ clientViews, activeVideosSubgridTrigger, shouldShowSubgrid, spotlightedParticipants, maximizedParticipant, }) {
1472
1473
  if (!shouldShowSubgrid) {
1473
1474
  return [];
1474
1475
  }
1475
1476
  const hasSpotlights = spotlightedParticipants.length > 0;
1476
1477
  const hasPresentationStage = hasSpotlights;
1477
- const notSpotlighted = clientViews.filter((client) => !client.isPresentation && !spotlightedParticipants.includes(client));
1478
+ const notMaximized = maximizedParticipant
1479
+ ? clientViews.filter((client) => client.id !== maximizedParticipant.id)
1480
+ : clientViews;
1481
+ const notSpotlighted = notMaximized.filter((client) => !client.isPresentation && !spotlightedParticipants.includes(client));
1478
1482
  const noVideoViews = notSpotlighted.filter((client) => !client.isVideoEnabled);
1479
1483
  const videoLimitReached = notSpotlighted.filter((client) => client.isVideoEnabled).length > activeVideosSubgridTrigger;
1480
1484
  const unmutedVideos = notSpotlighted.filter((client) => !noVideoViews.includes(client) && client.isAudioEnabled);
@@ -1494,27 +1498,34 @@ function calculateSubgridViews({ clientViews, activeVideosSubgridTrigger, should
1494
1498
  }
1495
1499
  return noVideoViews;
1496
1500
  }
1497
- function useGridParticipants({ activeVideosSubgridTrigger = ACTIVE_VIDEO_SUBGRID_TRIGGER, stageParticipantLimit = STAGE_PARTICIPANT_LIMIT, forceSubgrid = true, } = {}) {
1501
+ function useGridParticipants({ activeVideosSubgridTrigger = ACTIVE_VIDEO_SUBGRID_TRIGGER, stageParticipantLimit = STAGE_PARTICIPANT_LIMIT, forceSubgrid = true, enableSubgrid = true, maximizedParticipant, } = {}) {
1498
1502
  const allClientViews = useAppSelector(selectAllClientViews);
1499
1503
  const spotlightedParticipants = useAppSelector(selectSpotlightedClientViews);
1500
1504
  const numParticipants = useAppSelector(selectNumParticipants);
1501
1505
  const shouldShowSubgrid = React.useMemo(() => {
1506
+ if (!enableSubgrid) {
1507
+ return false;
1508
+ }
1502
1509
  return forceSubgrid ? true : numParticipants > stageParticipantLimit;
1503
- }, [forceSubgrid, numParticipants, stageParticipantLimit]);
1510
+ }, [forceSubgrid, numParticipants, stageParticipantLimit, enableSubgrid]);
1504
1511
  const clientViewsInSubgrid = React.useMemo(() => {
1505
1512
  return calculateSubgridViews({
1506
1513
  clientViews: allClientViews,
1507
1514
  activeVideosSubgridTrigger,
1508
1515
  shouldShowSubgrid,
1509
1516
  spotlightedParticipants,
1517
+ maximizedParticipant,
1510
1518
  });
1511
1519
  }, [allClientViews, shouldShowSubgrid, activeVideosSubgridTrigger, spotlightedParticipants]);
1512
1520
  const clientViewsOnStage = React.useMemo(() => {
1513
1521
  return allClientViews.filter((client) => !clientViewsInSubgrid.includes(client));
1514
1522
  }, [allClientViews, clientViewsInSubgrid]);
1515
1523
  const clientViewsInPresentationGrid = React.useMemo(() => {
1524
+ if (maximizedParticipant) {
1525
+ return [maximizedParticipant];
1526
+ }
1516
1527
  return spotlightedParticipants;
1517
- }, [spotlightedParticipants]);
1528
+ }, [spotlightedParticipants, maximizedParticipant]);
1518
1529
  const clientViewsInGrid = React.useMemo(() => {
1519
1530
  return clientViewsOnStage.filter((client) => !clientViewsInPresentationGrid.includes(client));
1520
1531
  }, [clientViewsOnStage, clientViewsInPresentationGrid]);
@@ -1525,13 +1536,16 @@ function useGridParticipants({ activeVideosSubgridTrigger = ACTIVE_VIDEO_SUBGRID
1525
1536
  };
1526
1537
  }
1527
1538
 
1528
- function useGrid({ activeVideosSubgridTrigger, forceSubgrid, stageParticipantLimit = STAGE_PARTICIPANT_LIMIT, videoGridGap = 8, } = {}) {
1539
+ function useGrid({ activeVideosSubgridTrigger, forceSubgrid, stageParticipantLimit = STAGE_PARTICIPANT_LIMIT, gridGap = 8, videoGridGap = 8, enableSubgrid = true, } = {}) {
1529
1540
  const [containerBounds, setContainerBounds] = React.useState({ width: 0, height: 0 });
1530
1541
  const [clientAspectRatios, setClientAspectRatios] = React.useState({});
1542
+ const [maximizedParticipant, setMaximizedParticipant] = React.useState(null);
1531
1543
  const { clientViewsInGrid, clientViewsInPresentationGrid, clientViewsInSubgrid } = useGridParticipants({
1532
1544
  activeVideosSubgridTrigger,
1533
1545
  forceSubgrid,
1534
1546
  stageParticipantLimit,
1547
+ enableSubgrid,
1548
+ maximizedParticipant,
1535
1549
  });
1536
1550
  const cellViewsVideoGrid = React.useMemo(() => {
1537
1551
  return clientViewsInGrid.map((client) => {
@@ -1570,7 +1584,7 @@ function useGrid({ activeVideosSubgridTrigger, forceSubgrid, stageParticipantLim
1570
1584
  const videoStage = React.useMemo(() => {
1571
1585
  return calculateLayout({
1572
1586
  frame: containerFrame,
1573
- gridGap: 8,
1587
+ gridGap,
1574
1588
  isConstrained: false,
1575
1589
  roomBounds: containerFrame.bounds,
1576
1590
  videos: cellViewsVideoGrid,
@@ -1578,14 +1592,17 @@ function useGrid({ activeVideosSubgridTrigger, forceSubgrid, stageParticipantLim
1578
1592
  presentationVideos: cellViewsInPresentationGrid,
1579
1593
  subgridVideos: cellViewsInSubgrid,
1580
1594
  });
1581
- }, [containerFrame, cellViewsVideoGrid, cellViewsInPresentationGrid, cellViewsInSubgrid]);
1595
+ }, [containerFrame, cellViewsVideoGrid, cellViewsInPresentationGrid, cellViewsInSubgrid, gridGap, videoGridGap]);
1582
1596
  return {
1583
1597
  cellViewsVideoGrid,
1584
1598
  cellViewsInPresentationGrid,
1585
1599
  cellViewsInSubgrid,
1600
+ clientAspectRatios,
1586
1601
  videoStage,
1587
1602
  setContainerBounds,
1588
1603
  setClientAspectRatios,
1604
+ maximizedParticipant,
1605
+ setMaximizedParticipant,
1589
1606
  };
1590
1607
  }
1591
1608
 
@@ -1647,36 +1664,200 @@ function VideoMutedIndicator({ avatarUrl, displayName, isSmallCell, withRoundedC
1647
1664
  React.createElement(Avatar, { variant: "square", avatarUrl: avatarUrl, name: displayName, size: isSmallCell ? 60 : 80 }))));
1648
1665
  }
1649
1666
 
1650
- function renderCellView({ cellView, onSetClientAspectRatio }) {
1651
- switch (cellView.type) {
1652
- case "video":
1653
- return (React.createElement(GridVideoCellView, { aspectRatio: cellView.aspectRatio, participant: cellView.client, isPlaceholder: cellView.isPlaceholder, isSubgrid: cellView.isSubgrid, key: cellView.clientId, onSetClientAspectRatio: onSetClientAspectRatio }));
1667
+ const Popover = PopoverPrimitive.Root;
1668
+ const PopoverTrigger = PopoverPrimitive.Trigger;
1669
+ const PopoverContent = React.forwardRef((_a, ref) => {
1670
+ var { style, align = "center", sideOffset = 4 } = _a, props = __rest(_a, ["style", "align", "sideOffset"]);
1671
+ return (React.createElement(PopoverPrimitive.Portal, null,
1672
+ React.createElement(PopoverPrimitive.Content, Object.assign({ ref: ref, align: align, sideOffset: sideOffset, style: Object.assign({ width: "200px", backgroundColor: "#fff", border: "1px solid #e5e5e5", borderRadius: "0.375rem", boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)", color: "#333", fontSize: "14px", lineHeight: "1.5", padding: "16px", zIndex: 50, outline: "none" }, style) }, props))));
1673
+ });
1674
+ PopoverContent.displayName = PopoverPrimitive.Content.displayName;
1675
+
1676
+ const GridContext = React.createContext({});
1677
+ const GridCellContext = React.createContext({});
1678
+ const useGridCell = () => {
1679
+ const gridContext = React.useContext(GridContext);
1680
+ const gridCellContext = React.useContext(GridCellContext);
1681
+ if (!gridCellContext) {
1682
+ throw new Error("useGridCell must be used within a GridCell");
1683
+ }
1684
+ return Object.assign(Object.assign({}, gridContext), gridCellContext);
1685
+ };
1686
+
1687
+ const ParticipantMenuContext = React.createContext({});
1688
+ const useParticipantMenu = () => {
1689
+ const context = React.useContext(ParticipantMenuContext);
1690
+ const gridCellContext = useGridCell();
1691
+ if (!context) {
1692
+ throw new Error("useParticipantMenu must be used within a ParticipantMenu");
1693
+ }
1694
+ return Object.assign(Object.assign({}, context), gridCellContext);
1695
+ };
1696
+ const ParticipantMenu = (props) => {
1697
+ const { children } = props, rest = __rest(props, ["children"]);
1698
+ const [open, setOpen] = React.useState(false);
1699
+ return (React.createElement(ParticipantMenuContext.Provider, { value: { open, setOpen } },
1700
+ React.createElement(Popover, Object.assign({}, rest, { open: open, onOpenChange: setOpen }), children)));
1701
+ };
1702
+ const ParticipantMenuContent = React.forwardRef((_a, ref) => {
1703
+ var { children, style } = _a, props = __rest(_a, ["children", "style"]);
1704
+ return (React.createElement(PopoverContent, Object.assign({ ref: ref, style: Object.assign({ display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", minWidth: "180px", maxWidth: "300px", maxHeight: "100vh", overflowY: "auto", padding: 0 }, style) }, props), children));
1705
+ });
1706
+ ParticipantMenuContent.displayName = "ParticipantMenuContent";
1707
+ const ParticipantMenuTrigger = React.forwardRef((_a, ref) => {
1708
+ var { children, style } = _a, props = __rest(_a, ["children", "style"]);
1709
+ return (React.createElement(PopoverTrigger, Object.assign({ ref: ref, style: Object.assign({ position: "absolute", top: "10px", right: "20px", textDecoration: "none", whiteSpace: "nowrap", border: "none", cursor: "pointer" }, style) }, props), children));
1710
+ });
1711
+ ParticipantMenuTrigger.displayName = PopoverTrigger.displayName;
1712
+ const ParticipantMenuItem = React.forwardRef((_a, ref) => {
1713
+ var { children, style, participantAction } = _a, props = __rest(_a, ["children", "style", "participantAction"]);
1714
+ const { participant, setOpen, maximizedParticipant, setMaximizedParticipant } = useParticipantMenu();
1715
+ const dispatch = useAppDispatch();
1716
+ const spotlightedParticipants = useAppSelector(selectSpotlightedClientViews);
1717
+ const isSpotlighted = spotlightedParticipants.find((p) => p.id === participant.id);
1718
+ const isMaximized = (maximizedParticipant === null || maximizedParticipant === void 0 ? void 0 : maximizedParticipant.id) === participant.id;
1719
+ let onClick;
1720
+ switch (participantAction) {
1721
+ case "maximize":
1722
+ onClick = () => {
1723
+ if (isMaximized) {
1724
+ setMaximizedParticipant(null);
1725
+ }
1726
+ else {
1727
+ setMaximizedParticipant(participant);
1728
+ }
1729
+ setOpen(false);
1730
+ };
1731
+ break;
1732
+ case "spotlight":
1733
+ onClick = () => {
1734
+ if (isSpotlighted) {
1735
+ dispatch(doRemoveSpotlight({ id: participant.id }));
1736
+ }
1737
+ else {
1738
+ dispatch(doSpotlightParticipant({ id: participant.id }));
1739
+ }
1740
+ setOpen(false);
1741
+ };
1742
+ break;
1743
+ }
1744
+ return (React.createElement("button", Object.assign({ ref: ref, role: "menuitem", tabIndex: -1, onClick: onClick !== null && onClick !== void 0 ? onClick : props.onClick, style: Object.assign({ alignItems: "stretch", backgroundColor: "transparent", border: "none", cursor: "pointer", display: "flex", height: "40px", lineHeight: "40px", minWidth: "140px", padding: "0 12px", textAlign: "left", textDecoration: "none", whiteSpace: "nowrap", width: "100%" }, style) }, props), children));
1745
+ });
1746
+ ParticipantMenuItem.displayName = "ParticipantMenuItem";
1747
+
1748
+ function EllipsisIcon(props) {
1749
+ return (React.createElement("svg", Object.assign({ width: "100%", height: "100%", viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg" }, props),
1750
+ React.createElement("g", null,
1751
+ React.createElement("circle", { cx: "6", cy: "12", r: "2" }),
1752
+ React.createElement("circle", { cx: "12", cy: "12", r: "2" }),
1753
+ React.createElement("circle", { cx: "18", cy: "12", r: "2" }))));
1754
+ }
1755
+
1756
+ function MaximizeOnIcon(props) {
1757
+ return (React.createElement("svg", Object.assign({ width: "100%", height: "100%", viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg" }, props),
1758
+ React.createElement("path", { fillRule: "evenodd", clipRule: "evenodd", d: "m13.875 3c0 .62132.5037 1.125 1.125 1.125h3.284l-4.0795 4.0795c-.4393.43934-.4393 1.15166 0 1.591.4393.4393 1.1517.4393 1.591 0l4.0795-4.07951v3.28401c0 .62132.5037 1.125 1.125 1.125s1.125-.50368 1.125-1.125v-6c0-.15254-.0304-.29799-.0854-.43063-.0536-.12968-.1326-.25135-.2368-.35749l-.0147-.01469c-.203-.19928-.4812-.32219-.7881-.32219h-6c-.6213 0-1.125.50368-1.125 1.125zm-9.75 12c0-.6213-.50368-1.125-1.125-1.125s-1.125.5037-1.125 1.125v6c0 .3069.12291.5851.32219.7881.00486.005.00975.0099.01469.0147.10614.1042.22781.1832.35749.2368.13264.055.27809.0854.43063.0854h6c.62132 0 1.125-.5037 1.125-1.125s-.50368-1.125-1.125-1.125h-3.28401l4.07951-4.0795c.4393-.4393.4393-1.1517 0-1.591-.43934-.4393-1.15166-.4393-1.591 0l-4.0795 4.0795z" })));
1759
+ }
1760
+
1761
+ function SpotlightIcon(props) {
1762
+ return (React.createElement("svg", Object.assign({ width: "100%", height: "100%", viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg" }, props),
1763
+ React.createElement("path", { fillRule: "evenodd", clipRule: "evenodd", d: "m15.241.146691c-.1011-.1955882-.3809-.1955879-.482 0l-1.1063 2.139639c-.0258.04991-.0665.09058-.1164.11639l-2.1396 1.10629c-.1956.10113-.1956.38085 0 .48198l2.1396 1.10629c.0499.02581.0906.06648.1164.11639l1.1063 2.13964c.1011.19559.3809.19559.482 0l1.1063-2.13964c.0258-.04991.0665-.09058.1164-.11639l2.1396-1.10629c.1956-.10113.1956-.38085 0-.48198l-2.1396-1.10629c-.0499-.02581-.0906-.06648-.1164-.11639zm-13.741 2.900259v14.67495c0 .0511.00116.1018.00345.1523-.00229.0418-.00345.0837-.00345.1258 0 .5674.21005 1.1103.59339 1.6103.24265.3472.55042.6455.90566.8773 1.61225 1.213 4.36988 2.0124 7.50095 2.0124 4.3315 0 7.9484-1.53 8.8068-3.5681.0651-.1472.1148-.3024.1455-.4658.0315-.1532.0477-.3087.0477-.4661 0-.6032-.2374-1.1788-.668-1.7045-.1377-.1975-.2995-.4017-.4877-.6127l-14.22219-13.74809c-.28845-.27884-.67396-.43471-1.07516-.43471-.85436 0-1.54695.69259-1.54695 1.54695zm19.2646 4.59631c.0988-.19101.372-.19101.4708 0l.8546 1.65303c.0253.04874.065.08846.1137.11366l1.653.85465c.1911.0988.1911.372 0 .4708l-1.653.8546c-.0487.0253-.0884.065-.1137.1137l-.8546 1.653c-.0988.1911-.372.1911-.4708 0l-.8546-1.653c-.0253-.0487-.065-.0884-.1137-.1137l-1.653-.8546c-.1911-.0988-.1911-.372 0-.4708l1.653-.85465c.0487-.0252.0884-.06492.1137-.11366zm-3.2651 10.35684c0 .0769-.1144.6851-1.5305 1.3931-1.2964.6482-3.2274 1.1069-5.4695 1.1069s-4.17308-.4587-5.46952-1.1069c-1.41607-.708-1.53047-1.3162-1.53047-1.3931 0-.077.1144-.6851 1.53047-1.3932 1.29644-.6482 3.22742-1.1068 5.46952-1.1068s4.1731.4586 5.4695 1.1068c1.4161.7081 1.5305 1.3162 1.5305 1.3932z" })));
1764
+ }
1765
+
1766
+ function DefaultParticipantMenu({ participant }) {
1767
+ const spotlightedParticipants = useAppSelector(selectSpotlightedClientViews);
1768
+ const isSpotlighted = spotlightedParticipants.find((p) => p.id === participant.id);
1769
+ const { isHovered, maximizedParticipant } = useGridCell();
1770
+ const isMaximized = (maximizedParticipant === null || maximizedParticipant === void 0 ? void 0 : maximizedParticipant.id) === participant.id;
1771
+ if (!isHovered) {
1772
+ return null;
1654
1773
  }
1774
+ return (React.createElement(ParticipantMenu, null,
1775
+ React.createElement(ParticipantMenuTrigger, { style: {
1776
+ display: "flex",
1777
+ justifyContent: "center",
1778
+ alignItems: "center",
1779
+ backgroundColor: "#fff",
1780
+ borderRadius: "6px",
1781
+ padding: "4px",
1782
+ } },
1783
+ React.createElement(EllipsisIcon, { height: 20, width: 20, transform: "rotate(90)" })),
1784
+ React.createElement(ParticipantMenuContent, null,
1785
+ React.createElement(ParticipantMenuItem, { participantAction: "maximize", style: {
1786
+ display: "flex",
1787
+ alignItems: "center",
1788
+ gap: "10px",
1789
+ } },
1790
+ React.createElement(MaximizeOnIcon, { height: 16, width: 16 }),
1791
+ isMaximized ? "Minimize" : "Maximize"),
1792
+ React.createElement(ParticipantMenuItem, { participantAction: "spotlight", style: {
1793
+ display: "flex",
1794
+ alignItems: "center",
1795
+ gap: "10px",
1796
+ } },
1797
+ React.createElement(SpotlightIcon, { height: 16, width: 16 }),
1798
+ isSpotlighted ? "Remove spotlight" : "Spotlight"))));
1655
1799
  }
1656
- function GridVideoCellView({ aspectRatio, participant, render }) {
1800
+
1801
+ const GridCell = React.forwardRef(({ className, participant, children }, ref) => {
1802
+ const [isHovered, setIsHovered] = React.useState(false);
1803
+ const handleMouseEnter = React.useCallback(() => {
1804
+ setIsHovered(true);
1805
+ }, []);
1806
+ const handleMouseLeave = React.useCallback(() => {
1807
+ setIsHovered(false);
1808
+ }, []);
1809
+ return (React.createElement(GridCellContext.Provider, { value: { participant, isHovered } },
1810
+ React.createElement("div", { ref: ref, className: className, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave }, children)));
1811
+ });
1812
+ GridCell.displayName = "GridCell";
1813
+ const GridVideoView = React.forwardRef((_a, ref) => {
1814
+ var { stream, style } = _a, rest = __rest(_a, ["stream", "style"]);
1657
1815
  const videoEl = React.useRef(null);
1816
+ const { onSetClientAspectRatio, clientAspectRatios, participant } = useGridCell();
1817
+ if (!participant)
1818
+ return null;
1819
+ const aspectRatio = clientAspectRatios[participant.id];
1820
+ React.useImperativeHandle(ref, () => {
1821
+ return videoEl.current;
1822
+ });
1658
1823
  const handleResize = React.useCallback(() => {
1659
1824
  const ar = videoEl.current && videoEl.current.captureAspectRatio();
1660
- if (ar && ar !== aspectRatio && (participant === null || participant === void 0 ? void 0 : participant.id)) ;
1661
- }, []);
1662
- return (React.createElement("div", null, render ? (render()) : (participant === null || participant === void 0 ? void 0 : participant.stream) && participant.isVideoEnabled ? (React.createElement(VideoView, { ref: videoEl, stream: participant.stream, onVideoResize: handleResize, style: {
1663
- borderRadius: "8px",
1664
- } })) : (React.createElement(VideoMutedIndicator, { isSmallCell: false, displayName: (participant === null || participant === void 0 ? void 0 : participant.displayName) || "Guest", withRoundedCorners: true }))));
1825
+ if (ar && ar !== aspectRatio && participant.id) {
1826
+ onSetClientAspectRatio({ aspectRatio: ar, clientId: participant.id });
1827
+ }
1828
+ }, [clientAspectRatios, participant.id, onSetClientAspectRatio]);
1829
+ const s = stream || participant.stream;
1830
+ if (!s) {
1831
+ return null;
1832
+ }
1833
+ return (React.createElement(VideoView, Object.assign({ ref: videoEl, style: Object.assign({ borderRadius: "8px" }, style) }, rest, { stream: s, onVideoResize: handleResize })));
1834
+ });
1835
+ GridVideoView.displayName = "GridVideoView";
1836
+ function renderCellView({ cellView, enableParticipantMenu, render }) {
1837
+ const participant = cellView.client;
1838
+ if (!participant) {
1839
+ return undefined;
1840
+ }
1841
+ switch (cellView.type) {
1842
+ case "video":
1843
+ return (React.createElement(GridCell, { participant: participant }, participant.isVideoEnabled ? (React.createElement(React.Fragment, null, render ? (render({ participant })) : (React.createElement(React.Fragment, null,
1844
+ React.createElement(GridVideoView, null),
1845
+ enableParticipantMenu ? (React.createElement(DefaultParticipantMenu, { participant: participant })) : null)))) : (React.createElement(VideoMutedIndicator, { isSmallCell: false, displayName: (participant === null || participant === void 0 ? void 0 : participant.displayName) || "Guest", withRoundedCorners: true }))));
1846
+ }
1665
1847
  }
1666
- function Grid({ renderParticipant, stageParticipantLimit, videoGridGap }) {
1848
+ function Grid({ renderParticipant, stageParticipantLimit, gridGap, videoGridGap, enableSubgrid, enableParticipantMenu, }) {
1667
1849
  const gridRef = React.useRef(null);
1668
- const { cellViewsVideoGrid, cellViewsInPresentationGrid, cellViewsInSubgrid, videoStage, setContainerBounds, setClientAspectRatios, } = useGrid({ activeVideosSubgridTrigger: 12, stageParticipantLimit, videoGridGap });
1669
- const presentationGridContent = React.useMemo(() => cellViewsInPresentationGrid.map((cellView) => renderCellView({
1670
- cellView,
1671
- onSetClientAspectRatio: ({ aspectRatio, clientId }) => setClientAspectRatios((prev) => (Object.assign(Object.assign({}, prev), { [clientId]: aspectRatio }))),
1672
- })), [cellViewsInPresentationGrid]);
1673
- const gridContent = React.useMemo(() => cellViewsVideoGrid.map((cellView) => renderCellView({
1674
- cellView,
1675
- onSetClientAspectRatio: ({ aspectRatio, clientId }) => setClientAspectRatios((prev) => (Object.assign(Object.assign({}, prev), { [clientId]: aspectRatio }))),
1676
- })), [cellViewsVideoGrid]);
1850
+ const { cellViewsVideoGrid, cellViewsInPresentationGrid, cellViewsInSubgrid, clientAspectRatios, videoStage, setContainerBounds, setClientAspectRatios, maximizedParticipant, setMaximizedParticipant, } = useGrid({ activeVideosSubgridTrigger: 12, stageParticipantLimit, gridGap, videoGridGap, enableSubgrid });
1851
+ const handleSetClientAspectRatio = React.useCallback(({ aspectRatio, clientId }) => {
1852
+ setClientAspectRatios((prev) => (Object.assign(Object.assign({}, prev), { [clientId]: aspectRatio })));
1853
+ }, [setClientAspectRatios]);
1854
+ const presentationGridContent = React.useMemo(() => cellViewsInPresentationGrid.map((cellView) => renderCellView(Object.assign({ cellView,
1855
+ enableParticipantMenu }, (renderParticipant ? { render: ({ participant }) => renderParticipant({ participant }) } : {})))), [cellViewsInPresentationGrid]);
1856
+ const gridContent = React.useMemo(() => cellViewsVideoGrid.map((cellView) => renderCellView(Object.assign({ cellView,
1857
+ enableParticipantMenu }, (renderParticipant ? { render: ({ participant }) => renderParticipant({ participant }) } : {})))), [cellViewsVideoGrid]);
1677
1858
  const subgridContent = React.useMemo(() => cellViewsInSubgrid.map((cellView) => renderCellView({
1678
1859
  cellView,
1679
- onSetClientAspectRatio: ({ aspectRatio, clientId }) => setClientAspectRatios((prev) => (Object.assign(Object.assign({}, prev), { [clientId]: aspectRatio }))),
1860
+ enableParticipantMenu,
1680
1861
  })), [cellViewsInSubgrid]);
1681
1862
  React.useEffect(() => {
1682
1863
  if (!gridRef.current) {
@@ -1694,12 +1875,21 @@ function Grid({ renderParticipant, stageParticipantLimit, videoGridGap }) {
1694
1875
  resizeObserver.disconnect();
1695
1876
  };
1696
1877
  }, []);
1697
- return (React.createElement("div", { ref: gridRef, style: {
1698
- width: "100%",
1699
- height: "100%",
1700
- position: "relative",
1878
+ return (React.createElement(GridContext.Provider, { value: {
1879
+ onSetClientAspectRatio: handleSetClientAspectRatio,
1880
+ cellViewsVideoGrid,
1881
+ cellViewsInPresentationGrid,
1882
+ cellViewsInSubgrid,
1883
+ clientAspectRatios,
1884
+ maximizedParticipant,
1885
+ setMaximizedParticipant,
1701
1886
  } },
1702
- React.createElement(VideoStageLayout, { layoutVideoStage: videoStage, presentationGridContent: presentationGridContent, gridContent: gridContent, subgridContent: subgridContent })));
1887
+ React.createElement("div", { ref: gridRef, style: {
1888
+ width: "100%",
1889
+ height: "100%",
1890
+ position: "relative",
1891
+ } },
1892
+ React.createElement(VideoStageLayout, { layoutVideoStage: videoStage, presentationGridContent: presentationGridContent, gridContent: gridContent, subgridContent: subgridContent }))));
1703
1893
  }
1704
1894
 
1705
- export { Grid as VideoGrid, VideoView, Provider as WherebyProvider, useLocalMedia, useRoomConnection };
1895
+ export { GridCell, GridVideoView, ParticipantMenu, ParticipantMenuContent, ParticipantMenuItem, ParticipantMenuTrigger, Grid as VideoGrid, VideoView, Provider as WherebyProvider, useLocalMedia, useRoomConnection };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whereby.com/browser-sdk",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Modules for integration Whereby video in web apps",
5
5
  "author": "Whereby AS",
6
6
  "license": "MIT",
@@ -72,8 +72,10 @@
72
72
  "yalc": "^1.0.0-pre.53"
73
73
  },
74
74
  "dependencies": {
75
+ "@radix-ui/react-popover": "^1.0.7",
75
76
  "@reduxjs/toolkit": "^2.2.3",
76
- "@whereby.com/core": "0.16.3",
77
+ "@whereby.com/core": "0.17.0",
78
+ "clsx": "^2.1.1",
77
79
  "heresy": "^1.0.4",
78
80
  "react-redux": "^9.1.1",
79
81
  "runes": "^0.4.3"