@streamplace/components 0.7.32 → 0.7.35

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.
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useCreateFollowRecord = useCreateFollowRecord;
4
+ exports.useDeleteFollowRecord = useDeleteFollowRecord;
5
+ exports.useGraphManager = useGraphManager;
6
+ const react_1 = require("react");
7
+ const streamplace_store_1 = require("./streamplace-store");
8
+ const xrpc_1 = require("./xrpc");
9
+ function useCreateFollowRecord() {
10
+ let agent = (0, xrpc_1.usePDSAgent)();
11
+ const [isLoading, setIsLoading] = (0, react_1.useState)(false);
12
+ const createFollow = async (subjectDID) => {
13
+ if (!agent) {
14
+ throw new Error("No PDS agent found");
15
+ }
16
+ if (!agent.did) {
17
+ throw new Error("No user DID found, assuming not logged in");
18
+ }
19
+ setIsLoading(true);
20
+ try {
21
+ const record = {
22
+ $type: "app.bsky.graph.follow",
23
+ subject: subjectDID,
24
+ createdAt: new Date().toISOString(),
25
+ };
26
+ const result = await agent.com.atproto.repo.createRecord({
27
+ repo: agent.did,
28
+ collection: "app.bsky.graph.follow",
29
+ record,
30
+ });
31
+ return result;
32
+ }
33
+ finally {
34
+ setIsLoading(false);
35
+ }
36
+ };
37
+ return { createFollow, isLoading };
38
+ }
39
+ function useDeleteFollowRecord() {
40
+ let agent = (0, xrpc_1.usePDSAgent)();
41
+ const [isLoading, setIsLoading] = (0, react_1.useState)(false);
42
+ const deleteFollow = async (followRecordUri) => {
43
+ if (!agent) {
44
+ throw new Error("No PDS agent found");
45
+ }
46
+ if (!agent.did) {
47
+ throw new Error("No user DID found, assuming not logged in");
48
+ }
49
+ setIsLoading(true);
50
+ try {
51
+ const result = await agent.com.atproto.repo.deleteRecord({
52
+ repo: agent.did,
53
+ collection: "app.bsky.graph.follow",
54
+ rkey: followRecordUri.split("/").pop(),
55
+ });
56
+ return result;
57
+ }
58
+ finally {
59
+ setIsLoading(false);
60
+ }
61
+ };
62
+ return { deleteFollow, isLoading };
63
+ }
64
+ function useGraphManager(subjectDID) {
65
+ const agent = (0, xrpc_1.usePDSAgent)();
66
+ const [isFollowing, setIsFollowing] = (0, react_1.useState)(null);
67
+ const [followUri, setFollowUri] = (0, react_1.useState)(null);
68
+ const [isLoading, setIsLoading] = (0, react_1.useState)(false);
69
+ const [error, setError] = (0, react_1.useState)(null);
70
+ const userDID = agent?.did;
71
+ const streamplaceUrl = (0, streamplace_store_1.useStreamplaceStore)((state) => state.url);
72
+ const fetchFollowStatus = async () => {
73
+ if (!userDID || !subjectDID || !streamplaceUrl) {
74
+ setIsFollowing(null);
75
+ setFollowUri(null);
76
+ return;
77
+ }
78
+ setIsLoading(true);
79
+ setError(null);
80
+ try {
81
+ const res = await fetch(`${streamplaceUrl}/xrpc/place.stream.graph.getFollowingUser?subjectDID=${encodeURIComponent(subjectDID)}&userDID=${encodeURIComponent(userDID)}`, {
82
+ credentials: "include",
83
+ });
84
+ if (!res.ok) {
85
+ const errorText = await res.text();
86
+ throw new Error(`Failed to fetch follow status: ${errorText}`);
87
+ }
88
+ const data = await res.json();
89
+ if (data.follow) {
90
+ setIsFollowing(true);
91
+ setFollowUri(data.follow.uri);
92
+ }
93
+ else {
94
+ setIsFollowing(false);
95
+ setFollowUri(null);
96
+ }
97
+ }
98
+ catch (err) {
99
+ setError(`Could not determine follow state: ${err instanceof Error ? err.message : `Unknown error: ${err}`}`);
100
+ setIsFollowing(null);
101
+ }
102
+ finally {
103
+ setIsLoading(false);
104
+ }
105
+ };
106
+ (0, react_1.useEffect)(() => {
107
+ if (!userDID || !subjectDID) {
108
+ setIsFollowing(null);
109
+ setFollowUri(null);
110
+ setError(null);
111
+ return;
112
+ }
113
+ fetchFollowStatus();
114
+ }, [userDID, subjectDID, streamplaceUrl]);
115
+ const follow = async () => {
116
+ if (!agent || !subjectDID) {
117
+ throw new Error("Cannot follow: not logged in or no subject DID");
118
+ }
119
+ if (!agent.did) {
120
+ throw new Error("No user DID found, assuming not logged in");
121
+ }
122
+ setIsLoading(true);
123
+ setError(null);
124
+ const previousState = isFollowing;
125
+ setIsFollowing(true); // Optimistic
126
+ try {
127
+ const record = {
128
+ $type: "app.bsky.graph.follow",
129
+ subject: subjectDID,
130
+ createdAt: new Date().toISOString(),
131
+ };
132
+ const result = await agent.com.atproto.repo.createRecord({
133
+ repo: agent.did,
134
+ collection: "app.bsky.graph.follow",
135
+ record,
136
+ });
137
+ setFollowUri(result.data.uri);
138
+ setIsFollowing(true);
139
+ }
140
+ catch (err) {
141
+ setIsFollowing(previousState);
142
+ const errorMsg = `Failed to follow: ${err instanceof Error ? err.message : "Unknown error"}`;
143
+ setError(errorMsg);
144
+ throw new Error(errorMsg);
145
+ }
146
+ finally {
147
+ setIsLoading(false);
148
+ }
149
+ };
150
+ const unfollow = async () => {
151
+ if (!agent || !subjectDID) {
152
+ throw new Error("Cannot unfollow: not logged in or no subject DID");
153
+ }
154
+ if (!agent.did) {
155
+ throw new Error("No user DID found, assuming not logged in");
156
+ }
157
+ if (!followUri) {
158
+ throw new Error("Cannot unfollow: no follow URI found");
159
+ }
160
+ setIsLoading(true);
161
+ setError(null);
162
+ const previousState = isFollowing;
163
+ const previousUri = followUri;
164
+ setIsFollowing(false); // Optimistic
165
+ setFollowUri(null);
166
+ try {
167
+ await agent.com.atproto.repo.deleteRecord({
168
+ repo: agent.did,
169
+ collection: "app.bsky.graph.follow",
170
+ rkey: followUri.split("/").pop(),
171
+ });
172
+ setIsFollowing(false);
173
+ setFollowUri(null);
174
+ }
175
+ catch (err) {
176
+ setIsFollowing(previousState);
177
+ setFollowUri(previousUri);
178
+ const errorMsg = `Failed to unfollow: ${err instanceof Error ? err.message : "Unknown error"}`;
179
+ setError(errorMsg);
180
+ throw new Error(errorMsg);
181
+ }
182
+ finally {
183
+ setIsLoading(false);
184
+ }
185
+ };
186
+ return {
187
+ isFollowing,
188
+ followUri,
189
+ isLoading,
190
+ error,
191
+ follow,
192
+ unfollow,
193
+ refresh: fetchFollowStatus,
194
+ };
195
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamplace/components",
3
- "version": "0.7.32",
3
+ "version": "0.7.35",
4
4
  "description": "Streamplace React (Native) Components",
5
5
  "main": "dist/index.js",
6
6
  "types": "src/index.tsx",
@@ -54,5 +54,5 @@
54
54
  "start": "tsc --watch --preserveWatchOutput",
55
55
  "prepare": "tsc"
56
56
  },
