achived chats
This commit is contained in:
+4
-2
@@ -60,11 +60,13 @@ No router library — a tiny history wrapper (`lib/route.ts`: `navigate()`,
|
||||
share button), message list (`Markdown` for assistant, emerald bubble for
|
||||
user), rounded composer with paperclip attach menu, attachment chips
|
||||
(Enter to send, Shift+Enter for newline). With `readOnly` (archived chat)
|
||||
the composer and share button are replaced by an unarchive banner.
|
||||
the composer is replaced by an unarchive banner with a share button
|
||||
(archived chats stay shareable, but an archived public chat cannot go
|
||||
private again — `DELETE …/share` answers 403 until unarchived).
|
||||
- `dialogs.tsx` — `ConvEditDialog` (title + topic), `ShareModal` (public link,
|
||||
copy, unshare), `SettingsModal` (display name, avatar upload box with
|
||||
drag & drop, avatar URL, theme, plus an "Archived chats" category to
|
||||
view / unarchive / delete archived conversations).
|
||||
view / unarchive / delete / share archived conversations).
|
||||
- `AdminPanel.tsx` — No-KYC account management: create (key shown once),
|
||||
regenerate (revokes sessions), edit, delete with confirm, plus server-side
|
||||
search / sort (newest, oldest, A–Z, Z–A) / email filter / pagination.
|
||||
|
||||
@@ -625,6 +625,15 @@ export function Chat({
|
||||
<Archive size={17} />
|
||||
</span>
|
||||
<p className="min-w-0 flex-1 text-sm opacity-70">{t("archived.readonly")}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onShare}
|
||||
aria-label={t("sidebar.share")}
|
||||
title={t("sidebar.share")}
|
||||
className="grid h-10 w-10 shrink-0 place-items-center rounded-full transition hover:bg-stone-200/70 active:scale-95 dark:hover:bg-zinc-700"
|
||||
>
|
||||
<Share2 size={16} className="opacity-70" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onUnarchive}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Archive, ArchiveRestore, Eye, Fingerprint, Globe, FileText, ImagePlus, KeyRound, Link2, Mail, Mic, Palette, Pencil, Play, Plug, ScanText, Search, Settings2, ShieldCheck, Sparkles, Square, Tag, Trash2, Unplug, User as UserIcon, Volume2, X } from "lucide-react";
|
||||
import { Archive, ArchiveRestore, Eye, Fingerprint, Globe, FileText, ImagePlus, KeyRound, Link2, Mail, Mic, Palette, Pencil, Play, Plug, ScanText, Search, Settings2, Share2, ShieldCheck, Sparkles, Square, Tag, Trash2, Unplug, User as UserIcon, Volume2, X } from "lucide-react";
|
||||
import { api, type UserProvider, type VoicePreview } from "../lib/api.ts";
|
||||
import { pushToast } from "../lib/toasts.ts";
|
||||
import { cn } from "../lib/cn.ts";
|
||||
@@ -229,11 +229,18 @@ export function ShareModal({ conv, onClose }: { conv: Conversation | null; onClo
|
||||
|
||||
async function unshare() {
|
||||
if (!conv) return;
|
||||
await api.unshare(conv.id);
|
||||
pushToast(t("toast.unshared"), { icon: "unshare" });
|
||||
onClose();
|
||||
setError("");
|
||||
try {
|
||||
await api.unshare(conv.id);
|
||||
pushToast(t("toast.unshared"), { icon: "unshare" });
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("dlg.reqFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
const archived = (conv?.archived_at ?? 0) !== 0;
|
||||
|
||||
return (
|
||||
<Modal open={conv !== null} onClose={onClose} title={t("dlg.shareTitle")} icon={Globe}>
|
||||
{busy && <p className="flex items-center gap-2 text-sm opacity-70"><Spinner size={15} /> {t("dlg.shareCreating")}</p>}
|
||||
@@ -246,8 +253,13 @@ export function ShareModal({ conv, onClose }: { conv: Conversation | null; onClo
|
||||
<span className="min-w-0 flex-1 truncate">{url}</span>
|
||||
<CopyButton text={url} />
|
||||
</div>
|
||||
{archived && <p className="text-xs opacity-60">{t("dlg.unshareBlocked")}</p>}
|
||||
<div className="flex justify-between">
|
||||
<Button variant="ghost" size="sm" onClick={unshare}><Unplug size={15} /> {t("dlg.unshare")}</Button>
|
||||
{!archived ? (
|
||||
<Button variant="ghost" size="sm" onClick={unshare}><Unplug size={15} /> {t("dlg.unshare")}</Button>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<Button size="sm" onClick={onClose}>{t("common.done")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -651,6 +663,7 @@ function ArchivedSection({ onView, onChanged }: { onView: (c: Conversation) => v
|
||||
const [error, setError] = useState("");
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<Conversation | null>(null);
|
||||
const [shareTarget, setShareTarget] = useState<Conversation | null>(null);
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
@@ -738,6 +751,16 @@ function ArchivedSection({ onView, onChanged }: { onView: (c: Conversation) => v
|
||||
>
|
||||
<ArchiveRestore size={15} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title={t("sidebar.share")}
|
||||
aria-label={`${t("sidebar.share")}: ${c.title}`}
|
||||
disabled={busyId === c.id}
|
||||
onClick={() => setShareTarget(c)}
|
||||
className="rounded-full p-2 transition hover:bg-stone-100 disabled:opacity-50 dark:hover:bg-zinc-800"
|
||||
>
|
||||
<Share2 size={15} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title={t("common.delete")}
|
||||
@@ -753,6 +776,7 @@ function ArchivedSection({ onView, onChanged }: { onView: (c: Conversation) => v
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<ShareModal conv={shareTarget} onClose={() => setShareTarget(null)} />
|
||||
<ConfirmDialog
|
||||
open={deleteTarget !== null}
|
||||
onClose={() => setDeleteTarget(null)}
|
||||
|
||||
@@ -160,6 +160,7 @@ const en = {
|
||||
"dlg.shareIntro":
|
||||
"Anyone with this link can read {title}. No login required.",
|
||||
"dlg.unshare": "Unshare",
|
||||
"dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
|
||||
"dlg.reviewOcr": "Review transcription",
|
||||
"dlg.reviewText": "Review text attachment",
|
||||
"dlg.transcribing": "Transcribing the image on the server (OCR)…",
|
||||
@@ -881,6 +882,7 @@ const es: Dict = {
|
||||
"dlg.shareCreating": "Creando enlace público…",
|
||||
"dlg.shareIntro": "Cualquiera con este enlace puede leer {title}. Sin login.",
|
||||
"dlg.unshare": "Dejar de compartir",
|
||||
"dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
|
||||
"dlg.reviewOcr": "Revisar transcripción",
|
||||
"dlg.reviewText": "Revisar adjunto de texto",
|
||||
"dlg.transcribing": "Transcribiendo la imagen en el servidor (OCR)…",
|
||||
@@ -1598,6 +1600,7 @@ const fr: Dict = {
|
||||
"dlg.shareCreating": "Création du lien public…",
|
||||
"dlg.shareIntro": "Quiconque a ce lien peut lire {title}. Sans connexion.",
|
||||
"dlg.unshare": "Ne plus partager",
|
||||
"dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
|
||||
"dlg.reviewOcr": "Relire la transcription",
|
||||
"dlg.reviewText": "Relire la pièce jointe",
|
||||
"dlg.transcribing": "Transcription de l'image sur le serveur (OCR)…",
|
||||
@@ -2309,6 +2312,7 @@ const ru: Dict = {
|
||||
"dlg.shareCreating": "Создаём публичную ссылку…",
|
||||
"dlg.shareIntro": "Каждый с этой ссылкой прочитает {title}. Вход не нужен.",
|
||||
"dlg.unshare": "Закрыть доступ",
|
||||
"dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
|
||||
"dlg.reviewOcr": "Проверить распознавание",
|
||||
"dlg.reviewText": "Проверить вложение",
|
||||
"dlg.transcribing": "Распознаём изображение на сервере (OCR)…",
|
||||
@@ -3011,6 +3015,7 @@ const it: Dict = {
|
||||
"dlg.shareIntro":
|
||||
"Chiunque abbia questo link può leggere {title}. Senza login.",
|
||||
"dlg.unshare": "Rimuovi condivisione",
|
||||
"dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
|
||||
"dlg.reviewOcr": "Rivedi trascrizione",
|
||||
"dlg.reviewText": "Rivedi allegato",
|
||||
"dlg.transcribing": "Trascrizione immagine sul server (OCR)…",
|
||||
@@ -3717,6 +3722,7 @@ const pt: Dict = {
|
||||
"dlg.shareCreating": "A criar link público…",
|
||||
"dlg.shareIntro": "Quem tiver este link pode ler {title}. Sem login.",
|
||||
"dlg.unshare": "Remover partilha",
|
||||
"dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
|
||||
"dlg.reviewOcr": "Rever transcrição",
|
||||
"dlg.reviewText": "Rever anexo",
|
||||
"dlg.transcribing": "A transcrever imagem no servidor (OCR)…",
|
||||
|
||||
@@ -1751,6 +1751,8 @@ const server = Bun.serve<{ ticketId: string | null; isAdmin: boolean }>({
|
||||
return publicId ? json({ publicId, url: `/share/${publicId}` }) : json({ shared: false });
|
||||
}
|
||||
if (req.method === "DELETE") {
|
||||
// Archived public chats stay public: unarchive first to go private.
|
||||
if (conv.archived_at) return json({ error: "conversation is archived (read-only) — unarchive it to remove the public link" }, 403);
|
||||
await deleteShare(conv.id);
|
||||
return json({ ok: true });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user