@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.
- package/dist/components/content-metadata/content-metadata-form.js +404 -0
- package/dist/components/content-metadata/content-rights.js +78 -0
- package/dist/components/content-metadata/content-warnings.js +68 -0
- package/dist/components/content-metadata/index.js +11 -0
- package/dist/components/dashboard/header.js +16 -2
- package/dist/components/dashboard/problems.js +29 -28
- package/dist/components/mobile-player/player.js +4 -0
- package/dist/components/mobile-player/ui/report-modal.js +3 -2
- package/dist/components/mobile-player/ui/viewer-context-menu.js +44 -1
- package/dist/components/ui/button.js +9 -9
- package/dist/components/ui/checkbox.js +87 -0
- package/dist/components/ui/dialog.js +188 -83
- package/dist/components/ui/dropdown.js +15 -10
- package/dist/components/ui/icons.js +6 -0
- package/dist/components/ui/primitives/button.js +0 -7
- package/dist/components/ui/primitives/input.js +13 -1
- package/dist/components/ui/primitives/modal.js +2 -2
- package/dist/components/ui/select.js +89 -0
- package/dist/components/ui/textarea.js +23 -4
- package/dist/components/ui/toast.js +464 -114
- package/dist/components/ui/tooltip.js +103 -0
- package/dist/index.js +2 -0
- package/dist/lib/metadata-constants.js +157 -0
- package/dist/lib/theme/theme.js +5 -3
- package/dist/lib/theme/tokens.js +9 -0
- package/dist/streamplace-provider/index.js +14 -4
- package/dist/streamplace-store/content-metadata-actions.js +118 -0
- package/dist/streamplace-store/graph.js +195 -0
- package/dist/streamplace-store/streamplace-store.js +18 -5
- package/dist/streamplace-store/user.js +67 -7
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/package.json +3 -3
- package/src/components/content-metadata/content-metadata-form.tsx +761 -0
- package/src/components/content-metadata/content-rights.tsx +104 -0
- package/src/components/content-metadata/content-warnings.tsx +100 -0
- package/src/components/content-metadata/index.tsx +18 -0
- package/src/components/dashboard/header.tsx +37 -3
- package/src/components/dashboard/index.tsx +1 -1
- package/src/components/dashboard/problems.tsx +57 -46
- package/src/components/mobile-player/player.tsx +5 -0
- package/src/components/mobile-player/ui/report-modal.tsx +13 -7
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +100 -1
- package/src/components/ui/button.tsx +10 -13
- package/src/components/ui/checkbox.tsx +147 -0
- package/src/components/ui/dialog.tsx +319 -99
- package/src/components/ui/dropdown.tsx +27 -13
- package/src/components/ui/icons.tsx +14 -0
- package/src/components/ui/primitives/button.tsx +0 -7
- package/src/components/ui/primitives/input.tsx +19 -2
- package/src/components/ui/primitives/modal.tsx +4 -2
- package/src/components/ui/select.tsx +175 -0
- package/src/components/ui/textarea.tsx +47 -29
- package/src/components/ui/toast.tsx +785 -179
- package/src/components/ui/tooltip.tsx +131 -0
- package/src/index.tsx +3 -0
- package/src/lib/metadata-constants.ts +180 -0
- package/src/lib/theme/theme.tsx +10 -6
- package/src/lib/theme/tokens.ts +9 -0
- package/src/streamplace-provider/index.tsx +20 -2
- package/src/streamplace-store/content-metadata-actions.tsx +142 -0
- package/src/streamplace-store/graph.tsx +232 -0
- package/src/streamplace-store/streamplace-store.tsx +30 -4
- package/src/streamplace-store/user.tsx +71 -7
- 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 (
|
|
134
|
-
console.
|
|
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
|
-
//
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
];
|