@streamplace/components 0.7.34 → 0.8.0

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 (64) hide show
  1. package/dist/components/content-metadata/content-metadata-form.js +404 -0
  2. package/dist/components/content-metadata/content-rights.js +78 -0
  3. package/dist/components/content-metadata/content-warnings.js +68 -0
  4. package/dist/components/content-metadata/index.js +11 -0
  5. package/dist/components/dashboard/header.js +16 -2
  6. package/dist/components/dashboard/problems.js +29 -28
  7. package/dist/components/mobile-player/player.js +4 -0
  8. package/dist/components/mobile-player/ui/report-modal.js +3 -2
  9. package/dist/components/mobile-player/ui/viewer-context-menu.js +44 -1
  10. package/dist/components/ui/button.js +9 -9
  11. package/dist/components/ui/checkbox.js +87 -0
  12. package/dist/components/ui/dialog.js +188 -83
  13. package/dist/components/ui/dropdown.js +15 -10
  14. package/dist/components/ui/icons.js +6 -0
  15. package/dist/components/ui/primitives/button.js +0 -7
  16. package/dist/components/ui/primitives/input.js +13 -1
  17. package/dist/components/ui/primitives/modal.js +2 -2
  18. package/dist/components/ui/select.js +89 -0
  19. package/dist/components/ui/textarea.js +23 -4
  20. package/dist/components/ui/toast.js +464 -114
  21. package/dist/components/ui/tooltip.js +103 -0
  22. package/dist/index.js +2 -0
  23. package/dist/lib/metadata-constants.js +157 -0
  24. package/dist/lib/theme/theme.js +5 -3
  25. package/dist/lib/theme/tokens.js +9 -0
  26. package/dist/streamplace-provider/index.js +14 -4
  27. package/dist/streamplace-store/content-metadata-actions.js +118 -0
  28. package/dist/streamplace-store/graph.js +195 -0
  29. package/dist/streamplace-store/streamplace-store.js +18 -5
  30. package/dist/streamplace-store/user.js +67 -7
  31. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  32. package/package.json +3 -3
  33. package/src/components/content-metadata/content-metadata-form.tsx +761 -0
  34. package/src/components/content-metadata/content-rights.tsx +104 -0
  35. package/src/components/content-metadata/content-warnings.tsx +100 -0
  36. package/src/components/content-metadata/index.tsx +18 -0
  37. package/src/components/dashboard/header.tsx +37 -3
  38. package/src/components/dashboard/index.tsx +1 -1
  39. package/src/components/dashboard/problems.tsx +57 -46
  40. package/src/components/mobile-player/player.tsx +5 -0
  41. package/src/components/mobile-player/ui/report-modal.tsx +13 -7
  42. package/src/components/mobile-player/ui/viewer-context-menu.tsx +100 -1
  43. package/src/components/ui/button.tsx +10 -13
  44. package/src/components/ui/checkbox.tsx +147 -0
  45. package/src/components/ui/dialog.tsx +319 -99
  46. package/src/components/ui/dropdown.tsx +27 -13
  47. package/src/components/ui/icons.tsx +14 -0
  48. package/src/components/ui/primitives/button.tsx +0 -7
  49. package/src/components/ui/primitives/input.tsx +19 -2
  50. package/src/components/ui/primitives/modal.tsx +4 -2
  51. package/src/components/ui/select.tsx +175 -0
  52. package/src/components/ui/textarea.tsx +47 -29
  53. package/src/components/ui/toast.tsx +785 -179
  54. package/src/components/ui/tooltip.tsx +131 -0
  55. package/src/index.tsx +3 -0
  56. package/src/lib/metadata-constants.ts +180 -0
  57. package/src/lib/theme/theme.tsx +10 -6
  58. package/src/lib/theme/tokens.ts +9 -0
  59. package/src/streamplace-provider/index.tsx +20 -2
  60. package/src/streamplace-store/content-metadata-actions.tsx +142 -0
  61. package/src/streamplace-store/graph.tsx +232 -0
  62. package/src/streamplace-store/streamplace-store.tsx +30 -4
  63. package/src/streamplace-store/user.tsx +71 -7
  64. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,232 @@
