@livo-tv/sdk 1.1.1 → 1.1.2

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.
@@ -11,6 +11,8 @@ var tailwindMerge = require('tailwind-merge');
11
11
  var button = require('@base-ui/react/button');
12
12
  var classVarianceAuthority = require('class-variance-authority');
13
13
  var jsxRuntime = require('react/jsx-runtime');
14
+ var mergeProps = require('@base-ui/react/merge-props');
15
+ var useRender = require('@base-ui/react/use-render');
14
16
  var menu = require('@base-ui/react/menu');
15
17
  var tooltip = require('@base-ui/react/tooltip');
16
18
  var DialogPrimitive = require('@radix-ui/react-dialog');
@@ -776,6 +778,58 @@ function postLivoEvent(type, payload) {
776
778
  window.parent.postMessage({ source: "livo", type, payload }, "*");
777
779
  }
778
780
 
781
+ // src/studio/lib/embed-urls.ts
782
+ function playerWatchUrl(streamId, origin = "https://player.livo.tv") {
783
+ return `${origin.replace(/\/$/, "")}/${streamId}`;
784
+ }
785
+
786
+ // src/studio/lib/clipboard.ts
787
+ async function copyText(value, clipboard = globalThis.navigator?.clipboard ?? null) {
788
+ if (!clipboard) return false;
789
+ try {
790
+ await clipboard.writeText(value);
791
+ return true;
792
+ } catch {
793
+ return false;
794
+ }
795
+ }
796
+
797
+ // src/studio/lib/share.ts
798
+ function isAbort(error) {
799
+ return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
800
+ }
801
+ function resolveShareApi(share = typeof navigator === "undefined" ? null : typeof navigator.share === "function" ? navigator : null) {
802
+ return share ?? null;
803
+ }
804
+ function canNativeShare(data, share = resolveShareApi()) {
805
+ if (!share) return false;
806
+ if (typeof share.canShare !== "function") return true;
807
+ try {
808
+ return share.canShare(data);
809
+ } catch {
810
+ return false;
811
+ }
812
+ }
813
+ async function shareOrCopyLink(options, deps) {
814
+ const data = {
815
+ url: options.url,
816
+ ...options.title ? { title: options.title } : {},
817
+ ...options.text ? { text: options.text } : {}
818
+ };
819
+ const share = resolveShareApi(deps?.share);
820
+ if (canNativeShare(data, share) && share) {
821
+ try {
822
+ await share.share(data);
823
+ return "shared";
824
+ } catch (error) {
825
+ if (isAbort(error)) return "cancelled";
826
+ }
827
+ }
828
+ const copy = copyText;
829
+ const ok = await copy(options.url);
830
+ return ok ? "copied" : "failed";
831
+ }
832
+
779
833
  // src/studio/lib/status.ts
780
834
  function canPublish(status) {
781
835
  return status === "preview";
@@ -1449,9 +1503,124 @@ function StudioRemoteAudio({
1449
1503
  }, [meeting, participantId]);
1450
1504
  return /* @__PURE__ */ jsxRuntime.jsx("audio", { ref, autoPlay: true, className: "sr-only" });
1451
1505
  }
