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
|
share button), message list (`Markdown` for assistant, emerald bubble for
|
||||||
user), rounded composer with paperclip attach menu, attachment chips
|
user), rounded composer with paperclip attach menu, attachment chips
|
||||||
(Enter to send, Shift+Enter for newline). With `readOnly` (archived chat)
|
(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,
|
- `dialogs.tsx` — `ConvEditDialog` (title + topic), `ShareModal` (public link,
|
||||||
copy, unshare), `SettingsModal` (display name, avatar upload box with
|
copy, unshare), `SettingsModal` (display name, avatar upload box with
|
||||||
drag & drop, avatar URL, theme, plus an "Archived chats" category to
|
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),
|
- `AdminPanel.tsx` — No-KYC account management: create (key shown once),
|
||||||
regenerate (revokes sessions), edit, delete with confirm, plus server-side
|
regenerate (revokes sessions), edit, delete with confirm, plus server-side
|
||||||
search / sort (newest, oldest, A–Z, Z–A) / email filter / pagination.
|
search / sort (newest, oldest, A–Z, Z–A) / email filter / pagination.
|
||||||
|
|||||||
@@ -625,6 +625,15 @@ export function Chat({
|
|||||||
<Archive size={17} />
|
<Archive size={17} />
|
||||||
</span>
|
</span>
|
||||||
<p className="min-w-0 flex-1 text-sm opacity-70">{t("archived.readonly")}</p>
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onUnarchive}
|
onClick={onUnarchive}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
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 { api, type UserProvider, type VoicePreview } from "../lib/api.ts";
|
||||||
import { pushToast } from "../lib/toasts.ts";
|
import { pushToast } from "../lib/toasts.ts";
|
||||||
import { cn } from "../lib/cn.ts";
|
import { cn } from "../lib/cn.ts";
|
||||||
@@ -229,10 +229,17 @@ export function ShareModal({ conv, onClose }: { conv: Conversation | null; onClo
|
|||||||
|
|
||||||
async function unshare() {
|
async function unshare() {
|
||||||
if (!conv) return;
|
if (!conv) return;
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
await api.unshare(conv.id);
|
await api.unshare(conv.id);
|
||||||
pushToast(t("toast.unshared"), { icon: "unshare" });
|
pushToast(t("toast.unshared"), { icon: "unshare" });
|
||||||
onClose();
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : t("dlg.reqFailed"));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const archived = (conv?.archived_at ?? 0) !== 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={conv !== null} onClose={onClose} title={t("dlg.shareTitle")} icon={Globe}>
|
<Modal open={conv !== null} onClose={onClose} title={t("dlg.shareTitle")} icon={Globe}>
|
||||||
@@ -246,8 +253,13 @@ export function ShareModal({ conv, onClose }: { conv: Conversation | null; onClo
|
|||||||
<span className="min-w-0 flex-1 truncate">{url}</span>
|
<span className="min-w-0 flex-1 truncate">{url}</span>
|
||||||
<CopyButton text={url} />
|
<CopyButton text={url} />
|
||||||
</div>
|
</div>
|
||||||
|
{archived && <p className="text-xs opacity-60">{t("dlg.unshareBlocked")}</p>}
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
|
{!archived ? (
|
||||||
<Button variant="ghost" size="sm" onClick={unshare}><Unplug size={15} /> {t("dlg.unshare")}</Button>
|
<Button variant="ghost" size="sm" onClick={unshare}><Unplug size={15} /> {t("dlg.unshare")}</Button>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
<Button size="sm" onClick={onClose}>{t("common.done")}</Button>
|
<Button size="sm" onClick={onClose}>{t("common.done")}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -651,6 +663,7 @@ function ArchivedSection({ onView, onChanged }: { onView: (c: Conversation) => v
|
|||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [busyId, setBusyId] = useState<string | null>(null);
|
const [busyId, setBusyId] = useState<string | null>(null);
|
||||||
const [deleteTarget, setDeleteTarget] = useState<Conversation | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<Conversation | null>(null);
|
||||||
|
const [shareTarget, setShareTarget] = useState<Conversation | null>(null);
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
try {
|
try {
|
||||||
@@ -738,6 +751,16 @@ function ArchivedSection({ onView, onChanged }: { onView: (c: Conversation) => v
|
|||||||
>
|
>
|
||||||
<ArchiveRestore size={15} />
|
<ArchiveRestore size={15} />
|
||||||
</button>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title={t("common.delete")}
|
title={t("common.delete")}
|
||||||
@@ -753,6 +776,7 @@ function ArchivedSection({ onView, onChanged }: { onView: (c: Conversation) => v
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
<ShareModal conv={shareTarget} onClose={() => setShareTarget(null)} />
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={deleteTarget !== null}
|
open={deleteTarget !== null}
|
||||||
onClose={() => setDeleteTarget(null)}
|
onClose={() => setDeleteTarget(null)}
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ const en = {
|
|||||||
"dlg.shareIntro":
|
"dlg.shareIntro":
|
||||||
"Anyone with this link can read {title}. No login required.",
|
"Anyone with this link can read {title}. No login required.",
|
||||||
"dlg.unshare": "Unshare",
|
"dlg.unshare": "Unshare",
|
||||||
|
"dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
|
||||||
"dlg.reviewOcr": "Review transcription",
|
"dlg.reviewOcr": "Review transcription",
|
||||||
"dlg.reviewText": "Review text attachment",
|
"dlg.reviewText": "Review text attachment",
|
||||||
"dlg.transcribing": "Transcribing the image on the server (OCR)…",
|
"dlg.transcribing": "Transcribing the image on the server (OCR)…",
|
||||||
@@ -881,6 +882,7 @@ const es: Dict = {
|
|||||||
"dlg.shareCreating": "Creando enlace público…",
|
"dlg.shareCreating": "Creando enlace público…",
|
||||||
"dlg.shareIntro": "Cualquiera con este enlace puede leer {title}. Sin login.",
|
"dlg.shareIntro": "Cualquiera con este enlace puede leer {title}. Sin login.",
|
||||||
"dlg.unshare": "Dejar de compartir",
|
"dlg.unshare": "Dejar de compartir",
|
||||||
|
"dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
|
||||||
"dlg.reviewOcr": "Revisar transcripción",
|
"dlg.reviewOcr": "Revisar transcripción",
|
||||||
"dlg.reviewText": "Revisar adjunto de texto",
|
"dlg.reviewText": "Revisar adjunto de texto",
|
||||||
"dlg.transcribing": "Transcribiendo la imagen en el servidor (OCR)…",
|
"dlg.transcribing": "Transcribiendo la imagen en el servidor (OCR)…",
|
||||||
@@ -1598,6 +1600,7 @@ const fr: Dict = {
|
|||||||
"dlg.shareCreating": "Création du lien public…",
|
"dlg.shareCreating": "Création du lien public…",
|
||||||
"dlg.shareIntro": "Quiconque a ce lien peut lire {title}. Sans connexion.",
|
"dlg.shareIntro": "Quiconque a ce lien peut lire {title}. Sans connexion.",
|
||||||
"dlg.unshare": "Ne plus partager",
|
"dlg.unshare": "Ne plus partager",
|
||||||
|
"dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
|
||||||
"dlg.reviewOcr": "Relire la transcription",
|
"dlg.reviewOcr": "Relire la transcription",
|
||||||
"dlg.reviewText": "Relire la pièce jointe",
|
"dlg.reviewText": "Relire la pièce jointe",
|
||||||
"dlg.transcribing": "Transcription de l'image sur le serveur (OCR)…",
|
"dlg.transcribing": "Transcription de l'image sur le serveur (OCR)…",
|
||||||
@@ -2309,6 +2312,7 @@ const ru: Dict = {
|
|||||||
"dlg.shareCreating": "Создаём публичную ссылку…",
|
"dlg.shareCreating": "Создаём публичную ссылку…",
|
||||||
"dlg.shareIntro": "Каждый с этой ссылкой прочитает {title}. Вход не нужен.",
|
"dlg.shareIntro": "Каждый с этой ссылкой прочитает {title}. Вход не нужен.",
|
||||||
"dlg.unshare": "Закрыть доступ",
|
"dlg.unshare": "Закрыть доступ",
|
||||||
|
"dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
|
||||||
"dlg.reviewOcr": "Проверить распознавание",
|
"dlg.reviewOcr": "Проверить распознавание",
|
||||||
"dlg.reviewText": "Проверить вложение",
|
"dlg.reviewText": "Проверить вложение",
|
||||||
"dlg.transcribing": "Распознаём изображение на сервере (OCR)…",
|
"dlg.transcribing": "Распознаём изображение на сервере (OCR)…",
|
||||||
@@ -3011,6 +3015,7 @@ const it: Dict = {
|
|||||||
"dlg.shareIntro":
|
"dlg.shareIntro":
|
||||||
"Chiunque abbia questo link può leggere {title}. Senza login.",
|
"Chiunque abbia questo link può leggere {title}. Senza login.",
|
||||||
"dlg.unshare": "Rimuovi condivisione",
|
"dlg.unshare": "Rimuovi condivisione",
|
||||||
|
"dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
|
||||||
"dlg.reviewOcr": "Rivedi trascrizione",
|
"dlg.reviewOcr": "Rivedi trascrizione",
|
||||||
"dlg.reviewText": "Rivedi allegato",
|
"dlg.reviewText": "Rivedi allegato",
|
||||||
"dlg.transcribing": "Trascrizione immagine sul server (OCR)…",
|
"dlg.transcribing": "Trascrizione immagine sul server (OCR)…",
|
||||||
@@ -3717,6 +3722,7 @@ const pt: Dict = {
|
|||||||
"dlg.shareCreating": "A criar link público…",
|
"dlg.shareCreating": "A criar link público…",
|
||||||
"dlg.shareIntro": "Quem tiver este link pode ler {title}. Sem login.",
|
"dlg.shareIntro": "Quem tiver este link pode ler {title}. Sem login.",
|
||||||
"dlg.unshare": "Remover partilha",
|
"dlg.unshare": "Remover partilha",
|
||||||
|
"dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
|
||||||
"dlg.reviewOcr": "Rever transcrição",
|
"dlg.reviewOcr": "Rever transcrição",
|
||||||
"dlg.reviewText": "Rever anexo",
|
"dlg.reviewText": "Rever anexo",
|
||||||
"dlg.transcribing": "A transcrever imagem no servidor (OCR)…",
|
"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 });
|
return publicId ? json({ publicId, url: `/share/${publicId}` }) : json({ shared: false });
|
||||||
}
|
}
|
||||||
if (req.method === "DELETE") {
|
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);
|
await deleteShare(conv.id);
|
||||||
return json({ ok: true });
|
return json({ ok: true });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user