@openrails/contentkit-ui 0.82.0 → 0.85.1
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/README.md +11 -3
- package/dist/client/beacons.d.ts +8 -5
- package/dist/client/beacons.js +15 -7
- package/dist/client/beacons.js.map +1 -1
- package/dist/client/content/moderation.d.ts +14 -2
- package/dist/client/content/moderation.js +34 -0
- package/dist/client/content/moderation.js.map +1 -1
- package/dist/client/content/types.d.ts +13 -0
- package/dist/client/content/types.js.map +1 -1
- package/dist/client/discovery.d.ts +7 -1
- package/dist/client/discovery.js.map +1 -1
- package/dist/client/generated/error-codes.d.ts +1 -1
- package/dist/client/generated/error-codes.js +8 -0
- package/dist/client/generated/error-codes.js.map +1 -1
- package/dist/client/generated/routes.d.ts +90 -12
- package/dist/client/generated/routes.js +118 -1
- package/dist/client/generated/routes.js.map +1 -1
- package/dist/client/generated/wire.d.ts +74 -0
- package/dist/client/generated/wire.js.map +1 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/reader-meta.d.ts +27 -0
- package/dist/client/reader-meta.js +99 -0
- package/dist/client/reader-meta.js.map +1 -0
- package/dist/client/versions.d.ts +1 -1
- package/dist/client/versions.js +7 -2
- package/dist/client/versions.js.map +1 -1
- package/dist/client.js +2 -1
- package/dist/components/admin/reader-manifest.d.ts +18 -0
- package/dist/components/admin/reader-manifest.js +490 -0
- package/dist/components/admin/reader-manifest.js.map +1 -0
- package/dist/components/content/card.js +1 -1
- package/dist/components/content/card.js.map +1 -1
- package/dist/components/content/editors.d.ts +2 -1
- package/dist/components/content/editors.js +291 -256
- package/dist/components/content/editors.js.map +1 -1
- package/dist/components/groups/banner.d.ts +1 -1
- package/dist/components/groups/banner.js +1 -1
- package/dist/components/groups/banner.js.map +1 -1
- package/dist/components/groups/editor.js +3 -3
- package/dist/components/groups/editor.js.map +1 -1
- package/dist/components/groups/parts.d.ts +2 -2
- package/dist/components/groups/parts.js +71 -72
- package/dist/components/groups/parts.js.map +1 -1
- package/dist/components/social/comments.js +59 -45
- package/dist/components/social/comments.js.map +1 -1
- package/dist/components/social/index.d.ts +1 -0
- package/dist/components/social/moderation.d.ts +5 -4
- package/dist/components/social/moderation.js +143 -130
- package/dist/components/social/moderation.js.map +1 -1
- package/dist/components/social/parts.js +22 -14
- package/dist/components/social/parts.js.map +1 -1
- package/dist/components/social/rules.d.ts +23 -0
- package/dist/components/social/rules.js +261 -0
- package/dist/components/social/rules.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +26 -24
- package/dist/locales/admin/de.js +78 -0
- package/dist/locales/admin/de.js.map +1 -1
- package/dist/locales/admin/en.d.ts +78 -0
- package/dist/locales/admin/en.js +78 -0
- package/dist/locales/admin/en.js.map +1 -1
- package/dist/locales/admin/es.js +78 -0
- package/dist/locales/admin/es.js.map +1 -1
- package/dist/locales/admin/ja.js +78 -0
- package/dist/locales/admin/ja.js.map +1 -1
- package/dist/locales/admin/ko.js +78 -0
- package/dist/locales/admin/ko.js.map +1 -1
- package/dist/locales/admin/zh.js +78 -0
- package/dist/locales/admin/zh.js.map +1 -1
- package/dist/locales/de.js +3 -1
- package/dist/locales/de.js.map +1 -1
- package/dist/locales/discovery/de.js +2 -0
- package/dist/locales/discovery/de.js.map +1 -1
- package/dist/locales/discovery/en.d.ts +2 -0
- package/dist/locales/discovery/en.js +2 -0
- package/dist/locales/discovery/en.js.map +1 -1
- package/dist/locales/discovery/es.js +2 -0
- package/dist/locales/discovery/es.js.map +1 -1
- package/dist/locales/discovery/ja.js +2 -0
- package/dist/locales/discovery/ja.js.map +1 -1
- package/dist/locales/discovery/ko.js +2 -0
- package/dist/locales/discovery/ko.js.map +1 -1
- package/dist/locales/discovery/zh.js +2 -0
- package/dist/locales/discovery/zh.js.map +1 -1
- package/dist/locales/en.d.ts +85 -0
- package/dist/locales/en.js +3 -1
- package/dist/locales/en.js.map +1 -1
- package/dist/locales/es.js +3 -1
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/ja.js +3 -1
- package/dist/locales/ja.js.map +1 -1
- package/dist/locales/ko.js +3 -1
- package/dist/locales/ko.js.map +1 -1
- package/dist/locales/social/de.js +3 -0
- package/dist/locales/social/de.js.map +1 -1
- package/dist/locales/social/en.d.ts +3 -0
- package/dist/locales/social/en.js +3 -0
- package/dist/locales/social/en.js.map +1 -1
- package/dist/locales/social/es.js +3 -0
- package/dist/locales/social/es.js.map +1 -1
- package/dist/locales/social/ja.js +3 -0
- package/dist/locales/social/ja.js.map +1 -1
- package/dist/locales/social/ko.js +3 -0
- package/dist/locales/social/ko.js.map +1 -1
- package/dist/locales/social/zh.js +3 -0
- package/dist/locales/social/zh.js.map +1 -1
- package/dist/locales/zh.js +3 -1
- package/dist/locales/zh.js.map +1 -1
- package/dist/react/admin.d.ts +1 -1
- package/dist/react/admin.js +2 -0
- package/dist/react/admin.js.map +1 -1
- package/dist/react/comments.d.ts +29 -1
- package/dist/react/comments.js +30 -2
- package/dist/react/comments.js.map +1 -1
- package/dist/react/context.d.ts +1 -1
- package/dist/react/context.js.map +1 -1
- package/dist/react/discovery.js +1 -1
- package/dist/react/discovery.js.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/resources.js +6 -0
- package/dist/react/resources.js.map +1 -1
- package/dist/react.js +14 -14
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"moderation.js","names":[],"sources":["../../../src/components/social/moderation.tsx"],"sourcesContent":["import { cn } from \"cn\";\nimport { useEffect, useId, useMemo, useState } from \"react\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport type { Decision, HeldKind } from \"../../client/content/types.js\";\nimport { toContentKitError, type ContentKitError } from \"../../client/errors.js\";\nimport type { AdminComment, HeldItem, PublicUser } from \"../../client/generated/wire.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { useAdminLocation, useFlowClient, useStaffAccess, type AdminLocation, type FlowClientOptions, type StaffAccess } from \"../../react/admin.js\";\nimport { useModerationQueue } from \"../../react/comments.js\";\nimport { useErrorReporter, type ContentKitErrorHandler } from \"../../react/context.js\";\nimport { keyOf, useContentScope, usePaged } from \"../../react/use-resource.js\";\nimport { Badge } from \"#ckui/ui/badge\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Input } from \"#ckui/ui/input\";\nimport { Label } from \"#ckui/ui/label\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"#ckui/ui/select\";\nimport { Skeleton } from \"#ckui/ui/skeleton\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"#ckui/ui/tabs\";\nimport { AdminRoot, NoAccess, Pager, Rows, pageOf, useGuarded, type AdminFlowProps, type AdminRouteProps } from \"../admin/shell.js\";\nimport { CommentBanDialog, CommentBans } from \"./bans.js\";\nimport { contentError, ErrorLine, RelativeTime, UserAvatar, UserName } from \"./parts.js\";\n\nexport type ModerationSection = \"comments\" | \"queue\" | \"bans\";\n\nexport interface CommentModerationProps extends AdminFlowProps, AdminRouteProps {\n /** Content kinds offered as a filter of the comment list: codes, or codes with labels. */\n contentKinds?: readonly (string | { value: string; label: string })[];\n /** A link to a commented item. */\n itemHref?: (item: { kind: string; id: string }) => string | undefined;\n userHref?: (user: PublicUser) => string | undefined;\n /** The sections, in order; the first is at basePath, the others at basePath/{section}. Default all. */\n sections?: readonly ModerationSection[];\n pageSize?: number;\n}\n\nconst ALL_SECTIONS: readonly ModerationSection[] = [\"comments\", \"queue\", \"bans\"];\n\n/** Which moderation sections the caller may use: every comment, the review queue, the site's bans. */\nexport function useCommentModerationAccess(o: FlowClientOptions = {}): StaffAccess<ModerationSection> {\n return useStaffAccess(ALL_SECTIONS, o);\n}\n\n/**\n * Staff: every comment (deleted, held and rejected ones with their text) to\n * delete, restore or ban its author, by content kind or author; the review\n * queue of held comments and content to approve or reject; and the site's\n * comment bans. Sections, filters and pages live in the URL under basePath;\n * the caller sees only the sections its permissions allow.\n */\nexport function CommentModeration({ client: own, baseUrl, auth, guard, appearance, className, language, messages, sections = ALL_SECTIONS, ...p }: CommentModerationProps) {\n const client = useFlowClient({ client: own, baseUrl, auth });\n return (\n <AdminRoot flow=\"comment-moderation\" appearance={appearance} className={className} language={language} messages={messages} guard={guard}>\n <Moderation {...p} client={client} sections={sections} />\n </AdminRoot>\n );\n}\n\ntype Rest = Omit<CommentModerationProps, \"client\" | \"baseUrl\" | \"auth\" | \"guard\" | \"appearance\" | \"className\" | \"language\" | \"messages\" | \"sections\" | \"basePath\" | \"navigation\"> & {\n client: ContentKitClient;\n};\n\nfunction Moderation({ sections, basePath, navigation, ...p }: Rest & { sections: readonly ModerationSection[]; basePath?: string; navigation?: CommentModerationProps[\"navigation\"] }) {\n const { t } = useMessages();\n const loc = useAdminLocation(basePath, navigation);\n const access = useCommentModerationAccess({ client: p.client });\n const allowed = sections.filter((s) => access.allowed[s]);\n const label: Record<ModerationSection, string> = { comments: t(\"moderation.all\"), queue: t(\"moderation.queue\"), bans: t(\"moderation.bans\") };\n const title = <h2 className=\"text-base font-semibold\">{t(\"moderation.title\")}</h2>;\n if (access.loading)\n return (\n <>\n {title}\n <Rows />\n </>\n );\n if (!allowed.length)\n return (\n <>\n {title}\n <NoAccess>{t(\"moderation.noAccess\")}</NoAccess>\n </>\n );\n const viewOf = (s: ModerationSection) => (s === sections[0] ? \"\" : s);\n const asked = (loc.view || sections[0]) as ModerationSection;\n const section = allowed.includes(asked) ? asked : allowed[0]!;\n const view = viewOf(section);\n return (\n <>\n {title}\n <Tabs value={section} onValueChange={(v) => loc.go(viewOf(v as ModerationSection))}>\n {allowed.length > 1 && (\n <TabsList>\n {allowed.map((s) => (\n <TabsTrigger key={s} value={s}>\n {label[s]}\n </TabsTrigger>\n ))}\n </TabsList>\n )}\n <TabsContent value={section} className=\"pt-3\">\n {section === \"comments\" && <AllComments {...p} loc={loc} view={view} />}\n {section === \"queue\" && <Queue {...p} loc={loc} view={view} />}\n {section === \"bans\" && (\n <CommentBans\n scope=\"global\"\n pageSize={p.pageSize}\n page={pageOf(loc.query)}\n onPageChange={(n) => loc.go(view, { page: n > 1 ? n : undefined })}\n userHref={p.userHref}\n client={p.client}\n onError={p.onError}\n />\n )}\n </TabsContent>\n </Tabs>\n </>\n );\n}\n\ntype SectionProps = Rest & { loc: AdminLocation; view: string };\n\nfunction status(c: AdminComment): \"deleted\" | \"held\" | \"rejected\" | \"published\" {\n if (c.deleted) return \"deleted\";\n return c.moderation === \"held\" ? \"held\" : c.moderation === \"rejected\" ? \"rejected\" : \"published\";\n}\n\nfunction AllComments({ loc, view, client, ...p }: SectionProps) {\n const m = useMessages();\n const { t } = m;\n const ids = useId();\n const kinds = useMemo(() => (p.contentKinds ?? []).map((k) => (typeof k === \"string\" ? { value: k, label: k } : k)), [p.contentKinds]);\n const kind = loc.query.get(\"kind\") ?? \"\";\n const author = loc.query.get(\"author\") ?? \"\";\n const page = pageOf(loc.query);\n const size = p.pageSize ?? 25;\n const filter = (next: { kind?: string; author?: string }) => loc.go(view, { kind, author, ...next });\n const [authorInput, setAuthorInput] = useState(author);\n useEffect(() => setAuthorInput(author), [author]);\n const { store, scope } = useContentScope(client);\n const list = usePaged(store, keyOf(\"admin-comments-page\", scope, kind, author, size), { type: \"admin-comments\" }, page, size, (q, signal) =>\n client.comments.adminList({ ...q, contentKind: kind || undefined, author: author || undefined }, signal),\n );\n const report = useErrorReporter(p.onError);\n const guarded = useGuarded();\n const [error, setError] = useState<ContentKitError | null>(null);\n const [banning, setBanning] = useState<{ id: string; name: string } | null>(null);\n const act = (op: \"comment.delete\" | \"comment.restore\", fn: () => Promise<unknown>) => {\n setError(null);\n guarded(fn).catch((e) => {\n const err = toContentKitError(e);\n setError(err);\n report(err, op);\n });\n };\n const byName = author ? (list.items.find((c) => c.user_id === author)?.author?.username ?? author) : \"\";\n return (\n <div className=\"grid gap-3\">\n <div className=\"flex flex-wrap items-end gap-2\">\n {kinds.length > 0 && (\n <Select value={kind} onValueChange={(v) => filter({ kind: String(v ?? \"\") })}>\n <SelectTrigger size=\"sm\" aria-label={t(\"moderation.kind\")}>\n <SelectValue>{(v: string) => (v ? (kinds.find((k) => k.value === v)?.label ?? v) : t(\"moderation.allKinds\"))}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"\">{t(\"moderation.allKinds\")}</SelectItem>\n {kinds.map((k) => (\n <SelectItem key={k.value} value={k.value}>\n {k.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n <form\n className=\"flex min-w-56 flex-1 items-end gap-2\"\n onSubmit={(e) => {\n e.preventDefault();\n filter({ author: authorInput.trim() });\n }}\n >\n <div className=\"grid flex-1 gap-1\">\n <Label htmlFor={`${ids}-author`} className=\"sr-only\">\n {t(\"moderation.author\")}\n </Label>\n <Input id={`${ids}-author`} className=\"h-8\" value={authorInput} placeholder={t(\"moderation.authorPlaceholder\")} onChange={(e) => setAuthorInput(e.target.value)} />\n </div>\n <Button type=\"submit\" size=\"sm\" variant=\"outline\">\n {t(\"moderation.filter\")}\n </Button>\n </form>\n </div>\n {author && (\n <div className=\"flex flex-wrap items-center gap-2\" data-ckui=\"author-filter\">\n <span>{t(\"moderation.byAuthor\", { name: byName })}</span>\n <Button size=\"sm\" variant=\"ghost\" onClick={() => filter({ author: \"\" })}>\n {t(\"moderation.clearAuthor\")}\n </Button>\n </div>\n )}\n <ErrorLine error={error} />\n <Listing loading={list.loading} error={list.error} empty={t(\"moderation.empty\")} count={list.items.length} reload={list.reload}>\n {list.items.map((c) => {\n const name = c.author?.username ?? c.anon_name ?? c.user_id ?? t(\"comments.anonymous\");\n const s = status(c);\n const href = p.itemHref?.({ kind: c.content_kind, id: c.content_id });\n return (\n <li key={c.id} className=\"grid gap-2 p-3\" data-ckui=\"admin-comment\" data-status={s}>\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n <UserAvatar user={c.author} name={name} size=\"sm\" />\n <UserName user={c.author} name={name} href={p.userHref} />\n <RelativeTime at={c.created_at} className=\"text-xs text-muted-foreground\" />\n <Badge variant={s === \"published\" ? \"outline\" : s === \"held\" ? \"secondary\" : \"destructive\"}>{t(`moderation.${s}`)}</Badge>\n {c.reply_to_id && <Badge variant=\"outline\">{t(\"moderation.reply\")}</Badge>}\n <span className=\"text-xs text-muted-foreground\">\n {t(\"moderation.on\")}{\" \"}\n {href ? (\n <a className=\"hover:underline\" href={href}>\n {c.content_kind}/{c.content_id}\n </a>\n ) : (\n `${c.content_kind}/${c.content_id}`\n )}\n </span>\n </div>\n <p className={cn(\"line-clamp-4 break-words whitespace-pre-wrap\", c.deleted && \"text-muted-foreground line-through\")}>{c.body}</p>\n {c.moderation_reason && <p className=\"text-xs text-muted-foreground\">{t(\"moderation.heldBecause\", { reason: c.moderation_reason })}</p>}\n <div className=\"flex flex-wrap gap-2\">\n {c.deleted ? (\n <Button size=\"sm\" variant=\"outline\" onClick={() => act(\"comment.restore\", () => client.comments.restore(c.id))}>\n {t(\"moderation.restore\")}\n </Button>\n ) : (\n <Button size=\"sm\" variant=\"destructive\" onClick={() => act(\"comment.delete\", () => client.comments.delete(c.id))}>\n {t(\"moderation.delete\")}\n </Button>\n )}\n {c.user_id && c.user_id !== author && (\n <Button size=\"sm\" variant=\"ghost\" onClick={() => filter({ author: c.user_id! })}>\n {t(\"moderation.theirComments\")}\n </Button>\n )}\n {c.user_id && (\n <Button size=\"sm\" variant=\"ghost\" onClick={() => setBanning({ id: c.user_id!, name })}>\n {t(\"comments.ban\")}\n </Button>\n )}\n </div>\n </li>\n );\n })}\n </Listing>\n <Pager page={page} hasNext={list.hasNext} disabled={list.loading} onPage={(n) => loc.go(view, { kind, author, page: n > 1 ? n : undefined })} />\n {banning && <CommentBanDialog open onOpenChange={(o) => !o && setBanning(null)} user={banning} scopes={[\"global\"]} client={client} onError={p.onError} />}\n </div>\n );\n}\n\nfunction Queue({ loc, view, ...p }: SectionProps) {\n const m = useMessages();\n const { t } = m;\n const kind: HeldKind = loc.query.get(\"kind\") === \"content\" ? \"content\" : \"comment\";\n const queue = useModerationQueue({ kind, pageSize: p.pageSize, client: p.client });\n return (\n <div className=\"grid gap-3\">\n <div className=\"flex gap-2\" role=\"group\" aria-label={t(\"moderation.queue\")}>\n {([\"comment\", \"content\"] as const).map((k) => (\n <Button key={k} size=\"sm\" variant={kind === k ? \"default\" : \"outline\"} aria-pressed={kind === k} onClick={() => loc.go(view, { kind: k === \"comment\" ? undefined : k })}>\n {t(k === \"content\" ? \"moderation.queueContent\" : \"moderation.queueComments\")}\n </Button>\n ))}\n </div>\n <Listing loading={queue.loading} error={queue.error} empty={t(\"moderation.queueEmpty\")} count={queue.items.length} reload={queue.reload}>\n {queue.items.map((it) => (\n <HeldRow key={it.id} item={it} itemHref={p.itemHref} userHref={p.userHref} onResolve={queue.resolve} reload={queue.reload} onError={p.onError} />\n ))}\n </Listing>\n {queue.hasMore && (\n <Button variant=\"outline\" className=\"justify-self-center\" disabled={queue.loadingMore} onClick={() => void queue.loadMore()}>\n {t(\"moderation.loadMore\")}\n </Button>\n )}\n </div>\n );\n}\n\nfunction HeldRow(p: {\n item: HeldItem;\n itemHref?: Rest[\"itemHref\"];\n userHref?: Rest[\"userHref\"];\n onResolve: (item: HeldItem, d: Decision, reason?: string) => Promise<void>;\n reload: () => void;\n onError?: ContentKitErrorHandler;\n}) {\n const m = useMessages();\n const { t } = m;\n const report = useErrorReporter(p.onError);\n const guarded = useGuarded();\n const { item: it } = p;\n const [rejecting, setRejecting] = useState(false);\n const [reason, setReason] = useState(\"\");\n const [pending, setPending] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const name = it.author?.username ?? it.anon_name ?? it.author_id ?? t(\"comments.anonymous\");\n const href = p.itemHref?.({ kind: it.ref.content_kind, id: it.ref.content_id });\n const resolve = async (d: Decision) => {\n setPending(true);\n setError(null);\n try {\n await guarded(() => p.onResolve(it, d, d === \"reject\" ? reason.trim() || undefined : undefined));\n } catch (e) {\n const err = toContentKitError(e);\n report(err, \"moderation.resolve\");\n // A stale revision: the text changed since it was listed.\n if (err.code === \"not_found\" || err.code === \"conflict\") {\n setError(t(\"moderation.stale\"));\n p.reload();\n } else setError(contentError(m, err));\n } finally {\n setPending(false);\n }\n };\n return (\n <li className=\"grid gap-2 p-3\" data-ckui=\"held-item\">\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n <UserAvatar user={it.author} name={name} size=\"sm\" />\n <UserName user={it.author} name={name} href={p.userHref} />\n <RelativeTime at={it.held_at} className=\"text-xs text-muted-foreground\" />\n {href && (\n <a className=\"text-xs text-muted-foreground hover:underline\" href={href}>\n {t(\"moderation.on\")} {it.ref.content_kind}/{it.ref.content_id}\n </a>\n )}\n </div>\n {it.title && <p className=\"font-medium\">{it.title}</p>}\n <p className=\"line-clamp-6 break-words whitespace-pre-wrap\">{it.body}</p>\n {(it.reason || it.error) && <p className=\"text-xs text-muted-foreground\">{t(\"moderation.heldBecause\", { reason: it.reason || it.error || \"\" })}</p>}\n {rejecting && (\n <Input value={reason} maxLength={500} placeholder={t(\"moderation.rejectReason\")} aria-label={t(\"moderation.rejectReason\")} onChange={(e) => setReason(e.target.value)} autoFocus />\n )}\n <div className=\"flex flex-wrap gap-2\">\n <Button size=\"sm\" disabled={pending} onClick={() => void resolve(\"approve\")}>\n {t(\"moderation.approve\")}\n </Button>\n <Button size=\"sm\" variant=\"destructive\" disabled={pending} onClick={() => (rejecting ? void resolve(\"reject\") : setRejecting(true))}>\n {t(\"moderation.reject\")}\n </Button>\n {rejecting && (\n <Button size=\"sm\" variant=\"ghost\" onClick={() => setRejecting(false)}>\n {t(\"common.cancel\")}\n </Button>\n )}\n </div>\n {error && <ErrorLine>{error}</ErrorLine>}\n </li>\n );\n}\n\nfunction Listing(p: { loading: boolean; error?: ContentKitError; empty: string; count: number; reload: () => void; children: React.ReactNode }) {\n const m = useMessages();\n const { t } = m;\n if (p.loading && !p.count) {\n return (\n <div className=\"grid gap-3\" aria-busy>\n <Skeleton className=\"h-16 w-full\" />\n <Skeleton className=\"h-16 w-full\" />\n </div>\n );\n }\n if (p.error && !p.count) {\n return (\n <div className=\"grid justify-items-start gap-2\">\n <ErrorLine>{p.error.code === \"forbidden\" || p.error.code === \"unauthorized\" ? contentError(m, p.error) : t(\"moderation.loadFailed\")}</ErrorLine>\n <Button size=\"sm\" variant=\"outline\" onClick={p.reload}>\n {t(\"common.retry\")}\n </Button>\n </div>\n );\n }\n if (!p.count) return <p className=\"text-muted-foreground\">{p.empty}</p>;\n return <ul className=\"grid divide-y divide-border rounded-lg border border-border\">{p.children}</ul>;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"moderation.js","names":[],"sources":["../../../src/components/social/moderation.tsx"],"sourcesContent":["import { cn } from \"cn\";\nimport { useEffect, useId, useMemo, useState } from \"react\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport type { Decision, HeldKind } from \"../../client/content/types.js\";\nimport { toContentKitError, type ContentKitError } from \"../../client/errors.js\";\nimport type { AdminComment, HeldItem, PublicUser } from \"../../client/generated/wire.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { useAdminLocation, useFlowClient, useStaffAccess, type AdminLocation, type FlowClientOptions, type StaffAccess } from \"../../react/admin.js\";\nimport { useModerationQueue } from \"../../react/comments.js\";\nimport { useErrorReporter, type ContentKitErrorHandler } from \"../../react/context.js\";\nimport { keyOf, useContentScope, usePaged } from \"../../react/use-resource.js\";\nimport { Badge } from \"#ckui/ui/badge\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Input } from \"#ckui/ui/input\";\nimport { Label } from \"#ckui/ui/label\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"#ckui/ui/select\";\nimport { Skeleton } from \"#ckui/ui/skeleton\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"#ckui/ui/tabs\";\nimport { AdminRoot, NoAccess, Pager, Rows, pageOf, useGuarded, type AdminFlowProps, type AdminRouteProps } from \"../admin/shell.js\";\nimport { CommentBanDialog, CommentBans } from \"./bans.js\";\nimport { BannedWords, IPBans } from \"./rules.js\";\nimport { contentError, ErrorLine, RelativeTime, UserAvatar, UserName } from \"./parts.js\";\n\nexport type ModerationSection = \"comments\" | \"queue\" | \"bans\" | \"words\" | \"ips\";\n\nexport interface CommentModerationProps extends AdminFlowProps, AdminRouteProps {\n /** Content kinds offered as a filter of the comment list: codes, or codes with labels. */\n contentKinds?: readonly (string | { value: string; label: string })[];\n /** A link to a commented item. */\n itemHref?: (item: { kind: string; id: string }) => string | undefined;\n userHref?: (user: PublicUser) => string | undefined;\n /** The sections, in order; the first is at basePath, the others at basePath/{section}. Default all. */\n sections?: readonly ModerationSection[];\n pageSize?: number;\n}\n\nconst ALL_SECTIONS: readonly ModerationSection[] = [\"comments\", \"queue\", \"bans\", \"words\", \"ips\"];\n\n/** Which moderation sections the caller may use: every comment, the review queue, the site's bans, banned words and IP bans. */\nexport function useCommentModerationAccess(o: FlowClientOptions = {}): StaffAccess<ModerationSection> {\n return useStaffAccess(ALL_SECTIONS, o);\n}\n\n/**\n * Staff: every comment (deleted, held and rejected ones with their text) to\n * delete, restore or ban its author, by content kind or author; the review\n * queue of held comments and content to approve or reject; the site's\n * comment bans; and the rules every comment and answer meets (banned words,\n * IP bans). Sections, filters and pages live in the URL under basePath;\n * the caller sees only the sections its permissions allow.\n */\nexport function CommentModeration({ client: own, baseUrl, auth, guard, appearance, className, language, messages, sections = ALL_SECTIONS, ...p }: CommentModerationProps) {\n const client = useFlowClient({ client: own, baseUrl, auth });\n return (\n <AdminRoot flow=\"comment-moderation\" appearance={appearance} className={className} language={language} messages={messages} guard={guard}>\n <Moderation {...p} client={client} sections={sections} />\n </AdminRoot>\n );\n}\n\ntype Rest = Omit<CommentModerationProps, \"client\" | \"baseUrl\" | \"auth\" | \"guard\" | \"appearance\" | \"className\" | \"language\" | \"messages\" | \"sections\" | \"basePath\" | \"navigation\"> & {\n client: ContentKitClient;\n};\n\nfunction Moderation({ sections, basePath, navigation, ...p }: Rest & { sections: readonly ModerationSection[]; basePath?: string; navigation?: CommentModerationProps[\"navigation\"] }) {\n const { t } = useMessages();\n const loc = useAdminLocation(basePath, navigation);\n const access = useCommentModerationAccess({ client: p.client });\n const allowed = sections.filter((s) => access.allowed[s]);\n const label: Record<ModerationSection, string> = {\n comments: t(\"moderation.all\"),\n queue: t(\"moderation.queue\"),\n bans: t(\"moderation.bans\"),\n words: t(\"moderationRules.words\"),\n ips: t(\"moderationRules.ips\"),\n };\n const title = <h2 className=\"text-base font-semibold\">{t(\"moderation.title\")}</h2>;\n if (access.loading)\n return (\n <>\n {title}\n <Rows />\n </>\n );\n if (!allowed.length)\n return (\n <>\n {title}\n <NoAccess>{t(\"moderation.noAccess\")}</NoAccess>\n </>\n );\n const viewOf = (s: ModerationSection) => (s === sections[0] ? \"\" : s);\n const asked = (loc.view || sections[0]) as ModerationSection;\n const section = allowed.includes(asked) ? asked : allowed[0]!;\n const view = viewOf(section);\n return (\n <>\n {title}\n <Tabs value={section} onValueChange={(v) => loc.go(viewOf(v as ModerationSection))}>\n {allowed.length > 1 && (\n <TabsList>\n {allowed.map((s) => (\n <TabsTrigger key={s} value={s}>\n {label[s]}\n </TabsTrigger>\n ))}\n </TabsList>\n )}\n <TabsContent value={section} className=\"pt-3\">\n {section === \"comments\" && <AllComments {...p} loc={loc} view={view} />}\n {section === \"queue\" && <Queue {...p} loc={loc} view={view} />}\n {section === \"bans\" && (\n <CommentBans\n scope=\"global\"\n pageSize={p.pageSize}\n page={pageOf(loc.query)}\n onPageChange={(n) => loc.go(view, { page: n > 1 ? n : undefined })}\n userHref={p.userHref}\n client={p.client}\n onError={p.onError}\n />\n )}\n {section === \"words\" && <BannedWords client={p.client} onError={p.onError} />}\n {section === \"ips\" && <IPBans client={p.client} onError={p.onError} />}\n </TabsContent>\n </Tabs>\n </>\n );\n}\n\ntype SectionProps = Rest & { loc: AdminLocation; view: string };\n\nfunction status(c: AdminComment): \"deleted\" | \"held\" | \"rejected\" | \"published\" {\n if (c.deleted) return \"deleted\";\n return c.moderation === \"held\" ? \"held\" : c.moderation === \"rejected\" ? \"rejected\" : \"published\";\n}\n\nfunction AllComments({ loc, view, client, ...p }: SectionProps) {\n const m = useMessages();\n const { t } = m;\n const ids = useId();\n const kinds = useMemo(() => (p.contentKinds ?? []).map((k) => (typeof k === \"string\" ? { value: k, label: k } : k)), [p.contentKinds]);\n const kind = loc.query.get(\"kind\") ?? \"\";\n const author = loc.query.get(\"author\") ?? \"\";\n const page = pageOf(loc.query);\n const size = p.pageSize ?? 25;\n const filter = (next: { kind?: string; author?: string }) => loc.go(view, { kind, author, ...next });\n const [authorInput, setAuthorInput] = useState(author);\n useEffect(() => setAuthorInput(author), [author]);\n const { store, scope } = useContentScope(client);\n const list = usePaged(store, keyOf(\"admin-comments-page\", scope, kind, author, size), { type: \"admin-comments\" }, page, size, (q, signal) =>\n client.comments.adminList({ ...q, contentKind: kind || undefined, author: author || undefined }, signal),\n );\n const report = useErrorReporter(p.onError);\n const guarded = useGuarded();\n const [error, setError] = useState<ContentKitError | null>(null);\n const [banning, setBanning] = useState<{ id: string; name: string } | null>(null);\n const act = (op: \"comment.delete\" | \"comment.restore\", fn: () => Promise<unknown>) => {\n setError(null);\n guarded(fn).catch((e) => {\n const err = toContentKitError(e);\n setError(err);\n report(err, op);\n });\n };\n const byName = author ? (list.items.find((c) => c.user_id === author)?.author?.username ?? author) : \"\";\n return (\n <div className=\"grid gap-3\">\n <div className=\"flex flex-wrap items-end gap-2\">\n {kinds.length > 0 && (\n <Select value={kind} onValueChange={(v) => filter({ kind: String(v ?? \"\") })}>\n <SelectTrigger size=\"sm\" aria-label={t(\"moderation.kind\")}>\n <SelectValue>{(v: string) => (v ? (kinds.find((k) => k.value === v)?.label ?? v) : t(\"moderation.allKinds\"))}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"\">{t(\"moderation.allKinds\")}</SelectItem>\n {kinds.map((k) => (\n <SelectItem key={k.value} value={k.value}>\n {k.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n <form\n className=\"flex min-w-56 flex-1 items-end gap-2\"\n onSubmit={(e) => {\n e.preventDefault();\n filter({ author: authorInput.trim() });\n }}\n >\n <div className=\"grid flex-1 gap-1\">\n <Label htmlFor={`${ids}-author`} className=\"sr-only\">\n {t(\"moderation.author\")}\n </Label>\n <Input id={`${ids}-author`} className=\"h-8\" value={authorInput} placeholder={t(\"moderation.authorPlaceholder\")} onChange={(e) => setAuthorInput(e.target.value)} />\n </div>\n <Button type=\"submit\" size=\"sm\" variant=\"outline\">\n {t(\"moderation.filter\")}\n </Button>\n </form>\n </div>\n {author && (\n <div className=\"flex flex-wrap items-center gap-2\" data-ckui=\"author-filter\">\n <span>{t(\"moderation.byAuthor\", { name: byName })}</span>\n <Button size=\"sm\" variant=\"ghost\" onClick={() => filter({ author: \"\" })}>\n {t(\"moderation.clearAuthor\")}\n </Button>\n </div>\n )}\n <ErrorLine error={error} />\n <Listing loading={list.loading} error={list.error} empty={t(\"moderation.empty\")} count={list.items.length} reload={list.reload}>\n {list.items.map((c) => {\n const name = c.author?.username ?? c.anon_name ?? c.user_id ?? t(\"comments.anonymous\");\n const s = status(c);\n const href = p.itemHref?.({ kind: c.content_kind, id: c.content_id });\n return (\n <li key={c.id} className=\"grid gap-2 p-3\" data-ckui=\"admin-comment\" data-status={s}>\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n <UserAvatar user={c.author} name={name} size=\"sm\" />\n <UserName user={c.author} name={name} href={p.userHref} />\n <RelativeTime at={c.created_at} className=\"text-xs text-muted-foreground\" />\n <Badge variant={s === \"published\" ? \"outline\" : s === \"held\" ? \"secondary\" : \"destructive\"}>{t(`moderation.${s}`)}</Badge>\n {c.reply_to_id && <Badge variant=\"outline\">{t(\"moderation.reply\")}</Badge>}\n <span className=\"text-xs text-muted-foreground\">\n {t(\"moderation.on\")}{\" \"}\n {href ? (\n <a className=\"hover:underline\" href={href}>\n {c.content_kind}/{c.content_id}\n </a>\n ) : (\n `${c.content_kind}/${c.content_id}`\n )}\n </span>\n </div>\n <p className={cn(\"line-clamp-4 break-words whitespace-pre-wrap\", c.deleted && \"text-muted-foreground line-through\")}>{c.body}</p>\n {c.moderation_reason && <p className=\"text-xs text-muted-foreground\">{t(\"moderation.heldBecause\", { reason: c.moderation_reason })}</p>}\n <div className=\"flex flex-wrap gap-2\">\n {c.deleted ? (\n <Button size=\"sm\" variant=\"outline\" onClick={() => act(\"comment.restore\", () => client.comments.restore(c.id))}>\n {t(\"moderation.restore\")}\n </Button>\n ) : (\n <Button size=\"sm\" variant=\"destructive\" onClick={() => act(\"comment.delete\", () => client.comments.delete(c.id))}>\n {t(\"moderation.delete\")}\n </Button>\n )}\n {c.user_id && c.user_id !== author && (\n <Button size=\"sm\" variant=\"ghost\" onClick={() => filter({ author: c.user_id! })}>\n {t(\"moderation.theirComments\")}\n </Button>\n )}\n {c.user_id && (\n <Button size=\"sm\" variant=\"ghost\" onClick={() => setBanning({ id: c.user_id!, name })}>\n {t(\"comments.ban\")}\n </Button>\n )}\n </div>\n </li>\n );\n })}\n </Listing>\n <Pager page={page} hasNext={list.hasNext} disabled={list.loading} onPage={(n) => loc.go(view, { kind, author, page: n > 1 ? n : undefined })} />\n {banning && <CommentBanDialog open onOpenChange={(o) => !o && setBanning(null)} user={banning} scopes={[\"global\"]} client={client} onError={p.onError} />}\n </div>\n );\n}\n\nfunction Queue({ loc, view, ...p }: SectionProps) {\n const m = useMessages();\n const { t } = m;\n const kind: HeldKind = loc.query.get(\"kind\") === \"content\" ? \"content\" : \"comment\";\n const queue = useModerationQueue({ kind, pageSize: p.pageSize, client: p.client });\n return (\n <div className=\"grid gap-3\">\n <div className=\"flex gap-2\" role=\"group\" aria-label={t(\"moderation.queue\")}>\n {([\"comment\", \"content\"] as const).map((k) => (\n <Button key={k} size=\"sm\" variant={kind === k ? \"default\" : \"outline\"} aria-pressed={kind === k} onClick={() => loc.go(view, { kind: k === \"comment\" ? undefined : k })}>\n {t(k === \"content\" ? \"moderation.queueContent\" : \"moderation.queueComments\")}\n </Button>\n ))}\n </div>\n <Listing loading={queue.loading} error={queue.error} empty={t(\"moderation.queueEmpty\")} count={queue.items.length} reload={queue.reload}>\n {queue.items.map((it) => (\n <HeldRow key={it.id} item={it} itemHref={p.itemHref} userHref={p.userHref} onResolve={queue.resolve} reload={queue.reload} onError={p.onError} />\n ))}\n </Listing>\n {queue.hasMore && (\n <Button variant=\"outline\" className=\"justify-self-center\" disabled={queue.loadingMore} onClick={() => void queue.loadMore()}>\n {t(\"moderation.loadMore\")}\n </Button>\n )}\n </div>\n );\n}\n\nfunction HeldRow(p: {\n item: HeldItem;\n itemHref?: Rest[\"itemHref\"];\n userHref?: Rest[\"userHref\"];\n onResolve: (item: HeldItem, d: Decision, reason?: string) => Promise<void>;\n reload: () => void;\n onError?: ContentKitErrorHandler;\n}) {\n const m = useMessages();\n const { t } = m;\n const report = useErrorReporter(p.onError);\n const guarded = useGuarded();\n const { item: it } = p;\n const [rejecting, setRejecting] = useState(false);\n const [reason, setReason] = useState(\"\");\n const [pending, setPending] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const name = it.author?.username ?? it.anon_name ?? it.author_id ?? t(\"comments.anonymous\");\n const href = p.itemHref?.({ kind: it.ref.content_kind, id: it.ref.content_id });\n const resolve = async (d: Decision) => {\n setPending(true);\n setError(null);\n try {\n await guarded(() => p.onResolve(it, d, d === \"reject\" ? reason.trim() || undefined : undefined));\n } catch (e) {\n const err = toContentKitError(e);\n report(err, \"moderation.resolve\");\n // A stale revision: the text changed since it was listed.\n if (err.code === \"not_found\" || err.code === \"conflict\") {\n setError(t(\"moderation.stale\"));\n p.reload();\n } else setError(contentError(m, err));\n } finally {\n setPending(false);\n }\n };\n return (\n <li className=\"grid gap-2 p-3\" data-ckui=\"held-item\">\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n <UserAvatar user={it.author} name={name} size=\"sm\" />\n <UserName user={it.author} name={name} href={p.userHref} />\n <RelativeTime at={it.held_at} className=\"text-xs text-muted-foreground\" />\n {href && (\n <a className=\"text-xs text-muted-foreground hover:underline\" href={href}>\n {t(\"moderation.on\")} {it.ref.content_kind}/{it.ref.content_id}\n </a>\n )}\n </div>\n {it.title && <p className=\"font-medium\">{it.title}</p>}\n <p className=\"line-clamp-6 break-words whitespace-pre-wrap\">{it.body}</p>\n {(it.reason || it.error) && <p className=\"text-xs text-muted-foreground\">{t(\"moderation.heldBecause\", { reason: it.reason || it.error || \"\" })}</p>}\n {rejecting && (\n <Input value={reason} maxLength={500} placeholder={t(\"moderation.rejectReason\")} aria-label={t(\"moderation.rejectReason\")} onChange={(e) => setReason(e.target.value)} autoFocus />\n )}\n <div className=\"flex flex-wrap gap-2\">\n <Button size=\"sm\" disabled={pending} onClick={() => void resolve(\"approve\")}>\n {t(\"moderation.approve\")}\n </Button>\n <Button size=\"sm\" variant=\"destructive\" disabled={pending} onClick={() => (rejecting ? void resolve(\"reject\") : setRejecting(true))}>\n {t(\"moderation.reject\")}\n </Button>\n {rejecting && (\n <Button size=\"sm\" variant=\"ghost\" onClick={() => setRejecting(false)}>\n {t(\"common.cancel\")}\n </Button>\n )}\n </div>\n {error && <ErrorLine>{error}</ErrorLine>}\n </li>\n );\n}\n\nfunction Listing(p: { loading: boolean; error?: ContentKitError; empty: string; count: number; reload: () => void; children: React.ReactNode }) {\n const m = useMessages();\n const { t } = m;\n if (p.loading && !p.count) {\n return (\n <div className=\"grid gap-3\" aria-busy>\n <Skeleton className=\"h-16 w-full\" />\n <Skeleton className=\"h-16 w-full\" />\n </div>\n );\n }\n if (p.error && !p.count) {\n return (\n <div className=\"grid justify-items-start gap-2\">\n <ErrorLine>{p.error.code === \"forbidden\" || p.error.code === \"unauthorized\" ? contentError(m, p.error) : t(\"moderation.loadFailed\")}</ErrorLine>\n <Button size=\"sm\" variant=\"outline\" onClick={p.reload}>\n {t(\"common.retry\")}\n </Button>\n </div>\n );\n }\n if (!p.count) return <p className=\"text-muted-foreground\">{p.empty}</p>;\n return <ul className=\"grid divide-y divide-border rounded-lg border border-border\">{p.children}</ul>;\n}\n"],"mappings":"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"}
|
|
@@ -9,11 +9,19 @@ import { useCallback as _, useEffect as v, useId as y, useState as b } from "rea
|
|
|
9
9
|
import { jsx as x, jsxs as S } from "react/jsx-runtime";
|
|
10
10
|
import { cn as C } from "cn";
|
|
11
11
|
//#region src/components/social/parts.tsx
|
|
12
|
-
|
|
12
|
+
var w = [
|
|
13
|
+
"forbidden",
|
|
14
|
+
"unauthorized",
|
|
15
|
+
"not_found",
|
|
16
|
+
"conflict",
|
|
17
|
+
"comments_locked",
|
|
18
|
+
"not_joinable"
|
|
19
|
+
], T = (e) => w.includes(e);
|
|
20
|
+
function E(t, n) {
|
|
13
21
|
let r = e(n);
|
|
14
|
-
return r.code === "rate_limited" ? t.t("contentErrors.rate_limited", { seconds: r.retryAfter ?? 60 }) : r.code
|
|
22
|
+
return r.code === "rate_limited" ? t.t("contentErrors.rate_limited", { seconds: r.retryAfter ?? 60 }) : T(r.code) ? t.t(`contentErrors.${r.code}`) : r.code === "moderation_rejected" ? r.message ? t.t("comments.rejected", { reason: r.message }) : t.t("comments.rejectedNoReason") : t.error(r);
|
|
15
23
|
}
|
|
16
|
-
function
|
|
24
|
+
function D(t, i) {
|
|
17
25
|
let a = n(i), o = r(), [s, c] = b(!1), [l, u] = b(null);
|
|
18
26
|
return {
|
|
19
27
|
run: _(async (n) => {
|
|
@@ -37,28 +45,28 @@ function T(t, i) {
|
|
|
37
45
|
setError: u
|
|
38
46
|
};
|
|
39
47
|
}
|
|
40
|
-
function
|
|
48
|
+
function O(e, t, n = Date.now()) {
|
|
41
49
|
let r = (Date.parse(t) - n) / 1e3, i = Math.abs(r);
|
|
42
50
|
if (i < 45) return e.t("time.justNow");
|
|
43
51
|
let a = new Intl.RelativeTimeFormat(e.language, { numeric: "auto" });
|
|
44
52
|
return i < 2700 ? a.format(Math.round(r / 60), "minute") : i < 79200 ? a.format(Math.round(r / 3600), "hour") : i < 2246400 ? a.format(Math.round(r / 86400), "day") : i < 28512e3 ? a.format(Math.round(r / 2592e3), "month") : a.format(Math.round(r / 31536e3), "year");
|
|
45
53
|
}
|
|
46
|
-
function
|
|
54
|
+
function k(e, t, n = !0) {
|
|
47
55
|
return new Intl.DateTimeFormat(e.language, n ? {
|
|
48
56
|
dateStyle: "medium",
|
|
49
57
|
timeStyle: "short"
|
|
50
58
|
} : { dateStyle: "medium" }).format(new Date(t));
|
|
51
59
|
}
|
|
52
|
-
function
|
|
60
|
+
function A({ at: e, className: n }) {
|
|
53
61
|
let r = t();
|
|
54
62
|
return /* @__PURE__ */ x("time", {
|
|
55
63
|
dateTime: e,
|
|
56
|
-
title:
|
|
64
|
+
title: k(r, e),
|
|
57
65
|
className: n,
|
|
58
|
-
children:
|
|
66
|
+
children: O(r, e)
|
|
59
67
|
});
|
|
60
68
|
}
|
|
61
|
-
function
|
|
69
|
+
function j({ user: e, name: t, size: n = "default" }) {
|
|
62
70
|
return /* @__PURE__ */ S(m, {
|
|
63
71
|
size: n,
|
|
64
72
|
children: [e?.avatar && /* @__PURE__ */ x(g, {
|
|
@@ -73,7 +81,7 @@ function k({ user: e, name: t, size: n = "default" }) {
|
|
|
73
81
|
})]
|
|
74
82
|
});
|
|
75
83
|
}
|
|
76
|
-
function
|
|
84
|
+
function M({ user: e, name: t, href: n, className: r }) {
|
|
77
85
|
let i = e && n?.(e), a = C("font-medium text-foreground", r);
|
|
78
86
|
return i ? /* @__PURE__ */ x("a", {
|
|
79
87
|
href: i,
|
|
@@ -84,7 +92,7 @@ function A({ user: e, name: t, href: n, className: r }) {
|
|
|
84
92
|
children: t
|
|
85
93
|
});
|
|
86
94
|
}
|
|
87
|
-
function
|
|
95
|
+
function N(e) {
|
|
88
96
|
let { t: n } = t(), r = y(), [m, h] = b("");
|
|
89
97
|
v(() => {
|
|
90
98
|
e.open && h("");
|
|
@@ -118,15 +126,15 @@ function j(e) {
|
|
|
118
126
|
] })
|
|
119
127
|
});
|
|
120
128
|
}
|
|
121
|
-
function
|
|
129
|
+
function P({ error: e, children: n, className: r }) {
|
|
122
130
|
let i = t();
|
|
123
131
|
return !e && !n ? null : /* @__PURE__ */ x("p", {
|
|
124
132
|
role: "alert",
|
|
125
133
|
className: C("text-sm text-destructive", r),
|
|
126
|
-
children: n ??
|
|
134
|
+
children: n ?? E(i, e)
|
|
127
135
|
});
|
|
128
136
|
}
|
|
129
137
|
//#endregion
|
|
130
|
-
export {
|
|
138
|
+
export { N as ConfirmDialog, P as ErrorLine, A as RelativeTime, j as UserAvatar, M as UserName, E as contentError, k as formatDate, O as relativeTime, D as useAction };
|
|
131
139
|
|
|
132
140
|
//# sourceMappingURL=parts.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parts.js","names":[],"sources":["../../../src/components/social/parts.tsx"],"sourcesContent":["import { cn } from \"cn\";\nimport { useCallback, useEffect, useId, useState, type ReactNode } from \"react\";\nimport { toContentKitError, type ContentKitError } from \"../../client/errors.js\";\nimport type { PublicUser } from \"../../client/generated/wire.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport type { Translator } from \"../../i18n/messages.js\";\nimport { useErrorReporter, type ContentKitErrorHandler, type SocialOperation } from \"../../react/context.js\";\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from \"#ckui/ui/alert-dialog\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"#ckui/ui/avatar\";\nimport { Input } from \"#ckui/ui/input\";\nimport { Label } from \"#ckui/ui/label\";\nimport { useGuarded } from \"../../react/context.js\";\n\n/** A content refusal or fault in words: the content wording for shared codes, else the code's. */\nexport function contentError(m: Translator, e: unknown): string {\n const err = toContentKitError(e);\n if (err.code === \"rate_limited\") return m.t(\"contentErrors.rate_limited\", { seconds: err.retryAfter ?? 60 });\n if (err.code === \"forbidden\" || err.code === \"unauthorized\" || err.code === \"not_found\" || err.code === \"conflict\") return m.t(`contentErrors.${err.code}`);\n if (err.code === \"moderation_rejected\") return err.message ? m.t(\"comments.rejected\", { reason: err.message }) : m.t(\"comments.rejectedNoReason\");\n return m.error(err);\n}\n\n/** Runs one async action at a time (through the flow's guard): its pending state and last error, reported once. */\nexport function useAction(operation: SocialOperation, onError?: ContentKitErrorHandler) {\n const report = useErrorReporter(onError);\n const guarded = useGuarded();\n const [pending, setPending] = useState(false);\n const [error, setError] = useState<ContentKitError | null>(null);\n const run = useCallback(\n async <T,>(fn: () => Promise<T>): Promise<T | undefined> => {\n setPending(true);\n setError(null);\n try {\n return await guarded(fn);\n } catch (e) {\n const err = toContentKitError(e);\n if (err.code !== \"aborted\") {\n setError(err);\n report(err, operation);\n }\n return undefined;\n } finally {\n setPending(false);\n }\n },\n [operation, report, guarded],\n );\n return { run, pending, error, setError };\n}\n\n/** \"3 minutes ago\", in the UI's language. */\nexport function relativeTime(m: Translator, iso: string, now = Date.now()): string {\n const s = (Date.parse(iso) - now) / 1000;\n const a = Math.abs(s);\n if (a < 45) return m.t(\"time.justNow\");\n const f = new Intl.RelativeTimeFormat(m.language, { numeric: \"auto\" });\n if (a < 2700) return f.format(Math.round(s / 60), \"minute\");\n if (a < 79200) return f.format(Math.round(s / 3600), \"hour\");\n if (a < 2246400) return f.format(Math.round(s / 86400), \"day\");\n if (a < 28512000) return f.format(Math.round(s / 2592000), \"month\");\n return f.format(Math.round(s / 31536000), \"year\");\n}\n\nexport function formatDate(m: Translator, iso: string, time = true): string {\n return new Intl.DateTimeFormat(m.language, time ? { dateStyle: \"medium\", timeStyle: \"short\" } : { dateStyle: \"medium\" }).format(new Date(iso));\n}\n\n/** A relative time with the full date as its title. */\nexport function RelativeTime({ at, className }: { at: string; className?: string }) {\n const m = useMessages();\n return (\n <time dateTime={at} title={formatDate(m, at)} className={className}>\n {relativeTime(m, at)}\n </time>\n );\n}\n\n/** A user's avatar, or their initial. */\nexport function UserAvatar({ user, name, size = \"default\" }: { user?: PublicUser; name: string; size?: \"default\" | \"sm\" | \"lg\" }) {\n return (\n <Avatar size={size}>\n {user?.avatar && <AvatarImage src={user.avatar} srcSet={user.avatar_srcset} sizes={size === \"lg\" ? \"40px\" : size === \"sm\" ? \"24px\" : \"32px\"} alt=\"\" loading=\"lazy\" />}\n <AvatarFallback aria-hidden>{(name.trim()[0] ?? \"?\").toUpperCase()}</AvatarFallback>\n </Avatar>\n );\n}\n\n/** A user's name, linked when the host gives a profile URL. */\nexport function UserName({ user, name, href, className }: { user?: PublicUser; name: string; href?: (u: PublicUser) => string | undefined; className?: string }) {\n const url = user && href?.(user);\n const cls = cn(\"font-medium text-foreground\", className);\n return url ? (\n <a href={url} className={cn(cls, \"hover:underline\")}>\n {name}\n </a>\n ) : (\n <span className={cls}>{name}</span>\n );\n}\n\n/** A confirmation for a destructive action; with typed, the word must be typed first (acts that cannot be undone). */\nexport function ConfirmDialog(p: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: ReactNode;\n description?: ReactNode;\n confirm: ReactNode;\n typed?: string;\n onConfirm: () => void;\n}) {\n const { t } = useMessages();\n const id = useId();\n const [word, setWord] = useState(\"\");\n useEffect(() => {\n if (p.open) setWord(\"\");\n }, [p.open]);\n const ready = !p.typed || word.trim().toLowerCase() === p.typed.toLowerCase();\n return (\n <AlertDialog open={p.open} onOpenChange={p.onOpenChange}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>{p.title}</AlertDialogTitle>\n {p.description && <AlertDialogDescription>{p.description}</AlertDialogDescription>}\n </AlertDialogHeader>\n {p.typed && (\n <div className=\"grid gap-1.5\">\n <Label htmlFor={`${id}-typed`}>{t(\"admin.typeToConfirm\", { word: p.typed })}</Label>\n <Input id={`${id}-typed`} value={word} autoComplete=\"off\" onChange={(e) => setWord(e.target.value)} />\n </div>\n )}\n <AlertDialogFooter>\n <AlertDialogCancel>{t(\"common.cancel\")}</AlertDialogCancel>\n <AlertDialogAction\n variant=\"destructive\"\n disabled={!ready}\n onClick={() => {\n if (!ready) return;\n p.onConfirm();\n p.onOpenChange(false);\n }}\n >\n {p.confirm}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n\n/** A failure line, announced politely. */\nexport function ErrorLine({ error, children, className }: { error?: ContentKitError | null; children?: ReactNode; className?: string }) {\n const m = useMessages();\n if (!error && !children) return null;\n return (\n <p role=\"alert\" className={cn(\"text-sm text-destructive\", className)}>\n {children ?? contentError(m, error)}\n </p>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"parts.js","names":[],"sources":["../../../src/components/social/parts.tsx"],"sourcesContent":["import { cn } from \"cn\";\nimport { useCallback, useEffect, useId, useState, type ReactNode } from \"react\";\nimport { toContentKitError, type ContentKitError } from \"../../client/errors.js\";\nimport type { PublicUser } from \"../../client/generated/wire.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport type { Translator } from \"../../i18n/messages.js\";\nimport { useErrorReporter, type ContentKitErrorHandler, type SocialOperation } from \"../../react/context.js\";\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from \"#ckui/ui/alert-dialog\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"#ckui/ui/avatar\";\nimport { Input } from \"#ckui/ui/input\";\nimport { Label } from \"#ckui/ui/label\";\nimport { useGuarded } from \"../../react/context.js\";\n\nconst WORDED = [\"forbidden\", \"unauthorized\", \"not_found\", \"conflict\", \"comments_locked\", \"not_joinable\"] as const;\nconst worded = (code: string): code is (typeof WORDED)[number] => (WORDED as readonly string[]).includes(code);\n\n/** A content refusal or fault in words: the content wording for shared codes, else the code's. */\nexport function contentError(m: Translator, e: unknown): string {\n const err = toContentKitError(e);\n if (err.code === \"rate_limited\") return m.t(\"contentErrors.rate_limited\", { seconds: err.retryAfter ?? 60 });\n if (worded(err.code)) return m.t(`contentErrors.${err.code}`);\n if (err.code === \"moderation_rejected\") return err.message ? m.t(\"comments.rejected\", { reason: err.message }) : m.t(\"comments.rejectedNoReason\");\n return m.error(err);\n}\n\n/** Runs one async action at a time (through the flow's guard): its pending state and last error, reported once. */\nexport function useAction(operation: SocialOperation, onError?: ContentKitErrorHandler) {\n const report = useErrorReporter(onError);\n const guarded = useGuarded();\n const [pending, setPending] = useState(false);\n const [error, setError] = useState<ContentKitError | null>(null);\n const run = useCallback(\n async <T,>(fn: () => Promise<T>): Promise<T | undefined> => {\n setPending(true);\n setError(null);\n try {\n return await guarded(fn);\n } catch (e) {\n const err = toContentKitError(e);\n if (err.code !== \"aborted\") {\n setError(err);\n report(err, operation);\n }\n return undefined;\n } finally {\n setPending(false);\n }\n },\n [operation, report, guarded],\n );\n return { run, pending, error, setError };\n}\n\n/** \"3 minutes ago\", in the UI's language. */\nexport function relativeTime(m: Translator, iso: string, now = Date.now()): string {\n const s = (Date.parse(iso) - now) / 1000;\n const a = Math.abs(s);\n if (a < 45) return m.t(\"time.justNow\");\n const f = new Intl.RelativeTimeFormat(m.language, { numeric: \"auto\" });\n if (a < 2700) return f.format(Math.round(s / 60), \"minute\");\n if (a < 79200) return f.format(Math.round(s / 3600), \"hour\");\n if (a < 2246400) return f.format(Math.round(s / 86400), \"day\");\n if (a < 28512000) return f.format(Math.round(s / 2592000), \"month\");\n return f.format(Math.round(s / 31536000), \"year\");\n}\n\nexport function formatDate(m: Translator, iso: string, time = true): string {\n return new Intl.DateTimeFormat(m.language, time ? { dateStyle: \"medium\", timeStyle: \"short\" } : { dateStyle: \"medium\" }).format(new Date(iso));\n}\n\n/** A relative time with the full date as its title. */\nexport function RelativeTime({ at, className }: { at: string; className?: string }) {\n const m = useMessages();\n return (\n <time dateTime={at} title={formatDate(m, at)} className={className}>\n {relativeTime(m, at)}\n </time>\n );\n}\n\n/** A user's avatar, or their initial. */\nexport function UserAvatar({ user, name, size = \"default\" }: { user?: PublicUser; name: string; size?: \"default\" | \"sm\" | \"lg\" }) {\n return (\n <Avatar size={size}>\n {user?.avatar && <AvatarImage src={user.avatar} srcSet={user.avatar_srcset} sizes={size === \"lg\" ? \"40px\" : size === \"sm\" ? \"24px\" : \"32px\"} alt=\"\" loading=\"lazy\" />}\n <AvatarFallback aria-hidden>{(name.trim()[0] ?? \"?\").toUpperCase()}</AvatarFallback>\n </Avatar>\n );\n}\n\n/** A user's name, linked when the host gives a profile URL. */\nexport function UserName({ user, name, href, className }: { user?: PublicUser; name: string; href?: (u: PublicUser) => string | undefined; className?: string }) {\n const url = user && href?.(user);\n const cls = cn(\"font-medium text-foreground\", className);\n return url ? (\n <a href={url} className={cn(cls, \"hover:underline\")}>\n {name}\n </a>\n ) : (\n <span className={cls}>{name}</span>\n );\n}\n\n/** A confirmation for a destructive action; with typed, the word must be typed first (acts that cannot be undone). */\nexport function ConfirmDialog(p: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: ReactNode;\n description?: ReactNode;\n confirm: ReactNode;\n typed?: string;\n onConfirm: () => void;\n}) {\n const { t } = useMessages();\n const id = useId();\n const [word, setWord] = useState(\"\");\n useEffect(() => {\n if (p.open) setWord(\"\");\n }, [p.open]);\n const ready = !p.typed || word.trim().toLowerCase() === p.typed.toLowerCase();\n return (\n <AlertDialog open={p.open} onOpenChange={p.onOpenChange}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>{p.title}</AlertDialogTitle>\n {p.description && <AlertDialogDescription>{p.description}</AlertDialogDescription>}\n </AlertDialogHeader>\n {p.typed && (\n <div className=\"grid gap-1.5\">\n <Label htmlFor={`${id}-typed`}>{t(\"admin.typeToConfirm\", { word: p.typed })}</Label>\n <Input id={`${id}-typed`} value={word} autoComplete=\"off\" onChange={(e) => setWord(e.target.value)} />\n </div>\n )}\n <AlertDialogFooter>\n <AlertDialogCancel>{t(\"common.cancel\")}</AlertDialogCancel>\n <AlertDialogAction\n variant=\"destructive\"\n disabled={!ready}\n onClick={() => {\n if (!ready) return;\n p.onConfirm();\n p.onOpenChange(false);\n }}\n >\n {p.confirm}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n\n/** A failure line, announced politely. */\nexport function ErrorLine({ error, children, className }: { error?: ContentKitError | null; children?: ReactNode; className?: string }) {\n const m = useMessages();\n if (!error && !children) return null;\n return (\n <p role=\"alert\" className={cn(\"text-sm text-destructive\", className)}>\n {children ?? contentError(m, error)}\n </p>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ContentKitUiAppearance } from '../../appearance.js';
|
|
2
|
+
import { ContentKitClient } from '../../client/client.js';
|
|
3
|
+
import { ContentKitErrorHandler } from '../../react/context.js';
|
|
4
|
+
export interface ModerationRulesProps {
|
|
5
|
+
client?: ContentKitClient;
|
|
6
|
+
onError?: ContentKitErrorHandler;
|
|
7
|
+
className?: string;
|
|
8
|
+
appearance?: ContentKitUiAppearance;
|
|
9
|
+
}
|
|
10
|
+
/** Words split on commas and new lines, trimmed, without repeats. */
|
|
11
|
+
export declare function wordsOf(text: string): string[];
|
|
12
|
+
/**
|
|
13
|
+
* Staff: the banned words every comment and free-text answer meets: add
|
|
14
|
+
* several at once (comma-separated), mask them (the default) or refuse the
|
|
15
|
+
* text, switch what one does, or unban it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function BannedWords({ client, onError, className, appearance }: ModerationRulesProps): import('react').JSX.Element;
|
|
18
|
+
/**
|
|
19
|
+
* Staff: the IP bans on commenting and answering polls, newest first:
|
|
20
|
+
* ban an address or a network (CIDR) with a reason and an optional end, or
|
|
21
|
+
* lift a ban. Holders of root:comments:moderate are never refused.
|
|
22
|
+
*/
|
|
23
|
+
export declare function IPBans({ client, onError, className, appearance }: ModerationRulesProps): import('react').JSX.Element;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { useMessages as e } from "../../i18n/context.js";
|
|
2
|
+
import { ContentKitUiRoot as t } from "../../scope.js";
|
|
3
|
+
import { Button as n } from "../../ui/button.js";
|
|
4
|
+
import { useBannedWords as r, useIPBans as i } from "../../react/comments.js";
|
|
5
|
+
import { Badge as a } from "../../ui/badge.js";
|
|
6
|
+
import { Input as o } from "../../ui/input.js";
|
|
7
|
+
import { Label as s } from "../../ui/label.js";
|
|
8
|
+
import { ErrorLine as c, formatDate as l, useAction as u } from "./parts.js";
|
|
9
|
+
import { LoadFailed as d, Rows as f } from "../admin/shell.js";
|
|
10
|
+
import { Switch as p } from "../../ui/switch.js";
|
|
11
|
+
import { fromLocalInput as m } from "../../client/group-fields.js";
|
|
12
|
+
import { useId as h, useMemo as g, useState as _ } from "react";
|
|
13
|
+
import { Fragment as v, jsx as y, jsxs as b } from "react/jsx-runtime";
|
|
14
|
+
import { cn as x } from "cn";
|
|
15
|
+
//#region src/components/social/rules.tsx
|
|
16
|
+
function S(e) {
|
|
17
|
+
return [...new Set(e.split(/[,\n]/).map((e) => e.trim()).filter(Boolean))];
|
|
18
|
+
}
|
|
19
|
+
function C({ client: i, onError: l, className: m, appearance: C }) {
|
|
20
|
+
let { t: w } = e(), T = h(), E = r({ client: i }), D = u("rules.save", l), [O, k] = _(""), [A, j] = _(!1), [M, N] = _(""), P = g(() => {
|
|
21
|
+
let e = M.trim().toLowerCase();
|
|
22
|
+
return e ? E.words.filter((t) => t.word.includes(e)) : E.words;
|
|
23
|
+
}, [E.words, M]), F = async () => {
|
|
24
|
+
let e = S(O);
|
|
25
|
+
if (!e.length) return;
|
|
26
|
+
let t = A ? "reject" : "mask";
|
|
27
|
+
await D.run(() => E.ban(e.map((e) => ({
|
|
28
|
+
word: e,
|
|
29
|
+
action: t
|
|
30
|
+
})))) && k("");
|
|
31
|
+
};
|
|
32
|
+
return /* @__PURE__ */ b(t, {
|
|
33
|
+
appearance: C,
|
|
34
|
+
className: x("grid gap-4 text-sm", m),
|
|
35
|
+
"data-ckui": "banned-words",
|
|
36
|
+
children: [
|
|
37
|
+
/* @__PURE__ */ b("form", {
|
|
38
|
+
className: "grid gap-2",
|
|
39
|
+
onSubmit: (e) => {
|
|
40
|
+
e.preventDefault(), F();
|
|
41
|
+
},
|
|
42
|
+
children: [/* @__PURE__ */ b("div", {
|
|
43
|
+
className: "flex flex-wrap items-end gap-2",
|
|
44
|
+
children: [/* @__PURE__ */ b("div", {
|
|
45
|
+
className: "grid min-w-48 flex-1 gap-1.5",
|
|
46
|
+
children: [/* @__PURE__ */ y(s, {
|
|
47
|
+
htmlFor: `${T}-word`,
|
|
48
|
+
children: w("moderationRules.word")
|
|
49
|
+
}), /* @__PURE__ */ y(o, {
|
|
50
|
+
id: `${T}-word`,
|
|
51
|
+
value: O,
|
|
52
|
+
placeholder: w("moderationRules.wordPlaceholder"),
|
|
53
|
+
onChange: (e) => k(e.target.value)
|
|
54
|
+
})]
|
|
55
|
+
}), /* @__PURE__ */ y(n, {
|
|
56
|
+
type: "submit",
|
|
57
|
+
variant: "outline",
|
|
58
|
+
disabled: D.pending || !S(O).length,
|
|
59
|
+
children: w("moderationRules.ban")
|
|
60
|
+
})]
|
|
61
|
+
}), /* @__PURE__ */ b("label", {
|
|
62
|
+
className: "flex items-center gap-2",
|
|
63
|
+
children: [/* @__PURE__ */ y(p, {
|
|
64
|
+
size: "sm",
|
|
65
|
+
checked: A,
|
|
66
|
+
onCheckedChange: j
|
|
67
|
+
}), w("moderationRules.rejectNew")]
|
|
68
|
+
})]
|
|
69
|
+
}),
|
|
70
|
+
/* @__PURE__ */ y("p", {
|
|
71
|
+
className: "text-muted-foreground",
|
|
72
|
+
children: w("moderationRules.wordsHint")
|
|
73
|
+
}),
|
|
74
|
+
/* @__PURE__ */ y(c, { error: D.error }),
|
|
75
|
+
E.loading && !E.words.length ? /* @__PURE__ */ y(f, {}) : E.error && !E.words.length ? /* @__PURE__ */ y(d, {
|
|
76
|
+
error: E.error,
|
|
77
|
+
message: w("moderationRules.loadFailed"),
|
|
78
|
+
retry: E.reload
|
|
79
|
+
}) : E.words.length ? /* @__PURE__ */ b(v, { children: [E.words.length > 10 && /* @__PURE__ */ b("div", {
|
|
80
|
+
className: "grid max-w-xs gap-1.5",
|
|
81
|
+
children: [/* @__PURE__ */ y(s, {
|
|
82
|
+
htmlFor: `${T}-filter`,
|
|
83
|
+
className: "sr-only",
|
|
84
|
+
children: w("moderationRules.filter")
|
|
85
|
+
}), /* @__PURE__ */ y(o, {
|
|
86
|
+
id: `${T}-filter`,
|
|
87
|
+
className: "h-8",
|
|
88
|
+
type: "search",
|
|
89
|
+
value: M,
|
|
90
|
+
placeholder: w("moderationRules.filter"),
|
|
91
|
+
onChange: (e) => N(e.target.value)
|
|
92
|
+
})]
|
|
93
|
+
}), /* @__PURE__ */ y("ul", {
|
|
94
|
+
className: "grid divide-y divide-border rounded-lg border border-border",
|
|
95
|
+
"aria-busy": E.loading,
|
|
96
|
+
children: P.map((e) => {
|
|
97
|
+
let t = e.action === "reject";
|
|
98
|
+
return /* @__PURE__ */ b("li", {
|
|
99
|
+
className: "flex flex-wrap items-center gap-3 p-3",
|
|
100
|
+
"data-ckui": "banned-word",
|
|
101
|
+
"data-action": e.action,
|
|
102
|
+
children: [
|
|
103
|
+
/* @__PURE__ */ y("span", {
|
|
104
|
+
className: "min-w-0 flex-1 font-medium break-words",
|
|
105
|
+
children: e.word
|
|
106
|
+
}),
|
|
107
|
+
/* @__PURE__ */ y(a, {
|
|
108
|
+
variant: t ? "destructive" : "secondary",
|
|
109
|
+
children: w(t ? "moderationRules.rejects" : "moderationRules.masks")
|
|
110
|
+
}),
|
|
111
|
+
/* @__PURE__ */ y(n, {
|
|
112
|
+
size: "sm",
|
|
113
|
+
variant: "outline",
|
|
114
|
+
disabled: D.pending,
|
|
115
|
+
"aria-label": w(t ? "moderationRules.maskWord" : "moderationRules.rejectWord", { word: e.word }),
|
|
116
|
+
onClick: () => void D.run(() => E.ban([{
|
|
117
|
+
word: e.word,
|
|
118
|
+
action: t ? "mask" : "reject"
|
|
119
|
+
}])),
|
|
120
|
+
children: w(t ? "moderationRules.mask" : "moderationRules.reject")
|
|
121
|
+
}),
|
|
122
|
+
/* @__PURE__ */ y(n, {
|
|
123
|
+
size: "sm",
|
|
124
|
+
variant: "destructive",
|
|
125
|
+
disabled: D.pending,
|
|
126
|
+
"aria-label": w("moderationRules.unbanWord", { word: e.word }),
|
|
127
|
+
onClick: () => void D.run(() => E.unban([e.word])),
|
|
128
|
+
children: w("moderationRules.unban")
|
|
129
|
+
})
|
|
130
|
+
]
|
|
131
|
+
}, e.word);
|
|
132
|
+
})
|
|
133
|
+
})] }) : /* @__PURE__ */ y("p", {
|
|
134
|
+
className: "text-muted-foreground",
|
|
135
|
+
children: w("moderationRules.noWords")
|
|
136
|
+
})
|
|
137
|
+
]
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function w({ client: r, onError: p, className: g, appearance: v }) {
|
|
141
|
+
let S = e(), { t: C } = S, w = h(), T = i({ client: r }), E = u("rules.save", p), [D, O] = _(""), [k, A] = _(""), [j, M] = _(""), N = async () => {
|
|
142
|
+
let e = D.trim();
|
|
143
|
+
if (!e) return;
|
|
144
|
+
let t = k.trim(), n = m(j);
|
|
145
|
+
await E.run(() => T.ban([{
|
|
146
|
+
network: e,
|
|
147
|
+
...t ? { reason: t } : {},
|
|
148
|
+
...n ? { until: n } : {}
|
|
149
|
+
}])) && (O(""), A(""), M(""));
|
|
150
|
+
};
|
|
151
|
+
return /* @__PURE__ */ b(t, {
|
|
152
|
+
appearance: v,
|
|
153
|
+
className: x("grid gap-4 text-sm", g),
|
|
154
|
+
"data-ckui": "ip-bans",
|
|
155
|
+
children: [
|
|
156
|
+
/* @__PURE__ */ b("form", {
|
|
157
|
+
className: "grid gap-2 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto_auto] sm:items-end",
|
|
158
|
+
onSubmit: (e) => {
|
|
159
|
+
e.preventDefault(), N();
|
|
160
|
+
},
|
|
161
|
+
children: [
|
|
162
|
+
/* @__PURE__ */ b("div", {
|
|
163
|
+
className: "grid gap-1.5",
|
|
164
|
+
children: [/* @__PURE__ */ y(s, {
|
|
165
|
+
htmlFor: `${w}-network`,
|
|
166
|
+
children: C("moderationRules.network")
|
|
167
|
+
}), /* @__PURE__ */ y(o, {
|
|
168
|
+
id: `${w}-network`,
|
|
169
|
+
value: D,
|
|
170
|
+
placeholder: "203.0.113.7, 203.0.113.0/24",
|
|
171
|
+
onChange: (e) => O(e.target.value)
|
|
172
|
+
})]
|
|
173
|
+
}),
|
|
174
|
+
/* @__PURE__ */ b("div", {
|
|
175
|
+
className: "grid gap-1.5",
|
|
176
|
+
children: [/* @__PURE__ */ y(s, {
|
|
177
|
+
htmlFor: `${w}-reason`,
|
|
178
|
+
children: C("moderationRules.reason")
|
|
179
|
+
}), /* @__PURE__ */ y(o, {
|
|
180
|
+
id: `${w}-reason`,
|
|
181
|
+
value: k,
|
|
182
|
+
onChange: (e) => A(e.target.value)
|
|
183
|
+
})]
|
|
184
|
+
}),
|
|
185
|
+
/* @__PURE__ */ b("div", {
|
|
186
|
+
className: "grid gap-1.5",
|
|
187
|
+
children: [/* @__PURE__ */ y(s, {
|
|
188
|
+
htmlFor: `${w}-until`,
|
|
189
|
+
children: C("moderationRules.until")
|
|
190
|
+
}), /* @__PURE__ */ y(o, {
|
|
191
|
+
id: `${w}-until`,
|
|
192
|
+
type: "datetime-local",
|
|
193
|
+
value: j,
|
|
194
|
+
onChange: (e) => M(e.target.value)
|
|
195
|
+
})]
|
|
196
|
+
}),
|
|
197
|
+
/* @__PURE__ */ y(n, {
|
|
198
|
+
type: "submit",
|
|
199
|
+
variant: "outline",
|
|
200
|
+
disabled: E.pending || !D.trim(),
|
|
201
|
+
children: C("moderationRules.ban")
|
|
202
|
+
})
|
|
203
|
+
]
|
|
204
|
+
}),
|
|
205
|
+
/* @__PURE__ */ y(c, { error: E.error }),
|
|
206
|
+
T.loading && !T.bans.length ? /* @__PURE__ */ y(f, {}) : T.error && !T.bans.length ? /* @__PURE__ */ y(d, {
|
|
207
|
+
error: T.error,
|
|
208
|
+
message: C("moderationRules.loadFailed"),
|
|
209
|
+
retry: T.reload
|
|
210
|
+
}) : T.bans.length ? /* @__PURE__ */ y("ul", {
|
|
211
|
+
className: "grid divide-y divide-border rounded-lg border border-border",
|
|
212
|
+
"aria-busy": T.loading,
|
|
213
|
+
children: T.bans.map((e) => /* @__PURE__ */ b("li", {
|
|
214
|
+
className: "flex flex-wrap items-start gap-3 p-3",
|
|
215
|
+
"data-ckui": "ip-ban",
|
|
216
|
+
children: [/* @__PURE__ */ b("div", {
|
|
217
|
+
className: "grid min-w-0 flex-1 gap-0.5",
|
|
218
|
+
children: [
|
|
219
|
+
/* @__PURE__ */ b("div", {
|
|
220
|
+
className: "flex flex-wrap items-center gap-2",
|
|
221
|
+
children: [/* @__PURE__ */ y("span", {
|
|
222
|
+
className: "font-medium break-all",
|
|
223
|
+
children: e.network
|
|
224
|
+
}), /* @__PURE__ */ y(a, {
|
|
225
|
+
variant: e.expired ? "outline" : "destructive",
|
|
226
|
+
children: C(e.expired ? "bans.expired" : "bans.active")
|
|
227
|
+
})]
|
|
228
|
+
}),
|
|
229
|
+
e.reason && /* @__PURE__ */ y("p", {
|
|
230
|
+
className: "break-words",
|
|
231
|
+
children: C("bans.reason", { reason: e.reason })
|
|
232
|
+
}),
|
|
233
|
+
/* @__PURE__ */ b("p", {
|
|
234
|
+
className: "text-muted-foreground",
|
|
235
|
+
children: [
|
|
236
|
+
e.until ? C("bans.until", { date: l(S, e.until) }) : C("bans.untilLifted"),
|
|
237
|
+
" · ",
|
|
238
|
+
C("bans.bannedAt", { date: l(S, e.banned_at, !1) })
|
|
239
|
+
]
|
|
240
|
+
})
|
|
241
|
+
]
|
|
242
|
+
}), /* @__PURE__ */ y(n, {
|
|
243
|
+
size: "sm",
|
|
244
|
+
variant: "destructive",
|
|
245
|
+
disabled: E.pending,
|
|
246
|
+
"aria-label": C("moderationRules.liftNetwork", { network: e.network }),
|
|
247
|
+
onClick: () => void E.run(() => T.lift([e.network])),
|
|
248
|
+
children: C("bans.lift")
|
|
249
|
+
})]
|
|
250
|
+
}, e.network))
|
|
251
|
+
}) : /* @__PURE__ */ y("p", {
|
|
252
|
+
className: "text-muted-foreground",
|
|
253
|
+
children: C("moderationRules.noIPBans")
|
|
254
|
+
})
|
|
255
|
+
]
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
//#endregion
|
|
259
|
+
export { C as BannedWords, w as IPBans, S as wordsOf };
|
|
260
|
+
|
|
261
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","names":[],"sources":["../../../src/components/social/rules.tsx"],"sourcesContent":["import { cn } from \"cn\";\nimport { useId, useMemo, useState } from \"react\";\nimport type { ContentKitUiAppearance } from \"../../appearance.js\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport { fromLocalInput } from \"../../client/group-fields.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { useBannedWords, useIPBans } from \"../../react/comments.js\";\nimport type { ContentKitErrorHandler } from \"../../react/context.js\";\nimport { ContentKitUiRoot } from \"../../scope.js\";\nimport { Badge } from \"#ckui/ui/badge\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Input } from \"#ckui/ui/input\";\nimport { Label } from \"#ckui/ui/label\";\nimport { Switch } from \"#ckui/ui/switch\";\nimport { LoadFailed, Rows } from \"../admin/shell.js\";\nimport { ErrorLine, formatDate, useAction } from \"./parts.js\";\n\nexport interface ModerationRulesProps {\n client?: ContentKitClient;\n onError?: ContentKitErrorHandler;\n className?: string;\n appearance?: ContentKitUiAppearance;\n}\n\n/** Words split on commas and new lines, trimmed, without repeats. */\nexport function wordsOf(text: string): string[] {\n return [...new Set(text.split(/[,\\n]/).map((w) => w.trim()).filter(Boolean))];\n}\n\n/**\n * Staff: the banned words every comment and free-text answer meets: add\n * several at once (comma-separated), mask them (the default) or refuse the\n * text, switch what one does, or unban it.\n */\nexport function BannedWords({ client, onError, className, appearance }: ModerationRulesProps) {\n const { t } = useMessages();\n const ids = useId();\n const rules = useBannedWords({ client });\n const save = useAction(\"rules.save\", onError);\n const [text, setText] = useState(\"\");\n const [reject, setReject] = useState(false);\n const [filter, setFilter] = useState(\"\");\n const shown = useMemo(() => {\n const f = filter.trim().toLowerCase();\n return f ? rules.words.filter((w) => w.word.includes(f)) : rules.words;\n }, [rules.words, filter]);\n const add = async () => {\n const words = wordsOf(text);\n if (!words.length) return;\n const action = reject ? \"reject\" : \"mask\";\n if (await save.run(() => rules.ban(words.map((word) => ({ word, action }))))) setText(\"\");\n };\n return (\n <ContentKitUiRoot appearance={appearance} className={cn(\"grid gap-4 text-sm\", className)} data-ckui=\"banned-words\">\n <form\n className=\"grid gap-2\"\n onSubmit={(e) => {\n e.preventDefault();\n void add();\n }}\n >\n <div className=\"flex flex-wrap items-end gap-2\">\n <div className=\"grid min-w-48 flex-1 gap-1.5\">\n <Label htmlFor={`${ids}-word`}>{t(\"moderationRules.word\")}</Label>\n <Input id={`${ids}-word`} value={text} placeholder={t(\"moderationRules.wordPlaceholder\")} onChange={(e) => setText(e.target.value)} />\n </div>\n <Button type=\"submit\" variant=\"outline\" disabled={save.pending || !wordsOf(text).length}>\n {t(\"moderationRules.ban\")}\n </Button>\n </div>\n <label className=\"flex items-center gap-2\">\n <Switch size=\"sm\" checked={reject} onCheckedChange={setReject} />\n {t(\"moderationRules.rejectNew\")}\n </label>\n </form>\n <p className=\"text-muted-foreground\">{t(\"moderationRules.wordsHint\")}</p>\n <ErrorLine error={save.error} />\n {rules.loading && !rules.words.length ? (\n <Rows />\n ) : rules.error && !rules.words.length ? (\n <LoadFailed error={rules.error} message={t(\"moderationRules.loadFailed\")} retry={rules.reload} />\n ) : !rules.words.length ? (\n <p className=\"text-muted-foreground\">{t(\"moderationRules.noWords\")}</p>\n ) : (\n <>\n {rules.words.length > 10 && (\n <div className=\"grid max-w-xs gap-1.5\">\n <Label htmlFor={`${ids}-filter`} className=\"sr-only\">\n {t(\"moderationRules.filter\")}\n </Label>\n <Input id={`${ids}-filter`} className=\"h-8\" type=\"search\" value={filter} placeholder={t(\"moderationRules.filter\")} onChange={(e) => setFilter(e.target.value)} />\n </div>\n )}\n <ul className=\"grid divide-y divide-border rounded-lg border border-border\" aria-busy={rules.loading}>\n {shown.map((w) => {\n const rejects = w.action === \"reject\";\n return (\n <li key={w.word} className=\"flex flex-wrap items-center gap-3 p-3\" data-ckui=\"banned-word\" data-action={w.action}>\n <span className=\"min-w-0 flex-1 font-medium break-words\">{w.word}</span>\n <Badge variant={rejects ? \"destructive\" : \"secondary\"}>{t(rejects ? \"moderationRules.rejects\" : \"moderationRules.masks\")}</Badge>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={save.pending}\n aria-label={t(rejects ? \"moderationRules.maskWord\" : \"moderationRules.rejectWord\", { word: w.word })}\n onClick={() => void save.run(() => rules.ban([{ word: w.word, action: rejects ? \"mask\" : \"reject\" }]))}\n >\n {t(rejects ? \"moderationRules.mask\" : \"moderationRules.reject\")}\n </Button>\n <Button size=\"sm\" variant=\"destructive\" disabled={save.pending} aria-label={t(\"moderationRules.unbanWord\", { word: w.word })} onClick={() => void save.run(() => rules.unban([w.word]))}>\n {t(\"moderationRules.unban\")}\n </Button>\n </li>\n );\n })}\n </ul>\n </>\n )}\n </ContentKitUiRoot>\n );\n}\n\n/**\n * Staff: the IP bans on commenting and answering polls, newest first:\n * ban an address or a network (CIDR) with a reason and an optional end, or\n * lift a ban. Holders of root:comments:moderate are never refused.\n */\nexport function IPBans({ client, onError, className, appearance }: ModerationRulesProps) {\n const m = useMessages();\n const { t } = m;\n const ids = useId();\n const rules = useIPBans({ client });\n const save = useAction(\"rules.save\", onError);\n const [network, setNetwork] = useState(\"\");\n const [reason, setReason] = useState(\"\");\n const [until, setUntil] = useState(\"\");\n const add = async () => {\n const net = network.trim();\n if (!net) return;\n const r = reason.trim();\n const end = fromLocalInput(until);\n if (await save.run(() => rules.ban([{ network: net, ...(r ? { reason: r } : {}), ...(end ? { until: end } : {}) }]))) {\n setNetwork(\"\");\n setReason(\"\");\n setUntil(\"\");\n }\n };\n return (\n <ContentKitUiRoot appearance={appearance} className={cn(\"grid gap-4 text-sm\", className)} data-ckui=\"ip-bans\">\n <form\n className=\"grid gap-2 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto_auto] sm:items-end\"\n onSubmit={(e) => {\n e.preventDefault();\n void add();\n }}\n >\n <div className=\"grid gap-1.5\">\n <Label htmlFor={`${ids}-network`}>{t(\"moderationRules.network\")}</Label>\n <Input id={`${ids}-network`} value={network} placeholder=\"203.0.113.7, 203.0.113.0/24\" onChange={(e) => setNetwork(e.target.value)} />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor={`${ids}-reason`}>{t(\"moderationRules.reason\")}</Label>\n <Input id={`${ids}-reason`} value={reason} onChange={(e) => setReason(e.target.value)} />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor={`${ids}-until`}>{t(\"moderationRules.until\")}</Label>\n <Input id={`${ids}-until`} type=\"datetime-local\" value={until} onChange={(e) => setUntil(e.target.value)} />\n </div>\n <Button type=\"submit\" variant=\"outline\" disabled={save.pending || !network.trim()}>\n {t(\"moderationRules.ban\")}\n </Button>\n </form>\n <ErrorLine error={save.error} />\n {rules.loading && !rules.bans.length ? (\n <Rows />\n ) : rules.error && !rules.bans.length ? (\n <LoadFailed error={rules.error} message={t(\"moderationRules.loadFailed\")} retry={rules.reload} />\n ) : !rules.bans.length ? (\n <p className=\"text-muted-foreground\">{t(\"moderationRules.noIPBans\")}</p>\n ) : (\n <ul className=\"grid divide-y divide-border rounded-lg border border-border\" aria-busy={rules.loading}>\n {rules.bans.map((b) => (\n <li key={b.network} className=\"flex flex-wrap items-start gap-3 p-3\" data-ckui=\"ip-ban\">\n <div className=\"grid min-w-0 flex-1 gap-0.5\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"font-medium break-all\">{b.network}</span>\n <Badge variant={b.expired ? \"outline\" : \"destructive\"}>{t(b.expired ? \"bans.expired\" : \"bans.active\")}</Badge>\n </div>\n {b.reason && <p className=\"break-words\">{t(\"bans.reason\", { reason: b.reason })}</p>}\n <p className=\"text-muted-foreground\">\n {b.until ? t(\"bans.until\", { date: formatDate(m, b.until) }) : t(\"bans.untilLifted\")} · {t(\"bans.bannedAt\", { date: formatDate(m, b.banned_at, false) })}\n </p>\n </div>\n <Button size=\"sm\" variant=\"destructive\" disabled={save.pending} aria-label={t(\"moderationRules.liftNetwork\", { network: b.network })} onClick={() => void save.run(() => rules.lift([b.network]))}>\n {t(\"bans.lift\")}\n </Button>\n </li>\n ))}\n </ul>\n )}\n </ContentKitUiRoot>\n );\n}\n"],"mappings":"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"}
|