1506
+ function BubbleGroup({ className, ...props }) {
1507
+ return /* @__PURE__ */ jsxRuntime.jsx(
1508
+ "div",
1509
+ {
1510
+ "data-slot": "bubble-group",
1511
+ className: cn("flex min-w-0 flex-col gap-2", className),
1512
+ ...props
1513
+ }
1514
+ );
1515
+ }
1516
+ var bubbleVariants = classVarianceAuthority.cva(
1517
+ "group/bubble relative flex w-fit max-w-[80%] min-w-0 flex-col gap-1 group-data-[align=end]/message:self-end data-[align=end]:self-end data-[variant=ghost]:max-w-full",
1518
+ {
1519
+ variants: {
1520
+ variant: {
1521
+ default: "*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80",
1522
+ secondary: "*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]",
1523
+ muted: "*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]",
1524
+ tinted: "*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]",
1525
+ outline: "*:data-[slot=bubble-content]:border-border *:data-[slot=bubble-content]:bg-background [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30",
1526
+ ghost: "border-none *:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50",
1527
+ destructive: "*:data-[slot=bubble-content]:bg-destructive/10 *:data-[slot=bubble-content]:text-destructive dark:*:data-[slot=bubble-content]:bg-destructive/20 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20 dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/30"
1528
+ }
1529
+ },
1530
+ defaultVariants: {
1531
+ variant: "default"
1532
+ }
1533
+ }
1534
+ );
1535
+ function Bubble({
1536
+ variant = "default",
1537
+ align = "start",
1538
+ className,
1539
+ ...props
1540
+ }) {
1541
+ return /* @__PURE__ */ jsxRuntime.jsx(
1542
+ "div",
1543
+ {
1544
+ "data-slot": "bubble",
1545
+ "data-variant": variant,
1546
+ "data-align": align,
1547
+ className: cn(bubbleVariants({ variant }), className),
1548
+ ...props
1549
+ }
1550
+ );
1551
+ }
1552
+ function BubbleContent({
1553
+ className,
1554
+ render,
1555
+ ...props
1556
+ }) {
1557
+ return useRender.useRender({
1558
+ defaultTagName: "div",
1559
+ props: mergeProps.mergeProps(
1560
+ {
1561
+ className: cn(
1562
+ "w-fit max-w-full min-w-0 overflow-hidden rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed wrap-break-word group-data-[align=end]/bubble:self-end [button]:text-left [button,a]:transition-colors [button,a]:outline-none [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50",
1563
+ className
1564
+ )
1565
+ },
1566
+ props
1567
+ ),
1568
+ render,
1569
+ state: {
1570
+ slot: "bubble-content"
1571
+ }
1572
+ });
1573
+ }
1574
+ classVarianceAuthority.cva(
1575
+ "absolute z-10 flex w-fit shrink-0 items-center justify-center gap-1 rounded-full bg-muted px-1.5 py-0.5 text-sm ring-3 ring-card has-[button]:p-0",
1576
+ {
1577
+ variants: {
1578
+ side: {
1579
+ top: "top-0 -translate-y-3/4",
1580
+ bottom: "bottom-0 translate-y-3/4"
1581
+ },
1582
+ align: {
1583
+ start: "left-3",
1584
+ end: "right-3"
1585
+ }
1586
+ },
1587
+ defaultVariants: {
1588
+ side: "bottom",
1589
+ align: "end"
1590
+ }
1591
+ }
1592
+ );
1593
+
1594
+ // src/studio/lib/chat-groups.ts
1595
+ function groupChatMessages(messages, selfId) {
1596
+ const groups = [];
1597
+ for (const row of messages) {
1598
+ const mine = Boolean(selfId) && row.userId === selfId;
1599
+ const last = groups[groups.length - 1];
1600
+ if (last && last.userId === row.userId && last.mine === mine) {
1601
+ last.rows.push(row);
1602
+ continue;
1603
+ }
1604
+ groups.push({
1605
+ userId: row.userId,
1606
+ name: row.name,
1607
+ mine,
1608
+ rows: [row]
1609
+ });
1610
+ }
1611
+ return groups;
1612
+ }
1613
+ function senderInitials(name) {
1614
+ const parts = name.trim().split(/\s+/).filter((part) => part.length > 0);
1615
+ if (parts.length === 0) return "?";
1616
+ const first = parts[0];
1617
+ if (parts.length === 1) return first.slice(0, 2).toUpperCase();
1618
+ return `${first[0]}${parts[1][0]}`.toUpperCase();
1619
+ }
1452
1620
  function StudioChatPanel({
1453
1621
  meeting,
1454
1622
  selfId,
1623
+ participants,
1455
1624
  isModerator: isModerator2,
1456
1625
  labels
1457
1626
  }) {
@@ -1469,6 +1638,17 @@ function StudioChatPanel({
1469
1638
  );
1470
1639
  const pinned = react.useMemo(() => meeting.pinnedChatMessages(), [meeting, tick]);
1471
1640
  const limit = meeting.chatMaxTextLimit();
1641
+ const pictures = react.useMemo(() => {
1642
+ const map = /* @__PURE__ */ new Map();
1643
+ for (const row of participants) {
1644
+ if (row.userId) map.set(row.userId, row.picture);
1645
+ }
1646
+ return map;
1647
+ }, [participants]);
1648
+ const groups = react.useMemo(
1649
+ () => groupChatMessages(messages, selfId),
1650
+ [messages, selfId]
1651
+ );
1472
1652
  react.useEffect(() => {
1473
1653
  const list = listRef.current;
1474
1654
  if (!list) return;
@@ -1492,6 +1672,44 @@ function StudioChatPanel({
1492
1672
  );
1493
1673
  }
1494
1674
  }
1675
+ function messageActions(row, mine) {
1676
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1677
+ "p",
1678
+ {
1679
+ className: mine ? "text-muted-foreground flex flex-wrap justify-end gap-2 text-xs" : "text-muted-foreground flex flex-wrap gap-2 text-xs",
1680
+ children: [
1681
+ row.isEdited ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: labels.chatEdited }) : null,
1682
+ mine ? /* @__PURE__ */ jsxRuntime.jsx(
1683
+ "button",
1684
+ {
1685
+ type: "button",
1686
+ onClick: () => {
1687
+ setEditingId(row.id);
1688
+ setDraft(row.text);
1689
+ },
1690
+ children: labels.chatEdit
1691
+ }
1692
+ ) : null,
1693
+ isModerator2 || mine ? /* @__PURE__ */ jsxRuntime.jsx(
1694
+ "button",
1695
+ {
1696
+ type: "button",
1697
+ onClick: () => void meeting.deleteChatMessage(row.id).catch(() => sonner.toast.error(labels.chatError)),
1698
+ children: labels.chatDelete
1699
+ }
1700
+ ) : null,
1701
+ isModerator2 ? /* @__PURE__ */ jsxRuntime.jsx(
1702
+ "button",
1703
+ {
1704
+ type: "button",
1705
+ onClick: () => void meeting.pinChatMessage(row.id, row.pinned !== true).catch(() => sonner.toast.error(labels.chatError)),
1706
+ children: row.pinned ? labels.chatUnpin : labels.chatPin
1707
+ }
1708
+ ) : null
1709
+ ]
1710
+ }
1711
+ );
1712
+ }
1495
1713
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [
1496
1714
  pinned[0] ? /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "status", className: "bg-muted border-b px-3 py-2 text-sm", children: [
1497
1715
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-xs", children: labels.chatPinned }),
@@ -1503,7 +1721,7 @@ function StudioChatPanel({
1503
1721
  ref: listRef,
1504
1722
  role: "log",
1505
1723
  "aria-live": "polite",
1506
- className: "flex min-h-0 flex-1 flex-col gap-2 overflow-auto p-3",
1724
+ className: "flex min-h-0 flex-1 flex-col gap-3 overflow-auto p-3",
1507
1725
  children: [
1508
1726
  /* @__PURE__ */ jsxRuntime.jsx(
1509
1727
  Button,
@@ -1521,48 +1739,42 @@ function StudioChatPanel({
1521
1739
  children: labels.chatLoadOlder
1522
1740
  }
1523
1741
  ),
1524
- messages.map((row) => {
1525
- const mine = Boolean(selfId) && row.userId === selfId;
1742
+ groups.map((group) => {
1743
+ const bubbles = group.rows.map((row) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [
1744
+ /* @__PURE__ */ jsxRuntime.jsx(
1745
+ Bubble,
1746
+ {
1747
+ align: group.mine ? "end" : "start",
1748
+ variant: group.mine ? "default" : "secondary",
1749
+ children: /* @__PURE__ */ jsxRuntime.jsx(BubbleContent, { children: row.text })
1750
+ }
1751
+ ),
1752
+ messageActions(row, group.mine)
1753
+ ] }, row.id));
1754
+ if (group.mine) {
1755
+ return /* @__PURE__ */ jsxRuntime.jsx(BubbleGroup, { className: "items-end", children: bubbles }, group.rows[0].id);
1756
+ }
1757
+ const picture = pictures.get(group.userId) ?? null;
1526
1758
  return /* @__PURE__ */ jsxRuntime.jsxs(
1527
1759
  "div",
1528
1760
  {
1529
- className: mine ? "self-end text-right" : "self-start",
1761
+ className: "flex max-w-full gap-2 self-start",
1530
1762
  children: [
1531
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-xs", children: row.name }),
1532
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "bg-muted max-w-64 rounded-lg px-2 py-1 text-sm", children: row.text }),
1533
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-muted-foreground flex flex-wrap justify-end gap-2 text-xs", children: [
1534
- row.isEdited ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: labels.chatEdited }) : null,
1535
- mine ? /* @__PURE__ */ jsxRuntime.jsx(
1536
- "button",
1537
- {
1538
- type: "button",
1539
- onClick: () => {
1540
- setEditingId(row.id);
1541
- setDraft(row.text);
1542
- },
1543
- children: labels.chatEdit
1544
- }
1545
- ) : null,
1546
- isModerator2 || mine ? /* @__PURE__ */ jsxRuntime.jsx(
1547
- "button",
1548
- {
1549
- type: "button",
1550
- onClick: () => void meeting.deleteChatMessage(row.id).catch(() => sonner.toast.error(labels.chatError)),
1551
- children: labels.chatDelete
1552
- }
1553
- ) : null,
1554
- isModerator2 ? /* @__PURE__ */ jsxRuntime.jsx(
1555
- "button",
1556
- {
1557
- type: "button",
1558
- onClick: () => void meeting.pinChatMessage(row.id, row.pinned !== true).catch(() => sonner.toast.error(labels.chatError)),
1559
- children: row.pinned ? labels.chatUnpin : labels.chatPin
1560
- }
1561
- ) : null
1763
+ picture ? /* @__PURE__ */ jsxRuntime.jsx(
1764
+ "img",
1765
+ {
1766
+ src: picture,
1767
+ alt: "",
1768
+ className: "mt-0.5 size-7 shrink-0 rounded-full object-cover"
1769
+ }
1770
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "bg-muted text-muted-foreground mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full text-xs", children: senderInitials(group.name) }),
1771
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
1772
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground mb-1 text-xs", children: group.name }),
1773
+ /* @__PURE__ */ jsxRuntime.jsx(BubbleGroup, { children: bubbles })
1562
1774
  ] })
1563
1775
  ]
1564
1776
  },
1565
- row.id
1777
+ group.rows[0].id
1566
1778
  );
