@streamplace/components 0.7.3 → 0.7.7

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.
Files changed (85) hide show
  1. package/dist/components/chat/chat-box.js +212 -24
  2. package/dist/components/chat/chat-message.js +5 -5
  3. package/dist/components/chat/chat.js +83 -5
  4. package/dist/components/chat/emoji-suggestions.js +35 -0
  5. package/dist/components/chat/mod-view.js +59 -8
  6. package/dist/components/chat/system-message.js +19 -0
  7. package/dist/components/icons/bluesky-icon.js +9 -0
  8. package/dist/components/keep-awake.js +7 -0
  9. package/dist/components/keep-awake.native.js +16 -0
  10. package/dist/components/mobile-player/fullscreen.js +2 -1
  11. package/dist/components/mobile-player/fullscreen.native.js +3 -3
  12. package/dist/components/mobile-player/player.js +15 -30
  13. package/dist/components/mobile-player/ui/index.js +2 -1
  14. package/dist/components/mobile-player/ui/report-modal.js +90 -0
  15. package/dist/components/mobile-player/ui/{loading.js → streamer-loading-overlay.js} +1 -1
  16. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -1
  17. package/dist/components/mobile-player/ui/viewer-loading-overlay.js +49 -0
  18. package/dist/components/mobile-player/use-webrtc.js +7 -1
  19. package/dist/components/mobile-player/video-retry.js +29 -0
  20. package/dist/components/mobile-player/video.js +84 -9
  21. package/dist/components/mobile-player/video.native.js +24 -10
  22. package/dist/components/share/sharesheet.js +91 -0
  23. package/dist/components/ui/dialog.js +1 -1
  24. package/dist/components/ui/dropdown.js +6 -6
  25. package/dist/components/ui/index.js +2 -0
  26. package/dist/components/ui/primitives/modal.js +0 -1
  27. package/dist/components/ui/slider.js +5 -0
  28. package/dist/hooks/index.js +1 -0
  29. package/dist/hooks/usePointerDevice.js +71 -0
  30. package/dist/index.js +10 -3
  31. package/dist/lib/system-messages.js +101 -0
  32. package/dist/livestream-store/chat.js +111 -18
  33. package/dist/livestream-store/livestream-store.js +3 -0
  34. package/dist/livestream-store/problems.js +76 -0
  35. package/dist/livestream-store/websocket-consumer.js +39 -4
  36. package/dist/player-store/player-store.js +30 -4
  37. package/dist/streamplace-store/block.js +51 -12
  38. package/dist/ui/index.js +79 -0
  39. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  40. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  41. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  42. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  43. package/package.json +6 -2
  44. package/src/components/chat/chat-box.tsx +295 -25
  45. package/src/components/chat/chat-message.tsx +6 -7
  46. package/src/components/chat/chat.tsx +192 -41
  47. package/src/components/chat/emoji-suggestions.tsx +94 -0
  48. package/src/components/chat/mod-view.tsx +119 -40
  49. package/src/components/chat/system-message.tsx +38 -0
  50. package/src/components/icons/bluesky-icon.tsx +9 -0
  51. package/src/components/keep-awake.native.tsx +13 -0
  52. package/src/components/keep-awake.tsx +3 -0
  53. package/src/components/mobile-player/fullscreen.native.tsx +12 -3
  54. package/src/components/mobile-player/fullscreen.tsx +10 -3
  55. package/src/components/mobile-player/player.tsx +28 -36
  56. package/src/components/mobile-player/props.tsx +1 -0
  57. package/src/components/mobile-player/ui/index.ts +2 -1
  58. package/src/components/mobile-player/ui/report-modal.tsx +195 -0
  59. package/src/components/mobile-player/ui/{loading.tsx → streamer-loading-overlay.tsx} +1 -1
  60. package/src/components/mobile-player/ui/viewer-context-menu.tsx +31 -3
  61. package/src/components/mobile-player/ui/viewer-loading-overlay.tsx +66 -0
  62. package/src/components/mobile-player/use-webrtc.tsx +10 -2
  63. package/src/components/mobile-player/video-retry.tsx +28 -0
  64. package/src/components/mobile-player/video.native.tsx +24 -10
  65. package/src/components/mobile-player/video.tsx +100 -21
  66. package/src/components/share/sharesheet.tsx +185 -0
  67. package/src/components/ui/dialog.tsx +1 -1
  68. package/src/components/ui/dropdown.tsx +13 -13
  69. package/src/components/ui/index.ts +2 -0
  70. package/src/components/ui/primitives/modal.tsx +0 -1
  71. package/src/components/ui/slider.tsx +1 -0
  72. package/src/hooks/index.ts +1 -0
  73. package/src/hooks/usePointerDevice.ts +89 -0
  74. package/src/index.tsx +11 -2
  75. package/src/lib/system-messages.ts +135 -0
  76. package/src/livestream-store/chat.tsx +145 -17
  77. package/src/livestream-store/livestream-state.tsx +10 -0
  78. package/src/livestream-store/livestream-store.tsx +3 -0
  79. package/src/livestream-store/problems.tsx +96 -0
  80. package/src/livestream-store/websocket-consumer.tsx +44 -4
  81. package/src/player-store/player-state.tsx +21 -4
  82. package/src/player-store/player-store.tsx +38 -5
  83. package/src/streamplace-store/block.tsx +55 -13
  84. package/src/ui/index.ts +86 -0
  85. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ShareSheet = ShareSheet;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const lucide_react_native_1 = require("lucide-react-native");
