@streamplace/components 0.7.19 → 0.7.25

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 (36) hide show
  1. package/dist/components/chat/chat-box.js +5 -0
  2. package/dist/components/chat/chat-message.js +5 -4
  3. package/dist/components/chat/chat.js +14 -4
  4. package/dist/components/chat/mod-view.js +19 -1
  5. package/dist/components/mobile-player/fullscreen.js +2 -0
  6. package/dist/components/mobile-player/ui/autoplay-button.js +68 -0
  7. package/dist/components/mobile-player/ui/index.js +1 -0
  8. package/dist/components/mobile-player/video.js +11 -1
  9. package/dist/components/mobile-player/webrtc-diagnostics.js +67 -13
  10. package/dist/lib/system-messages.js +1 -0
  11. package/dist/livestream-store/chat.js +25 -1
  12. package/dist/livestream-store/stream-key.js +1 -0
  13. package/dist/livestream-store/websocket-consumer.js +4 -1
  14. package/dist/player-store/player-provider.js +2 -1
  15. package/dist/player-store/player-store.js +2 -0
  16. package/dist/streamplace-store/stream.js +2 -0
  17. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  18. package/package.json +4 -4
  19. package/src/components/chat/chat-box.tsx +3 -0
  20. package/src/components/chat/chat-message.tsx +5 -4
  21. package/src/components/chat/chat.tsx +20 -4
  22. package/src/components/chat/mod-view.tsx +39 -5
  23. package/src/components/mobile-player/fullscreen.tsx +2 -0
  24. package/src/components/mobile-player/ui/autoplay-button.tsx +86 -0
  25. package/src/components/mobile-player/ui/index.ts +1 -0
  26. package/src/components/mobile-player/video.tsx +11 -1
  27. package/src/components/mobile-player/webrtc-diagnostics.tsx +73 -15
  28. package/src/lib/system-messages.ts +1 -0
  29. package/src/livestream-store/chat.tsx +24 -0
  30. package/src/livestream-store/stream-key.tsx +1 -0
  31. package/src/livestream-store/websocket-consumer.tsx +4 -1
  32. package/src/player-store/player-provider.tsx +2 -1
  33. package/src/player-store/player-state.tsx +6 -0
  34. package/src/player-store/player-store.tsx +4 -0
  35. package/src/streamplace-store/stream.tsx +2 -0
  36. package/tsconfig.tsbuildinfo +1 -1
@@ -42,6 +42,11 @@ function ChatBox({ isPopout, chatBoxStyle, emojiData, setIsChatVisible, }) {
42
42
  if (!chat)
43
43
  return null;
44
44
  return chat.reduce((acc, msg) => {
45
+ // our fake system user "did"
46
+ if (msg.author.did === "did:sys:system")
47
+ return acc;
48
+ if (acc.has(msg.author.handle))
49
+ return acc;
45
50
  acc.set(msg.author.handle, msg.chatProfile);
46
51
  return acc;
47
52
  }, new Map());
@@ -48,7 +48,8 @@ exports.RenderChatMessage = (0, react_1.memo)(function RenderChatMessage({ item,
48
48
  hour12: false,
49
49
  });
50
50
  }, []);
51
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [item.replyTo && showReply && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
51
+ const replyTo = item.replyTo || null;
52
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [replyTo && showReply && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
52
53
  atoms_1.gap.all[2],
53
54
  ui_1.layout.flex.row,
54
55
  { minWidth: 0, maxWidth: "100%" },
@@ -62,12 +63,12 @@ exports.RenderChatMessage = (0, react_1.memo)(function RenderChatMessage({ item,
62
63
  atoms_1.mr[4],
63
64
  { minWidth: 0, overflow: "hidden" },
64
65
  ], children: [(0, jsx_runtime_1.jsxs)(text_1.Text, { style: {
65
- color: getRgbColor(item.replyTo.chatProfile.color),
66
+ color: getRgbColor(replyTo.chatProfile?.color),
66
67
  fontWeight: "thin",
67
- }, children: ["@", item.replyTo.author.handle] }), " ", (0, jsx_runtime_1.jsx)(text_1.Text, { style: {
68
+ }, children: ["@", replyTo.author.handle] }), " ", (0, jsx_runtime_1.jsx)(text_1.Text, { style: {
68
69
  color: ui_1.colors.gray[300],
69
70
  fontStyle: "italic",
70
- }, children: item.replyTo.record.text })] }) })), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
71
+ }, children: replyTo.record.text })] }) })), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
71
72
  atoms_1.gap.all[2],
72
73
  ui_1.layout.flex.row,
73
74
  { minWidth: 0, maxWidth: "100%" },