1567
1779
  })
1568
1780
  ]
@@ -1692,7 +1904,9 @@ function StudioSidePanel({
1692
1904
  onDeny,
1693
1905
  onGrantStage,
1694
1906
  onDenyStage,
1695
- onTakeOffAir
1907
+ onTakeOffAir,
1908
+ guestUrl,
1909
+ onShareGuestLink
1696
1910
  }) {
1697
1911
  const panelists = participants.filter((row) => isOnStage(row.stageStatus));
1698
1912
  const audience = participants.filter((row) => !isOnStage(row.stageStatus));
@@ -1749,6 +1963,19 @@ function StudioSidePanel({
1749
1963
  labels
1750
1964
  }
1751
1965
  ) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-4 overflow-auto p-3", children: [
1966
+ guestUrl ? /* @__PURE__ */ jsxRuntime.jsxs(
1967
+ Button,
1968
+ {
1969
+ type: "button",
1970
+ variant: "outline",
1971
+ className: "w-full",
1972
+ onClick: onShareGuestLink,
1973
+ children: [
1974
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, {}),
1975
+ labels.shareLink
1976
+ ]
1977
+ }
1978
+ ) : null,
1752
1979
  /* @__PURE__ */ jsxRuntime.jsx(
1753
1980
  StudioWaitingRoom,
1754
1981
  {
@@ -2300,58 +2527,6 @@ function TooltipContent({
2300
2527
  }
2301
2528
  ) });