1
+ import { AppBskyGraphFollow } from "@atproto/api";
2
+ import { useEffect, useState } from "react";
3
+ import { useStreamplaceStore } from "./streamplace-store";
4
+ import { usePDSAgent } from "./xrpc";
5
+
6
+ export function useCreateFollowRecord() {
7
+ let agent = usePDSAgent();
8
+ const [isLoading, setIsLoading] = useState(false);
9
+
10
+ const createFollow = async (subjectDID: string) => {
11
+ if (!agent) {
12
+ throw new Error("No PDS agent found");
13
+ }
14
+
15
+ if (!agent.did) {
16
+ throw new Error("No user DID found, assuming not logged in");
17
+ }
18
+
19
+ setIsLoading(true);
20
+ try {
21
+ const record: AppBskyGraphFollow.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
+ } finally {
33
+ setIsLoading(false);
34
+ }
35
+ };
36
+
37
+ return { createFollow, isLoading };
38
+ }
39
+
40
+ export function useDeleteFollowRecord() {
41
+ let agent = usePDSAgent();
42
+ const [isLoading, setIsLoading] = useState(false);
43
+
44
+ const deleteFollow = async (followRecordUri: string) => {
45
+ if (!agent) {
46
+ throw new Error("No PDS agent found");
47
+ }
48
+
49
+ if (!agent.did) {
50
+ throw new Error("No user DID found, assuming not logged in");
51
+ }
52
+
53
+ setIsLoading(true);
54
+ try {
55
+ const result = await agent.com.atproto.repo.deleteRecord({
56
+ repo: agent.did,
57
+ collection: "app.bsky.graph.follow",
58
+ rkey: followRecordUri.split("/").pop()!,
59
+ });
60
+ return result;
61
+ } finally {
62
+ setIsLoading(false);
63
+ }
64
+ };
65
+
66
+ return { deleteFollow, isLoading };
67
+ }
68
+
69
+ interface GraphManagerState {
70
+ isFollowing: boolean | null;
71
+ followUri: string | null;
72
+ isLoading: boolean;
73
+ error: string | null;
74
+ }
75
+
76
+ interface GraphManagerActions {
77
+ follow: () => Promise<void>;
78
+ unfollow: () => Promise<void>;
79
+ refresh: () => Promise<void>;
80
+ }
81
+
82
+ export function useGraphManager(
83
+ subjectDID: string | null | undefined,
84
+ ): GraphManagerState & GraphManagerActions {
85
+ const agent = usePDSAgent();
86
+ const [isFollowing, setIsFollowing] = useState<boolean | null>(null);
87
+ const [followUri, setFollowUri] = useState<string | null>(null);
88
+ const [isLoading, setIsLoading] = useState(false);
89
+ const [error, setError] = useState<string | null>(null);
90
+
91
+ const userDID = agent?.did;
92
+
93
+ const streamplaceUrl = useStreamplaceStore((state) => state.url);
94
+
95
+ const fetchFollowStatus = async () => {
96
+ if (!userDID || !subjectDID || !streamplaceUrl) {
97
+ setIsFollowing(null);
98
+ setFollowUri(null);
99
+ return;
100
+ }
101
+
102
+ setIsLoading(true);
103
+ setError(null);
104
+ try {
105
+ const res = await fetch(
106
+ `${streamplaceUrl}/xrpc/place.stream.graph.getFollowingUser?subjectDID=${encodeURIComponent(subjectDID)}&userDID=${encodeURIComponent(userDID)}`,
107
+ {
108
+ credentials: "include",
109
+ },
110
+ );
111
+
112
+ if (!res.ok) {
113
+ const errorText = await res.text();
114
+ throw new Error(`Failed to fetch follow status: ${errorText}`);
115
+ }
116
+
117
+ const data = await res.json();
118
+
119
+ if (data.follow) {
120
+ setIsFollowing(true);
121
+ setFollowUri(data.follow.uri);
122
+ } else {
123
+ setIsFollowing(false);
124
+ setFollowUri(null);
125
+ }
126
+ } catch (err) {
127
+ setError(
128
+ `Could not determine follow state: ${err instanceof Error ? err.message : `Unknown error: ${err}`}`,
129
+ );
130
+ setIsFollowing(null);
131
+ } finally {
132
+ setIsLoading(false);
133
+ }
134
+ };
135
+
136
+ useEffect(() => {
137
+ if (!userDID || !subjectDID) {
138
+ setIsFollowing(null);
139
+ setFollowUri(null);
140
+ setError(null);
141
+ return;
142
+ }
143
+
144
+ fetchFollowStatus();
145
+ }, [userDID, subjectDID, streamplaceUrl]);
146
+
147
+ const follow = async () => {
148
+ if (!agent || !subjectDID) {
149
+ throw new Error("Cannot follow: not logged in or no subject DID");
150
+ }
151
+
152
+ if (!agent.did) {
153
+ throw new Error("No user DID found, assuming not logged in");
154
+ }
155
+
156
+ setIsLoading(true);
157
+ setError(null);
158
+ const previousState = isFollowing;
159
+ setIsFollowing(true); // Optimistic
160
+
161
+ try {
162
+ const record: AppBskyGraphFollow.Record = {
163
+ $type: "app.bsky.graph.follow",
164
+ subject: subjectDID,
165
+ createdAt: new Date().toISOString(),
166
+ };
167
+ const result = await agent.com.atproto.repo.createRecord({
168
+ repo: agent.did,
169
+ collection: "app.bsky.graph.follow",
170
+ record,
171
+ });
172
+ setFollowUri(result.data.uri);
173
+ setIsFollowing(true);
174
+ } catch (err) {
175
+ setIsFollowing(previousState);
176
+ const errorMsg = `Failed to follow: ${err instanceof Error ? err.message : "Unknown error"}`;
177
+ setError(errorMsg);
178
+ throw new Error(errorMsg);
179
+ } finally {
180
+ setIsLoading(false);
181
+ }
182
+ };
183
+
184
+ const unfollow = async () => {
185
+ if (!agent || !subjectDID) {
186
+ throw new Error("Cannot unfollow: not logged in or no subject DID");
187
+ }
188
+
189
+ if (!agent.did) {
190
+ throw new Error("No user DID found, assuming not logged in");
191
+ }
192
+
193
+ if (!followUri) {
194
+ throw new Error("Cannot unfollow: no follow URI found");
195
+ }
196
+
197
+ setIsLoading(true);
198
+ setError(null);
199
+ const previousState = isFollowing;
200
+ const previousUri = followUri;
201
+ setIsFollowing(false); // Optimistic
202
+ setFollowUri(null);
203
+
204
+ try {
205
+ await agent.com.atproto.repo.deleteRecord({
206
+ repo: agent.did,
207
+ collection: "app.bsky.graph.follow",
208
+ rkey: followUri.split("/").pop()!,
209
+ });
210
+ setIsFollowing(false);
211
+ setFollowUri(null);
212
+ } catch (err) {
213
+ setIsFollowing(previousState);
214
+ setFollowUri(previousUri);
215
+ const errorMsg = `Failed to unfollow: ${err instanceof Error ? err.message : "Unknown error"}`;
216
+ setError(errorMsg);
217
+ throw new Error(errorMsg);
218
+ } finally {
219
+ setIsLoading(false);
220
+ }
221
+ };
222
+
223
+ return {
224
+ isFollowing,
225
+ followUri,
226
+ isLoading,
227
+ error,
228
+ follow,
229
+ unfollow,
230
+ refresh: fetchFollowStatus,
231
+ };
232
+ }
@@ -5,6 +5,13 @@ import { createStore, StoreApi, useStore } from "zustand";
5
5
  import storage from "../storage";