@@ -122,9 +122,7 @@ const ChatLine = (0, react_1.memo)(({ item, canModerate, }) => {
122
122
  isHovered && atoms_1.bg.gray[950],
123
123
  ], onPointerEnter: handleHoverIn, onPointerLeave: handleHoverOut, children: [(0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: [{ minWidth: 0, maxWidth: "100%" }], children: (0, jsx_runtime_1.jsx)(chat_message_1.RenderChatMessage, { item: item }) }), (0, jsx_runtime_1.jsx)(ActionsBar, { item: item, visible: isHovered, hoverTimeoutRef: hoverTimeoutRef })] }));
124
124
  }
125
- return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsx)(ReanimatedSwipeable_1.default, { containerStyle: [atoms_1.py[1]], friction: 2, enableTrackpadTwoFingerGesture: true, rightThreshold: 40, leftThreshold: 40, renderRightActions: react_native_1.Platform.OS === "android" ? undefined : RightAction, renderLeftActions: react_native_1.Platform.OS === "android" ? undefined : LeftAction, overshootFriction: 9, ref: (ref) => {
126
- swipeableRef.current = ref;
127
- }, onSwipeableOpen: (r) => {
125
+ return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsx)(ReanimatedSwipeable_1.default, { containerStyle: [atoms_1.py[1]], friction: 2, enableTrackpadTwoFingerGesture: true, rightThreshold: 40, leftThreshold: 40, renderRightActions: react_native_1.Platform.OS === "android" ? undefined : RightAction, renderLeftActions: react_native_1.Platform.OS === "android" ? undefined : LeftAction, overshootFriction: 9, ref: swipeableRef, onSwipeableOpen: (r) => {
128
126
  if (r === (react_native_1.Platform.OS === "android" ? "right" : "left")) {
129
127
  setReply(item);
130
128
  }
@@ -140,11 +138,23 @@ const ChatLine = (0, react_1.memo)(({ item, canModerate, }) => {
140
138
  });
141
139
  function Chat({ shownMessages = SHOWN_MSGS, style: propsStyle, canModerate = false, ...props }) {
142
140
  const chat = (0, __1.useChat)();
141
+ const [isScrolledUp, setIsScrolledUp] = (0, react_1.useState)(false);
142
+ const handleScroll = (event) => {
143
+ const { contentOffset } = event.nativeEvent;
144
+ const scrolledUp = contentOffset.y > 20; // threshold
145
+ if (scrolledUp !== isScrolledUp) {
146
+ setIsScrolledUp(scrolledUp);
147
+ // Dismiss keyboard when scrolled up
148
+ if (scrolledUp && react_native_1.Platform.OS !== "web") {
149
+ react_native_1.Keyboard.dismiss();
150
+ }
151
+ }
152
+ };
143
153
  if (!chat)
144
154
  return ((0, jsx_runtime_1.jsx)(__1.View, { style: [atoms_1.flex.shrink[1], { minWidth: 0, maxWidth: "100%" }], children: (0, jsx_runtime_1.jsx)(__1.Text, { children: "Loading chaat..." }) }));
145
155
  return ((0, jsx_runtime_1.jsxs)(__1.View, { style: [atoms_1.flex.shrink[1], { minWidth: 0, maxWidth: "100%" }].concat(propsStyle || []), children: [(0, jsx_runtime_1.jsx)(react_native_gesture_handler_1.FlatList, { style: [
146
156
  atoms_1.flex.grow[1],
147
157
  atoms_1.flex.shrink[1],
148
158
  { minWidth: 0, maxWidth: "100%" },
149
- ], data: chat.slice(0, shownMessages), inverted: true, keyExtractor: keyExtractor, renderItem: ({ item, index }) => ((0, jsx_runtime_1.jsx)(ChatLine, { item: item, canModerate: canModerate })), removeClippedSubviews: true, maxToRenderPerBatch: 10, initialNumToRender: 10, updateCellsBatchingPeriod: 50 }), (0, jsx_runtime_1.jsx)(mod_view_1.ModView, {})] }));
159
+ ], data: chat.slice(0, shownMessages), inverted: true, keyExtractor: keyExtractor, renderItem: ({ item, index }) => ((0, jsx_runtime_1.jsx)(ChatLine, { item: item, canModerate: canModerate })), removeClippedSubviews: true, maxToRenderPerBatch: 10, initialNumToRender: 10, updateCellsBatchingPeriod: 50, onScroll: handleScroll, scrollEventThrottle: 16 }), (0, jsx_runtime_1.jsx)(mod_view_1.ModView, {})] }));
150
160
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ModView = void 0;
4
+ exports.DeleteButton = DeleteButton;
4
5
  exports.ReportButton = ReportButton;
5
6
  const jsx_runtime_1 = require("react/jsx-runtime");
6
7
  const dropdown_menu_1 = require("@rn-primitives/dropdown-menu");
