@streamplace/components 0.9.4 → 0.9.7

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 (67) hide show
  1. package/dist/components/chat/chat-box.d.ts.map +1 -1
  2. package/dist/components/chat/chat-box.js +18 -1
  3. package/dist/components/chat/chat-box.js.map +1 -1
  4. package/dist/components/chat/mod-view.d.ts.map +1 -1
  5. package/dist/components/chat/mod-view.js +7 -79
  6. package/dist/components/chat/mod-view.js.map +1 -1
  7. package/dist/components/chat/update-stream-title-dialog.d.ts +9 -0
  8. package/dist/components/chat/update-stream-title-dialog.d.ts.map +1 -0
  9. package/dist/components/chat/update-stream-title-dialog.js +74 -0
  10. package/dist/components/chat/update-stream-title-dialog.js.map +1 -0
  11. package/dist/components/content-metadata/content-metadata-form.d.ts.map +1 -1
  12. package/dist/components/content-metadata/content-metadata-form.js +7 -8
  13. package/dist/components/content-metadata/content-metadata-form.js.map +1 -1
  14. package/dist/components/ui/admonition.d.ts +14 -0
  15. package/dist/components/ui/admonition.d.ts.map +1 -0
  16. package/dist/components/ui/admonition.js +117 -0
  17. package/dist/components/ui/admonition.js.map +1 -0
  18. package/dist/components/ui/button.d.ts +1 -0
  19. package/dist/components/ui/button.d.ts.map +1 -1
  20. package/dist/components/ui/button.js +2 -2
  21. package/dist/components/ui/button.js.map +1 -1
  22. package/dist/components/ui/index.d.ts +1 -0
  23. package/dist/components/ui/index.d.ts.map +1 -1
  24. package/dist/components/ui/index.js +1 -0
  25. package/dist/components/ui/index.js.map +1 -1
  26. package/dist/components/ui/primitives/button.d.ts +3 -2
  27. package/dist/components/ui/primitives/button.d.ts.map +1 -1
  28. package/dist/components/ui/primitives/button.js +20 -2
  29. package/dist/components/ui/primitives/button.js.map +1 -1
  30. package/dist/components/ui/resizeable.d.ts.map +1 -1
  31. package/dist/components/ui/resizeable.js +2 -1
  32. package/dist/components/ui/resizeable.js.map +1 -1
  33. package/dist/components/ui/text.d.ts +2 -1
  34. package/dist/components/ui/text.d.ts.map +1 -1
  35. package/dist/components/ui/text.js.map +1 -1
  36. package/dist/components/ui/textarea.d.ts.map +1 -1
  37. package/dist/components/ui/textarea.js +3 -1
  38. package/dist/components/ui/textarea.js.map +1 -1
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +2 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/lib/theme/atoms.d.ts +148 -148
  44. package/dist/lib/theme/tokens.d.ts +11 -11
  45. package/dist/lib/theme/tokens.js +11 -11
  46. package/dist/utils/did.d.ts +13 -0
  47. package/dist/utils/did.d.ts.map +1 -0
  48. package/dist/utils/did.js +43 -0
  49. package/dist/utils/did.js.map +1 -0
  50. package/locales/en-US/settings.ftl +2 -1
  51. package/node-compile-cache/v22.15.0-x64-92db9086-0/37be0eec +0 -0
  52. package/package.json +3 -2
  53. package/src/components/chat/chat-box.tsx +24 -2
  54. package/src/components/chat/mod-view.tsx +0 -205
  55. package/src/components/chat/update-stream-title-dialog.tsx +169 -0
  56. package/src/components/content-metadata/content-metadata-form.tsx +37 -10
  57. package/src/components/ui/admonition.tsx +177 -0
  58. package/src/components/ui/button.tsx +3 -0
  59. package/src/components/ui/index.ts +1 -0
  60. package/src/components/ui/primitives/button.tsx +37 -11
  61. package/src/components/ui/resizeable.tsx +2 -1
  62. package/src/components/ui/text.tsx +11 -1
  63. package/src/components/ui/textarea.tsx +3 -0
  64. package/src/index.tsx +2 -0
  65. package/src/lib/theme/tokens.ts +11 -11
  66. package/src/utils/did.ts +61 -0
  67. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
@@ -329,17 +329,17 @@ export declare const colors: {
329
329
  readonly 950: "#052e16";
330
330
  };
