@streamplace/components 0.8.18 → 0.9.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 (120) hide show
  1. package/dist/components/chat/chat-box.d.ts +1 -1
  2. package/dist/components/chat/chat-box.d.ts.map +1 -1
  3. package/dist/components/chat/chat-box.js +3 -0
  4. package/dist/components/chat/chat-box.js.map +1 -1
  5. package/dist/components/chat/mod-view.d.ts +4 -2
  6. package/dist/components/chat/mod-view.d.ts.map +1 -1
  7. package/dist/components/chat/mod-view.js +142 -42
  8. package/dist/components/chat/mod-view.js.map +1 -1
  9. package/dist/components/dashboard/chat-panel.d.ts +2 -1
  10. package/dist/components/dashboard/chat-panel.d.ts.map +1 -1
  11. package/dist/components/dashboard/chat-panel.js +2 -3
  12. package/dist/components/dashboard/chat-panel.js.map +1 -1
  13. package/dist/components/dashboard/index.d.ts +1 -0
  14. package/dist/components/dashboard/index.d.ts.map +1 -1
  15. package/dist/components/dashboard/index.js +3 -1
  16. package/dist/components/dashboard/index.js.map +1 -1
  17. package/dist/components/dashboard/moderator-panel.d.ts +7 -0
  18. package/dist/components/dashboard/moderator-panel.d.ts.map +1 -0
  19. package/dist/components/dashboard/moderator-panel.js +256 -0
  20. package/dist/components/dashboard/moderator-panel.js.map +1 -0
  21. package/dist/components/ui/dialog.d.ts +1 -1
  22. package/dist/components/ui/menu.d.ts.map +1 -1
  23. package/dist/components/ui/menu.js +2 -2
  24. package/dist/components/ui/menu.js.map +1 -1
  25. package/dist/crypto-polyfill.native.js +7 -1
  26. package/dist/crypto-polyfill.native.js.map +1 -1
  27. package/dist/hooks/index.d.ts +1 -0
  28. package/dist/hooks/index.d.ts.map +1 -1
  29. package/dist/hooks/index.js +1 -0
  30. package/dist/hooks/index.js.map +1 -1
  31. package/dist/hooks/useDocumentTitle.d.ts +6 -0
  32. package/dist/hooks/useDocumentTitle.d.ts.map +1 -0
  33. package/dist/hooks/useDocumentTitle.js +40 -0
  34. package/dist/hooks/useDocumentTitle.js.map +1 -0
  35. package/dist/lib/theme/atoms.d.ts +138 -138
  36. package/dist/lib/theme/branded-theme-provider.d.ts +13 -0
  37. package/dist/lib/theme/branded-theme-provider.d.ts.map +1 -0
  38. package/dist/lib/theme/branded-theme-provider.js +34 -0
  39. package/dist/lib/theme/branded-theme-provider.js.map +1 -0
  40. package/dist/lib/theme/index.d.ts +1 -0
  41. package/dist/lib/theme/index.d.ts.map +1 -1
  42. package/dist/lib/theme/index.js +4 -1
  43. package/dist/lib/theme/index.js.map +1 -1
  44. package/dist/livestream-store/chat.d.ts +1 -1
  45. package/dist/livestream-store/chat.d.ts.map +1 -1
  46. package/dist/livestream-store/chat.js +1 -3
  47. package/dist/livestream-store/chat.js.map +1 -1
  48. package/dist/livestream-store/livestream-state.d.ts +3 -1
  49. package/dist/livestream-store/livestream-state.d.ts.map +1 -1
  50. package/dist/livestream-store/livestream-store.d.ts.map +1 -1
  51. package/dist/livestream-store/livestream-store.js +2 -0
  52. package/dist/livestream-store/livestream-store.js.map +1 -1
  53. package/dist/livestream-store/stream-key.d.ts +1 -0
  54. package/dist/livestream-store/stream-key.d.ts.map +1 -1
  55. package/dist/livestream-store/stream-key.js +2 -0
  56. package/dist/livestream-store/stream-key.js.map +1 -1
  57. package/dist/livestream-store/websocket-consumer.d.ts.map +1 -1
  58. package/dist/livestream-store/websocket-consumer.js +48 -0
  59. package/dist/livestream-store/websocket-consumer.js.map +1 -1
  60. package/dist/streamplace-provider/index.d.ts +3 -0
  61. package/dist/streamplace-provider/index.d.ts.map +1 -1
  62. package/dist/streamplace-provider/index.js +12 -1
  63. package/dist/streamplace-provider/index.js.map +1 -1
  64. package/dist/streamplace-store/block.d.ts +36 -2
  65. package/dist/streamplace-store/block.d.ts.map +1 -1
  66. package/dist/streamplace-store/block.js +121 -18
  67. package/dist/streamplace-store/block.js.map +1 -1
  68. package/dist/streamplace-store/branding.d.ts +27 -0
  69. package/dist/streamplace-store/branding.d.ts.map +1 -0
  70. package/dist/streamplace-store/branding.js +195 -0
  71. package/dist/streamplace-store/branding.js.map +1 -0
  72. package/dist/streamplace-store/index.d.ts +4 -0
  73. package/dist/streamplace-store/index.d.ts.map +1 -1
  74. package/dist/streamplace-store/index.js +4 -0
  75. package/dist/streamplace-store/index.js.map +1 -1
  76. package/dist/streamplace-store/moderation.d.ts +16 -0
  77. package/dist/streamplace-store/moderation.d.ts.map +1 -0
  78. package/dist/streamplace-store/moderation.js +141 -0
  79. package/dist/streamplace-store/moderation.js.map +1 -0
  80. package/dist/streamplace-store/moderator-management.d.ts +44 -0
  81. package/dist/streamplace-store/moderator-management.d.ts.map +1 -0
  82. package/dist/streamplace-store/moderator-management.js +136 -0
  83. package/dist/streamplace-store/moderator-management.js.map +1 -0
  84. package/dist/streamplace-store/streamplace-store.d.ts +6 -0
  85. package/dist/streamplace-store/streamplace-store.d.ts.map +1 -1
  86. package/dist/streamplace-store/streamplace-store.js +6 -0
  87. package/dist/streamplace-store/streamplace-store.js.map +1 -1
  88. package/dist/streamplace-store/xrpc.d.ts +1 -0
  89. package/dist/streamplace-store/xrpc.d.ts.map +1 -1
  90. package/dist/streamplace-store/xrpc.js +16 -0
  91. package/dist/streamplace-store/xrpc.js.map +1 -1
  92. package/locales/en-US/settings.ftl +91 -0
  93. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  94. package/package.json +3 -3
  95. package/src/components/chat/chat-box.tsx +3 -1
  96. package/src/components/chat/mod-view.tsx +431 -121
  97. package/src/components/dashboard/chat-panel.tsx +2 -1
  98. package/src/components/dashboard/index.tsx +1 -0
  99. package/src/components/dashboard/moderator-panel.tsx +632 -0
  100. package/src/components/ui/menu.tsx +1 -2
  101. package/src/crypto-polyfill.native.tsx +8 -1
  102. package/src/hooks/index.ts +1 -0
  103. package/src/hooks/useDocumentTitle.tsx +45 -0
  104. package/src/lib/theme/branded-theme-provider.tsx +58 -0
  105. package/src/lib/theme/index.ts +3 -0
  106. package/src/livestream-store/chat.tsx +0 -2
  107. package/src/livestream-store/livestream-state.tsx +5 -0
  108. package/src/livestream-store/livestream-store.tsx +2 -0
  109. package/src/livestream-store/stream-key.tsx +3 -0
  110. package/src/livestream-store/websocket-consumer.tsx +60 -0
  111. package/src/streamplace-provider/index.tsx +23 -4
  112. package/src/streamplace-store/block.tsx +139 -19
  113. package/src/streamplace-store/branding.tsx +216 -0
  114. package/src/streamplace-store/index.tsx +4 -0
  115. package/src/streamplace-store/moderation.tsx +185 -0
  116. package/src/streamplace-store/moderator-management.tsx +175 -0
  117. package/src/streamplace-store/streamplace-store.tsx +15 -0
  118. package/src/streamplace-store/xrpc.tsx +18 -1
  119. package/dist/assets/emoji-data.json +0 -19371
  120. package/src/assets/emoji-data.json +0 -19371
