diff --git a/js/web/src/components/dashboard/moderators.tsx b/js/web/src/components/dashboard/moderators.tsx index 5658f1074..7bb61dd50 100644 --- a/js/web/src/components/dashboard/moderators.tsx +++ b/js/web/src/components/dashboard/moderators.tsx @@ -26,10 +26,10 @@ interface ModeratorRecord { } const PERMISSION_OPTIONS = [ - "ban", - "hide", - "livestream.manage", - "message.pin", + { perm: "ban", key: "ban" }, + { perm: "hide", key: "hide" }, + { perm: "livestream.manage", key: "manage" }, + { perm: "message.pin", key: "pin" }, ] as const; /** @@ -388,23 +388,23 @@ function AddModeratorDialog({

{t("moderators-permissions-label")}

- {PERMISSION_OPTIONS.map((perm) => ( + {PERMISSION_OPTIONS.map(({ perm, key }) => (

- {t(`moderators-permission-${perm}`)} + {t(`moderators-permission-${key}`)}

- {t(`moderators-permission-${perm}-desc`)} + {t(`moderators-permission-${key}-desc`)}

togglePermission(perm)} - aria-label={t(`moderators-permission-${perm}`)} + aria-label={t(`moderators-permission-${key}`)} />
))} diff --git a/js/web/src/components/stream/stream-info.tsx b/js/web/src/components/stream/stream-info.tsx index db8827d26..61ff3fe14 100644 --- a/js/web/src/components/stream/stream-info.tsx +++ b/js/web/src/components/stream/stream-info.tsx @@ -1,16 +1,33 @@ +import { useToast } from "@/hooks/use-toast"; import { useStore } from "@/lib/store"; import { useStreamplaceUrl } from "@/lib/store/hooks"; import { cn } from "@/lib/utils"; import type { LivestreamStore } from "@streamplace/core"; -import { Check, ChevronRight, ClipboardCopy, Plus, Share2 } from "lucide-react"; -import { useState } from "react"; +import { + Check, + ChevronRight, + ClipboardCopy, + Pencil, + Plus, + Share2, +} from "lucide-react"; +import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { place } from "streamplace"; import { useStore as useLivestreamStore } from "zustand"; import { useShallow } from "zustand/react/shallow"; +import { useCanModerate } from "../../hooks/use-can-moderate"; import type { Liveness } from "../../hooks/use-liveness-state"; +import { useModerationActions } from "../../hooks/use-moderation-actions"; import { useSession } from "../../lib/session"; import { Button, buttonVariants } from "../ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; import { DropdownMenu, DropdownMenuContent, @@ -19,6 +36,7 @@ import { DropdownMenuLabel, DropdownMenuTrigger, } from "../ui/dropdown-menu"; +import { Textarea } from "../ui/textarea"; import { StreamAvatar } from "./stream-avatar"; const ACTIVITY_I18N_KEYS: Record = { @@ -82,6 +100,8 @@ export function StreamInfo({ const activity = activityLabel(record?.activity, t); const tags = record?.tags; const isLive = liveness === "live"; + const moderation = useCanModerate(store); + const [showEditTitle, setShowEditTitle] = useState(false); const node = useStreamplaceUrl(); @@ -124,9 +144,22 @@ export function StreamInfo({ )} -

- {title} -

+
+

+ {title} +

+ {moderation.canManageLivestream && record && ( + + )} +
{activity && ( @@ -175,6 +208,12 @@ export function StreamInfo({
+ {showEditTitle && ( + setShowEditTitle(false)} + /> + )} ); } @@ -286,3 +325,69 @@ export function CopyButton({ ); } + +function EditTitleDialog({ + store, + onClose, +}: { + store: LivestreamStore; + onClose: () => void; +}) { + const { t } = useTranslation("common"); + const toast = useToast(); + const { updateStreamTitle } = useModerationActions(); + const livestream = useLivestreamStore(store, (s) => s.livestream); + const [title, setTitle] = useState(livestream?.record.title ?? ""); + const [isSaving, setIsSaving] = useState(false); + + const handleSave = useCallback(async () => { + if (!livestream || isSaving) return; + const next = title.trim(); + if (!next) return; + setIsSaving(true); + try { + await updateStreamTitle(livestream.uri, next, livestream.author.did); + toast.show(t("edit-title-saved"), "", { duration: 3000 }); + onClose(); + } catch (e) { + console.error(e); + toast.show( + t("edit-title-failed"), + e instanceof Error ? e.message : String(e), + { duration: 5000 }, + ); + } finally { + setIsSaving(false); + } + }, [isSaving, livestream, onClose, t, title, toast, updateStreamTitle]); + + return ( + { + if (!open) onClose(); + }} + > + + + {t("edit-title-dialog-title")} + +