2302
2529
  }
2303
-
2304
- // src/studio/lib/clipboard.ts
2305
- async function copyText(value, clipboard = globalThis.navigator?.clipboard ?? null) {
2306
- if (!clipboard) return false;
2307
- try {
2308
- await clipboard.writeText(value);
2309
- return true;
2310
- } catch {
2311
- return false;
2312
- }
2313
- }
2314
-
2315
- // src/studio/lib/share.ts
2316
- function isAbort(error) {
2317
- return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
2318
- }
2319
- function resolveShareApi(share = typeof navigator === "undefined" ? null : typeof navigator.share === "function" ? navigator : null) {
2320
- return share ?? null;
2321
- }
2322
- function canNativeShare(data, share = resolveShareApi()) {
2323
- if (!share) return false;
2324
- if (typeof share.canShare !== "function") return true;
2325
- try {
2326
- return share.canShare(data);
2327
- } catch {
2328
- return false;
2329
- }
2330
- }
2331
- async function shareOrCopyLink(options, deps) {
2332
- const data = {
2333
- url: options.url,
2334
- ...options.title ? { title: options.title } : {},
2335
- ...options.text ? { text: options.text } : {}
2336
- };
2337
- const share = resolveShareApi(deps?.share);
2338
- if (canNativeShare(data, share) && share) {
2339
- try {
2340
- await share.share(data);
2341
- return "shared";
2342
- } catch (error) {
2343
- if (isAbort(error)) return "cancelled";
2344
- }
2345
- }
2346
- const copy = copyText;
2347
- const ok = await copy(options.url);
2348
- return ok ? "copied" : "failed";
2349
- }
2350
-
2351
- // src/studio/lib/embed-urls.ts
2352
- function playerWatchUrl(streamId, origin = "https://player.livo.tv") {
2353
- return `${origin.replace(/\/$/, "")}/${streamId}`;
2354
- }
2355
2530
  function Dialog({ ...props }) {
2356
2531
  return /* @__PURE__ */ jsxRuntime.jsx(DialogPrimitive__namespace.Root, { "data-slot": "dialog", ...props });
2357
2532
  }