@@ -0,0 +1,185 @@
1
+ import { useEffect, useState } from "react";
2
+ import { PlaceStreamModerationPermission } from "streamplace";
3
+ import { useLivestreamStore } from "../livestream-store/livestream-store";
4
+ import { usePDSAgent } from "./xrpc";
5
+
6
+ export interface ModerationPermissions {
7
+ canBan: boolean;
8
+ canHide: boolean;
9
+ canManageLivestream: boolean;
10
+ isOwner: boolean;
11
+ isLoading: boolean;
12
+ error: string | null;
13
+ }
14
+
15
+ /**
16
+ * Hook to check if the current user can moderate for a given streamer.
17
+ * Returns permission flags based on:
18
+ * - Owner: full permissions if userDID === streamerDID
19
+ * - Delegated: permissions from place.stream.moderation.permission records
20
+ */
21
+ export function useCanModerate(
22
+ streamerDID: string | null | undefined,
23
+ ): ModerationPermissions {
24
+ const agent = usePDSAgent();
25
+ const userDID = agent?.did;
26
+
27
+ // Get moderation permissions from livestream store (updated via WebSocket)
28
+ const moderationPermissions = useLivestreamStore(
29
+ (state) => state.moderationPermissions,
30
+ );
31
+ const setModerationPermissions = useLivestreamStore(
32
+ (state) => state.setModerationPermissions,
33
+ );
34
+
35
+ const [isOwner, setIsOwner] = useState(false);
36
+ const [isLoading, setIsLoading] = useState(false);
37
+ const [error, setError] = useState<string | null>(null);
38
+
39
+ useEffect(() => {
40
+ if (!userDID || !streamerDID) {
41
+ setModerationPermissions([]);
42
+ setIsOwner(false);
43
+ setError(null);
44
+ return;
45
+ }
46
+
47
+ // If user is the streamer, they have full permissions
48
+ if (userDID === streamerDID) {
49
+ setIsOwner(true);
50
+ setModerationPermissions([]); // Not needed for owner
51
+ setIsLoading(false);
52
+ setError(null);
53
+ return;
54
+ }
55
+
56
+ // Otherwise, fetch delegation records from the streamer's repo
57
+ // This initial fetch populates the store, then WebSocket updates will keep it in sync
58
+ const fetchDelegation = async () => {
59
+ if (!agent) {
60
+ setModerationPermissions([]);
61
+ setIsLoading(false);
62
+ return;
63
+ }
64
+
65
+ setIsLoading(true);
66
+ setError(null);
67
+ setIsOwner(false);
68
+
69
+ try {
70
+ // Use authenticated agent to list permission records from the streamer's repo
71
+ const result = await agent.com.atproto.repo.listRecords({
72
+ repo: streamerDID,
73
+ collection: "place.stream.moderation.permission",
74
+ limit: 100,
75
+ });
76
+
77
+ const records = result.data.records || [];
78
+ const permissionRecords: PlaceStreamModerationPermission.Record[] =
79
+ records
80
+ .map((r: { value: any }) => r.value)
81
+ .filter(
82
+ (v: any) => v && v.$type === "place.stream.moderation.permission",
83
+ );
84
+
85
+ // Store all permissions in the livestream store
86
+ // WebSocket updates will keep this in sync
87
+ setModerationPermissions(permissionRecords);
88
+ } catch (err) {
89
+ console.error("[useCanModerate] Error fetching permissions:", err);
90
+ setError(
91
+ `Could not fetch moderation permissions: ${err instanceof Error ? err.message : `Unknown error: ${err}`}`,
92
+ );
93
+ setModerationPermissions([]);
94
+ } finally {
95
+ setIsLoading(false);
96
+ }
97
+ };
98
+
99
+ // Fetch immediately on mount or when dependencies change
100
+ fetchDelegation();
101
+ }, [userDID, streamerDID, agent, setModerationPermissions]);
102
+
103
+ // If permissions were cleared (e.g., due to deletion), trigger a refetch
104
+ useEffect(() => {
105
+ // If permissions were cleared and we're not the owner, refetch
106
+ if (
107
+ moderationPermissions.length === 0 &&
108
+ !isOwner &&
109
+ userDID &&
110
+ streamerDID &&
111
+ agent
112
+ ) {
113
+ const fetchDelegation = async () => {
114
+ try {
115
+ const result = await agent.com.atproto.repo.listRecords({
116
+ repo: streamerDID,
117
+ collection: "place.stream.moderation.permission",
118
+ limit: 100,
119
+ });
120
+
121
+ const records = result.data.records || [];
122
+ const permissionRecords: PlaceStreamModerationPermission.Record[] =
123
+ records
124
+ .map((r: { value: any }) => r.value)
125
+ .filter(
126
+ (v: any) =>
127
+ v && v.$type === "place.stream.moderation.permission",
128
+ );
129
+
130
+ setModerationPermissions(permissionRecords);
131
+ } catch (err) {
132
+ console.error("[useCanModerate] Error refetching permissions:", err);
133
+ }
134
+ };
135
+
136
+ // Small delay to avoid rapid refetches
137
+ const timeout = setTimeout(fetchDelegation, 100);
138
+ return () => clearTimeout(timeout);
139
+ }
140
+ }, [
141
+ moderationPermissions.length,
142
+ isOwner,
143
+ userDID,
144
+ streamerDID,
145
+ agent,
146
+ setModerationPermissions,
147
+ ]);
148
+
149
+ // Find ALL delegation records for this moderator and merge their permissions
150
+ const delegations = moderationPermissions.filter(
151
+ (perm) => perm.moderator === userDID,
152
+ );
153
+
154
+ // Merge permissions from all delegation records for this moderator
155
+ const permissions: string[] = delegations.reduce(
156
+ (acc: string[], delegation) => {
157
+ // Check if delegation has expired
158
+ if (delegation.expirationTime) {
159
+ const expiration = new Date(delegation.expirationTime);
160
+ if (new Date() > expiration) {
161
+ return acc; // Skip expired delegations
162
+ }
163
+ }
164
+
165
+ // Add all permissions from this delegation, avoiding duplicates
166
+ const delegationPerms = delegation.permissions || [];
167
+ for (const perm of delegationPerms) {
168
+ if (!acc.includes(perm)) {
169
+ acc.push(perm);
170
+ }
171
+ }
172
+ return acc;
173
+ },
174
+ [],
175
+ );
176
+
177
+ return {
178
+ canBan: isOwner || permissions.includes("ban"),
179
+ canHide: isOwner || permissions.includes("hide"),
180
+ canManageLivestream: isOwner || permissions.includes("livestream.manage"),
181
+ isOwner,
182
+ isLoading,
183
+ error,
184
+ };
185
+ }
@@ -0,0 +1,175 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { PlaceStreamModerationPermission } from "streamplace";
3
+ import { usePDSAgent } from "./xrpc";
4
+
5
+ interface ModeratorRecord {
6
+ uri: string;
7
+ cid: string;
8
+ value: PlaceStreamModerationPermission.Record;
9
+ rkey: string;
10
+ }
11
+
12
+ interface ListModeratorsResult {
13
+ moderators: ModeratorRecord[];
14
+ isLoading: boolean;
15
+ error: string | null;
16
+ refresh: () => void;
17
+ }
18
+
19
+ /**
20
+ * Hook to list all moderators for the current user's stream.
21
+ * Fetches place.stream.moderation.permission records from the user's repo.
22
+ */
23
+ export function useListModerators(): ListModeratorsResult {
24
+ const agent = usePDSAgent();
25
+ const [moderators, setModerators] = useState<ModeratorRecord[]>([]);
26
+ const [isLoading, setIsLoading] = useState(false);
27
+ const [error, setError] = useState<string | null>(null);
28
+ const [refreshTrigger, setRefreshTrigger] = useState(0);
29
+
30
+ const refresh = useCallback(() => {
31
+ setRefreshTrigger((prev) => prev + 1);
32
+ }, []);
33
+
34
+ useEffect(() => {
35
+ if (!agent?.did) {
36
+ setModerators([]);
37
+ setError(null);
38
+ return;
39
+ }
40
+
41
+ const fetchModerators = async () => {
42
+ setIsLoading(true);
43
+ setError(null);
44
+
45
+ try {
46
+ const result = await agent.place.stream.moderation.permission.list({
47
+ repo: agent.did!,
48
+ });
49
+
50
+ const records = result.records.map((record: any) => {
51
+ const rkey = record.uri.split("/").pop();
52
+ return {
53
+ uri: record.uri,
54
+ cid: record.cid || "",
55
+ value: record.value,
56
+ rkey: rkey || "",
57
+ };
58
+ });
59
+
60
+ setModerators(records);
61
+ } catch (err) {
62
+ setError(
63
+ `Failed to fetch moderators: ${err instanceof Error ? err.message : "Unknown error"}`,
64
+ );
65
+ setModerators([]);
66
+ } finally {
67
+ setIsLoading(false);
68
+ }
69
+ };
70
+
71
+ fetchModerators();
72
+ }, [agent?.did, refreshTrigger]);
73
+
74
+ return {
75
+ moderators,
76
+ isLoading,
77
+ error,
78
+ refresh,
79
+ };
80
+ }
81
+
82
+ interface AddModeratorParams {
83
+ moderatorDID: string;
84
+ permissions: ("ban" | "hide" | "livestream.manage")[];
85
+ expirationTime?: string; // ISO 8601 datetime string
86
+ }
87
+
88
+ /**
89
+ * Hook to add a new moderator.
90
+ * Creates a place.stream.moderation.permission record in the current user's repo.
91
+ */
92
+ export function useAddModerator() {
93
+ const agent = usePDSAgent();
94
+ const [isLoading, setIsLoading] = useState(false);
95
+
96
+ const addModerator = async (params: AddModeratorParams) => {
97
+ if (!agent?.did) {
98
+ throw new Error("Not logged in");
99
+ }
100
+
101
+ if (params.permissions.length === 0) {
102
+ throw new Error("At least one permission must be selected");
103
+ }
104
+
105
+ setIsLoading(true);
106
+ try {
107
+ // Resolve handle to DID if needed
108
+ let moderatorDID = params.moderatorDID.trim();
109
+ if (!moderatorDID.startsWith("did:")) {
110
+ if (moderatorDID.startsWith("@")) {
111
+ moderatorDID = moderatorDID.substring(1); // Remove @
112
+ }
113
+ try {
114
+ const resolved = await agent.com.atproto.identity.resolveHandle({
115
+ handle: moderatorDID,
116
+ });
117
+ moderatorDID = resolved.data.did;
118
+ } catch (e) {
119
+ throw new Error(
120
+ `Invalid DID or handle: ${moderatorDID}. Please use a valid DID (did:plc:...) or handle (@handle.bsky.social)`,
121
+ );
122
+ }
123
+ }
124
+
125
+ const record: PlaceStreamModerationPermission.Record = {
126
+ $type: "place.stream.moderation.permission",
127
+ moderator: moderatorDID,
128
+ permissions: params.permissions,
129
+ createdAt: new Date().toISOString(),
130
+ };
131
+
132
+ if (params.expirationTime) {
133
+ (record as any).expirationTime = params.expirationTime;
134
+ }
135
+
136
+ const result = await agent.place.stream.moderation.permission.create(
137
+ { repo: agent.did },
138
+ record,
139
+ );
140
+
141
+ return result;
142
+ } finally {
143
+ setIsLoading(false);
144
+ }
145
+ };
146
+
147
+ return { addModerator, isLoading };
148
+ }
149
+
150
+ /**
151
+ * Hook to remove a moderator.
152
+ * Deletes a place.stream.moderation.permission record by its rkey.
153
+ */
154
+ export function useRemoveModerator() {
155
+ const agent = usePDSAgent();
156
+ const [isLoading, setIsLoading] = useState(false);
157
+
158
+ const removeModerator = async (rkey: string) => {
159
+ if (!agent?.did) {
160
+ throw new Error("Not logged in");
161
+ }
162
+
163
+ setIsLoading(true);
164
+ try {
165
+ await agent.place.stream.moderation.permission.delete({
166
+ repo: agent.did,
167
+ rkey,
168
+ });
169
+ } finally {
170
+ setIsLoading(false);
171
+ }
172
+ };
173
+
174
+ return { removeModerator, isLoading };
175
+ }
@@ -4,6 +4,7 @@ import { PlaceStreamChatProfile, PlaceStreamLivestream } from "streamplace";
4
4
  import { createStore, StoreApi, useStore } from "zustand";
