@livo-tv/sdk 1.1.0 → 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.
@@ -164,6 +164,7 @@ type StudioMeetingHandle = {
164
164
  setStageAccess: (userId: string, on: boolean) => Promise<void>;
165
165
  denyStageAccess: (userId: string) => Promise<void>;
166
166
  requestStageAccess: () => Promise<void>;
167
+ joinStage: () => Promise<void>;
167
168
  leaveStage: () => Promise<void>;
168
169
  stageStatus: () => StudioStageStatus;
169
170
  stageRequests: () => StudioStageRequest[];
@@ -164,6 +164,7 @@ type StudioMeetingHandle = {
164
164
  setStageAccess: (userId: string, on: boolean) => Promise<void>;
165
165
  denyStageAccess: (userId: string) => Promise<void>;
166
166
  requestStageAccess: () => Promise<void>;
167
+ joinStage: () => Promise<void>;
167
168
  leaveStage: () => Promise<void>;
168
169
  stageStatus: () => StudioStageStatus;
169
170
  stageRequests: () => StudioStageRequest[];
@@ -1,6 +1,7 @@
1
+ "use client";
1
2
  import { createContext, useContext, useState, useRef, useEffect, useCallback, useMemo } from 'react';
2
3
  import { createPortal } from 'react-dom';
3
- import { Loader2, MicOff, Mic, Video, VideoOff, X, EyeOff, Eye, Users, MonitorUp, MessageSquare, Share2, Link2, Maximize, PictureInPicture2, Settings, PhoneOff, Undo2, Volume2, XIcon, Pin, MessageSquareOff, MoreVertical } from 'lucide-react';
4
+ import { Loader2, Share2, MicOff, Mic, Video, VideoOff, X, Link2, EyeOff, Eye, Users, MonitorUp, MessageSquare, Maximize, PictureInPicture2, Settings, PhoneOff, Undo2, Volume2, XIcon, Pin, MessageSquareOff, MoreVertical } from 'lucide-react';
4
5
  import { toast } from 'sonner';
5
6
  import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
6
7
  import { clsx } from 'clsx';
@@ -8,6 +9,8 @@ import { twMerge } from 'tailwind-merge';
8
9
  import { Button as Button$1 } from '@base-ui/react/button';
9
10
  import { cva } from 'class-variance-authority';
10
11
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
12
+ import { mergeProps } from '@base-ui/react/merge-props';
13
+ import { useRender } from '@base-ui/react/use-render';
11
14
  import { Menu } from '@base-ui/react/menu';
12
15
  import { Tooltip as Tooltip$1 } from '@base-ui/react/tooltip';
13
16
  import * as DialogPrimitive from '@radix-ui/react-dialog';
@@ -751,6 +754,58 @@ function postLivoEvent(type, payload) {
751
754
  window.parent.postMessage({ source: "livo", type, payload }, "*");
752
755
  }
753
756
 
757
+ // src/studio/lib/embed-urls.ts
758
+ function playerWatchUrl(streamId, origin = "https://player.livo.tv") {
759
+ return `${origin.replace(/\/$/, "")}/${streamId}`;
760
+ }
761
+
762
+ // src/studio/lib/clipboard.ts
763
+ async function copyText(value, clipboard = globalThis.navigator?.clipboard ?? null) {
764
+ if (!clipboard) return false;
765
+ try {
766
+ await clipboard.writeText(value);
767
+ return true;
768
+ } catch {
769
+ return false;
770
+ }
771
+ }
772
+
773
+ // src/studio/lib/share.ts
774
+ function isAbort(error) {
775
+ return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
776
+ }
777
+ function resolveShareApi(share = typeof navigator === "undefined" ? null : typeof navigator.share === "function" ? navigator : null) {
778
+ return share ?? null;
779
+ }
780
+ function canNativeShare(data, share = resolveShareApi()) {
781
+ if (!share) return false;
782
+ if (typeof share.canShare !== "function") return true;
783
+ try {
784
+ return share.canShare(data);
785
+ } catch {
786
+ return false;
787
+ }
788
+ }
789
+ async function shareOrCopyLink(options, deps) {
790
+ const data = {
791
+ url: options.url,
792
+ ...options.title ? { title: options.title } : {},
793
+ ...options.text ? { text: options.text } : {}
794
+ };
795
+ const share = resolveShareApi(deps?.share);
796
+ if (canNativeShare(data, share) && share) {
797
+ try {
798
+ await share.share(data);
799
+ return "shared";
800
+ } catch (error) {
801
+ if (isAbort(error)) return "cancelled";
802
+ }
803
+ }
804
+ const copy = copyText;
805
+ const ok = await copy(options.url);
806
+ return ok ? "copied" : "failed";
807
+ }
808
+
754
809
  // src/studio/lib/status.ts
755
810
  function canPublish(status) {
756
811
  return status === "preview";
@@ -1424,9 +1479,124 @@ function StudioRemoteAudio({
1424
1479
  }, [meeting, participantId]);
1425
1480
  return /* @__PURE__ */ jsx("audio", { ref, autoPlay: true, className: "sr-only" });
1426
1481
  }
1482
+ function BubbleGroup({ className, ...props }) {
1483
+ return /* @__PURE__ */ jsx(
1484
+ "div",
1485
+ {
1486
+ "data-slot": "bubble-group",
1487
+ className: cn("flex min-w-0 flex-col gap-2", className),
1488
+ ...props
1489
+ }
1490
+ );
1491
+ }
1492
+ var bubbleVariants = cva(
1493
+ "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",
1494
+ {
1495
+ variants: {
1496
+ variant: {
1497
+ 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",
1498
+ 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%)]",
1499
+ 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%)]",
1500
+ 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)]",
1501
+ 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",
1502
+ 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",
1503
+ 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"
1504
+ }
1505
+ },
1506
+ defaultVariants: {
1507
+ variant: "default"
1508
+ }
1509
+ }
1510
+ );
1511
+ function Bubble({
1512
+ variant = "default",
1513
+ align = "start",
1514
+ className,
1515
+ ...props
1516
+ }) {
1517
+ return /* @__PURE__ */ jsx(
1518
+ "div",
1519
+ {
1520
+ "data-slot": "bubble",
1521
+ "data-variant": variant,
1522
+ "data-align": align,
1523
+ className: cn(bubbleVariants({ variant }), className),
1524
+ ...props
1525
+ }
1526
+ );
1527
+ }
1528
+ function BubbleContent({
1529
+ className,
1530
+ render,
1531
+ ...props
1532
+ }) {
1533
+ return useRender({
1534
+ defaultTagName: "div",
1535
+ props: mergeProps(
1536
+ {
1537
+ className: cn(
1538
+ "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",
1539
+ className
1540
+ )
1541
+ },
1542
+ props
1543
+ ),
1544
+ render,
1545
+ state: {
1546
+ slot: "bubble-content"
1547
+ }
1548
+ });
1549
+ }
1550
+ cva(
1551
+ "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",
1552
+ {
1553
+ variants: {
1554
+ side: {
1555
+ top: "top-0 -translate-y-3/4",
1556
+ bottom: "bottom-0 translate-y-3/4"
1557
+ },
1558
+ align: {
1559
+ start: "left-3",
1560
+ end: "right-3"
1561
+ }
1562
+ },
1563
+ defaultVariants: {
1564
+ side: "bottom",
1565
+ align: "end"
1566
+ }
1567
+ }
1568
+ );
1569
+
1570
+ // src/studio/lib/chat-groups.ts
1571
+ function groupChatMessages(messages, selfId) {
1572
+ const groups = [];
1573
+ for (const row of messages) {
1574
+ const mine = Boolean(selfId) && row.userId === selfId;
1575
+ const last = groups[groups.length - 1];
1576
+ if (last && last.userId === row.userId && last.mine === mine) {
1577
+ last.rows.push(row);
1578
+ continue;
1579
+ }
1580
+ groups.push({
1581
+ userId: row.userId,
1582
+ name: row.name,
1583
+ mine,
1584
+ rows: [row]
1585
+ });
1586
+ }
1587
+ return groups;
1588
+ }
1589
+ function senderInitials(name) {
1590
+ const parts = name.trim().split(/\s+/).filter((part) => part.length > 0);
1591
+ if (parts.length === 0) return "?";
1592
+ const first = parts[0];
1593
+ if (parts.length === 1) return first.slice(0, 2).toUpperCase();
1594
+ return `${first[0]}${parts[1][0]}`.toUpperCase();
1595
+ }
1427
1596
  function StudioChatPanel({
1428
1597
  meeting,
1429
1598
  selfId,
1599
+ participants,
1430
1600
  isModerator: isModerator2,
1431
1601
  labels
1432
1602
  }) {
@@ -1444,6 +1614,17 @@ function StudioChatPanel({
1444
1614
  );
1445
1615
  const pinned = useMemo(() => meeting.pinnedChatMessages(), [meeting, tick]);
1446
1616
  const limit = meeting.chatMaxTextLimit();
1617
+ const pictures = useMemo(() => {
1618
+ const map = /* @__PURE__ */ new Map();
1619
+ for (const row of participants) {
1620
+ if (row.userId) map.set(row.userId, row.picture);
1621
+ }
1622
+ return map;
1623
+ }, [participants]);
1624
+ const groups = useMemo(
1625
+ () => groupChatMessages(messages, selfId),
1626
+ [messages, selfId]
1627
+ );
1447
1628
  useEffect(() => {
1448
1629
  const list = listRef.current;
1449
1630
  if (!list) return;
@@ -1467,6 +1648,44 @@ function StudioChatPanel({
1467
1648
  );
1468
1649
  }
1469
1650
  }
1651
+ function messageActions(row, mine) {
1652
+ return /* @__PURE__ */ jsxs(
1653
+ "p",
1654
+ {
1655
+ className: mine ? "text-muted-foreground flex flex-wrap justify-end gap-2 text-xs" : "text-muted-foreground flex flex-wrap gap-2 text-xs",
1656
+ children: [
1657
+ row.isEdited ? /* @__PURE__ */ jsx("span", { children: labels.chatEdited }) : null,
1658
+ mine ? /* @__PURE__ */ jsx(
1659
+ "button",
1660
+ {
1661
+ type: "button",
1662
+ onClick: () => {
1663
+ setEditingId(row.id);
1664
+ setDraft(row.text);
1665
+ },
1666
+ children: labels.chatEdit
1667
+ }
1668
+ ) : null,
1669
+ isModerator2 || mine ? /* @__PURE__ */ jsx(
1670
+ "button",
1671
+ {
1672
+ type: "button",
1673
+ onClick: () => void meeting.deleteChatMessage(row.id).catch(() => toast.error(labels.chatError)),
1674
+ children: labels.chatDelete
1675
+ }
1676
+ ) : null,
1677
+ isModerator2 ? /* @__PURE__ */ jsx(
1678
+ "button",
1679
+ {
1680
+ type: "button",
1681
+ onClick: () => void meeting.pinChatMessage(row.id, row.pinned !== true).catch(() => toast.error(labels.chatError)),
1682
+ children: row.pinned ? labels.chatUnpin : labels.chatPin
1683
+ }
1684
+ ) : null
1685
+ ]
1686
+ }
1687
+ );
1688
+ }
1470
1689
  return /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [
1471
1690
  pinned[0] ? /* @__PURE__ */ jsxs("div", { role: "status", className: "bg-muted border-b px-3 py-2 text-sm", children: [
1472
1691
  /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-xs", children: labels.chatPinned }),
@@ -1478,7 +1697,7 @@ function StudioChatPanel({
1478
1697
  ref: listRef,
1479
1698
  role: "log",
1480
1699
  "aria-live": "polite",
1481
- className: "flex min-h-0 flex-1 flex-col gap-2 overflow-auto p-3",
1700
+ className: "flex min-h-0 flex-1 flex-col gap-3 overflow-auto p-3",
1482
1701
  children: [
1483
1702
  /* @__PURE__ */ jsx(
1484
1703
  Button,
@@ -1496,48 +1715,42 @@ function StudioChatPanel({
1496
1715
  children: labels.chatLoadOlder
1497
1716
  }
1498
1717
  ),
1499
- messages.map((row) => {
1500
- const mine = Boolean(selfId) && row.userId === selfId;
1718
+ groups.map((group) => {
1719
+ const bubbles = group.rows.map((row) => /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [
1720
+ /* @__PURE__ */ jsx(
1721
+ Bubble,
1722
+ {
1723
+ align: group.mine ? "end" : "start",
1724
+ variant: group.mine ? "default" : "secondary",
1725
+ children: /* @__PURE__ */ jsx(BubbleContent, { children: row.text })
1726
+ }
1727
+ ),
1728
+ messageActions(row, group.mine)
1729
+ ] }, row.id));
1730
+ if (group.mine) {
1731
+ return /* @__PURE__ */ jsx(BubbleGroup, { className: "items-end", children: bubbles }, group.rows[0].id);
1732
+ }
1733
+ const picture = pictures.get(group.userId) ?? null;
1501
1734
  return /* @__PURE__ */ jsxs(
1502
1735
  "div",
1503
1736
  {
1504
- className: mine ? "self-end text-right" : "self-start",
1737
+ className: "flex max-w-full gap-2 self-start",
1505
1738
  children: [
1506
- /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-xs", children: row.name }),
1507
- /* @__PURE__ */ jsx("p", { className: "bg-muted max-w-64 rounded-lg px-2 py-1 text-sm", children: row.text }),
1508
- /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground flex flex-wrap justify-end gap-2 text-xs", children: [
1509
- row.isEdited ? /* @__PURE__ */ jsx("span", { children: labels.chatEdited }) : null,
1510
- mine ? /* @__PURE__ */ jsx(
1511
- "button",
1512
- {
1513
- type: "button",
1514
- onClick: () => {
1515
- setEditingId(row.id);
1516
- setDraft(row.text);
1517
- },
1518
- children: labels.chatEdit
1519
- }
1520
- ) : null,
1521
- isModerator2 || mine ? /* @__PURE__ */ jsx(
1522
- "button",
1523
- {
1524
- type: "button",
1525
- onClick: () => void meeting.deleteChatMessage(row.id).catch(() => toast.error(labels.chatError)),
1526
- children: labels.chatDelete
1527
- }
1528
- ) : null,
1529
- isModerator2 ? /* @__PURE__ */ jsx(
1530
- "button",
1531
- {
1532
- type: "button",
1533
- onClick: () => void meeting.pinChatMessage(row.id, row.pinned !== true).catch(() => toast.error(labels.chatError)),
1534
- children: row.pinned ? labels.chatUnpin : labels.chatPin
1535
- }
1536
- ) : null
1739
+ picture ? /* @__PURE__ */ jsx(
1740
+ "img",
1741
+ {
1742
+ src: picture,
1743
+ alt: "",
1744
+ className: "mt-0.5 size-7 shrink-0 rounded-full object-cover"
1745
+ }
1746
+ ) : /* @__PURE__ */ 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) }),
1747
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1748
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground mb-1 text-xs", children: group.name }),
1749
+ /* @__PURE__ */ jsx(BubbleGroup, { children: bubbles })
1537
1750
  ] })
1538
1751
  ]
1539
1752
  },
1540
- row.id
1753
+ group.rows[0].id
1541
1754
  );
1542
1755
  })
1543
1756
  ]
@@ -1667,7 +1880,9 @@ function StudioSidePanel({
1667
1880
  onDeny,
1668
1881
  onGrantStage,
1669
1882
  onDenyStage,
1670
- onTakeOffAir
1883
+ onTakeOffAir,
1884
+ guestUrl,
1885
+ onShareGuestLink
1671
1886
  }) {
1672
1887
  const panelists = participants.filter((row) => isOnStage(row.stageStatus));
1673
1888
  const audience = participants.filter((row) => !isOnStage(row.stageStatus));
@@ -1724,6 +1939,19 @@ function StudioSidePanel({
1724
1939
  labels
1725
1940
  }
1726
1941
  ) : /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-4 overflow-auto p-3", children: [
1942
+ guestUrl ? /* @__PURE__ */ jsxs(
1943
+ Button,
1944
+ {
1945
+ type: "button",
1946
+ variant: "outline",
1947
+ className: "w-full",
1948
+ onClick: onShareGuestLink,
1949
+ children: [
1950
+ /* @__PURE__ */ jsx(Link2, {}),
1951
+ labels.shareLink
1952
+ ]
1953
+ }
1954
+ ) : null,
1727
1955
  /* @__PURE__ */ jsx(
1728
1956
  StudioWaitingRoom,
1729
1957
  {
@@ -2275,58 +2503,6 @@ function TooltipContent({
2275
2503
  }
2276
2504
  ) });
2277
2505
  }
2278
-
2279
- // src/studio/lib/clipboard.ts
2280
- async function copyText(value, clipboard = globalThis.navigator?.clipboard ?? null) {
2281
- if (!clipboard) return false;
2282
- try {
2283
- await clipboard.writeText(value);
2284
- return true;
2285
- } catch {
2286
- return false;
2287
- }
2288
- }
2289
-
2290
- // src/studio/lib/share.ts
2291
- function isAbort(error) {
2292
- return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
2293
- }
2294
- function resolveShareApi(share = typeof navigator === "undefined" ? null : typeof navigator.share === "function" ? navigator : null) {
2295
- return share ?? null;
2296
- }
2297
- function canNativeShare(data, share = resolveShareApi()) {
2298
- if (!share) return false;
2299
- if (typeof share.canShare !== "function") return true;
2300
- try {
2301
- return share.canShare(data);
2302
- } catch {
2303
- return false;
2304
- }
2305
- }
2306
- async function shareOrCopyLink(options, deps) {
2307
- const data = {
2308
- url: options.url,
2309
- ...options.title ? { title: options.title } : {},
2310
- ...options.text ? { text: options.text } : {}
2311
- };
2312
- const share = resolveShareApi(deps?.share);
2313
- if (canNativeShare(data, share) && share) {
2314
- try {
2315
- await share.share(data);
2316
- return "shared";
2317
- } catch (error) {
2318
- if (isAbort(error)) return "cancelled";
2319
- }
2320
- }
2321
- const copy = copyText;
2322
- const ok = await copy(options.url);
2323
- return ok ? "copied" : "failed";
2324
- }
2325
-
2326
- // src/studio/lib/embed-urls.ts
2327
- function playerWatchUrl(streamId, origin = "https://player.livo.tv") {
2328
- return `${origin.replace(/\/$/, "")}/${streamId}`;
2329
- }
2330
2506
  function Dialog({ ...props }) {
2331
2507
  return /* @__PURE__ */ jsx(DialogPrimitive.Root, { "data-slot": "dialog", ...props });
2332
2508
  }
@@ -2796,7 +2972,6 @@ function StudioToolbar({
2796
2972
  stageEnabled,
2797
2973
  canLeaveStage,
2798
2974
  reducedMotion,
2799
- playerOrigin,
2800
2975
  onToggleMute,
2801
2976
  onToggleCamera,
2802
2977
  onToggleScreen,
@@ -2920,41 +3095,6 @@ function StudioToolbar({
2920
3095
  children: /* @__PURE__ */ jsx(Users, {})
2921
3096
  }
2922
3097
  ) : null,
2923
- /* @__PURE__ */ jsx(
2924
- ControlIcon,
2925
- {
2926
- label: labels.share,
2927
- onClick: () => {
2928
- void navigator.clipboard.writeText(
2929
- playerWatchUrl(session.stream.id, playerOrigin)
2930
- );
2931
- toast.success(labels.shareCopied);
2932
- },
2933
- children: /* @__PURE__ */ jsx(Share2, {})
2934
- }
2935
- ),
2936
- session.role === "moderator" && session.guestUrl ? /* @__PURE__ */ jsx(
2937
- ControlIcon,
2938
- {
2939
- label: labels.shareLink,
2940
- onClick: () => {
2941
- const url = session.guestUrl;
2942
- if (!url) return;
2943
- void shareOrCopyLink({
2944
- url,
2945
- title: session.stream.title,
2946
- text: labels.shareGuestText.replace(
2947
- "{title}",
2948
- session.stream.title
2949
- )
2950
- }).then((result) => {
2951
- if (result === "copied") toast.success(labels.copied);
2952
- if (result === "failed") toast.error(labels.shareFailed);
2953
- });
2954
- },
2955
- children: /* @__PURE__ */ jsx(Link2, {})
2956
- }
2957
- ) : null,
2958
3098
  /* @__PURE__ */ jsx(
2959
3099
  ControlIcon,
2960
3100
  {
@@ -3727,6 +3867,22 @@ function StudioRoom({
3727
3867
  trigger: /* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive", disabled: pending, children: labels.stop })
3728
3868
  }
3729
3869
  ) : null,
3870
+ session.role === "moderator" ? /* @__PURE__ */ jsx(
3871
+ Button,
3872
+ {
3873
+ type: "button",
3874
+ variant: "outline",
3875
+ size: "icon-sm",
3876
+ "aria-label": labels.share,
3877
+ onClick: () => {
3878
+ void navigator.clipboard.writeText(
3879
+ playerWatchUrl(session.stream.id, playerOrigin)
3880
+ );
3881
+ toast.success(labels.shareCopied);
3882
+ },
3883
+ children: /* @__PURE__ */ jsx(Share2, {})
3884
+ }
3885
+ ) : null,
3730
3886
  embed ? null : chrome
3731
3887
  ] }),
3732
3888
  !stageEnabled && session.role === "guest" ? /* @__PURE__ */ jsx(
@@ -3814,6 +3970,22 @@ function StudioRoom({
3814
3970
  onTakeOffAir: (userId) => {
3815
3971
  const tile = participants.find((row) => row.userId === userId);
3816
3972
  if (tile) void onSetStage(tile, false);
3973
+ },
3974
+ guestUrl: session.guestUrl,
3975
+ onShareGuestLink: () => {
3976
+ const url = session.guestUrl;
3977
+ if (!url) return;
3978
+ void shareOrCopyLink({
3979
+ url,
3980
+ title: session.stream.title,
3981
+ text: labels.shareGuestText.replace(
3982
+ "{title}",
3983
+ session.stream.title
3984
+ )
3985
+ }).then((result) => {
3986
+ if (result === "copied") toast.success(labels.copied);
3987
+ if (result === "failed") toast.error(labels.shareFailed);
3988
+ });
3817
3989
  }
3818
3990
  }
3819
3991
  ) : null
@@ -3832,7 +4004,6 @@ function StudioRoom({
3832
4004
  session,
3833
4005
  meeting,
3834
4006
  labels,
3835
- playerOrigin,
3836
4007
  muted,
3837
4008
  cameraOn,
3838
4009
  screenOn,
@@ -3862,9 +4033,9 @@ function StudioRoom({
3862
4033
  onCancelShareRequest: () => setShareGrant("idle"),
3863
4034
  onAskStage: () => {
3864
4035
  if (session.role === "moderator") {
3865
- const userId = self?.userId;
3866
- if (userId) void meeting.setStageAccess(userId, true);
3867
- else void meeting.requestStageAccess();
4036
+ void Promise.resolve(meeting.joinStage()).catch(
4037
+ () => toast.error(labels.stageError)
4038
+ );
3868
4039
  return;
3869
4040
  }
3870
4041
  void meeting.requestStageAccess();
@@ -4612,6 +4783,21 @@ function studioMeetingFromRtk(raw) {
4612
4783
  await meeting.stage?.requestAccess?.();
4613
4784
  notifyStage();
4614
4785
  },
4786
+ async joinStage() {
4787
+ if (currentStageStatus() === "ON_STAGE") {
4788
+ notifyStage();
4789
+ return;
4790
+ }
4791
+ if (meeting.self?.permissions?.stageAccess !== "ALLOWED") {
4792
+ await meeting.stage?.requestAccess?.();
4793
+ notifyStage();
4794
+ return;
4795
+ }
4796
+ await meeting.stage?.join?.();
4797
+ joinedStageLocally = true;
4798
+ await republishCameraIfOn();
4799
+ notifyStage();
4800
+ },
4615
4801
  async leaveStage() {
4616
4802
  await meeting.stage?.leave?.();
4617
4803
  notifyStage();