@@ -2821,7 +2996,6 @@ function StudioToolbar({
2821
2996
  stageEnabled,
2822
2997
  canLeaveStage,
2823
2998
  reducedMotion,
2824
- playerOrigin,
2825
2999
  onToggleMute,
2826
3000
  onToggleCamera,
2827
3001
  onToggleScreen,
@@ -2945,41 +3119,6 @@ function StudioToolbar({
2945
3119
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Users, {})
2946
3120
  }
2947
3121
  ) : null,
2948
- /* @__PURE__ */ jsxRuntime.jsx(
2949
- ControlIcon,
2950
- {
2951
- label: labels.share,
2952
- onClick: () => {
2953
- void navigator.clipboard.writeText(
2954
- playerWatchUrl(session.stream.id, playerOrigin)
2955
- );
2956
- sonner.toast.success(labels.shareCopied);
2957
- },
2958
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Share2, {})
2959
- }
2960
- ),
2961
- session.role === "moderator" && session.guestUrl ? /* @__PURE__ */ jsxRuntime.jsx(
2962
- ControlIcon,
2963
- {
2964
- label: labels.shareLink,
2965
- onClick: () => {
2966
- const url = session.guestUrl;
2967
- if (!url) return;
2968
- void shareOrCopyLink({
2969
- url,
2970
- title: session.stream.title,
2971
- text: labels.shareGuestText.replace(
2972
- "{title}",
2973
- session.stream.title
2974
- )
2975
- }).then((result) => {
2976
- if (result === "copied") sonner.toast.success(labels.copied);
2977
- if (result === "failed") sonner.toast.error(labels.shareFailed);
2978
- });
2979
- },
2980
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, {})
2981
- }
2982
- ) : null,
2983
3122
  /* @__PURE__ */ jsxRuntime.jsx(
2984
3123
  ControlIcon,
2985
3124
  {
@@ -3752,6 +3891,22 @@ function StudioRoom({
3752
3891
  trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { type: "button", variant: "destructive", disabled: pending, children: labels.stop })
3753
3892
  }
3754
3893
  ) : null,
3894
+ session.role === "moderator" ? /* @__PURE__ */ jsxRuntime.jsx(
3895
+ Button,
3896
+ {
3897
+ type: "button",
3898
+ variant: "outline",
3899
+ size: "icon-sm",
3900
+ "aria-label": labels.share,
3901
+ onClick: () => {
3902
+ void navigator.clipboard.writeText(
3903
+ playerWatchUrl(session.stream.id, playerOrigin)
3904
+ );
3905
+ sonner.toast.success(labels.shareCopied);
3906
+ },
3907
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Share2, {})
3908
+ }
3909
+ ) : null,
3755
3910
  embed ? null : chrome