6
6
  import { StreamplaceContext } from "../streamplace-provider/context";
7
7
 
8
+ export interface ContentMetadataResult {
9
+ record: any;
10
+ uri: string;
11
+ cid: string;
12
+ rkey?: string;
13
+ }
14
+
8
15
  // there are three categories of XRPC that we need to handle:
9
16
  // 1. Public (probably) OAuth XRPC to the users' PDS for apps that use this API.
10
17
  // 2. Confidental OAuth to the Streamplace server for doing things that require
@@ -33,6 +40,10 @@ export interface StreamplaceState {
33
40
  handle: string | null;
34
41
  chatProfile: PlaceStreamChatProfile.Record | null;
35
42
 
43
+ // Content metadata state
44
+ contentMetadata: ContentMetadataResult | null;
45
+ setContentMetadata: (metadata: ContentMetadataResult | null) => void;
46
+
36
47
  // Volume state
37
48
  volume: number;
38
49
  muted: boolean;
@@ -70,6 +81,10 @@ export const makeStreamplaceStore = ({
70
81
  handle: null,
71
82
  chatProfile: null,
72
83
 
84
+ // Content metadata
85
+ contentMetadata: null,
86
+ setContentMetadata: (metadata) => set({ contentMetadata: metadata }),
87
+
73
88
  // Volume state - start with defaults
74
89
  volume: 1.0,
75
90
  muted: false,
@@ -125,13 +140,12 @@ export const makeStreamplaceStore = ({
125
140
  initialMuted = storedMuted === "true";
126
141
  }
127
142
 
128
- // Update the store with loaded values
129
143
  store.setState({
130
144
  volume: initialVolume,
131
145
  muted: initialMuted,
132
146
  });
133
- } catch (e) {
134
- console.warn("Failed to load volume settings from storage:", e);
147
+ } catch (error) {
148
+ console.error("Failed to load volume state from storage:", error);
135
149
  }
136
150
  })();
137
151
 
@@ -164,7 +178,17 @@ export const useSetHandle = (): ((handle: string) => void) => {
164
178
  return (handle: string) => store.setState({ handle });
165
179
  };
166
180
 
167
- // Volume convenience hooks
181
+ // Content metadata hooks
182
+ export const useContentMetadata = () =>
183
+ useStreamplaceStore((x) => x.contentMetadata);
184
+
185
+ export const useSetContentMetadata = () => {
186
+ const store = getStreamplaceStoreFromContext();
187
+ return (metadata: ContentMetadataResult | null) =>
188
+ store.setState({ contentMetadata: metadata });
189
+ };
190
+
191
+ // Volume/muted hooks
168
192
  export const useVolume = () => useStreamplaceStore((x) => x.volume);
169
193
  export const useMuted = () => useStreamplaceStore((x) => x.muted);
170
194
  export const useSetVolume = () => useStreamplaceStore((x) => x.setVolume);
@@ -177,3 +201,5 @@ export const useEffectiveVolume = () =>
177
201
  // Ensure we always return a finite number for HTMLMediaElement.volume
178
202
  return Number.isFinite(effectiveVolume) ? effectiveVolume : 1.0;
179
203
  });
