@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.
@@ -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,7 +1,7 @@
1
1
  "use client";
2
2
  import { createContext, useContext, useState, useRef, useEffect, useCallback, useMemo } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
- 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';
5
5
  import { toast } from 'sonner';
6
6
  import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
7
7
  import { clsx } from 'clsx';
@@ -9,6 +9,8 @@ import { twMerge } from 'tailwind-merge';
9
9
  import { Button as Button$1 } from '@base-ui/react/button';
10
10
  import { cva } from 'class-variance-authority';
11
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';
12
14
  import { Menu } from '@base-ui/react/menu';
13
15
  import { Tooltip as Tooltip$1 } from '@base-ui/react/tooltip';
14
16
  import * as DialogPrimitive from '@radix-ui/react-dialog';
@@ -752,6 +754,58 @@ function postLivoEvent(type, payload) {
752
754
  window.parent.postMessage({ source: "livo", type, payload }, "*");
753
755
  }
754
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
+
755
809
  // src/studio/lib/status.ts
756
810
  function canPublish(status) {
757
811
  return status === "preview";
@@ -1425,9 +1479,124 @@ function StudioRemoteAudio({
1425
1479
  }, [meeting, participantId]);
1426
1480
  return /* @__PURE__ */ jsx("audio", { ref, autoPlay: true, className: "sr-only" });
1427
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
+ }
1428
1596
  function StudioChatPanel({
1429
1597
  meeting,
1430
1598
  selfId,
1599
+ participants,
1431
1600
  isModerator: isModerator2,
1432
1601
  labels
1433
1602
  }) {
@@ -1445,6 +1614,17 @@ function StudioChatPanel({
1445
1614
  );
1446
1615
  const pinned = useMemo(() => meeting.pinnedChatMessages(), [meeting, tick]);
1447
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
+ );
1448
1628
  useEffect(() => {
1449
1629
  const list = listRef.current;
1450
1630
  if (!list) return;
@@ -1468,6 +1648,44 @@ function StudioChatPanel({
1468
1648
  );
1469
1649
  }
1470
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
+ }
1471
1689
  return /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [
1472
1690
  pinned[0] ? /* @__PURE__ */ jsxs("div", { role: "status", className: "bg-muted border-b px-3 py-2 text-sm", children: [
1473
1691
  /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-xs", children: labels.chatPinned }),
@@ -1479,7 +1697,7 @@ function StudioChatPanel({
1479
1697
  ref: listRef,
1480
1698
  role: "log",
1481
1699
  "aria-live": "polite",
1482
- 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",
1483
1701
  children: [
1484
1702
  /* @__PURE__ */ jsx(
1485
1703
  Button,
@@ -1497,48 +1715,42 @@ function StudioChatPanel({
1497
1715
  children: labels.chatLoadOlder
1498
1716
  }
1499
1717
  ),
1500
- messages.map((row) => {
1501
- 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;
1502
1734
  return /* @__PURE__ */ jsxs(
1503
1735
  "div",
1504
1736
  {
1505
- className: mine ? "self-end text-right" : "self-start",
1737
+ className: "flex max-w-full gap-2 self-start",
1506
1738
  children: [
1507
- /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-xs", children: row.name }),
1508
- /* @__PURE__ */ jsx("p", { className: "bg-muted max-w-64 rounded-lg px-2 py-1 text-sm", children: row.text }),
1509
- /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground flex flex-wrap justify-end gap-2 text-xs", children: [
1510
- row.isEdited ? /* @__PURE__ */ jsx("span", { children: labels.chatEdited }) : null,
1511
- mine ? /* @__PURE__ */ jsx(
1512
- "button",
1513
- {
1514
- type: "button",
1515
- onClick: () => {
1516
- setEditingId(row.id);
1517
- setDraft(row.text);
1518
- },
1519
- children: labels.chatEdit
1520
- }
1521
- ) : null,
1522
- isModerator2 || mine ? /* @__PURE__ */ jsx(
1523
- "button",
1524
- {
1525
- type: "button",
1526
- onClick: () => void meeting.deleteChatMessage(row.id).catch(() => toast.error(labels.chatError)),
1527
- children: labels.chatDelete
1528
- }
1529
- ) : null,
1530
- isModerator2 ? /* @__PURE__ */ jsx(
1531
- "button",
1532
- {
1533
- type: "button",
1534
- onClick: () => void meeting.pinChatMessage(row.id, row.pinned !== true).catch(() => toast.error(labels.chatError)),
1535
- children: row.pinned ? labels.chatUnpin : labels.chatPin
1536
- }
1537
- ) : 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 })
1538
1750
  ] })
1539
1751
  ]
1540
1752
  },
1541
- row.id
1753
+ group.rows[0].id
1542
1754
  );
1543
1755
  })
1544
1756
  ]
@@ -1668,7 +1880,9 @@ function StudioSidePanel({
1668
1880
  onDeny,
1669
1881
  onGrantStage,
1670
1882
  onDenyStage,
1671
- onTakeOffAir
1883
+ onTakeOffAir,
1884
+ guestUrl,
1885
+ onShareGuestLink
1672
1886
  }) {
1673
1887
  const panelists = participants.filter((row) => isOnStage(row.stageStatus));
1674
1888
  const audience = participants.filter((row) => !isOnStage(row.stageStatus));
@@ -1725,6 +1939,19 @@ function StudioSidePanel({
1725
1939
  labels
1726
1940
  }
1727
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,
1728
1955
  /* @__PURE__ */ jsx(
1729
1956
  StudioWaitingRoom,
1730
1957
  {
@@ -2276,58 +2503,6 @@ function TooltipContent({
2276
2503
  }
2277
2504
  ) });
2278
2505
  }
2279
-
2280
- // src/studio/lib/clipboard.ts
2281
- async function copyText(value, clipboard = globalThis.navigator?.clipboard ?? null) {
2282
- if (!clipboard) return false;
2283
- try {
2284
- await clipboard.writeText(value);
2285
- return true;
2286
- } catch {
2287
- return false;
2288
- }
2289
- }
2290
-
2291
- // src/studio/lib/share.ts
2292
- function isAbort(error) {
2293
- return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
2294
- }
2295
- function resolveShareApi(share = typeof navigator === "undefined" ? null : typeof navigator.share === "function" ? navigator : null) {
2296
- return share ?? null;
2297
- }
2298
- function canNativeShare(data, share = resolveShareApi()) {
2299
- if (!share) return false;
2300
- if (typeof share.canShare !== "function") return true;
2301
- try {
2302
- return share.canShare(data);
2303
- } catch {
2304
- return false;
2305
- }
2306
- }
2307
- async function shareOrCopyLink(options, deps) {
2308
- const data = {
2309
- url: options.url,
2310
- ...options.title ? { title: options.title } : {},
2311
- ...options.text ? { text: options.text } : {}
2312
- };
2313
- const share = resolveShareApi(deps?.share);
2314
- if (canNativeShare(data, share) && share) {
2315
- try {
2316
- await share.share(data);
2317
- return "shared";
2318
- } catch (error) {
2319
- if (isAbort(error)) return "cancelled";
2320
- }
2321
- }
2322
- const copy = copyText;
2323
- const ok = await copy(options.url);
2324
- return ok ? "copied" : "failed";
2325
- }
2326
-
2327
- // src/studio/lib/embed-urls.ts
2328
- function playerWatchUrl(streamId, origin = "https://player.livo.tv") {
2329
- return `${origin.replace(/\/$/, "")}/${streamId}`;
2330
- }
2331
2506
  function Dialog({ ...props }) {
2332
2507
  return /* @__PURE__ */ jsx(DialogPrimitive.Root, { "data-slot": "dialog", ...props });
2333
2508
  }
@@ -2797,7 +2972,6 @@ function StudioToolbar({
2797
2972
  stageEnabled,
2798
2973
  canLeaveStage,
2799
2974
  reducedMotion,
2800
- playerOrigin,
2801
2975
  onToggleMute,
2802
2976
  onToggleCamera,
2803
2977
  onToggleScreen,
@@ -2921,41 +3095,6 @@ function StudioToolbar({
2921
3095
  children: /* @__PURE__ */ jsx(Users, {})
2922
3096
  }
2923
3097
  ) : null,
2924
- /* @__PURE__ */ jsx(
2925
- ControlIcon,
2926
- {
2927
- label: labels.share,
2928
- onClick: () => {
2929
- void navigator.clipboard.writeText(
2930
- playerWatchUrl(session.stream.id, playerOrigin)
2931
- );
2932
- toast.success(labels.shareCopied);
2933
- },
2934
- children: /* @__PURE__ */ jsx(Share2, {})
2935
- }
2936
- ),
2937
- session.role === "moderator" && session.guestUrl ? /* @__PURE__ */ jsx(
2938
- ControlIcon,
2939
- {
2940
- label: labels.shareLink,
2941
- onClick: () => {
2942
- const url = session.guestUrl;
2943
- if (!url) return;
2944
- void shareOrCopyLink({
2945
- url,
2946
- title: session.stream.title,
2947
- text: labels.shareGuestText.replace(
2948
- "{title}",
2949
- session.stream.title
2950
- )
2951
- }).then((result) => {
2952
- if (result === "copied") toast.success(labels.copied);
2953
- if (result === "failed") toast.error(labels.shareFailed);
2954
- });
2955
- },
2956
- children: /* @__PURE__ */ jsx(Link2, {})
2957
- }
2958
- ) : null,
2959
3098
  /* @__PURE__ */ jsx(
2960
3099
  ControlIcon,
2961
3100
  {
@@ -3728,6 +3867,22 @@ function StudioRoom({
3728
3867
  trigger: /* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive", disabled: pending, children: labels.stop })
3729
3868
  }
3730
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,
3731
3886
  embed ? null : chrome
3732
3887
  ] }),
3733
3888
  !stageEnabled && session.role === "guest" ? /* @__PURE__ */ jsx(
@@ -3815,6 +3970,22 @@ function StudioRoom({
3815
3970
  onTakeOffAir: (userId) => {
3816
3971
  const tile = participants.find((row) => row.userId === userId);
3817
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
+ });
3818
3989
  }
3819
3990
  }
3820
3991
  ) : null
@@ -3833,7 +4004,6 @@ function StudioRoom({
3833
4004
  session,
3834
4005
  meeting,
3835
4006
  labels,
3836
- playerOrigin,
3837
4007
  muted,
3838
4008
  cameraOn,
3839
4009
  screenOn,
@@ -3863,9 +4033,9 @@ function StudioRoom({
3863
4033
  onCancelShareRequest: () => setShareGrant("idle"),
3864
4034
  onAskStage: () => {
3865
4035
  if (session.role === "moderator") {
3866
- const userId = self?.userId;
3867
- if (userId) void meeting.setStageAccess(userId, true);
3868
- else void meeting.requestStageAccess();
4036
+ void Promise.resolve(meeting.joinStage()).catch(
4037
+ () => toast.error(labels.stageError)
4038
+ );
3869
4039
  return;
3870
4040
  }
3871
4041
  void meeting.requestStageAccess();
@@ -4613,6 +4783,21 @@ function studioMeetingFromRtk(raw) {
4613
4783
  await meeting.stage?.requestAccess?.();
4614
4784
  notifyStage();
4615
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
+ },
4616
4801
  async leaveStage() {
4617
4802
  await meeting.stage?.leave?.();
4618
4803
  notifyStage();