3756
3911
  ] }),
3757
3912
  !stageEnabled && session.role === "guest" ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -3839,6 +3994,22 @@ function StudioRoom({
3839
3994
  onTakeOffAir: (userId) => {
3840
3995
  const tile = participants.find((row) => row.userId === userId);
3841
3996
  if (tile) void onSetStage(tile, false);
3997
+ },
3998
+ guestUrl: session.guestUrl,
3999
+ onShareGuestLink: () => {
4000
+ const url = session.guestUrl;
4001
+ if (!url) return;
4002
+ void shareOrCopyLink({
4003
+ url,
4004
+ title: session.stream.title,
4005
+ text: labels.shareGuestText.replace(
4006
+ "{title}",
4007
+ session.stream.title
4008
+ )
4009
+ }).then((result) => {
4010
+ if (result === "copied") sonner.toast.success(labels.copied);
4011
+ if (result === "failed") sonner.toast.error(labels.shareFailed);
4012
+ });
3842
4013
  }
3843
4014
  }
3844
4015
  ) : null
@@ -3857,7 +4028,6 @@ function StudioRoom({
3857
4028
  session,
3858
4029
  meeting,
3859
4030
  labels,
3860
- playerOrigin,
3861
4031
  muted,
3862
4032
  cameraOn,
3863
4033
  screenOn,
@@ -3887,9 +4057,9 @@ function StudioRoom({
3887
4057
  onCancelShareRequest: () => setShareGrant("idle"),
3888
4058
  onAskStage: () => {
3889
4059
  if (session.role === "moderator") {
3890
- const userId = self?.userId;
3891
- if (userId) void meeting.setStageAccess(userId, true);
3892
- else void meeting.requestStageAccess();
4060
+ void Promise.resolve(meeting.joinStage()).catch(
4061
+ () => sonner.toast.error(labels.stageError)
4062
+ );
3893
4063
  return;
3894
4064
  }
3895
4065
  void meeting.requestStageAccess();
@@ -4637,6 +4807,21 @@ function studioMeetingFromRtk(raw) {
4637
4807
  await meeting.stage?.requestAccess?.();
4638
4808
  notifyStage();
4639
4809
  },
4810
+ async joinStage() {
4811
+ if (currentStageStatus() === "ON_STAGE") {
4812
+ notifyStage();
4813
+ return;
4814
+ }
4815
+ if (meeting.self?.permissions?.stageAccess !== "ALLOWED") {
4816
+ await meeting.stage?.requestAccess?.();
4817
+ notifyStage();
4818
+ return;
4819
+ }
4820
+ await meeting.stage?.join?.();
4821
+ joinedStageLocally = true;
4822
+ await republishCameraIfOn();
4823
+ notifyStage();
4824
+ },
4640
4825
  async leaveStage() {
4641
4826
  await meeting.stage?.leave?.();
4642
4827
  notifyStage();