204
+
205
+ export { useCreateStreamRecord, useUpdateStreamRecord } from "./stream";
@@ -10,18 +10,36 @@ export function useGetChatProfile() {
10
10
  const did = useDID();
11
11
  const pdsAgent = usePDSAgent();
12
12
  const store = getStreamplaceStoreFromContext();
13
+ const createEmptyChatProfile = useCreateEmptyChatProfile();
13
14
 
14
15
  return async () => {
15
16
  if (!did || !pdsAgent) {
16
17
  throw new Error("No DID or PDS agent");
17
18
  }
18
- const res = await pdsAgent.com.atproto.repo.getRecord({
19
- repo: did,
20
- collection: "place.stream.chat.profile",
21
- rkey: "self",
22
- });
23
- if (!res.success) {
24
- throw new Error("Failed to get chat profile record");
19
+ let res;
20
+ try {
21
+ res = await pdsAgent.com.atproto.repo.getRecord({
22
+ repo: did,
23
+ collection: "place.stream.chat.profile",
24
+ rkey: "self",
25
+ });
26
+ } catch (e) {
27
+ console.error(
28
+ "Failed to get chat profile record, attempting creation",
29
+ e,
30
+ );
31
+ }
32
+ if (!res || !res.success) {
33
+ try {
34
+ await createEmptyChatProfile();
35
+ res = await pdsAgent.com.atproto.repo.getRecord({
36
+ repo: did,
37
+ collection: "place.stream.chat.profile",
38
+ rkey: "self",
39
+ });
40
+ } catch (e) {
41
+ console.error("Failed to create empty chat profile record", e);
42
+ }
25
43
  }
26
44
 
27
45
  if (PlaceStreamChatProfile.isRecord(res.data.value)) {
@@ -32,6 +50,30 @@ export function useGetChatProfile() {
32
50
  };
33
51
  }
34
52
 
53
+ export function useCreateEmptyChatProfile() {
54
+ const did = useDID();
55
+ const pdsAgent = usePDSAgent();
56
+
57
+ return async () => {
58
+ if (!did || !pdsAgent) {
59
+ throw new Error("No DID or PDS agent");
60
+ }
61
+ const res = await pdsAgent.com.atproto.repo.putRecord({
62
+ repo: did,
63
+ collection: "place.stream.chat.profile",
64
+ rkey: "self",
65
+ record: {
66
+ $type: "place.stream.chat.profile",
67
+ color:
68
+ DEFAULT_COLORS[Math.floor(Math.random() * DEFAULT_COLORS.length)],
69
+ },
70
+ });
71
+ if (!res.success) {
72
+ throw new Error("Failed to create empty chat profile record");
73
+ }
74
+ };
75
+ }
76
+
35
77
  export function useGetBskyProfile() {
36
78
  const did = useDID();
37
79
  const pdsAgent = usePDSAgent();
@@ -55,3 +97,25 @@ export function useGetBskyProfile() {
55
97
  export function useChatProfile() {
56
98
  return useStreamplaceStore((x) => x.chatProfile);
57
99
  }
100
+
101
+ const DEFAULT_COLORS: PlaceStreamChatProfile.Color[] = [
102
+ { red: 244, green: 67, blue: 54 },
103
+ { red: 233, green: 30, blue: 99 },
104
+ { red: 156, green: 39, blue: 176 },
105
+ { red: 103, green: 58, blue: 183 },
106
+ { red: 63, green: 81, blue: 181 },
107
+ { red: 33, green: 150, blue: 243 },
108
+ { red: 3, green: 169, blue: 244 },
109
+ { red: 0, green: 188, blue: 212 },
110
+ { red: 0, green: 150, blue: 136 },
111
+ { red: 76, green: 175, blue: 80 },
112
+ { red: 139, green: 195, blue: 74 },
113
+ { red: 205, green: 220, blue: 57 },
114
+ { red: 255, green: 235, blue: 59 },
115
+ { red: 255, green: 193, blue: 7 },
116
+ { red: 255, green: 152, blue: 0 },
117
+ { red: 255, green: 87, blue: 34 },
118
+ { red: 121, green: 85, blue: 72 },
119
+ { red: 158, green: 158, blue: 158 },
120
+ { red: 96, green: 125, blue: 139 },
121
+ ];