57
- "gitHead": "8c9f6186d1e718bcc4dfe84fe8c681680dddcf0e"
57
+ "gitHead": "eaefc85434e81e296c7248f6e6965a2f31d50ff8"
58
58
  }
@@ -136,124 +136,112 @@ const ActionsBar = memo(
136
136
  },
137
137
  );
138
138
 
139
- const ChatLine = memo(
140
- ({
141
- item,
142
- canModerate,
143
- }: {
144
- item: ChatMessageViewHydrated;
145
- canModerate: boolean;
146
- }) => {
147
- const setReply = useSetReplyToMessage();
148
- const setModMsg = usePlayerStore((state) => state.setModMessage);
149
- const swipeableRef = useRef<SwipeableMethods | null>(null);
150
- const [isHovered, setIsHovered] = useState(false);
151
- const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
139
+ const ChatLine = memo(({ item }: { item: ChatMessageViewHydrated }) => {
140
+ const setReply = useSetReplyToMessage();
141
+ const setModMsg = usePlayerStore((state) => state.setModMessage);
142
+ const swipeableRef = useRef<SwipeableMethods | null>(null);
143
+ const [isHovered, setIsHovered] = useState(false);
144
+ const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
152
145
 
153
- const handleHoverIn = () => {
146
+ const handleHoverIn = () => {
147
+ if (hoverTimeoutRef.current) {
148
+ clearTimeout(hoverTimeoutRef.current);
149
+ hoverTimeoutRef.current = null;
150
+ }
151
+ setIsHovered(true);
152
+ };
153
+
154
+ const handleHoverOut = () => {
155
+ hoverTimeoutRef.current = setTimeout(() => {
156
+ setIsHovered(false);
157
+ }, 50);
158
+ };
159
+
160
+ useEffect(() => {
161
+ return () => {
154
162
  if (hoverTimeoutRef.current) {
155
163
  clearTimeout(hoverTimeoutRef.current);
156
- hoverTimeoutRef.current = null;
157
164
  }
158
- setIsHovered(true);
159
- };
160
-
161
- const handleHoverOut = () => {
162
- hoverTimeoutRef.current = setTimeout(() => {
163
- setIsHovered(false);
164
- }, 50);
165
165
  };
166
+ }, []);
166
167
 
167
- useEffect(() => {
168
- return () => {
169
- if (hoverTimeoutRef.current) {
170
- clearTimeout(hoverTimeoutRef.current);
171
- }
172
- };
173
- }, []);
174
-
175
- if (item.author.did === "did:sys:system") {
176
- return (
177
- <SystemMessage
178
- timestamp={new Date(item.record.createdAt)}
179
- title={item.record.text}
180
- />
181
- );
182
- }
183
-
184
- if (Platform.OS === "web") {
185
- return (
186
- <View
187
- style={[
188
- py[1],
189
- px[2],
190
- {
191
- position: "relative",
192
- borderRadius: 8,
193
- minWidth: 0,
194
- maxWidth: "100%",
195
- },
196
- isHovered && bg.gray[950],
197
- ]}
198
- onPointerEnter={handleHoverIn}
199
- onPointerLeave={handleHoverOut}
200
- >
201
- <Pressable style={[{ minWidth: 0, maxWidth: "100%" }]}>
202
- <RenderChatMessage item={item} />
203
- </Pressable>
204
- <ActionsBar
205
- item={item}
206
- visible={isHovered}
207
- hoverTimeoutRef={hoverTimeoutRef}
208
- />
209
- </View>
210
- );
211
- }
168
+ if (item.author.did === "did:sys:system") {
169
+ return (
170
+ <SystemMessage
171
+ timestamp={new Date(item.record.createdAt)}
172
+ title={item.record.text}
173
+ />
174
+ );
175
+ }
212
176
 
177
+ if (Platform.OS === "web") {
213
178
  return (
214
- <>
215
- <Swipeable
216
- containerStyle={[py[1]]}
217
- friction={2}
218
- enableTrackpadTwoFingerGesture
219
- rightThreshold={40}
220
- leftThreshold={40}
221
- renderRightActions={
222
- Platform.OS === "android" ? undefined : RightAction
223
- }
224
- renderLeftActions={Platform.OS === "android" ? undefined : LeftAction}
225
- overshootFriction={9}
226
- ref={swipeableRef}
227
- onSwipeableOpen={(r) => {
228
- if (r === (Platform.OS === "android" ? "right" : "left")) {
229
- setReply(item);
230
- }
231
- if (r === (Platform.OS === "android" ? "left" : "right")) {
232
- setModMsg(item);
233
- }
234
- // close this swipeable
235
- const swipeable = swipeableRef.current;
236
- if (swipeable) {
237
- swipeable.close();
238
- }
239
- }}
240
- >
179
+ <View
180
+ style={[
181
+ py[1],
182
+ px[2],
183
+ {
184
+ position: "relative",
185
+ borderRadius: 8,
186
+ minWidth: 0,
187
+ maxWidth: "100%",
188
+ },
189
+ isHovered && bg.gray[950],
190
+ ]}
191
+ onPointerEnter={handleHoverIn}
192
+ onPointerLeave={handleHoverOut}
193
+ >
194
+ <Pressable style={[{ minWidth: 0, maxWidth: "100%" }]}>
241
195
  <RenderChatMessage item={item} />
242
- </Swipeable>
243
- </>
196
+ </Pressable>
197
+ <ActionsBar
198
+ item={item}
199
+ visible={isHovered}
200
+ hoverTimeoutRef={hoverTimeoutRef}
201
+ />
202
+ </View>
244
203
  );
245
- },
246
- );
204
+ }
205
+
206
+ return (
207
+ <>
208
+ <Swipeable
209
+ containerStyle={[py[1]]}
210
+ friction={2}
211
+ enableTrackpadTwoFingerGesture
212
+ rightThreshold={40}
213
+ leftThreshold={40}
214
+ renderRightActions={Platform.OS === "android" ? undefined : RightAction}
215
+ renderLeftActions={Platform.OS === "android" ? undefined : LeftAction}
216
+ overshootFriction={9}
217
+ ref={swipeableRef}
218
+ onSwipeableOpen={(r) => {
219
+ if (r === (Platform.OS === "android" ? "right" : "left")) {
220
+ setReply(item);
221
+ }
222
+ if (r === (Platform.OS === "android" ? "left" : "right")) {
223
+ setModMsg(item);
224
+ }
225
+ // close this swipeable
226
+ const swipeable = swipeableRef.current;
227
+ if (swipeable) {
228
+ swipeable.close();
229
+ }
230
+ }}
231
+ >
232
+ <RenderChatMessage item={item} />
233
+ </Swipeable>
234
+ </>
235
+ );
236
+ });
247
237
 
248
238
  export function Chat({
249
239
  shownMessages = SHOWN_MSGS,
250
240
  style: propsStyle,
251
- canModerate = false,
252
241
  ...props
253
242
  }: ComponentProps<typeof View> & {
254
243
  shownMessages?: number;
255
244
  style?: ComponentProps<typeof View>["style"];
256
- canModerate?: boolean;
257
245
  }) {
258
246
  const chat = useChat();
259
247
  const [isScrolledUp, setIsScrolledUp] = useState(false);
@@ -276,7 +264,7 @@ export function Chat({
276
264
  if (!chat)
277
265
  return (
278
266
  <View style={[flex.shrink[1], { minWidth: 0, maxWidth: "100%" }]}>
279
- <Text>Loading chaat...</Text>
267
+ <Text>Loading chat...</Text>
280
268
  </View>
281
269
  );
282
270
 
@@ -295,9 +283,7 @@ export function Chat({
295
283
  data={chat.slice(0, shownMessages)}
296
284
  inverted={true}
297
285
  keyExtractor={keyExtractor}
298
- renderItem={({ item, index }) => (
299
- <ChatLine item={item} canModerate={canModerate} />
300
- )}
286
+ renderItem={({ item, index }) => <ChatLine item={item} />}
301
287
  removeClippedSubviews={true}
302
288
  maxToRenderPerBatch={10}
303
289
  initialNumToRender={10}
@@ -1,7 +1,7 @@
1
1
  import { TriggerRef, useRootContext } from "@rn-primitives/dropdown-menu";
2
2
  import { forwardRef, useEffect, useRef, useState } from "react";
3
3
  import { gap, mr, w } from "../../lib/theme/atoms";
4
- import { usePlayerStore } from "../../player-store";
4
+ import { useIsMyStream, usePlayerStore } from "../../player-store";
5
5
  import {
6
6
  useCreateBlockRecord,
7
7
  useCreateHideChatRecord,
@@ -51,6 +51,7 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
51
51
  const setReportSubject = usePlayerStore((x) => x.setReportSubject);
52
52
  const setModMessage = usePlayerStore((x) => x.setModMessage);
53
53
  const deleteChatMessage = useDeleteChatMessage();
54
+ const isMyStream = useIsMyStream();
54
55
 
55
56
  // get the channel did
56
57
  const channelId = usePlayerStore((state) => state.src);
@@ -119,7 +120,7 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
119
120
  </DropdownMenuGroup>
120
121
 
121
122
  {/* TODO: Checking for non-owner moderators */}
122
- {channelId === handle && (
123
+ {isMyStream() && (
123
124
  <DropdownMenuGroup title={`Moderation actions`}>
124
125
  <DropdownMenuItem
125
126
  disabled={isHideLoading || messageRemoved}
@@ -10,7 +10,6 @@ interface ChatPanelProps {
10
10
  isLive: boolean;
11
11
  isConnected: boolean;
12
12
  messagesPerMinute?: number;
13
- canModerate?: boolean;
14
13
  shownMessages?: number;
15
14
  }
16
15
 
@@ -18,7 +17,6 @@ export default function ChatPanel({
18
17
  isLive,
19
18
  isConnected,
20
19
  messagesPerMinute = 0,
21
- canModerate = false,
22
20
  shownMessages = 50,
23
21
  }: ChatPanelProps) {
24
22
  return (
@@ -59,7 +57,7 @@ export default function ChatPanel({
59
57
  </View>
60
58
  <View style={[flex.values[1], px[2], { minHeight: 0 }]}>
61
59
  <View style={[flex.values[1], { minHeight: 0 }]}>
62
- <Chat canModerate={canModerate} shownMessages={shownMessages} />
60
+ <Chat shownMessages={shownMessages} />
63
61
  </View>
64
62
  <View style={[{ flexShrink: 0 }]}>
65
63
  <ChatBox
@@ -1,5 +1,5 @@
1
- import { Car, Radio, Users } from "lucide-react-native";
2
- import { Text, View } from "react-native";
1
+ import { AlertCircle, Car, Radio, Users } from "lucide-react-native";
2
+ import { Pressable, Text, View } from "react-native";
3
3
  import * as zero from "../../ui";
4
4
 
5
5
  const { bg, r, borders, px, py, text, layout, gap } = zero;
@@ -103,6 +103,8 @@ interface HeaderProps {
103
103
  bitrate?: string;
104
104
  timeBetweenSegments?: number;
105
105
  connectionStatus?: "excellent" | "good" | "poor" | "offline";
106
+ problemsCount?: number;
107
+ onProblemsPress?: () => void;
106
108
  }
107
109
 
108
110
  export default function Header({
@@ -113,6 +115,8 @@ export default function Header({
113
115
  bitrate = "0 mbps",
114
116
  timeBetweenSegments = 0,
115
117
  connectionStatus = "offline",
118
+ problemsCount = 0,
119
+ onProblemsPress,
116
120
  }: HeaderProps) {
117
121
  const getConnectionQuality = (): "good" | "warning" | "error" => {
118
122
  if (timeBetweenSegments <= 1500) return "good";
@@ -139,7 +143,37 @@ export default function Header({
139
143
  <Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}>
140
144
  {streamTitle}
141
145
  </Text>
142
- <StatusIndicator status={connectionStatus} isLive={isLive} />
146
+ <View style={[layout.flex.row, layout.flex.alignCenter, gap.all[3]]}>
147
+ <StatusIndicator status={connectionStatus} isLive={isLive} />
148
+ {problemsCount > 0 && (
149
+ <Pressable onPress={onProblemsPress}>
150
+ <View
151
+ style={[
152
+ layout.flex.row,
153
+ layout.flex.alignCenter,
154
+ gap.all[1],
155
+ px[2],
156
+ py[1],
157
+ r.md,
158
+ bg.orange[900],
159
+ borders.width.thin,
160
+ borders.color.orange[700],
161
+ { marginVertical: -8 },
162
+ ]}
163
+ >
164
+ <AlertCircle size={14} color="#fb923c" />
165
+ <Text
166
+ style={[
167
+ text.orange[400],
168
+ { fontSize: 11, fontWeight: "600" },
169
+ ]}
170
+ >
171
+ {problemsCount} {problemsCount === 1 ? "Issue" : "Issues"}
172
+ </Text>
173
+ </View>
174
+ </Pressable>
175
+ )}
176
+ </View>
143
177
  </View>
144
178
  </View>
145
179
 
@@ -2,4 +2,4 @@ export { default as ChatPanel } from "./chat-panel";
2
2
  export { default as Header } from "./header";
3
3
  export { default as InformationWidget } from "./information-widget";
4
4
  export { default as ModActions } from "./mod-actions";
5
- export { default as Problems } from "./problems";
5
+ export { default as Problems, ProblemsWrapperRef } from "./problems";