@@ -10,6 +11,7 @@ const player_store_1 = require("../../player-store");
10
11
  const block_1 = require("../../streamplace-store/block");
11
12
  const xrpc_1 = require("../../streamplace-store/xrpc");
12
13
  const react_native_1 = require("react-native");
14
+ const livestream_store_1 = require("../../livestream-store");
13
15
  const streamplace_store_1 = require("../../streamplace-store");
14
16
  const ui_1 = require("../ui");
15
17
  const BSKY_FRONTEND_DOMAIN = "bsky.app";
@@ -75,8 +77,24 @@ exports.ModView = (0, react_1.forwardRef)(() => {
75
77
  ? "Blocking..."
76
78
  : `Block user @${message.author.handle} from this channel` })) })] })), (0, jsx_runtime_1.jsxs)(ui_1.DropdownMenuGroup, { title: `User actions`, children: [(0, jsx_runtime_1.jsx)(ui_1.DropdownMenuItem, { onPress: () => {
77
79
  react_native_1.Linking.openURL(`https://${BSKY_FRONTEND_DOMAIN}/profile/${message.author.handle}`);
78
- }, children: (0, jsx_runtime_1.jsxs)(ui_1.Text, { color: "primary", children: ["View user on ", BSKY_FRONTEND_DOMAIN] }) }), (0, jsx_runtime_1.jsx)(ReportButton, { message: message, setReportModalOpen: setReportModalOpen, setReportSubject: setReportSubject })] })] })) })] }));
80
+ }, children: (0, jsx_runtime_1.jsxs)(ui_1.Text, { color: "primary", children: ["View user on ", BSKY_FRONTEND_DOMAIN] }) }), message.author.did === agent?.did && ((0, jsx_runtime_1.jsx)(DeleteButton, { message: message })), message.author.did !== agent?.did && ((0, jsx_runtime_1.jsx)(ReportButton, { message: message, setReportModalOpen: setReportModalOpen, setReportSubject: setReportSubject }))] })] })) })] }));
79
81
  });