6
+ const react_1 = require("react");
7
+ const react_native_1 = require("react-native");
8
+ const theme_1 = require("../../lib/theme");
9
+ const livestream_store_1 = require("../../livestream-store");
10
+ const streamplace_store_1 = require("../../streamplace-store");
11
+ const bluesky_icon_1 = require("../icons/bluesky-icon");
12
+ const ui_1 = require("../ui");
13
+ function ShareSheet({ onShare } = {}) {
14
+ const profile = (0, livestream_store_1.useLivestreamStore)((x) => x.profile);
15
+ const [isCopying, setIsCopying] = (0, react_1.useState)(false);
16
+ const url = (0, streamplace_store_1.useUrl)();
17
+ // Get the current stream URL
18
+ const getStreamUrl = (0, react_1.useCallback)(() => {
19
+ return url + (profile ? `/@${profile.handle}` : "");
20
+ }, [profile]);
21
+ // Get the embed URL
22
+ const getEmbedUrl = (0, react_1.useCallback)(() => {
23
+ return url + (profile ? `/embed/${profile.handle}` : "");
24
+ }, [profile]);
25
+ // Get embed code
26
+ const getEmbedCode = (0, react_1.useCallback)(() => {
27
+ const embedUrl = getEmbedUrl();
28
+ return `<iframe src="${embedUrl}" width="640" height="360" frameborder="0" allowfullscreen></iframe>`;
29
+ }, [getEmbedUrl]);
30
+ // Copy to clipboard handler
31
+ const copyToClipboard = (0, react_1.useCallback)(async (text, label) => {
32
+ setIsCopying(true);
33
+ try {
34
+ if (react_native_1.Platform.OS === "web") {
35
+ await navigator.clipboard.writeText(text);
36
+ }
37
+ else {
38
+ react_native_1.Clipboard.setString(text);
39
+ }
40
+ onShare?.(`copy_${label.toLowerCase().replace(/\s+/g, "_")}`, true);
41
+ }
42
+ catch (error) {
43
+ onShare?.(`copy_${label.toLowerCase().replace(/\s+/g, "_")}`, false);
44
+ }
45
+ finally {
46
+ setIsCopying(false);
47
+ }
48
+ }, [onShare]);
49
+ // Share to Bluesky
50
+ const shareToBluesky = (0, react_1.useCallback)(() => {
51
+ const streamUrl = getStreamUrl();
52
+ const text = profile
53
+ ? `Check out @${profile.handle} live on Streamplace! ${streamUrl}`
54
+ : `Check out this stream on Streamplace! ${streamUrl}`;
55
+ const blueskyUrl = `https://bsky.app/intent/compose?text=${encodeURIComponent(text)}`;
56
+ react_native_1.Linking.openURL(blueskyUrl);
57
+ onShare?.("share_bluesky", true);
58
+ }, [profile, getStreamUrl, onShare]);
59
+ // Share to Twitter/X
60
+ const shareToTwitter = (0, react_1.useCallback)(() => {
61
+ const streamUrl = getStreamUrl();
62
+ const text = profile
63
+ ? `Check out @${profile.handle} live on Streamplace!`
64
+ : `Check out this stream on Streamplace!`;
65
+ const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(streamUrl)}`;
66
+ react_native_1.Linking.openURL(twitterUrl);
67
+ onShare?.("share_twitter", true);
68
+ }, [profile, getStreamUrl, onShare]);
69
+ // Native share (mobile)
70
+ const nativeShare = (0, react_1.useCallback)(async () => {
71
+ const streamUrl = getStreamUrl();
72
+ const text = profile
73
+ ? `Check out @${profile.handle} live on Streamplace!`
74
+ : `Check out this stream on Streamplace!`;
75
+ if (react_native_1.Platform.OS === "web" && navigator.share) {
76
+ try {
77
+ await navigator.share({
78
+ title: "Streamplace",
79
+ text: text,
80
+ url: streamUrl,
81
+ });
82
+ onShare?.("share_native", true);
83
+ }
84
+ catch (error) {
85
+ // User cancelled or error occurred
86
+ onShare?.("share_native", false);
87
+ }
88
+ }
89
+ }, [profile, getStreamUrl, onShare]);
90
+ return ((0, jsx_runtime_1.jsxs)(ui_1.DropdownMenu, { children: [(0, jsx_runtime_1.jsx)(ui_1.DropdownMenuTrigger, { children: (0, jsx_runtime_1.jsx)(lucide_react_native_1.Share2, { color: theme_1.colors.gray[200] }) }), (0, jsx_runtime_1.jsxs)(ui_1.ResponsiveDropdownMenuContent, { children: [(0, jsx_runtime_1.jsxs)(ui_1.DropdownMenuGroup, { title: "Share", children: [(0, jsx_runtime_1.jsx)(ui_1.DropdownMenuItem, { onPress: shareToBluesky, closeOnPress: true, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { flexDirection: "row", alignItems: "center", gap: 12 }, children: [(0, jsx_runtime_1.jsx)(bluesky_icon_1.BlueskyIcon, { size: 20, color: theme_1.colors.gray[400] }), (0, jsx_runtime_1.jsx)(ui_1.Text, { children: "Share to Bluesky" })] }) }), react_native_1.Platform.OS !== "web" || (navigator && navigator.share) ? ((0, jsx_runtime_1.jsx)(ui_1.DropdownMenuItem, { onPress: nativeShare, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { flexDirection: "row", alignItems: "center", gap: 12 }, children: [(0, jsx_runtime_1.jsx)(lucide_react_native_1.Share2, { size: 20, color: theme_1.colors.gray[400] }), (0, jsx_runtime_1.jsx)(ui_1.Text, { children: "More Options..." })] }) })) : null] }), (0, jsx_runtime_1.jsxs)(ui_1.DropdownMenuGroup, { title: "Copy", children: [(0, jsx_runtime_1.jsx)(ui_1.DropdownMenuItem, { onPress: () => copyToClipboard(getStreamUrl(), "Stream link"), disabled: isCopying, closeOnPress: true, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { flexDirection: "row", alignItems: "center", gap: 12 }, children: [(0, jsx_runtime_1.jsx)(lucide_react_native_1.Link2, { size: 20, color: theme_1.colors.gray[400] }), (0, jsx_runtime_1.jsx)(ui_1.Text, { children: "Copy Link" })] }) }), (0, jsx_runtime_1.jsx)(ui_1.DropdownMenuSeparator, {}), (0, jsx_runtime_1.jsx)(ui_1.DropdownMenuItem, { onPress: () => copyToClipboard(getEmbedCode(), "Embed code"), disabled: isCopying, closeOnPress: true, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { flexDirection: "row", alignItems: "center", gap: 12 }, children: [(0, jsx_runtime_1.jsx)(lucide_react_native_1.Code, { size: 20, color: theme_1.colors.gray[400] }), (0, jsx_runtime_1.jsx)(ui_1.Text, { children: "Copy Embed Code" })] }) }), (0, jsx_runtime_1.jsx)(ui_1.DropdownMenuSeparator, {}), (0, jsx_runtime_1.jsx)(ui_1.DropdownMenuItem, { closeOnPress: true, onPress: () => copyToClipboard(getEmbedUrl(), "Embed URL"), disabled: isCopying, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { flexDirection: "row", alignItems: "center", gap: 12 }, children: [(0, jsx_runtime_1.jsx)(lucide_react_native_1.Copy, { size: 20, color: theme_1.colors.gray[400] }), (0, jsx_runtime_1.jsx)(ui_1.Text, { children: "Copy Embed URL" })] }) })] })] })] }));
91
+ }
@@ -103,7 +103,7 @@ exports.DialogFooter = (0, react_1.forwardRef)(({ children, direction = "row", j
103
103
  exports.DialogFooter.displayName = "DialogFooter";
104
104
  // Dialog Close Icon component (Lucide X)
105
105
  const DialogCloseIcon = () => {
106
- return (0, jsx_runtime_1.jsx)(ThemedX, { size: "md", variant: "muted" });
106
+ return (0, jsx_runtime_1.jsx)(ThemedX, { size: "md", variant: "default" });
107
107
  };
108
108
  // Create theme-aware styles
109
109
  function createStyles(theme) {
@@ -23,7 +23,7 @@ exports.DropdownMenuBottomSheet = (0, react_1.forwardRef)(function DropdownMenuB
23
23
  const sheetRef = (0, react_1.useRef)(null);
24
24
  return ((0, jsx_runtime_1.jsx)(DropdownMenuPrimitive.Portal, { hostName: portalHost, children: (0, jsx_runtime_1.jsx)(bottom_sheet_1.default, { ref: sheetRef,
25
25
  // why the heck is this 1-indexed
26
- index: open ? 3 : -1, snapPoints: snapPoints, enablePanDownToClose: true, onClose: () => onOpenChange?.(false), style: [overlayStyle], backgroundStyle: [atoms_1.bg.black, atoms_1.a.radius.all.md, atoms_1.a.shadows.md, atoms_1.p[1]], handleIndicatorStyle: [
26
+ index: open ? 3 : -1, snapPoints: snapPoints, enablePanDownToClose: true, enableDynamicSizing: true, enableContentPanningGesture: false, backdropComponent: ({ style }) => ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: [style, react_native_1.StyleSheet.absoluteFill], onPress: () => onOpenChange?.(false) })), onClose: () => onOpenChange?.(false), style: [overlayStyle], backgroundStyle: [atoms_1.bg.black, atoms_1.a.radius.all.md, atoms_1.a.shadows.md, atoms_1.p[1]], handleIndicatorStyle: [
27
27
  atoms_1.a.sizes.width[12],
28
28
  atoms_1.a.sizes.height[1],
29
29
  atoms_1.bg.gray[500],
@@ -129,7 +129,7 @@ exports.DropdownMenuLabel = (0, react_1.forwardRef)(({ inset, ...props }, ref) =
129
129
  ], ...props }));
130
130
  });
131
131
  exports.DropdownMenuSeparator = (0, react_1.forwardRef)((props, ref) => {
132
- return ((0, jsx_runtime_1.jsx)(react_native_1.View, { ref: ref, style: [atoms_1.mx[2], atoms_1.h[0.5] || { height: 0.5 }, atoms_1.bg.gray[800]], ...props }));
132
+ return (0, jsx_runtime_1.jsx)(react_native_1.View, { ref: ref, style: [{ height: 0.5 }, atoms_1.bg.gray[800]], ...props });
133
133
  });
134
134
  function DropdownMenuShortcut(props) {
135
135
  return ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
@@ -141,11 +141,11 @@ function DropdownMenuShortcut(props) {
141
141
  }
142
142
  exports.DropdownMenuGroup = (0, react_1.forwardRef)((props, ref) => {
143
143
  const { inset, title, children, ...rest } = props;
144
- return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [atoms_1.pt[2], inset ? atoms_1.gap[2] : atoms_1.gap[1]], ref: ref, ...rest, children: [title && ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [atoms_1.textColors.gray[400], atoms_1.pb[1], atoms_1.pl[2]], children: title })), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
144
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [atoms_1.pt[2], inset && atoms_1.gap[2]], ref: ref, ...rest, children: [title && ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [atoms_1.textColors.gray[400], atoms_1.pb[1], atoms_1.pl[2]], children: title })), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
145
145
  atoms_1.bg.gray[900],
146
- react_native_1.Platform.OS === "web" ? atoms_1.px[2] : atoms_1.p[2],
147
- atoms_1.gap[2],
148
- { borderRadius: atoms_1.borderRadius.lg, gap: 10 },
146
+ react_native_1.Platform.OS === "web" ? [atoms_1.px[2], atoms_1.py[1]] : atoms_1.p[2],
147
+ atoms_1.gap.all[1],
148
+ { borderRadius: atoms_1.borderRadius.lg },
149
149
  ], children: children })] }));
150
150
  });
151
151
  exports.DropdownMenuInfo = (0, react_1.forwardRef)(({ description, ...props }, ref) => {
@@ -15,7 +15,9 @@ tslib_1.__exportStar(require("./icons"), exports);
15
15
  tslib_1.__exportStar(require("./input"), exports);
16
16
  tslib_1.__exportStar(require("./loader"), exports);
17
17
  tslib_1.__exportStar(require("./resizeable"), exports);
18
+ tslib_1.__exportStar(require("./slider"), exports);
18
19
  tslib_1.__exportStar(require("./text"), exports);
20
+ tslib_1.__exportStar(require("./textarea"), exports);
19
21
  tslib_1.__exportStar(require("./toast"), exports);
20
22
  tslib_1.__exportStar(require("./view"), exports);
21
23
  // Component collections for easy importing
@@ -121,7 +121,6 @@ const primitiveStyles = react_native_1.StyleSheet.create({
121
121
  padding: 16,
122
122
  },
123
123
  content: {
124
- backgroundColor: "white",
125
124
  borderRadius: 8,
126
125
  overflow: "hidden",
127
126
  },
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Slider = void 0;
4
+ const tslib_1 = require("tslib");
5
+ exports.Slider = tslib_1.__importStar(require("@rn-primitives/slider"));
@@ -9,5 +9,6 @@ tslib_1.__exportStar(require("./useKeyboardSlide"), exports);
9
9
  tslib_1.__exportStar(require("./useLivestreamInfo"), exports);
10
10
  tslib_1.__exportStar(require("./useOuterAndInnerDimensions"), exports);
11
11
  tslib_1.__exportStar(require("./usePlayerDimensions"), exports);
12
+ tslib_1.__exportStar(require("./usePointerDevice"), exports);
12
13
  tslib_1.__exportStar(require("./useSegmentDimensions"), exports);
13
14
  tslib_1.__exportStar(require("./useSegmentTiming"), exports);
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.usePointerDevice = usePointerDevice;
4
+ const react_1 = require("react");
5
+ const react_native_1 = require("react-native");
6
+ /**
7
+ * Hook to detect if the device is primarily mouse-driven vs touch-driven
8
+ * Uses CSS media queries to detect hover and pointer capabilities
9
+ */
10
+ function usePointerDevice() {
11
+ const [pointerDevice, setPointerDevice] = (0, react_1.useState)(() => {
12
+ // Default values for non-web platforms
13
+ if (react_native_1.Platform.OS !== "web") {
14
+ return {
15
+ hasHover: false,
16
+ hasFinePointer: false,
17
+ isMouseDriven: false,
18
+ isTouchDriven: true,
19
+ };
20
+ }
21
+ // Initial web detection
22
+ if (typeof window !== "undefined" && window.matchMedia) {
23
+ const hasHover = window.matchMedia("(hover: hover)").matches;
24
+ const hasFinePointer = window.matchMedia("(pointer: fine)").matches;
25
+ return {
26
+ hasHover,
27
+ hasFinePointer,
28
+ isMouseDriven: hasHover && hasFinePointer,
29
+ isTouchDriven: !hasHover || !hasFinePointer,
30
+ };
31
+ }
32
+ // Fallback for SSR or environments without matchMedia
33
+ return {
34
+ hasHover: false,
35
+ hasFinePointer: false,
36
+ isMouseDriven: false,
37
+ isTouchDriven: true,
38
+ };
39
+ });
40
+ (0, react_1.useEffect)(() => {
41
+ // Only run on web platforms
42
+ if (react_native_1.Platform.OS !== "web" ||
43
+ typeof window === "undefined" ||
44
+ !window.matchMedia) {
45
+ return;
46
+ }
47
+ const hoverQuery = window.matchMedia("(hover: hover)");
48
+ const pointerQuery = window.matchMedia("(pointer: fine)");
49
+ const updatePointerDevice = () => {
50
+ const hasHover = hoverQuery.matches;
51
+ const hasFinePointer = pointerQuery.matches;
52
+ setPointerDevice({
53
+ hasHover,
54
+ hasFinePointer,
55
+ isMouseDriven: hasHover && hasFinePointer,
56
+ isTouchDriven: !hasHover || !hasFinePointer,
57
+ });
58
+ };
59
+ // Set up listeners for media query changes
60
+ hoverQuery.addEventListener("change", updatePointerDevice);
61
+ pointerQuery.addEventListener("change", updatePointerDevice);
62
+ // Initial update
63
+ updatePointerDevice();
64
+ // Cleanup
65
+ return () => {
66
+ hoverQuery.removeEventListener("change", updatePointerDevice);
67
+ pointerQuery.removeEventListener("change", updatePointerDevice);
68
+ };
69
+ }, []);
70
+ return pointerDevice;
71
+ }
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.atoms = exports.theme = exports.ui = exports.PlayerUI = exports.Player = exports.usePlayerContext = exports.withPlayerProvider = exports.PlayerProvider = void 0;
3
+ exports.VideoRetry = exports.zero = exports.ui = exports.PlayerUI = exports.Player = exports.usePlayerContext = exports.withPlayerProvider = exports.PlayerProvider = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  // barrel file :)
6
6
  tslib_1.__exportStar(require("./livestream-provider"), exports);
@@ -18,8 +18,15 @@ Object.defineProperty(exports, "Player", { enumerable: true, get: function () {
18
18
  Object.defineProperty(exports, "PlayerUI", { enumerable: true, get: function () { return player_1.PlayerUI; } });
19
19
  exports.ui = tslib_1.__importStar(require("./components/ui"));
20
20
  tslib_1.__exportStar(require("./components/ui"), exports);
21
- exports.theme = tslib_1.__importStar(require("./lib/theme"));
22
- exports.atoms = tslib_1.__importStar(require("./lib/theme/atoms"));
21
+ exports.zero = tslib_1.__importStar(require("./ui"));
23
22
  tslib_1.__exportStar(require("./hooks"), exports);
23
+ // Theme system exports
24
+ tslib_1.__exportStar(require("./lib/theme"), exports);
24
25
  tslib_1.__exportStar(require("./components/chat/chat"), exports);
25
26
  tslib_1.__exportStar(require("./components/chat/chat-box"), exports);
27
+ tslib_1.__exportStar(require("./components/chat/system-message"), exports);
28
+ var video_retry_1 = require("./components/mobile-player/video-retry");
29
+ Object.defineProperty(exports, "VideoRetry", { enumerable: true, get: function () { return tslib_1.__importDefault(video_retry_1).default; } });
30
+ tslib_1.__exportStar(require("./lib/system-messages"), exports);
31
+ tslib_1.__exportStar(require("./components/share/sharesheet"), exports);
32
+ tslib_1.__exportStar(require("./components/keep-awake"), exports);
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSystemMessageMetadata = exports.getSystemMessageType = exports.isSystemMessage = exports.SystemMessages = exports.createSystemMessage = exports.SystemMessageType = void 0;
4
+ var SystemMessageType;
5
+ (function (SystemMessageType) {
6
+ SystemMessageType["stream_start"] = "stream_start";
7
+ SystemMessageType["stream_end"] = "stream_end";
8
+ SystemMessageType["notification"] = "notification";
9
+ })(SystemMessageType || (exports.SystemMessageType = SystemMessageType = {}));
10
+ /**
11
+ * Creates a system message with the proper structure
12
+ * @param type The type of system message
13
+ * @param text The message text
14
+ * @param metadata Optional metadata for the message
15
+ * @returns A properly formatted ChatMessageViewHydrated object
16
+ */
17
+ const createSystemMessage = (type, text, metadata, date = new Date()) => {
18
+ const now = date;
19
+ return {
20
+ uri: `at://did:sys:system/place.stream.chat.message/${now.getTime()}`,
21
+ cid: `system-${now.getTime()}`,
22
+ author: {
23
+ did: "did:sys:system",
24
+ handle: type, // Use handle to specify the type of system message
25
+ },
26
+ record: {
27
+ text,
28
+ createdAt: now.toISOString(),
29
+ streamer: "system",
30
+ $type: "place.stream.chat.message",
31
+ },
32
+ indexedAt: now.toISOString(),
33
+ chatProfile: {
34
+ color: { red: 128, green: 128, blue: 128 }, // Gray color for system messages
35
+ },
36
+ };
37
+ };
38
+ exports.createSystemMessage = createSystemMessage;
39
+ /**
40
+ * System message factory functions for common scenarios
41
+ */
42
+ exports.SystemMessages = {
43
+ streamStart: (streamerName) => (0, exports.createSystemMessage)(SystemMessageType.stream_start, `Now streaming - ${streamerName}`, {
44
+ streamerName,
45
+ }),
46
+ // technically, streams can't 'end' on Streamplace
47
+ // possibly we could use deleting or editing streams (`endedAt` param) for this?
48
+ streamEnd: (duration) => (0, exports.createSystemMessage)(SystemMessageType.stream_end, duration ? `Stream has ended. Duration: ${duration}` : "Stream has ended", { duration }),
49
+ notification: (message) => (0, exports.createSystemMessage)(SystemMessageType.notification, message),
50
+ };
51
+ /**
52
+ * Checks if a message is a system message
53
+ * @param message The message to check
54
+ * @returns True if the message is a system message
55
+ */
56
+ const isSystemMessage = (message) => {
57
+ return message.author.did === "did:sys:system";
58
+ };
59
+ exports.isSystemMessage = isSystemMessage;
60
+ /**
61
+ * Gets the system message type from a message
62
+ * @param message The message to check
63
+ * @returns The system message type or null if not a system message
64
+ */
65
+ const getSystemMessageType = (message) => {
66
+ if (!(0, exports.isSystemMessage)(message)) {
67
+ return null;
68
+ }
69
+ return message.author.handle;
70
+ };
71
+ exports.getSystemMessageType = getSystemMessageType;
72
+ /**
73
+ * Parses metadata from a system message based on its type
74
+ * @param message The system message to parse
75
+ * @returns The parsed metadata
76
+ */
77
+ const parseSystemMessageMetadata = (message) => {
78
+ const metadata = {};
79
+ const type = (0, exports.getSystemMessageType)(message);
80
+ const text = message.record.text;
81
+ if (!type)
82
+ return metadata;
83
+ switch (type) {
84
+ case "stream_end": {
85
+ const durationMatch = text.match(/Duration:\s*(\d+:\d+(?::\d+)?)/);
86
+ if (durationMatch) {
87
+ metadata.duration = durationMatch[1];
88
+ }
89
+ break;
90
+ }
91
+ case "stream_start": {
92
+ const streamerMatch = text.match(/^(.+?)\s+is now live!/);
93
+ if (streamerMatch) {
94
+ metadata.streamerName = streamerMatch[1];
95
+ }
96
+ break;
97
+ }
98
+ }
99
+ return metadata;
100
+ };
101
+ exports.parseSystemMessageMetadata = parseSystemMessageMetadata;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.reduceChat = exports.reduceChatIncremental = exports.useCreateChatMessage = exports.useSetReplyToMessage = exports.useReplyToMessage = void 0;
3
+ exports.reduceChat = exports.useReportChatMessage = exports.useSubmitReport = exports.reduceChatIncremental = exports.useCreateChatMessage = exports.useAddPendingHide = exports.usePendingHides = exports.useSetReplyToMessage = exports.useReplyToMessage = void 0;
4
4
  const api_1 = require("@atproto/api");