5
5
  import storage from "../storage";
6
6
  import { StreamplaceContext } from "../streamplace-provider/context";
7
+ import { BrandingAsset } from "./branding";
7
8
 
8
9
  export interface ContentMetadataResult {
9
10
  record: any;
@@ -48,6 +49,13 @@ export interface StreamplaceState {
48
49
  setBroadcasterDID: (broadcasterDID: string | null) => void;
49
50
  serverDID: string | null;
50
51
  setServerDID: (serverDID: string | null) => void;
52
+ adminDIDs: string[];
53
+ setAdminDIDs: (adminDIDs: string[]) => void;
54
+
55
+ // Branding state
56
+ branding: Record<string, BrandingAsset> | null;
57
+ brandingLoading: boolean;
58
+ brandingError: string | null;
51
59
 
52
60
  // Volume state
53
61
  volume: number;
@@ -111,11 +119,18 @@ export const makeStreamplaceStore = ({
111
119
  set({ broadcasterDID }),
112
120
  serverDID: null,
113
121
  setServerDID: (serverDID: string | null) => set({ serverDID }),
122
+ adminDIDs: [],
123
+ setAdminDIDs: (adminDIDs: string[]) => set({ adminDIDs }),
114
124
 
115
125
  // Content metadata
116
126
  contentMetadata: null,
117
127
  setContentMetadata: (metadata) => set({ contentMetadata: metadata }),
118
128
 
129
+ // Branding state
130
+ branding: null,
131
+ brandingLoading: false,
132
+ brandingError: null,
133
+
119
134
  // Volume state - start with defaults
120
135
  volume: 1.0,
121
136
  muted: false,
@@ -1,6 +1,6 @@
1
1
  import { useMemo } from "react";
2
2
  import { StreamplaceAgent } from "streamplace";
3
- import { useStreamplaceStore } from ".";
3
+ import { useStreamplaceStore, useUrl } from ".";
4
4
 
5
5
  export function usePDSAgent(): StreamplaceAgent | null {
6
6
  const oauthSession = useStreamplaceStore((state) => state.oauthSession);
@@ -20,3 +20,20 @@ export function usePDSAgent(): StreamplaceAgent | null {
20
20
  return new StreamplaceAgent(oauthSession);
21
21
  }, [oauthSession]);
22
22
  }
23
+
24
+ // can be unauthed, but will always use the current node URL
25
+ export function usePossiblyUnauthedPDSAgent(): StreamplaceAgent | null {
26
+ const nodeUrl = useUrl();
27
+ const oauthSession = useStreamplaceStore((state) => state.oauthSession);
28
+ // oauthsession is
29
+ // - undefined when loading
30
+ // - null when logged out, and
31
+ // - SessionManager when logged in
32
+ return useMemo(() => {
33
+ if (!oauthSession) {
34
+ return new StreamplaceAgent(nodeUrl);
35
+ }
36
+
37
+ return new StreamplaceAgent(oauthSession);
38
+ }, [oauthSession]);
39
+ }