82
+ function DeleteButton({ message, }) {
83
+ const deleteChatMessage = (0, livestream_store_1.useDeleteChatMessage)();
84
+ const [confirming, setConfirming] = (0, react_1.useState)(false);
85
+ const { onOpenChange } = (0, dropdown_menu_1.useRootContext)();
86
+ return ((0, jsx_runtime_1.jsx)(ui_1.DropdownMenuItem, { onPress: () => {
87
+ if (!message)
88
+ return;
89
+ if (!confirming) {
90
+ setConfirming(true);
91
+ return;
92
+ }
93
+ deleteChatMessage(message.uri).then(() => {
94
+ onOpenChange?.(false);
95
+ });
96
+ }, children: (0, jsx_runtime_1.jsx)(ui_1.Text, { color: "destructive", children: confirming ? "Are you sure?" : "Delete message" }) }));
97
+ }
80
98
  function ReportButton({ message, setReportModalOpen, setReportSubject, }) {
81
99
  const { onOpenChange } = (0, dropdown_menu_1.useRootContext)();
82
100
  return ((0, jsx_runtime_1.jsx)(ui_1.DropdownMenuItem, { onPress: () => {
@@ -14,10 +14,12 @@ function Fullscreen(props) {
14
14
  const fullscreen = (0, __1.usePlayerStore)((x) => x.fullscreen, playerId);
15
15
  const setFullscreen = (0, __1.usePlayerStore)((x) => x.setFullscreen, playerId);
16
16
  const setSrc = (0, __1.usePlayerStore)((x) => x.setSrc);
17
+ const setAutoplayFailed = (0, __1.usePlayerStore)((x) => x.setAutoplayFailed);
17
18
  const divRef = (0, react_1.useRef)(null);
18
19
  const videoRef = (0, react_1.useRef)(null);
19
20
  (0, react_1.useEffect)(() => {
20
21
  setSrc(props.src);
22
+ setAutoplayFailed(false);
21
23
  }, [props.src]);
22
24
  (0, react_1.useEffect)(() => {
23
25
  if (!divRef.current) {
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AutoplayButton = AutoplayButton;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const lucide_react_native_1 = require("lucide-react-native");
6
+ const react_native_1 = require("react-native");
7
+ const __1 = require("../../..");
8
+ const ui_1 = require("../../../ui");
9
+ function AutoplayButton() {
10
+ const autoplayFailed = (0, __1.usePlayerStore)((x) => x.autoplayFailed);
11
+ const setAutoplayFailed = (0, __1.usePlayerStore)((x) => x.setAutoplayFailed);
12
+ const setMuted = (0, __1.usePlayerStore)((x) => x.setMuted);
13
+ const setMuteWasForced = (0, __1.usePlayerStore)((x) => x.setMuteWasForced);
14
+ const setUserInteraction = (0, __1.usePlayerStore)((x) => x.setUserInteraction);
15
+ const videoRef = (0, __1.usePlayerStore)((x) => x.videoRef);
16
+ const handlePlayButtonPress = () => {
17
+ if (videoRef && typeof videoRef === "object" && videoRef.current) {
18
+ videoRef.current
19
+ .play()
20
+ .then(() => {
21
+ setAutoplayFailed(false);
22
+ setUserInteraction();
23
+ })
24
+ .catch((err) => {
25
+ console.error("Manual play failed", err);
26
+ if (err.name === "NotAllowedError") {
27
+ setMuted(true);
28
+ videoRef.current.muted = true;
29
+ videoRef
30
+ .current.play()
31
+ .then(() => {
32
+ setAutoplayFailed(false);
33
+ setMuteWasForced(true);
34
+ setUserInteraction();
35
+ })
36
+ .catch((err) => {
37
+ console.error("Manual muted play also failed", err);
38
+ });
39
+ }
40
+ });
41
+ }
42
+ };
43
+ if (!autoplayFailed)
44
+ return null;
45
+ return ((0, jsx_runtime_1.jsx)(__1.View, { style: [
46
+ __1.layout.position.absolute,
47
+ __1.layout.flex.center,
48
+ ui_1.h.percent[100],
49
+ ui_1.w.percent[100],
50
+ ], children: (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: handlePlayButtonPress, style: [
51
+ {
52
+ flexDirection: "column",
53
+ alignItems: "center",
54
+ justifyContent: "center",
55
+ gap: 8,
56
+ },
57
+ ], children: (0, jsx_runtime_1.jsx)(__1.View, { style: [
58
+ ui_1.p[4],
59
+ {
60
+ backgroundColor: "rgba(200,200,255, 0.1)",
61
+ borderRadius: 999,
62
+ borderWidth: 2,
63
+ borderColor: "rgba(200,200,255, 0.45)",
64
+ boxShadow: "0 0px 4px rgba(0, 0, 0, 1)",
65
+ shadowColor: "rgba(0, 0, 0, 1)",
66
+ },
67
+ ], children: (0, jsx_runtime_1.jsx)(lucide_react_native_1.Play, { size: "48", color: "rgba(120,120,120,0.3)", fill: "rgba(200,200,255,1)" }) }) }) }));
68
+ }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./autoplay-button"), exports);
4
5
  tslib_1.__exportStar(require("./countdown"), exports);
5
6
  tslib_1.__exportStar(require("./input"), exports);
6
7
  tslib_1.__exportStar(require("./metrics"), exports);
@@ -107,6 +107,7 @@ const VideoElement = (0, react_1.forwardRef)((props, ref) => {
107
107
  playerEvent(url, now.toISOString(), evType, {});
108
108
  };
109
109
  const [firstAttempt, setFirstAttempt] = (0, react_1.useState)(true);
110
+ const setAutoplayFailed = (0, __1.usePlayerStore)((x) => x.setAutoplayFailed);
110
111
  const localVideoRef = props.videoRef ?? (0, react_1.useRef)(null);
111
112
  // setPipAction comes from Zustand store
112
113
  (0, react_1.useEffect)(() => {
@@ -158,12 +159,21 @@ const VideoElement = (0, react_1.forwardRef)((props, ref) => {
158
159
  })
159
160
  .catch((err) => {
160
161
  console.error("Muted play also failed", err);
162
+ setAutoplayFailed(true);
161
163
  });
162
164
  }
163
165
  }
166
+ else {
167
+ // For other errors (not NotAllowedError), also show play button
168
+ setAutoplayFailed(true);
169
+ }
164
170
  });
165
171
  }
166
172
  };
173
+ const handlePlaying = (e) => {
174
+ setAutoplayFailed(false);
175
+ event("playing")(e);
176
+ };
167
177
  (0, react_1.useEffect)(() => {
168
178
  return () => {
169
179
  setStatus(__1.PlayerStatus.START);
@@ -198,7 +208,7 @@ const VideoElement = (0, react_1.forwardRef)((props, ref) => {
198
208
  console.log("Sending", evType, "status to", url);
199
209
  playerEvent(url, now.toISOString(), evType, {});
200
210
  };
201
- return ((0, jsx_runtime_1.jsx)("video", { autoPlay: true, playsInline: true, ref: handleVideoRef, controls: false, src: ingest ? undefined : props.url, muted: muted, crossOrigin: "anonymous", onMouseMove: setUserInteraction, onClick: setUserInteraction, onAbort: event("abort"), onCanPlay: eventLogger, onCanPlayThroughCapture: eventLogger, onCanPlayThrough: canPlayThrough, onEmptied: event("emptied"), onEncrypted: event("encrypted"), onEnded: event("ended"), onError: event("error"), onLoadedData: event("loadeddata"), onLoadedMetadata: event("loadedmetadata"), onLoadStart: event("loadstart"), onPause: event("pause"), onPlay: event("play"), onPlaying: event("playing"), onRateChange: event("ratechange"), onSeeked: event("seeked"), onSeeking: event("seeking"), onStalled: event("stalled"), onSuspend: event("suspend"), onVolumeChange: event("volumechange"), onWaiting: event("waiting"), style: {
211
+ return ((0, jsx_runtime_1.jsx)("video", { autoPlay: true, playsInline: true, ref: handleVideoRef, controls: false, src: ingest ? undefined : props.url, muted: muted, crossOrigin: "anonymous", onMouseMove: setUserInteraction, onClick: setUserInteraction, onAbort: event("abort"), onCanPlay: eventLogger, onCanPlayThroughCapture: eventLogger, onCanPlayThrough: canPlayThrough, onEmptied: event("emptied"), onEncrypted: event("encrypted"), onEnded: event("ended"), onError: event("error"), onLoadedData: event("loadeddata"), onLoadedMetadata: event("loadedmetadata"), onLoadStart: event("loadstart"), onPause: event("pause"), onPlay: event("play"), onPlaying: handlePlaying, onRateChange: event("ratechange"), onSeeked: event("seeked"), onSeeking: event("seeking"), onStalled: event("stalled"), onSuspend: event("suspend"), onVolumeChange: event("volumechange"), onWaiting: event("waiting"), style: {
202
212
  objectFit: props.objectFit || "contain",
203
213
  backgroundColor: "transparent",
204
214
  width: "100%",
@@ -11,12 +11,29 @@ function useWebRTCDiagnostics() {
11
11
  rtcSessionDescription: false,
12
12
  getUserMedia: false,
13
13
  getDisplayMedia: false,
14
+ isHwH264Supported: false,
14
15
  errors: [],
15
16
  warnings: [],
16
17
  });
17
18
  (0, react_1.useEffect)(() => {
18
19
  const errors = [];
19
20
  const warnings = [];
21
+ const checkH264Support = async () => {
22
+ try {
23
+ const pc = new RTCPeerConnection();
24
+ const offer = await pc.createOffer();
25
+ pc.close();
26
+ if (offer.sdp) {
27
+ const h264Match = offer.sdp.search(/rtpmap:([0-9]+) H264/g);
28
+ return h264Match !== -1;
29
+ }
30
+ return false;
31
+ }
32
+ catch (error) {
33
+ console.warn("Failed to check H.264 support:", error);
34
+ return false;
35
+ }
36
+ };
20
37
  // Check if we're in a browser environment
21
38
  if (typeof window === "undefined") {
22
39
  errors.push("Running in non-browser environment");
@@ -27,6 +44,7 @@ function useWebRTCDiagnostics() {
27
44
  rtcSessionDescription: false,
28
45
  getUserMedia: false,
29
46
  getDisplayMedia: false,
47
+ isHwH264Supported: false,
30
48
  errors,
31
49
  warnings,
32
50
  });
@@ -71,20 +89,42 @@ function useWebRTCDiagnostics() {
71
89
  warnings.push("Safari may have limited WebRTC codec support");
72
90
  }
73
91
  const browserSupport = rtcPeerConnection && rtcSessionDescription;
74
- setDiagnostics({
75
- done: true,
76
- browserSupport,
77
- rtcPeerConnection,
78
- rtcSessionDescription,
79
- getUserMedia,
80
- getDisplayMedia,
81
- errors,
82
- warnings,
83
- });
92
+ // Check H.264 support asynchronously
93
+ if (rtcPeerConnection) {
94
+ checkH264Support().then((isHwH264Supported) => {
95
+ if (!isHwH264Supported) {
96
+ warnings.push("H.264 hardware acceleration is not supported\n In Firefox, try enabling 'media.webrtc.hw.h264.enabled' in about:config");
97
+ }
98
+ setDiagnostics({
99
+ done: true,
100
+ browserSupport,
101
+ rtcPeerConnection,
102
+ rtcSessionDescription,
103
+ getUserMedia,
104
+ getDisplayMedia,
105
+ isHwH264Supported,
106
+ errors,
107
+ warnings,
108
+ });
109
+ });
110
+ }
111
+ else {
112
+ setDiagnostics({
113
+ done: true,
114
+ browserSupport,
115
+ rtcPeerConnection,
116
+ rtcSessionDescription,
117
+ getUserMedia,
118
+ getDisplayMedia,
119
+ isHwH264Supported: false,
120
+ errors,
121
+ warnings,
122
+ });
123
+ }
84
124
  }, []);
85
125
  return diagnostics;
86
126
  }
87
- function logWebRTCDiagnostics() {
127
+ async function logWebRTCDiagnostics() {
88
128
  console.group("WebRTC Diagnostics");
89
129
  // Log browser support
90
130
  console.log("RTCPeerConnection:", !!window.RTCPeerConnection);
@@ -95,14 +135,28 @@ function logWebRTCDiagnostics() {
95
135
  console.log("User Agent:", navigator.userAgent);
96
136
  console.log("Protocol:", location.protocol);
97
137
  console.log("Host:", location.hostname);
98
- // Test basic WebRTC functionality
138
+ console.groupEnd();
99
139
  if (window.RTCPeerConnection) {
100
140
  try {
101
141
  const pc = new RTCPeerConnection();
102
- console.log("RTCPeerConnection creation: Success");
142
+ // Check H.264 support
143
+ try {
144
+ const offer = await pc.createOffer({ offerToReceiveVideo: true });
145
+ const isHwH264Supported = offer.sdp
146
+ ? offer.sdp.search(/rtpmap:([0-9]+) H264/g) !== -1
147
+ : false;
148
+ console.group("WebRTC Peer Connection Test");
149
+ console.log("RTCPeerConnection creation: ✓ Success");
150
+ console.log("H.264 support:", isHwH264Supported ? "✓ Supported" : "✗ Not supported");
151
+ }
152
+ catch (error) {
153
+ console.group("WebRTC Peer Connection Test");
154
+ console.error("H.264 check failed:", error);
155
+ }
103
156
  pc.close();
104
157
  }
105
158
  catch (error) {
159
+ console.group("WebRTC Peer Connection Test");
106
160
  console.error("RTCPeerConnection creation: ✗ Failed", error);
107
161
  }
108
162
  }
@@ -32,6 +32,7 @@ const createSystemMessage = (type, text, metadata, date = new Date()) => {
32
32
  indexedAt: now.toISOString(),
33
33
  chatProfile: {
34
34
  color: { red: 128, green: 128, blue: 128 }, // Gray color for system messages
35
+ $type: "place.stream.chat.profile",
35
36
  },
36
37
  };
37
38
  };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.reduceChat = exports.useReportChatMessage = exports.useSubmitReport = exports.reduceChatIncremental = exports.useCreateChatMessage = exports.useAddPendingHide = exports.usePendingHides = exports.useSetReplyToMessage = exports.useReplyToMessage = void 0;
3
+ exports.reduceChat = exports.useReportChatMessage = exports.useSubmitReport = exports.reduceChatIncremental = exports.useDeleteChatMessage = 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");
@@ -50,6 +50,7 @@ const useCreateChatMessage = () => {
50
50
  const rt = new api_1.RichText({ text: msg.text });
51
51
  await rt.detectFacets(pdsAgent);
52
52
  const record = {
53
+ $type: "place.stream.chat.message",
53
54
  text: msg.text,
54
55
  createdAt: new Date().toISOString(),
55
56
  streamer: streamerProfile.did,
@@ -90,6 +91,28 @@ const useCreateChatMessage = () => {
90
91
  };
91
92
  };
92
93
  exports.useCreateChatMessage = useCreateChatMessage;
94
+ const useDeleteChatMessage = () => {
95
+ const pdsAgent = (0, xrpc_1.usePDSAgent)();
96
+ if (!pdsAgent) {
97
+ throw new Error("No PDS agent found");
98
+ }
99
+ const userDID = (0, streamplace_store_1.useDID)();
100
+ if (!userDID) {
101
+ throw new Error("No user DID found");
102
+ }
103
+ return async (uri) => {
104
+ const rkey = uri.split("/").pop();
105
+ if (!rkey) {
106
+ throw new Error("No rkey found");
107
+ }
108
+ return await pdsAgent.com.atproto.repo.deleteRecord({
109
+ repo: userDID,
110
+ collection: "place.stream.chat.message",
111
+ rkey: rkey,
112
+ });
113
+ };
114
+ };
115
+ exports.useDeleteChatMessage = useDeleteChatMessage;
93
116
  const buildSortedChatList = (chatIndex, existingChatList, newMessages, removedKeys) => {
94
117
  const sortedKeys = Object.keys(chatIndex).sort((a, b) => {
95
118
  const aTime = parseInt(a.split("-")[0], 10);
@@ -204,6 +227,7 @@ const reduceChatIncremental = (state, newMessages, blocks, hideUris = []) => {
204
227
  processedMessage = {
205
228
  ...message,
206
229
  replyTo: {
230
+ $type: "place.stream.chat.defs#messageView",
207
231
  cid: parentMsg.cid,
208
232
  uri: parentMsg.uri,
209
233
  author: parentMsg.author,
@@ -68,6 +68,7 @@ const useStreamKey = () => {
68
68
  platform = "Windows";
69
69
  }
70
70
  const record = {
71
+ $type: "place.stream.key",
71
72
  signingKey: keypair.did(),
72
73
  createdAt: new Date().toISOString(),
73
74
  createdBy: "Streamplace on " + platform,
@@ -8,7 +8,7 @@ const chat_1 = require("./chat");
8
8
  const problems_1 = require("./problems");
9
9
  const MAX_RECENT_SEGMENTS = 10;
10
10
  const handleWebSocketMessages = (state, messages) => {
11
- for (const message of messages) {
11
+ for (let message of messages) {
12
12
  if (streamplace_1.PlaceStreamLivestream.isLivestreamView(message)) {
13
13
  const newLivestream = message;
14
14
  const oldLivestream = state.livestream;
@@ -27,12 +27,14 @@ const handleWebSocketMessages = (state, messages) => {
27
27
  };
28
28
  }
29
29
  else if (streamplace_1.PlaceStreamLivestream.isViewerCount(message)) {
30
+ message = message;
30
31
  state = {
31
32
  ...state,
32
33
  viewers: message.count,
33
34
  };
34
35
  }
35
36
  else if (streamplace_1.PlaceStreamChatDefs.isMessageView(message)) {
37
+ message = message;
36
38
  // Explicitly map MessageView to MessageViewHydrated
37
39
  const hydrated = {
38
40
  uri: message.uri,
@@ -64,6 +66,7 @@ const handleWebSocketMessages = (state, messages) => {
64
66
  state = (0, chat_1.reduceChat)(state, [], [block], []);
65
67
  }
66
68
  else if (streamplace_1.PlaceStreamDefs.isRenditions(message)) {
69
+ message = message;
67
70
  state = {
68
71
  ...state,
69
72
  renditions: message.renditions,
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PlayerProvider = void 0;
4
4
  exports.withPlayerProvider = withPlayerProvider;
5
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const crypto_1 = require("crypto");
6
7
  const react_1 = require("react");
7
8
  const context_1 = require("./context");
8
9
  const player_store_1 = require("./player-store");
@@ -20,7 +21,7 @@ const PlayerProvider = ({ children, initialPlayers = [], defaultId = Math.random
20
21
  return initialPlayerStores;
21
22
  });
22
23
  const createPlayer = (0, react_1.useCallback)((id) => {
23
- const playerId = id || Math.random().toString(36).slice(8);
24
+ const playerId = id || (0, crypto_1.randomUUID)();
24
25
  const playerStore = (0, player_store_1.makePlayerStore)(playerId);
25
26
  setPlayers((prev) => ({
26
27
  ...prev,
@@ -58,6 +58,8 @@ const makePlayerStore = (id) => {
58
58
  // * Will get set to 'false' if the user has interacted with the volume
59
59
  muteWasForced: false,
60
60
  setMuteWasForced: (muteWasForced) => set(() => ({ muteWasForced })),
61
+ autoplayFailed: false,
62
+ setAutoplayFailed: (autoplayFailed) => set(() => ({ autoplayFailed })),
61
63
  embedded: false,
62
64
  setEmbedded: (embedded) => set(() => ({ embedded })),
63
65
  showControls: true,
@@ -179,6 +179,7 @@ function useCreateStreamRecord() {
179
179
  platVersion = (0, browser_1.getBrowserName)(window.navigator.userAgent);
180
180
  }
181
181
  const record = {
182
+ $type: "place.stream.livestream",
182
183
  title: title,
183
184
  url: finalUrl,
184
185
  createdAt: new Date().toISOString(),
@@ -230,6 +231,7 @@ function useUpdateStreamRecord(customUrl = null) {
230
231
  }
231
232
  }
232
233
  const record = {
234
+ $type: "place.stream.livestream",
233
235
  title: title,
234
236
  url: finalUrl,
235
237
  createdAt: new Date().toISOString(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamplace/components",
3
- "version": "0.7.19",
3
+ "version": "0.7.25",
4
4
  "description": "Streamplace React (Native) Components",
5
5
  "main": "dist/index.js",
6
6
  "types": "src/index.tsx",
@@ -20,7 +20,7 @@
20
20
  "tsup": "^8.5.0"
21
21
  },
22
22
  "dependencies": {
23
- "@atproto/api": "^0.15.7",
23
+ "@atproto/api": "^0.16.7",
24
24
  "@atproto/crypto": "^0.4.4",
25
25
  "@emoji-mart/react": "^1.1.1",
26
26
  "@gorhom/bottom-sheet": "^5.1.6",
@@ -40,7 +40,7 @@
40
40
  "react-native-svg": "^15.0.0",
41
41
  "react-native-webrtc": "git+https://github.com/streamplace/react-native-webrtc.git#6b8472a771ac47f89217d327058a8a4124a6ae56",
42
42
  "react-use-websocket": "^4.13.0",
43
- "streamplace": "0.7.2",
43
+ "streamplace": "0.7.25",
44
44
  "viem": "^2.21.44",
45
45
  "zustand": "^5.0.5"
46
46
  },
@@ -52,5 +52,5 @@
52
52
  "start": "tsc --watch --preserveWatchOutput",
53
53
  "prepare": "tsc"
54
54
  },
55
- "gitHead": "c168b5ba7cc4fcee07c6f68c844b6938de45a6c8"
55
+ "gitHead": "288afabcb270c01ae8012e2a5cd9d75d5e1aae28"
56
56
  }
@@ -83,6 +83,9 @@ export function ChatBox({
83
83
  const authors = useMemo(() => {
84
84
  if (!chat) return null;
85
85
  return chat.reduce((acc, msg) => {
86
+ // our fake system user "did"
87
+ if (msg.author.did === "did:sys:system") return acc;
88
+ if (acc.has(msg.author.handle)) return acc;
86
89
  acc.set(msg.author.handle, msg.chatProfile);
87
90
  return acc;
88
91
  }, new Map<string, ChatMessageViewHydrated["chatProfile"]>());
@@ -105,9 +105,10 @@ export const RenderChatMessage = memo(
105
105
  hour12: false,
106
106
  });
107
107
  }, []);
108
+ const replyTo = (item.replyTo as ChatMessageViewHydrated) || null;
108
109
  return (
109
110
  <>
110
- {item.replyTo && showReply && (
111
+ {replyTo && showReply && (
111
112
  <View
112
113
  style={[
113
114
  gap.all[2],
@@ -130,11 +131,11 @@ export const RenderChatMessage = memo(
130
131
  >
131
132
  <Text
132
133
  style={{
133
- color: getRgbColor((item.replyTo.chatProfile as any).color),
134
+ color: getRgbColor(replyTo.chatProfile?.color),
134
135
  fontWeight: "thin",
135
136
  }}
136
137
  >
137
- @{(item.replyTo.author as any).handle}
138
+ @{(replyTo.author as any).handle}
138
139
  </Text>{" "}
139
140
  <Text
140
141
  style={{
@@ -142,7 +143,7 @@ export const RenderChatMessage = memo(
142
143
  fontStyle: "italic",
143
144
  }}
144
145
  >
145
- {(item.replyTo.record as any).text}
146
+ {replyTo.record.text}
146
147
  </Text>
147
148
  </Text>
148
149
  </View>
@@ -1,6 +1,6 @@
1
1
  import { Ellipsis, Reply } from "lucide-react-native";
2
2
  import { ComponentProps, memo, useEffect, useRef, useState } from "react";
3
- import { Platform, Pressable } from "react-native";
3
+ import { Keyboard, Platform, Pressable } from "react-native";
4
4
  import { FlatList } from "react-native-gesture-handler";
5
5
  import Swipeable, {
6
6
  SwipeableMethods,
@@ -223,9 +223,7 @@ const ChatLine = memo(
223
223
  }
224
224
  renderLeftActions={Platform.OS === "android" ? undefined : LeftAction}
225
225
  overshootFriction={9}
226
- ref={(ref) => {
227
- swipeableRef.current = ref;
228
- }}
226
+ ref={swipeableRef}
229
227
  onSwipeableOpen={(r) => {
230
228
  if (r === (Platform.OS === "android" ? "right" : "left")) {
231
229
  setReply(item);
@@ -258,6 +256,22 @@ export function Chat({
258
256
  canModerate?: boolean;
259
257
  }) {
260
258
  const chat = useChat();
259
+ const [isScrolledUp, setIsScrolledUp] = useState(false);
260
+
261
+ const handleScroll = (event: any) => {
262
+ const { contentOffset } = event.nativeEvent;
263
+
264
+ const scrolledUp = contentOffset.y > 20; // threshold
265
+
266
+ if (scrolledUp !== isScrolledUp) {
267
+ setIsScrolledUp(scrolledUp);
268
+
269
+ // Dismiss keyboard when scrolled up
270
+ if (scrolledUp && Platform.OS !== "web") {
271
+ Keyboard.dismiss();
272
+ }
273
+ }
274
+ };
261
275
 
262
276
  if (!chat)
263
277
  return (
@@ -288,6 +302,8 @@ export function Chat({
288
302
  maxToRenderPerBatch={10}
289
303
  initialNumToRender={10}
290
304
  updateCellsBatchingPeriod={50}
305
+ onScroll={handleScroll}
306
+ scrollEventThrottle={16}
291
307
  />
292
308
  <ModView />
293
309
  </View>