5
5
  const react_1 = require("react");
6
6
  const streamplace_store_1 = require("../streamplace-store");
@@ -15,6 +15,23 @@ const useSetReplyToMessage = () => {
15
15
  }, [store]);
16
16
  };
17
17
  exports.useSetReplyToMessage = useSetReplyToMessage;
18
+ const usePendingHides = () => (0, livestream_store_1.useLivestreamStore)((state) => state.pendingHides);
19
+ exports.usePendingHides = usePendingHides;
20
+ const useAddPendingHide = () => {
21
+ const store = (0, livestream_store_1.getStoreFromContext)();
22
+ return (0, react_1.useCallback)((messageUri) => {
23
+ const state = store.getState();
24
+ if (!state.pendingHides.includes(messageUri)) {
25
+ const newPendingHides = [...state.pendingHides, messageUri];
26
+ const newState = (0, exports.reduceChat)(state, [], [], [messageUri]);
27
+ store.setState({
28
+ ...newState,
29
+ pendingHides: newPendingHides,
30
+ });
31
+ }
32
+ }, [store]);
33
+ };
34
+ exports.useAddPendingHide = useAddPendingHide;
18
35
  const useCreateChatMessage = () => {
19
36
  const pdsAgent = (0, xrpc_1.usePDSAgent)();
20
37
  const store = (0, livestream_store_1.getStoreFromContext)();
@@ -63,7 +80,7 @@ const useCreateChatMessage = () => {
63
80
  indexedAt: new Date().toISOString(),
64
81
  chatProfile: chatProfile || undefined,
65
82
  };
66
- state = (0, exports.reduceChat)(state, [localChat], []);
83
+ state = (0, exports.reduceChat)(state, [localChat], [], []);
67
84
  store.setState(state);
68
85
  await pdsAgent.com.atproto.repo.createRecord({
69
86
  repo: userDID,
@@ -79,7 +96,9 @@ const buildSortedChatList = (chatIndex, existingChatList, newMessages, removedKe
79
96
  const bTime = parseInt(b.split("-")[0], 10);
80
97
  return bTime - aTime;
81
98
  });
82
- return sortedKeys.map((key) => chatIndex[key]);
99
+ return sortedKeys
100
+ .map((key) => chatIndex[key])
101
+ .filter((msg) => !removedKeys.has(msg.uri));
83
102
  };
84
103
  const profileIsDifferent = (newProfile, oldProfile) => {
85
104
  if (!oldProfile) {
@@ -99,14 +118,25 @@ const profileIsDifferent = (newProfile, oldProfile) => {
99
118
  const { red: oldRed, green: oldGreen, blue: oldBlue } = oldProfile.color;
100
119
  return newRed !== oldRed || newGreen !== oldGreen || newBlue !== oldBlue;
101
120
  };
102
- const reduceChatIncremental = (state, newMessages, blocks) => {
103
- if (newMessages.length === 0 && blocks.length === 0) {
121
+ const reduceChatIncremental = (state, newMessages, blocks, hideUris = []) => {
122
+ if (newMessages.length === 0 &&
123
+ blocks.length === 0 &&
124
+ hideUris.length === 0) {
104
125
  return state;
105
126
  }
106
127
  const newChatIndex = { ...state.chatIndex };
107
128
  const newAuthors = { ...state.authors };
108
129
  let hasChanges = false;
109
130
  const removedKeys = new Set();
131
+ console.log("newMessages", newMessages);
132
+ for (const msg of newMessages) {
133
+ if (msg.deleted) {
134
+ hasChanges = true;
135
+ console.log("deleted", msg.uri);
136
+ removedKeys.add(msg.uri);
137
+ }
138
+ }
139
+ newMessages = newMessages.filter((msg) => msg.deleted !== true);
110
140
  // handle blocks
111
141
  if (blocks.length > 0) {
112
142
  const blockedDIDs = new Set(blocks.map((block) => block.record.subject));
@@ -118,13 +148,26 @@ const reduceChatIncremental = (state, newMessages, blocks) => {
118
148
  }
119
149
  }
120
150
  }
151
+ if (hideUris.length > 0) {
152
+ for (const [key, message] of Object.entries(newChatIndex)) {
153
+ if (hideUris.includes(message.uri)) {
154
+ delete newChatIndex[key];
155
+ removedKeys.add(key);
156
+ hasChanges = true;
157
+ }
158
+ }
159
+ }
121
160
  const messagesToAdd = [];
122
161
  for (const message of newMessages) {
162
+ // don't worry about messages that will be hidden
163
+ if (state.pendingHides.includes(message.uri)) {
164
+ continue;
165
+ }
123
166
  const date = new Date(message.record.createdAt);
124
167
  const key = `${date.getTime()}-${message.uri}`;
125
168
  // only change the ref if the profile is different to avoid re-renders elsewhere
126
- if (profileIsDifferent(message.chatProfile, newAuthors[message.author.handle])) {
127
- newAuthors[message.author.handle] = message.chatProfile;
169
+ if (profileIsDifferent(message.chatProfile, newAuthors[message.author.did])) {
170
+ newAuthors[message.author.did] = message.chatProfile;
128
171
  }
129
172
  // skip messages we already have
130
173
  if (newChatIndex[key] && newChatIndex[key].uri === message.uri) {
@@ -156,17 +199,20 @@ const reduceChatIncremental = (state, newMessages, blocks) => {
156
199
  const parentMsgKey = Object.keys(newChatIndex).find((k) => newChatIndex[k].uri === parentUri);
157
200
  if (parentMsgKey) {
158
201
  const parentMsg = newChatIndex[parentMsgKey];
159
- processedMessage = {
160
- ...message,
161
- replyTo: {
162
- cid: parentMsg.cid,
163
- uri: parentMsg.uri,
164
- author: parentMsg.author,
165
- record: parentMsg.record,
166
- chatProfile: parentMsg.chatProfile,
167
- indexedAt: parentMsg.indexedAt,
168
- },
169
- };
202
+ // Don't allow replies to system messages
203
+ if (parentMsg.author.did !== "did:sys:system") {
204
+ processedMessage = {
205
+ ...message,
206
+ replyTo: {
207
+ cid: parentMsg.cid,
208
+ uri: parentMsg.uri,
209
+ author: parentMsg.author,
210
+ record: parentMsg.record,
211
+ chatProfile: parentMsg.chatProfile,
212
+ indexedAt: parentMsg.indexedAt,
213
+ },
214
+ };
215
+ }
170
216
  }
171
217
  }
172
218
  }
@@ -183,11 +229,58 @@ const reduceChatIncremental = (state, newMessages, blocks) => {
183
229
  }
184
230
  // Build the new sorted chat list efficiently
185
231
  const newChatList = buildSortedChatList(newChatIndex, state.chat, messagesToAdd, removedKeys);
232
+ // Clean up pendingHides - remove URIs that we've now processed
233
+ let newPendingHides = state.pendingHides;
234
+ if (hideUris.length > 0) {
235
+ newPendingHides = state.pendingHides.filter((uri) => !hideUris.includes(uri));
236
+ }
186
237
  return {
187
238
  ...state,
239
+ authors: newAuthors,
188
240
  chatIndex: newChatIndex,
189
241
  chat: newChatList,
242
+ pendingHides: newPendingHides,
190
243
  };
191
244
  };
192
245
  exports.reduceChatIncremental = reduceChatIncremental;
246
+ const useSubmitReport = () => {
247
+ const pdsAgent = (0, xrpc_1.usePDSAgent)();
248
+ const userDID = (0, streamplace_store_1.useDID)();
249
+ return (0, react_1.useCallback)(async (subject, reasonType, reason,
250
+ // no clue about this
251
+ moderationSvcDid = "did:web:stream.place") => {
252
+ if (!pdsAgent || !userDID) {
253
+ throw new Error("No PDS agent or user DID found");
254
+ }
255
+ try {
256
+ const response = await pdsAgent.com.atproto.moderation.createReport({
257
+ reasonType,
258
+ reason,
259
+ subject: subject,
260
+ }, {
261
+ headers: {
262
+ // "atproto-proxy": `${userDID}#atproto_labeler`,
263
+ },
264
+ });
265
+ return response;
266
+ }
267
+ catch (error) {
268
+ console.error("Failed to submit report:", error);
269
+ throw error;
270
+ }
271
+ }, [pdsAgent, userDID]);
272
+ };
273
+ exports.useSubmitReport = useSubmitReport;
274
+ const useReportChatMessage = () => {
275
+ const submitReport = (0, exports.useSubmitReport)();
276
+ return (0, react_1.useCallback)(async (message, reasonType, reason) => {
277
+ const reportSubject = {
278
+ $type: "com.atproto.repo.strongRef",
279
+ uri: message.uri,
280
+ cid: message.cid,
281
+ };
282
+ return await submitReport(reportSubject, reasonType, reason);
283
+ }, [submitReport]);
284
+ };
285
+ exports.useReportChatMessage = useReportChatMessage;
193
286
  exports.reduceChat = exports.reduceChatIncremental;
@@ -14,12 +14,15 @@ const makeLivestreamStore = () => {
14
14
  chat: [],
15
15
  livestream: null,
16
16
  viewers: null,
17
+ pendingHides: [],
17
18
  segment: null,
18
19
  renditions: [],
19
20
  replyToMessage: null,
20
21
  streamKey: null,
21
22
  setStreamKey: (sk) => set({ streamKey: sk }),
22
23
  authors: {},
24
+ recentSegments: [],
25
+ problems: [],
23
26
  }));
24
27
  };
25
28
  exports.makeLivestreamStore = makeLivestreamStore;