331
331
  readonly warning: {
332
- readonly 50: "#fffbeb";
333
- readonly 100: "#fef3c7";
334
- readonly 200: "#fde68a";
335
- readonly 300: "#fcd34d";
336
- readonly 400: "#fbbf24";
337
- readonly 500: "#f59e0b";
338
- readonly 600: "#d97706";
339
- readonly 700: "#b45309";
340
- readonly 800: "#92400e";
341
- readonly 900: "#78350f";
342
- readonly 950: "#451a03";
332
+ readonly 50: "#fffaf0";
333
+ readonly 100: "#ffe6c7";
334
+ readonly 200: "#ffd99e";
335
+ readonly 300: "#ffcc75";
336
+ readonly 400: "#ffb94e";
337
+ readonly 500: "#ff9e1f";
338
+ readonly 600: "#e67e00";
339
+ readonly 700: "#cc6600";
340
+ readonly 800: "#998c00";
341
+ readonly 900: "#664200";
342
+ readonly 950: "#332900";
343
343
  };
344
344
  readonly ios: {
345
345
  readonly systemBlue: "#007AFF";
@@ -335,17 +335,17 @@ exports.colors = {
335
335
  950: "#052e16",
336
336
  },
337
337
  warning: {
338
- 50: "#fffbeb",
339
- 100: "#fef3c7",
340
- 200: "#fde68a",
341
- 300: "#fcd34d",
342
- 400: "#fbbf24",
343
- 500: "#f59e0b",
344
- 600: "#d97706",
345
- 700: "#b45309",
346
- 800: "#92400e",
347
- 900: "#78350f",
348
- 950: "#451a03",
338
+ 50: "#fffaf0",
339
+ 100: "#ffe6c7",
340
+ 200: "#ffd99e",
341
+ 300: "#ffcc75",
342
+ 400: "#ffb94e",
343
+ 500: "#ff9e1f",
344
+ 600: "#e67e00",
345
+ 700: "#cc6600",
346
+ 800: "#998c00",
347
+ 900: "#664200",
348
+ 950: "#332900",
349
349
  },
350
350
  // iOS system colors (adaptive)
351
351
  ios: {
@@ -0,0 +1,13 @@
1
+ export interface DIDDocument {
2
+ id: string;
3
+ service?: Array<{
4
+ id: string;
5
+ type?: string;
6
+ serviceEndpoint?: string;
7
+ }>;
8
+ [key: string]: any;
9
+ }
10
+ export declare function resolveDIDDocument(did: string): Promise<DIDDocument>;
11
+ export declare function getPDSServiceEndpoint(didDoc: DIDDocument): string;
12
+ export declare function getBlob(did: string, cid: string, didDoc?: DIDDocument): Promise<Blob>;
13
+ //# sourceMappingURL=did.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"did.d.ts","sourceRoot":"","sources":["../../src/utils/did.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,CAAC,EAAE,KAAK,CAAC;QACd,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC,CAAC;IACH,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAsB1E;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAQjE;AAED,wBAAsB,OAAO,CAC3B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAYf"}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveDIDDocument = resolveDIDDocument;
4
+ exports.getPDSServiceEndpoint = getPDSServiceEndpoint;
5
+ exports.getBlob = getBlob;
6
+ async function resolveDIDDocument(did) {
7
+ let didDocUrl;
8
+ if (did.startsWith("did:web:")) {
9
+ // For did:web, construct the URL directly
10
+ const domain = did.replace("did:web:", "").replace(/:/g, "/");
11
+ didDocUrl = `https://${domain}/.well-known/did.json`;
12
+ }
13
+ else if (did.startsWith("did:plc:")) {
14
+ // For did:plc, use plc.directory
15
+ didDocUrl = `https://plc.directory/${did}`;
16
+ }
17
+ else {
18
+ throw new Error(`Unsupported DID method: ${did}`);
19
+ }
20
+ const response = await fetch(didDocUrl);
21
+ if (!response.ok) {
22
+ throw new Error(`Failed to resolve DID document for ${did}: ${response.status}`);
23
+ }
24
+ return response.json();
25
+ }
26
+ function getPDSServiceEndpoint(didDoc) {
27
+ const pdsService = didDoc.service?.find((s) => s.id === "#atproto_pds");
28
+ if (!pdsService?.serviceEndpoint) {
29
+ throw new Error("No PDS service endpoint found in DID document");
30
+ }
31
+ return pdsService.serviceEndpoint;
32
+ }
33
+ async function getBlob(did, cid, didDoc) {
34
+ const doc = didDoc || (await resolveDIDDocument(did));
35
+ const pdsEndpoint = getPDSServiceEndpoint(doc);
36
+ const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}`;
37
+ const response = await fetch(blobUrl);
38
+ if (!response.ok) {
39
+ throw new Error(`Failed to fetch blob: ${response.status}`);
40
+ }
41
+ return response.blob();
42
+ }
43
+ //# sourceMappingURL=did.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"did.js","sourceRoot":"","sources":["../../src/utils/did.ts"],"names":[],"mappings":";;AAUA,gDAsBC;AAED,sDAQC;AAED,0BAgBC;AAlDM,KAAK,UAAU,kBAAkB,CAAC,GAAW;IAClD,IAAI,SAAiB,CAAC;IAEtB,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,0CAA0C;QAC1C,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC9D,SAAS,GAAG,WAAW,MAAM,uBAAuB,CAAC;IACvD,CAAC;SAAM,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACtC,iCAAiC;QACjC,SAAS,GAAG,yBAAyB,GAAG,EAAE,CAAC;IAC7C,CAAC;SAAM,CAAC;QACN,MAAM,IAAI,KAAK,CAAC,2BAA2B,GAAG,EAAE,CAAC,CAAC;IACpD,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,CAAC;IACxC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACb,sCAAsC,GAAG,KAAK,QAAQ,CAAC,MAAM,EAAE,CAChE,CAAC;IACJ,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;AACzB,CAAC;AAED,SAAgB,qBAAqB,CAAC,MAAmB;IACvD,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,cAAc,CAAC,CAAC;IAExE,IAAI,CAAC,UAAU,EAAE,eAAe,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACnE,CAAC;IAED,OAAO,UAAU,CAAC,eAAe,CAAC;AACpC,CAAC;AAEM,KAAK,UAAU,OAAO,CAC3B,GAAW,EACX,GAAW,EACX,MAAoB;IAEpB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC;IACtD,MAAM,WAAW,GAAG,qBAAqB,CAAC,GAAG,CAAC,CAAC;IAE/C,MAAM,OAAO,GAAG,GAAG,WAAW,sCAAsC,GAAG,QAAQ,GAAG,EAAE,CAAC;IAErF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;IACtC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,yBAAyB,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9D,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;AACzB,CAAC"}
@@ -121,7 +121,7 @@ inactive = Inactive
121
121
  active = Active
122
122
 
123
123
  ## Multistreaming
124
- multistreaming = Multistreaming
124
+ multistream = Multistreaming
125
125
  multistream-targets = Multistream Targets
126
126
  multistream-description = Automatically push your Streamplace livestreams to other streaming services like Twitch or YouTube.
127
127
  create-multistream-target = Create Multistream Target
@@ -168,6 +168,7 @@ need-setup-live-dashboard = Need to set up streaming first? Visit the live dashb
168
168
  no-languages-found = No languages found
169
169
 
170
170
  ## Branding Administration
171
+ branding = Branding
171
172
  branding-admin = Branding Administration
172
173
  branding-admin-description = Customize your Streamplace instance. Note that settings may take a few hours to propagate.
173
174
  branding-login-required = Please log in to manage branding
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamplace/components",
3
- "version": "0.9.4",
3
+ "version": "0.9.7",
4
4
  "description": "Streamplace React (Native) Components",
5
5
  "main": "dist/index.js",
6
6
  "types": "src/index.tsx",
@@ -42,6 +42,7 @@
42
42
  "expo-sensors": "^15.0.7",
43
43
  "expo-sqlite": "~15.2.12",
44
44
  "expo-video": "^2.0.0",
45
+ "graphemer": "^1.4.0",
45
46
  "hls.js": "^1.5.17",
46
47
  "i18next": "^25.4.2",
47
48
  "i18next-browser-languagedetector": "^8.2.0",
@@ -80,5 +81,5 @@
80
81
  "i18n:watch": "nodemon --watch 'locales/**/*.ftl' --exec 'node scripts/compile-translations.js'",
81
82
  "i18n:extract": "i18next-cli extract && node scripts/migrate-i18n.js"
82
83
  },
83
- "gitHead": "4c5523a92b26dbdae76dd3334ddf66ce4d32f999"
84
+ "gitHead": "46b5c8833f5c53934374899c417e6c83a9b0c1f2"
84
85
  }
@@ -1,10 +1,11 @@
1
1
  import Picker from "@emoji-mart/react";
2
+ import Graphemer from "graphemer";
2
3
  import { AtSignIcon, ExternalLink, X } from "lucide-react-native";
3
4
  import { env } from "process";
4
5
  import { useEffect, useMemo, useRef, useState } from "react";
5
6
  import { Platform, Pressable, TextInput } from "react-native";
6
7
  import { ChatMessageViewHydrated } from "streamplace";
7
- import { Button, Loader, Text, useTheme, View } from "../../";
8
+ import { Button, Loader, Text, toast, useTheme, View } from "../../";
8
9
  import { handleSlashCommand } from "../../lib/slash-commands";
9
10
  import { registerTeleportCommand } from "../../lib/slash-commands/teleport";
10
11
  import { StreamNotifications } from "../../lib/stream-notifications";
@@ -41,6 +42,8 @@ const COOL_EMOJI_LIST = [
41
42
  ..."😀🥸😍😘😁🥸😆🥸😜🥸😂😅🥸🙂🤫😱🥸🤣😗😄🥸😎🤓😲😯😰🥸😥🥸😣🥸😞😓🥸😩😩🥸😤🥱",
42
43
  ];
43
44
 
45
+ const graphemer = new Graphemer();
46
+
44
47
  export function ChatBox({
45
48
  isPopout,
46
49
  chatBoxStyle,
@@ -65,6 +68,7 @@ export function ChatBox({
65
68
  new Map(),
66
69
  );
67
70
  const [filteredEmojis, setFilteredEmojis] = useState<any[]>([]);
71
+ const isOverLimit = graphemer.countGraphemes(message) > 300;
68
72
 
69
73
  let linfo = useLivestream();
70
74
 
@@ -255,6 +259,17 @@ export function ChatBox({
255
259
 
256
260
  const submit = async () => {
257
261
  if (!message.trim()) return;
262
+ if (graphemer.countGraphemes(message) > 300) {
263
+ toast.show(
264
+ "Message too long",
265
+ "Please limit your message to 300 characters.",
266
+ {
267
+ variant: "error",
268
+ duration: 3,
269
+ },
270
+ );
271
+ return;
272
+ }
258
273
 
259
274
  const messageText = message;
260
275
  setMessage("");
@@ -457,7 +472,14 @@ export function ChatBox({
457
472
  }
458
473
  }
459
474
  }}
460
- style={[chatBoxStyle]}
475
+ style={[
476
+ chatBoxStyle,
477
+ isOverLimit && {
478
+ borderColor: "#ef4444",
479
+ borderWidth: 2,
480
+ outline: "none",
481
+ },
482
+ ]}
461
483
  // "submit" won't blur on enter
462
484
  submitBehavior="submit"
463
485
  placeholder="Type a message..."
@@ -5,7 +5,6 @@ import { usePlayerStore } from "../../player-store";
5
5
  import {
6
6
  useCreateBlockRecord,
7
7
  useCreateHideChatRecord,
8
- useUpdateLivestreamRecord,
9
8
  } from "../../streamplace-store/block";
10
9
  import {
11
10
  ModerationPermissions,
@@ -17,24 +16,19 @@ import { Linking } from "react-native";
17
16
  import { ChatMessageViewHydrated } from "streamplace";
18
17
  import {
19
18
  useDeleteChatMessage,
20
- useLivestream,
21
19
  useLivestreamStore,
22
20
  } from "../../livestream-store";
23
21
  import { useStreamplaceStore } from "../../streamplace-store";
24
22
  import { formatHandle, formatHandleWithAt } from "../../utils/format-handle";
25
23
  import {
26
24
  atoms,
27
- Button,
28
- DialogFooter,
29
25
  DropdownMenu,
30
26
  DropdownMenuGroup,
31
27
  DropdownMenuItem,
32
28
  DropdownMenuTrigger,
33
29
  layout,
34
- ResponsiveDialog,
35
30
  ResponsiveDropdownMenuContent,
36
31
  Text,
37
- Textarea,
38
32
  useToast,
39
33
  View,
40
34
  } from "../ui";
@@ -61,10 +55,6 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
61
55
  let [messageRemoved, setMessageRemoved] = useState(false);
62
56
  let { createBlock, isLoading: isBlockLoading } = useCreateBlockRecord();
63
57
  let { createHideChat, isLoading: isHideLoading } = useCreateHideChatRecord();
64
- let { updateLivestream, isLoading: isUpdateTitleLoading } =
65
- useUpdateLivestreamRecord();
66
- const livestream = useLivestream();
67
- const [showUpdateTitleDialog, setShowUpdateTitleDialog] = useState(false);
68
58
 
69
59
  const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen);
70
60
  const setReportSubject = usePlayerStore((x) => x.setReportSubject);
@@ -135,9 +125,6 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
135
125
  createHideChat={createHideChat}
136
126
  createBlock={createBlock}
137
127
  toast={toast}
138
- setShowUpdateTitleDialog={setShowUpdateTitleDialog}
139
- isUpdateTitleLoading={isUpdateTitleLoading}
140
- livestream={livestream}
141
128
  setReportModalOpen={setReportModalOpen}
142
129
  setReportSubject={setReportSubject}
143
130
  deleteChatMessage={deleteChatMessage}
@@ -145,17 +132,6 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
145
132
  )}
146
133
  </ResponsiveDropdownMenuContent>
147
134
  </DropdownMenu>
148
-
149
- {/* Update Stream Title Dialog - rendered outside dropdown */}
150
- {showUpdateTitleDialog && (
151
- <UpdateStreamTitleDialog
152
- livestream={livestream}
153
- streamerDID={streamerDID}
154
- updateLivestream={updateLivestream}
155
- isLoading={isUpdateTitleLoading}
156
- onClose={() => setShowUpdateTitleDialog(false)}
157
- />
158
- )}
159
135
  </>
160
136
  );
161
137
  });
@@ -173,9 +149,6 @@ interface ModViewContentProps {
173
149
  createHideChat: (uri: string, streamerDID?: string) => Promise<any>;
174
150
  createBlock: (did: string, streamerDID?: string) => Promise<any>;
175
151
  toast: ReturnType<typeof useToast>;
176
- setShowUpdateTitleDialog: (show: boolean) => void;
177
- isUpdateTitleLoading: boolean;
178
- livestream: any;
179
152
  setReportModalOpen: (open: boolean) => void;
180
153
  setReportSubject: (subject: any) => void;
181
154
  deleteChatMessage: (uri: string) => Promise<any>;
@@ -194,9 +167,6 @@ function ModViewContent({
194
167
  createHideChat,
195
168
  createBlock,
196
169
  toast,
197
- setShowUpdateTitleDialog,
198
- isUpdateTitleLoading,
199
- livestream,
200
170
  setReportModalOpen,
201
171
  setReportSubject,
202
172
  deleteChatMessage,
@@ -290,23 +260,6 @@ function ModViewContent({
290
260
  </DropdownMenuGroup>
291
261
  )}
292
262
 
293
- {modPermissions.canManageLivestream && (
294
- <DropdownMenuGroup key="stream-actions" title={`Stream actions`}>
295
- <DropdownMenuItem
296
- onPress={() => {
297
- setShowUpdateTitleDialog(true);
298
- }}
299
- disabled={isUpdateTitleLoading || !livestream}
300
- >
301
- <Text
302
- color={isUpdateTitleLoading || !livestream ? "muted" : "primary"}
303
- >
304
- {isUpdateTitleLoading ? "Updating..." : "Update stream title"}
305
- </Text>
306
- </DropdownMenuItem>
307
- </DropdownMenuGroup>
308
- )}
309
-
310
263
  <DropdownMenuGroup key="user-actions" title={`User actions`}>
311
264
  <DropdownMenuItem
312
265
  onPress={() => {
@@ -418,161 +371,3 @@ export function ReportButton({
418
371
  </DropdownMenuItem>
419
372
  );
420
373
  }
421
-
422
- interface UpdateStreamTitleDialogProps {
423
- livestream: any;
424
- streamerDID?: string;
425
- updateLivestream: (
426
- livestreamUri: string,
427
- title: string,
428
- streamerDID?: string,
429
- ) => Promise<any>;
430
- isLoading: boolean;
431
- onClose: () => void;
432
- }
433
-
434
- function UpdateStreamTitleDialog({
435
- livestream,
436
- streamerDID,
437
- updateLivestream,
438
- isLoading,
439
- onClose,
440
- }: UpdateStreamTitleDialogProps) {
441
- const [title, setTitle] = useState(livestream?.record?.title || "");
442
- const [error, setError] = useState<string | null>(null);
443
- const toast = useToast();
444
-
445
- useEffect(() => {
446
- if (livestream?.record?.title) {
447
- setTitle(livestream.record.title);
448
- }
449
- }, [livestream?.record?.title]);
450
-
451
- const handleUpdate = async () => {
452
- setError(null);
453
-
454
- if (!title.trim()) {
455
- setError("Please enter a stream title");
456
- return;
457
- }
458
-
459
- if (!livestream?.uri) {
460
- setError("No livestream found");
461
- return;
462
- }
463
-
464
- try {
465
- await updateLivestream(livestream.uri, title.trim(), streamerDID);
466
- toast.show(
467
- "Stream title updated",
468
- "The stream title has been successfully updated.",
469
- { duration: 3 },
470
- );
471
- onClose();
472
- } catch (err) {
473
- setError(
474
- err instanceof Error ? err.message : "Failed to update stream title",
475
- );
476
- }
477
- };
478
-
479
- return (
480
- <ResponsiveDialog
481
- open={true}
482
- onOpenChange={(open) => {
483
- if (!open) {
484
- onClose();
485
- setError(null);
486
- setTitle(livestream?.record?.title || "");
487
- }
488
- }}
489
- title="Update Stream Title"
490
- description="Update the title of the livestream."
491
- size="md"
492
- dismissible={false}
493
- >
494
- <View style={[{ padding: 16, paddingBottom: 0 }]}>
495
- <View style={[{ marginBottom: 16 }]}>
496
- <Text
497
- style={[
498
- { color: atoms.colors.gray[300], fontSize: 13, marginBottom: 8 },
499
- ]}
500
- >
501
- Stream Title
502
- </Text>
503
- <Textarea
504
- value={title}
505
- onChangeText={(text) => {
506
- setTitle(text);
507
- setError(null);
508
- }}
509
- placeholder="Enter stream title..."
510
- maxLength={140}
511
- multiline
512
- style={[
513
- {
514
- padding: 12,
515
- borderRadius: 8,
516
- backgroundColor: atoms.colors.neutral[800],
517
- color: atoms.colors.white,
518
- borderWidth: 1,
519
- borderColor: atoms.colors.neutral[600],
520
- minHeight: 100,
521
- fontSize: 16,
522
- },
523
- ]}
524
- />
525
- <Text
526
- style={[
527
- { color: atoms.colors.gray[400], fontSize: 12, marginTop: 4 },
528
- ]}
529
- >
530
- {title.length}/140 characters
531
- </Text>
532
- </View>
533
-
534
- {error && (
535
- <View
536
- style={[
537
- {
538
- backgroundColor: atoms.colors.red[900],
539
- padding: 12,
540
- borderRadius: 8,
541
- borderWidth: 1,
542
- borderColor: atoms.colors.red[700],
543
- marginBottom: 16,
544
- },
545
- ]}
546
- >
547
- <Text style={[{ color: atoms.colors.red[400], fontSize: 13 }]}>
548
- {error}
549
- </Text>
550
- </View>
551
- )}
552
- </View>
553
-
554
- <DialogFooter>
555
- <Button
556
- width="min"
557
- variant="secondary"
558
- onPress={() => {
559
- onClose();
560
- setError(null);
561
- setTitle(livestream?.record?.title || "");
562
- }}
563
- disabled={isLoading}
564
- >
565
- <Text>Cancel</Text>
566
- </Button>
567
- <Button
568
- variant="primary"
569
- width="min"
570
- onPress={handleUpdate}
571
- disabled={isLoading || !title.trim()}
572
- >
573
- <Text>{isLoading ? "Updating..." : "Update Title"}</Text>
574
- </Button>
575
- </DialogFooter>
576
- </ResponsiveDialog>
577
- );
578
- }
@@ -0,0 +1,169 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ atoms,
4
+ Button,
5
+ DialogFooter,
6
+ ResponsiveDialog,
7
+ Text,
8
+ Textarea,
9
+ useToast,
10
+ View,
11
+ } from "../ui";
12
+
13
+ export interface UpdateStreamTitleDialogProps {
14
+ livestream: any;
15
+ streamerDID?: string;
16
+ updateLivestream: (
17
+ livestreamUri: string,
18
+ title: string,
19
+ streamerDID?: string,
20
+ ) => Promise<any>;
21
+ isLoading: boolean;
22
+ onClose: () => void;
23
+ }
24
+
25
+ export function UpdateStreamTitleDialog({
26
+ livestream,
27
+ streamerDID,
28
+ updateLivestream,
29
+ isLoading,
30
+ onClose,
31
+ }: UpdateStreamTitleDialogProps) {
32
+ const [title, setTitle] = useState(livestream?.record?.title || "");
33
+ const [error, setError] = useState<string | null>(null);
34
+ const toast = useToast();
35
+
36
+ useEffect(() => {
37
+ if (livestream?.record?.title) {
38
+ setTitle(livestream.record.title);
39
+ }
40
+ }, [livestream?.record?.title]);
41
+
42
+ const handleUpdate = async () => {
43
+ setError(null);
44
+
45
+ if (!title.trim()) {
46
+ setError("Please enter a stream title");
47
+ return;
48
+ }
49
+
50
+ if (!livestream?.uri) {
51
+ setError("No livestream found");
52
+ return;
53
+ }
54
+
55
+ try {
56
+ await updateLivestream(livestream.uri, title.trim(), streamerDID);
57
+ toast.show(
58
+ "Stream title updated",
59
+ "The stream title has been successfully updated.",
60
+ { duration: 3 },
61
+ );
62
+ onClose();
63
+ } catch (err) {
64
+ setError(
65
+ err instanceof Error ? err.message : "Failed to update stream title",
66
+ );
67
+ }
68
+ };
69
+
70
+ return (
71
+ <ResponsiveDialog
72
+ open={true}
73
+ onOpenChange={(open) => {
74
+ if (!open) {
75
+ onClose();
76
+ setError(null);
77
+ setTitle(livestream?.record?.title || "");
78
+ }
79
+ }}
80
+ title="Update Stream Title"
81
+ description="Update the title of the livestream."
82
+ size="md"
83
+ dismissible={false}
84
+ >
85
+ <View style={[{ padding: 16, paddingBottom: 0 }]}>
86
+ <View style={[{ marginBottom: 16 }]}>
87
+ <Text
88
+ style={[
89
+ { color: atoms.colors.gray[300], fontSize: 13, marginBottom: 8 },
90
+ ]}
91
+ >
92
+ Stream Title
93
+ </Text>
94
+ <Textarea
95
+ value={title}
96
+ onChangeText={(text) => {
97
+ setTitle(text);
98
+ setError(null);
99
+ }}
100
+ placeholder="Enter stream title..."
101
+ maxLength={140}
102
+ multiline
103
+ style={[
104
+ {
105
+ padding: 12,
106
+ borderRadius: 8,
107
+ backgroundColor: atoms.colors.neutral[800],
108
+ color: atoms.colors.white,
109
+ borderWidth: 1,
110
+ borderColor: atoms.colors.neutral[600],
111
+ minHeight: 100,
112
+ fontSize: 16,
113
+ },
114
+ ]}
115
+ />
116
+ <Text
117
+ style={[
118
+ { color: atoms.colors.gray[400], fontSize: 12, marginTop: 4 },
119
+ ]}
120
+ >
121
+ {title.length}/140 characters
122
+ </Text>
123
+ </View>
124
+
125
+ {error && (
126
+ <View
127
+ style={[
128
+ {
129
+ backgroundColor: atoms.colors.red[900],
130
+ padding: 12,
131
+ borderRadius: 8,
132
+ borderWidth: 1,
133
+ borderColor: atoms.colors.red[700],
134
+ marginBottom: 16,
135
+ },
136
+ ]}
137
+ >
138
+ <Text style={[{ color: atoms.colors.red[400], fontSize: 13 }]}>
139
+ {error}
140
+ </Text>
141
+ </View>
142
+ )}
143
+ </View>
144
+
145
+ <DialogFooter>
146
+ <Button
147
+ width="min"
148
+ variant="secondary"
149
+ onPress={() => {
150
+ onClose();
151
+ setError(null);
152
+ setTitle(livestream?.record?.title || "");
153
+ }}
154
+ disabled={isLoading}
155
+ >
156
+ <Text>Cancel</Text>
157
+ </Button>
158
+ <Button
159
+ variant="primary"
160
+ width="min"
161
+ onPress={handleUpdate}
162
+ disabled={isLoading || !title.trim()}
163
+ >
164
+ <Text>{isLoading ? "Updating..." : "Update Title"}</Text>
165
+ </Button>
166
+ </DialogFooter>
167
+ </ResponsiveDialog>
168
+ );
169
+ }