diff --git a/js/app/components/mobile/desktop-ui/kebab.tsx b/js/app/components/mobile/desktop-ui/kebab.tsx
index 5573defc..4b741455 100644
--- a/js/app/components/mobile/desktop-ui/kebab.tsx
+++ b/js/app/components/mobile/desktop-ui/kebab.tsx
@@ -10,10 +10,14 @@ import {
DropdownMenuTrigger,
ResponsiveDropdownMenuContent,
Text,
+ UpdateStreamTitleDialog,
+ useCanModerate,
+ useLivestream,
useLivestreamInfo,
useLivestreamStore,
usePlayerStore,
useTheme,
+ useUpdateLivestreamRecord,
} from "@streamplace/components";
import { EllipsisVertical } from "lucide-react-native";
import { useState } from "react";
@@ -35,12 +39,21 @@ interface KebabMenuProps {
export function KebabMenu({ dropdownPortalContainer }: KebabMenuProps) {
const th = useTheme();
const [isOpen, setIsOpen] = useState(false);
+ const [showUpdateTitleDialog, setShowUpdateTitleDialog] = useState(false);
- const livestream = useLivestreamStore((x) => x.livestream);
+ const livestreamFromStore = useLivestreamStore((x) => x.livestream);
+ const livestream = useLivestream();
const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen);
const setReportSubject = usePlayerStore((x) => x.setReportSubject);
const { profile } = useLivestreamInfo();
+ // Get the streamer's DID from the profile
+ const streamerDID = profile?.did;
+ // Check moderation permissions for the current user on this streamer's channel
+ const modPermissions = useCanModerate(streamerDID);
+ const { updateLivestream, isLoading: isUpdateTitleLoading } =
+ useUpdateLivestreamRecord();
+
const iconRotate = useAnimatedStyle(() => {
return {
transform: [
@@ -55,31 +68,52 @@ export function KebabMenu({ dropdownPortalContainer }: KebabMenuProps) {
});
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+
+
+
+
+ {modPermissions.canManageLivestream && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ {showUpdateTitleDialog && (
+ setShowUpdateTitleDialog(false)}
+ />
+ )}
+ >
);
}
@@ -141,3 +175,29 @@ function ReportUserItem({
);
}
+
+function UpdateStreamTitleItem({
+ setShowUpdateTitleDialog,
+ isUpdateTitleLoading,
+ livestream,
+}: {
+ setShowUpdateTitleDialog: (show: boolean) => void;
+ isUpdateTitleLoading: boolean;
+ livestream: any;
+}) {
+ const { onOpenChange } = useRootContext();
+
+ return (
+ {
+ onOpenChange?.(false);
+ setShowUpdateTitleDialog(true);
+ }}
+ disabled={isUpdateTitleLoading || !livestream}
+ >
+
+ {isUpdateTitleLoading ? "Updating..." : "Update stream title"}
+
+
+ );
+}
diff --git a/js/components/src/components/chat/mod-view.tsx b/js/components/src/components/chat/mod-view.tsx
index e77b0f72..cdcaffae 100644
--- a/js/components/src/components/chat/mod-view.tsx
+++ b/js/components/src/components/chat/mod-view.tsx
@@ -5,7 +5,6 @@ import { usePlayerStore } from "../../player-store";
import {
useCreateBlockRecord,
useCreateHideChatRecord,
- useUpdateLivestreamRecord,
} from "../../streamplace-store/block";
import {
ModerationPermissions,
@@ -17,24 +16,19 @@ import { Linking } from "react-native";
import { ChatMessageViewHydrated } from "streamplace";
import {
useDeleteChatMessage,
- useLivestream,
useLivestreamStore,
} from "../../livestream-store";
import { useStreamplaceStore } from "../../streamplace-store";
import { formatHandle, formatHandleWithAt } from "../../utils/format-handle";
import {
atoms,
- Button,
- DialogFooter,
DropdownMenu,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
layout,
- ResponsiveDialog,
ResponsiveDropdownMenuContent,
Text,
- Textarea,
useToast,
View,
} from "../ui";
@@ -61,10 +55,6 @@ export const ModView = forwardRef(() => {
let [messageRemoved, setMessageRemoved] = useState(false);
let { createBlock, isLoading: isBlockLoading } = useCreateBlockRecord();
let { createHideChat, isLoading: isHideLoading } = useCreateHideChatRecord();
- let { updateLivestream, isLoading: isUpdateTitleLoading } =
- useUpdateLivestreamRecord();
- const livestream = useLivestream();
- const [showUpdateTitleDialog, setShowUpdateTitleDialog] = useState(false);
const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen);
const setReportSubject = usePlayerStore((x) => x.setReportSubject);
@@ -135,9 +125,6 @@ export const ModView = forwardRef(() => {
createHideChat={createHideChat}
createBlock={createBlock}
toast={toast}
- setShowUpdateTitleDialog={setShowUpdateTitleDialog}
- isUpdateTitleLoading={isUpdateTitleLoading}
- livestream={livestream}
setReportModalOpen={setReportModalOpen}
setReportSubject={setReportSubject}
deleteChatMessage={deleteChatMessage}
@@ -145,17 +132,6 @@ export const ModView = forwardRef(() => {
)}
-
- {/* Update Stream Title Dialog - rendered outside dropdown */}
- {showUpdateTitleDialog && (
- setShowUpdateTitleDialog(false)}
- />
- )}
>
);
});
@@ -173,9 +149,6 @@ interface ModViewContentProps {
createHideChat: (uri: string, streamerDID?: string) => Promise;
createBlock: (did: string, streamerDID?: string) => Promise;
toast: ReturnType;
- setShowUpdateTitleDialog: (show: boolean) => void;
- isUpdateTitleLoading: boolean;
- livestream: any;
setReportModalOpen: (open: boolean) => void;
setReportSubject: (subject: any) => void;
deleteChatMessage: (uri: string) => Promise;
@@ -194,9 +167,6 @@ function ModViewContent({
createHideChat,
createBlock,
toast,
- setShowUpdateTitleDialog,
- isUpdateTitleLoading,
- livestream,
setReportModalOpen,
setReportSubject,
deleteChatMessage,
@@ -290,23 +260,6 @@ function ModViewContent({
)}
- {modPermissions.canManageLivestream && (
-
- {
- setShowUpdateTitleDialog(true);
- }}
- disabled={isUpdateTitleLoading || !livestream}
- >
-
- {isUpdateTitleLoading ? "Updating..." : "Update stream title"}
-
-
-
- )}
-
{
@@ -418,161 +371,3 @@ export function ReportButton({
);
}
-
-interface UpdateStreamTitleDialogProps {
- livestream: any;
- streamerDID?: string;
- updateLivestream: (
- livestreamUri: string,
- title: string,
- streamerDID?: string,
- ) => Promise;
- isLoading: boolean;
- onClose: () => void;
-}
-
-function UpdateStreamTitleDialog({
- livestream,
- streamerDID,
- updateLivestream,
- isLoading,
- onClose,
-}: UpdateStreamTitleDialogProps) {
- const [title, setTitle] = useState(livestream?.record?.title || "");
- const [error, setError] = useState(null);
- const toast = useToast();
-
- useEffect(() => {
- if (livestream?.record?.title) {
- setTitle(livestream.record.title);
- }
- }, [livestream?.record?.title]);
-
- const handleUpdate = async () => {
- setError(null);
-
- if (!title.trim()) {
- setError("Please enter a stream title");
- return;
- }
-
- if (!livestream?.uri) {
- setError("No livestream found");
- return;
- }
-
- try {
- await updateLivestream(livestream.uri, title.trim(), streamerDID);
- toast.show(
- "Stream title updated",
- "The stream title has been successfully updated.",
- { duration: 3 },
- );
- onClose();
- } catch (err) {
- setError(
- err instanceof Error ? err.message : "Failed to update stream title",
- );
- }
- };
-
- return (
- {
- if (!open) {
- onClose();
- setError(null);
- setTitle(livestream?.record?.title || "");
- }
- }}
- title="Update Stream Title"
- description="Update the title of the livestream."
- size="md"
- dismissible={false}
- >
-
-
-
- Stream Title
-
-
-
- {error && (
-
-
- {error}
-
-
- )}
-
-
-
-
-
-
-
- );
-}
diff --git a/js/components/src/components/chat/update-stream-title-dialog.tsx b/js/components/src/components/chat/update-stream-title-dialog.tsx
new file mode 100644
index 00000000..260efee1
--- /dev/null
+++ b/js/components/src/components/chat/update-stream-title-dialog.tsx
@@ -0,0 +1,169 @@
+import { useEffect, useState } from "react";
+import {
+ atoms,
+ Button,
+ DialogFooter,
+ ResponsiveDialog,
+ Text,
+ Textarea,
+ useToast,
+ View,
+} from "../ui";
+
+export interface UpdateStreamTitleDialogProps {
+ livestream: any;
+ streamerDID?: string;
+ updateLivestream: (
+ livestreamUri: string,
+ title: string,
+ streamerDID?: string,
+ ) => Promise;
+ isLoading: boolean;
+ onClose: () => void;
+}
+
+export function UpdateStreamTitleDialog({
+ livestream,
+ streamerDID,
+ updateLivestream,
+ isLoading,
+ onClose,
+}: UpdateStreamTitleDialogProps) {
+ const [title, setTitle] = useState(livestream?.record?.title || "");
+ const [error, setError] = useState(null);
+ const toast = useToast();
+
+ useEffect(() => {
+ if (livestream?.record?.title) {
+ setTitle(livestream.record.title);
+ }
+ }, [livestream?.record?.title]);
+
+ const handleUpdate = async () => {
+ setError(null);
+
+ if (!title.trim()) {
+ setError("Please enter a stream title");
+ return;
+ }
+
+ if (!livestream?.uri) {
+ setError("No livestream found");
+ return;
+ }
+
+ try {
+ await updateLivestream(livestream.uri, title.trim(), streamerDID);
+ toast.show(
+ "Stream title updated",
+ "The stream title has been successfully updated.",
+ { duration: 3 },
+ );
+ onClose();
+ } catch (err) {
+ setError(
+ err instanceof Error ? err.message : "Failed to update stream title",
+ );
+ }
+ };
+
+ return (
+ {
+ if (!open) {
+ onClose();
+ setError(null);
+ setTitle(livestream?.record?.title || "");
+ }
+ }}
+ title="Update Stream Title"
+ description="Update the title of the livestream."
+ size="md"
+ dismissible={false}
+ >
+
+
+
+ Stream Title
+
+
+
+ {error && (
+
+
+ {error}
+
+
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/js/components/src/index.tsx b/js/components/src/index.tsx
index dcb952e4..0f406b61 100644
--- a/js/components/src/index.tsx
+++ b/js/components/src/index.tsx
@@ -34,6 +34,7 @@ export * from "./lib/theme";
export * from "./components/chat/chat";
export * from "./components/chat/chat-box";
export * from "./components/chat/system-message";
+export * from "./components/chat/update-stream-title-dialog";
export { default as VideoRetry } from "./components/mobile-player/video-retry";
export * from "./lib/system-messages";