feat(frontend): notes list with tag chips and the tag in the url, from the menu and settings, capture status dot on the menu and a line in settings
This commit is contained in:
1 parent
171d8d922d
commit
9c556ff8b9
8 files changed
+596
-9
No files matched your search
@@ -5,10 +5,12 @@
|
|||||||
import AddAccountDialog from "#lib/components/settings/AddAccountDialog.svelte";
|
import AddAccountDialog from "#lib/components/settings/AddAccountDialog.svelte";
|
||||||
import Button from "#lib/components/ui/Button.svelte";
|
import Button from "#lib/components/ui/Button.svelte";
|
||||||
import Icon from "#lib/components/ui/Icon.svelte";
|
import Icon from "#lib/components/ui/Icon.svelte";
|
||||||
|
import { captureLine } from "#lib/format/capture.js";
|
||||||
import { accountName } from "#lib/format/peer.js";
|
import { accountName } from "#lib/format/peer.js";
|
||||||
import { t } from "#lib/i18n/locale.svelte.js";
|
import { t } from "#lib/i18n/locale.svelte.js";
|
||||||
import type { MessageKey } from "#lib/i18n/messages.js";
|
import type { MessageKey } from "#lib/i18n/messages.js";
|
||||||
import { accounts } from "#lib/stores/accounts.svelte.js";
|
import { accounts } from "#lib/stores/accounts.svelte.js";
|
||||||
|
import { capture } from "#lib/stores/capture.svelte.js";
|
||||||
import { theme } from "#lib/stores/theme.svelte.js";
|
import { theme } from "#lib/stores/theme.svelte.js";
|
||||||
import { type RightPanel, ui } from "#lib/stores/ui.svelte.js";
|
import { type RightPanel, ui } from "#lib/stores/ui.svelte.js";
|
||||||
|
|
||||||
@@ -23,6 +25,7 @@
|
|||||||
const entries: Entry[] = [
|
const entries: Entry[] = [
|
||||||
{ icon: "eye", label: "settings.watches", panel: "watches" },
|
{ icon: "eye", label: "settings.watches", panel: "watches" },
|
||||||
{ icon: "unmute", label: "settings.alerts", panel: "alerts" },
|
{ icon: "unmute", label: "settings.alerts", panel: "alerts" },
|
||||||
|
{ icon: "note", label: "notes.title", panel: "notes" },
|
||||||
{ icon: "data", label: "settings.storage", panel: "storage" },
|
{ icon: "data", label: "settings.storage", panel: "storage" },
|
||||||
{ icon: "play-story", label: "settings.stories", panel: "stories-all" },
|
{ icon: "play-story", label: "settings.stories", panel: "stories-all" },
|
||||||
{ icon: "cloud-download", label: "settings.jobs", panel: "jobs" },
|
{ icon: "cloud-download", label: "settings.jobs", panel: "jobs" },
|
||||||
@@ -31,6 +34,12 @@
|
|||||||
|
|
||||||
let adding = $state(false);
|
let adding = $state(false);
|
||||||
|
|
||||||
|
const quiet = $derived(
|
||||||
|
capture.status !== null && !capture.status.live
|
||||||
|
? captureLine(capture.status, accounts.selected?.is_bot ?? false)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
function onkeydown(event: KeyboardEvent) {
|
function onkeydown(event: KeyboardEvent) {
|
||||||
const match = DIGIT.exec(event.code);
|
const match = DIGIT.exec(event.code);
|
||||||
if (!(event.altKey && match)) {
|
if (!(event.altKey && match)) {
|
||||||
@@ -54,14 +63,18 @@
|
|||||||
<span class="menu-trigger">
|
<span class="menu-trigger">
|
||||||
<Button
|
<Button
|
||||||
{...props}
|
{...props}
|
||||||
aria-label={t("menu.open")}
|
aria-label={quiet ? `${t("menu.open")}. ${quiet}` : t("menu.open")}
|
||||||
round
|
round
|
||||||
smaller
|
smaller
|
||||||
|
title={quiet ?? undefined}
|
||||||
variant="translucent"
|
variant="translucent"
|
||||||
>
|
>
|
||||||
<Icon name="menu" />
|
<Icon name="menu" />
|
||||||
</Button>
|
</Button>
|
||||||
<AlertBadge />
|
<AlertBadge />
|
||||||
|
{#if quiet}
|
||||||
|
<span class="capture-dot"></span>
|
||||||
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
@@ -142,6 +155,21 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.capture-dot {
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
right: 0.125rem;
|
||||||
|
bottom: 0.125rem;
|
||||||
|
|
||||||
|
width: 0.625rem;
|
||||||
|
height: 0.625rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
background-color: var(--color-error);
|
||||||
|
box-shadow: 0 0 0 2px var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
:global(.main-menu) {
|
:global(.main-menu) {
|
||||||
width: 17rem;
|
width: 17rem;
|
||||||
max-height: calc(100dvh - 5rem);
|
max-height: calc(100dvh - 5rem);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import AlertsPanel from "#lib/components/alerts/AlertsPanel.svelte";
|
import AlertsPanel from "#lib/components/alerts/AlertsPanel.svelte";
|
||||||
import AnnotationsPanel from "#lib/components/annotations/AnnotationsPanel.svelte";
|
import AnnotationsPanel from "#lib/components/annotations/AnnotationsPanel.svelte";
|
||||||
|
import NotesPanel from "#lib/components/annotations/NotesPanel.svelte";
|
||||||
import BotsPanel from "#lib/components/bots/BotsPanel.svelte";
|
import BotsPanel from "#lib/components/bots/BotsPanel.svelte";
|
||||||
import StoragePanel from "#lib/components/disk/StoragePanel.svelte";
|
import StoragePanel from "#lib/components/disk/StoragePanel.svelte";
|
||||||
import TrashPanel from "#lib/components/disk/TrashPanel.svelte";
|
import TrashPanel from "#lib/components/disk/TrashPanel.svelte";
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
import type { MessageKey } from "#lib/i18n/messages.js";
|
import type { MessageKey } from "#lib/i18n/messages.js";
|
||||||
import { accounts } from "#lib/stores/accounts.svelte.js";
|
import { accounts } from "#lib/stores/accounts.svelte.js";
|
||||||
import { chats } from "#lib/stores/chats.svelte.js";
|
import { chats } from "#lib/stores/chats.svelte.js";
|
||||||
|
import { notes } from "#lib/stores/notes.svelte.js";
|
||||||
import { policyUi } from "#lib/stores/policy.svelte.js";
|
import { policyUi } from "#lib/stores/policy.svelte.js";
|
||||||
import { storageUi } from "#lib/stores/storage.svelte.js";
|
import { storageUi } from "#lib/stores/storage.svelte.js";
|
||||||
import { type RightPanel, ui } from "#lib/stores/ui.svelte.js";
|
import { type RightPanel, ui } from "#lib/stores/ui.svelte.js";
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
changes: "feed.changes",
|
changes: "feed.changes",
|
||||||
jobs: "settings.jobs",
|
jobs: "settings.jobs",
|
||||||
links: "panel.links",
|
links: "panel.links",
|
||||||
|
notes: "notes.title",
|
||||||
policy: "settings.policy",
|
policy: "settings.policy",
|
||||||
presence: "chat.analytics",
|
presence: "chat.analytics",
|
||||||
profile: "chat.profile",
|
profile: "chat.profile",
|
||||||
@@ -49,7 +52,6 @@
|
|||||||
"storage-trash": "panel.trash",
|
"storage-trash": "panel.trash",
|
||||||
stories: "settings.stories",
|
stories: "settings.stories",
|
||||||
"stories-all": "panel.allStories",
|
"stories-all": "panel.allStories",
|
||||||
versions: "panel.versions",
|
|
||||||
watches: "settings.watches",
|
watches: "settings.watches",
|
||||||
webhooks: "watches.webhooks",
|
webhooks: "watches.webhooks",
|
||||||
};
|
};
|
||||||
@@ -65,6 +67,9 @@
|
|||||||
if (ui.rightPanel === null) {
|
if (ui.rightPanel === null) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
if (ui.rightPanel === "notes" && notes.tag !== null) {
|
||||||
|
return t("notes.tag", { tag: notes.tag });
|
||||||
|
}
|
||||||
const chat =
|
const chat =
|
||||||
ui.rightPanel === "storage-chat" && storageUi.chatId !== null
|
ui.rightPanel === "storage-chat" && storageUi.chatId !== null
|
||||||
? chats.byId(storageUi.chatId)
|
? chats.byId(storageUi.chatId)
|
||||||
@@ -139,6 +144,10 @@
|
|||||||
<AlertsPanel />
|
<AlertsPanel />
|
||||||
{:else if ui.rightPanel === "annotations"}
|
{:else if ui.rightPanel === "annotations"}
|
||||||
<AnnotationsPanel />
|
<AnnotationsPanel />
|
||||||
|
{:else if ui.rightPanel === "notes"}
|
||||||
|
{#key accounts.selectedId}
|
||||||
|
<NotesPanel />
|
||||||
|
{/key}
|
||||||
{:else if ui.rightPanel === "shares"}
|
{:else if ui.rightPanel === "shares"}
|
||||||
<SharesPanel />
|
<SharesPanel />
|
||||||
{:else if ui.rightPanel === "bots"}
|
{:else if ui.rightPanel === "bots"}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { t } from "#lib/i18n/locale.svelte.js";
|
import { t } from "#lib/i18n/locale.svelte.js";
|
||||||
import Icon from "../ui/Icon.svelte";
|
import Icon from "../ui/Icon.svelte";
|
||||||
import AnnotationEditor from "./AnnotationEditor.svelte";
|
import AnnotationEditor from "./AnnotationEditor.svelte";
|
||||||
|
import NoteText from "./NoteText.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
annotations: Annotation[];
|
annotations: Annotation[];
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
submitLabel={t("common.save")}
|
submitLabel={t("common.save")}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text">{annotation.text}</p>
|
<NoteText text={annotation.text} />
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span class="time">{formatFull(annotation.updated_at)}</span>
|
<span class="time">{formatFull(annotation.updated_at)}</span>
|
||||||
<button
|
<button
|
||||||
@@ -75,12 +76,6 @@
|
|||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
|
||||||
margin: 0;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta {
|
.meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
import Spinner from "#lib/components/ui/Spinner.svelte";
|
import Spinner from "#lib/components/ui/Spinner.svelte";
|
||||||
import { t } from "#lib/i18n/locale.svelte.js";
|
import { t } from "#lib/i18n/locale.svelte.js";
|
||||||
import { accounts } from "#lib/stores/accounts.svelte.js";
|
import { accounts } from "#lib/stores/accounts.svelte.js";
|
||||||
|
import { notes } from "#lib/stores/notes.svelte.js";
|
||||||
import { ui } from "#lib/stores/ui.svelte.js";
|
import { ui } from "#lib/stores/ui.svelte.js";
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
import AnnotationEditor from "./AnnotationEditor.svelte";
|
import AnnotationEditor from "./AnnotationEditor.svelte";
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
.then((rows) => {
|
.then((rows) => {
|
||||||
if (current === token) {
|
if (current === token) {
|
||||||
items = rows;
|
items = rows;
|
||||||
|
notes.mark(cid, mid, rows.length > 0);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -59,6 +61,7 @@
|
|||||||
try {
|
try {
|
||||||
const created = await createAnnotation(chatId, messageId, text);
|
const created = await createAnnotation(chatId, messageId, text);
|
||||||
items = [created, ...items];
|
items = [created, ...items];
|
||||||
|
notes.mark(chatId, messageId, true);
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
@@ -72,6 +75,7 @@
|
|||||||
async function remove(annotation: Annotation) {
|
async function remove(annotation: Annotation) {
|
||||||
await deleteAnnotation(annotation.id);
|
await deleteAnnotation(annotation.id);
|
||||||
items = items.filter((item) => item.id !== annotation.id);
|
items = items.filter((item) => item.id !== annotation.id);
|
||||||
|
notes.mark(annotation.chat_id, annotation.message_id, items.length > 0);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { noteParts, notes } from "#lib/stores/notes.svelte.js";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
interactive?: boolean;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { interactive = true, text }: Props = $props();
|
||||||
|
|
||||||
|
const parts = $derived(noteParts(text));
|
||||||
|
|
||||||
|
function openTag(event: MouseEvent, tag: string) {
|
||||||
|
event.stopPropagation();
|
||||||
|
notes.openTag(tag);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p class="note-text">
|
||||||
|
{#each parts as part, index (index)}
|
||||||
|
{#if "tag" in part && interactive}
|
||||||
|
<button
|
||||||
|
class="tag"
|
||||||
|
onclick={(event) => openTag(event, part.tag)}
|
||||||
|
type="button"
|
||||||
|
class:active={notes.tag === part.tag}
|
||||||
|
>
|
||||||
|
{part.label}
|
||||||
|
</button>
|
||||||
|
{:else if "tag" in part}
|
||||||
|
<span class="tag" class:active={notes.tag === part.tag}
|
||||||
|
>{part.label}</span
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
{part.text}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.note-text {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: inline;
|
||||||
|
|
||||||
|
padding: 0 0.3125rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.875em;
|
||||||
|
color: var(--color-primary);
|
||||||
|
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
box-shadow: inset 0 0 0 1px var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
&:is(button):hover {
|
||||||
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--color-primary) 20%,
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:is(button) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,402 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, untrack } from "svelte";
|
||||||
|
import { ripple } from "#lib/actions/ripple.js";
|
||||||
|
import {
|
||||||
|
listNotes,
|
||||||
|
listNoteTags,
|
||||||
|
type Note,
|
||||||
|
type NoteTag,
|
||||||
|
} from "#lib/api/notes.js";
|
||||||
|
import NoteText from "#lib/components/annotations/NoteText.svelte";
|
||||||
|
import Avatar from "#lib/components/ui/Avatar.svelte";
|
||||||
|
import Button from "#lib/components/ui/Button.svelte";
|
||||||
|
import EmptyState from "#lib/components/ui/EmptyState.svelte";
|
||||||
|
import Spinner from "#lib/components/ui/Spinner.svelte";
|
||||||
|
import { formatListDate } from "#lib/format/datetime.js";
|
||||||
|
import { t } from "#lib/i18n/locale.svelte.js";
|
||||||
|
import { replaceParam } from "#lib/params.js";
|
||||||
|
import { NOTES_PARAM, notes } from "#lib/stores/notes.svelte.js";
|
||||||
|
import { ui } from "#lib/stores/ui.svelte.js";
|
||||||
|
import { isMobile } from "#lib/viewport.js";
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
|
||||||
|
const PAGE = 50;
|
||||||
|
|
||||||
|
let items = $state<Note[]>([]);
|
||||||
|
let tags = $state<NoteTag[]>([]);
|
||||||
|
let loading = $state(true);
|
||||||
|
let loadingMore = $state(false);
|
||||||
|
let hasMore = $state(false);
|
||||||
|
let failed = $state(false);
|
||||||
|
let token = 0;
|
||||||
|
|
||||||
|
const chips = $derived.by(() => {
|
||||||
|
const current = notes.tag;
|
||||||
|
if (current === null || tags.some((item) => item.tag === current)) {
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
return [{ count: 0, tag: current }, ...tags];
|
||||||
|
});
|
||||||
|
|
||||||
|
async function load(tag: string | null, offset = 0) {
|
||||||
|
token += 1;
|
||||||
|
const mine = token;
|
||||||
|
if (offset === 0) {
|
||||||
|
loading = true;
|
||||||
|
} else {
|
||||||
|
loadingMore = true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const page = await listNotes(tag, offset, PAGE);
|
||||||
|
if (mine !== token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
items = offset === 0 ? page : [...items, ...page];
|
||||||
|
hasMore = page.length === PAGE;
|
||||||
|
failed = false;
|
||||||
|
} catch {
|
||||||
|
if (mine === token) {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mine === token) {
|
||||||
|
loading = false;
|
||||||
|
loadingMore = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const { tag } = notes;
|
||||||
|
untrack(() => {
|
||||||
|
replaceParam(NOTES_PARAM, tag ?? "");
|
||||||
|
load(tag);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
listNoteTags()
|
||||||
|
.then((result) => {
|
||||||
|
tags = result;
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
return () => {
|
||||||
|
notes.tag = null;
|
||||||
|
replaceParam(NOTES_PARAM, null);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function pick(tag: string | null) {
|
||||||
|
notes.tag = notes.tag === tag ? null : tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
function href(note: Note): string {
|
||||||
|
return `/app/${note.chat_id}?message=${note.message_id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(event: MouseEvent, note: Note) {
|
||||||
|
if (
|
||||||
|
event.metaKey ||
|
||||||
|
event.ctrlKey ||
|
||||||
|
event.shiftKey ||
|
||||||
|
event.button !== 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
ui.requestJump(note.chat_id, note.message_id);
|
||||||
|
if (isMobile()) {
|
||||||
|
ui.resetPanel();
|
||||||
|
goto(href(note));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tag = notes.tag === null ? "" : encodeURIComponent(notes.tag);
|
||||||
|
goto(`${href(note)}&${NOTES_PARAM}=${tag}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function chatTitle(note: Note): string {
|
||||||
|
return note.chat_title ?? t("chat.title", { id: note.chat_id });
|
||||||
|
}
|
||||||
|
|
||||||
|
function quote(note: Note): string {
|
||||||
|
if (note.message_date === null) {
|
||||||
|
return t("notes.messageGone");
|
||||||
|
}
|
||||||
|
return note.message_text ?? t("notes.mediaMessage");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="notes">
|
||||||
|
{#if chips.length > 0}
|
||||||
|
<div class="chips">
|
||||||
|
<button
|
||||||
|
aria-pressed={notes.tag === null}
|
||||||
|
class="chip"
|
||||||
|
onclick={() => pick(null)}
|
||||||
|
type="button"
|
||||||
|
class:on={notes.tag === null}
|
||||||
|
>
|
||||||
|
{t("notes.all")}
|
||||||
|
</button>
|
||||||
|
{#each chips as item (item.tag)}
|
||||||
|
<button
|
||||||
|
aria-pressed={notes.tag === item.tag}
|
||||||
|
class="chip"
|
||||||
|
onclick={() => pick(item.tag)}
|
||||||
|
type="button"
|
||||||
|
class:on={notes.tag === item.tag}
|
||||||
|
>
|
||||||
|
#{item.tag}
|
||||||
|
{#if item.count > 0}
|
||||||
|
<span class="value">{item.count}</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if loading && items.length === 0}
|
||||||
|
<div class="center"><Spinner /></div>
|
||||||
|
{:else if failed && items.length === 0}
|
||||||
|
<div class="state">
|
||||||
|
<p>{t("notes.failed")}</p>
|
||||||
|
<Button onclick={() => load(notes.tag)} variant="text">
|
||||||
|
{t("notes.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{:else if items.length === 0}
|
||||||
|
<EmptyState
|
||||||
|
description={notes.tag === null ? t("notes.empty") : undefined}
|
||||||
|
title={notes.tag === null
|
||||||
|
? t("notes.emptyTitle")
|
||||||
|
: t("notes.emptyTag", { tag: notes.tag })}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<ul class="list">
|
||||||
|
{#each items as note (note.id)}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="row"
|
||||||
|
href={href(note)}
|
||||||
|
onclick={(event) => open(event, note)}
|
||||||
|
use:ripple
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
avatar={{
|
||||||
|
id: note.chat_id,
|
||||||
|
kind: note.chat_id > 0 ? "peer" : "chat",
|
||||||
|
}}
|
||||||
|
colorKey={note.chat_id}
|
||||||
|
hasAvatar={note.chat_has_avatar}
|
||||||
|
name={chatTitle(note)}
|
||||||
|
size={2.75}
|
||||||
|
/>
|
||||||
|
<div class="body">
|
||||||
|
<div class="top">
|
||||||
|
<span class="title">{chatTitle(note)}</span>
|
||||||
|
<span class="date">{formatListDate(note.created_at)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text">
|
||||||
|
<NoteText interactive={false} text={note.text} />
|
||||||
|
</div>
|
||||||
|
<div class="quote" class:deleted={note.message_deleted}>
|
||||||
|
{quote(note)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{#if hasMore}
|
||||||
|
<div class="more">
|
||||||
|
<Button
|
||||||
|
loading={loadingMore}
|
||||||
|
onclick={() => load(notes.tag, items.length)}
|
||||||
|
variant="text"
|
||||||
|
>
|
||||||
|
{t("notes.more")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.notes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.375rem;
|
||||||
|
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--color-borders);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 0.375rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
height: 1.75rem;
|
||||||
|
padding: 0 0.5625rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0.875rem;
|
||||||
|
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--color-text);
|
||||||
|
|
||||||
|
background-color: var(--color-chat-hover);
|
||||||
|
|
||||||
|
transition:
|
||||||
|
background-color 0.15s ease-out,
|
||||||
|
box-shadow 0.15s ease-out;
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--color-primary) 12%,
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
height: 2.25rem;
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
border-radius: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
padding: 0.5rem 0.625rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
|
||||||
|
color: var(--color-text);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
|
--ripple-color: var(--color-interactive-element-hover);
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-chat-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin-top: 0.125rem;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
margin-top: 0.375rem;
|
||||||
|
padding-inline-start: 0.5rem;
|
||||||
|
border-inline-start: 2px solid var(--color-primary);
|
||||||
|
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.deleted {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.more {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { captureLine } from "#lib/format/capture.js";
|
||||||
|
import { accounts } from "#lib/stores/accounts.svelte.js";
|
||||||
|
import { capture } from "#lib/stores/capture.svelte.js";
|
||||||
|
|
||||||
|
const status = $derived(capture.status);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if status}
|
||||||
|
<div class="capture-line" role="status" class:quiet={!status.live}>
|
||||||
|
<span class="mark"><span class="dot"></span></span>
|
||||||
|
<span class="label"
|
||||||
|
>{captureLine(status, accounts.selected?.is_bot ?? false)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.capture-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
|
||||||
|
min-height: 3.5rem;
|
||||||
|
padding: 0 1.25rem;
|
||||||
|
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mark {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 0.625rem;
|
||||||
|
height: 0.625rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--color-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiet .dot {
|
||||||
|
background-color: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import AlertBadge from "#lib/components/alerts/AlertBadge.svelte";
|
import AlertBadge from "#lib/components/alerts/AlertBadge.svelte";
|
||||||
import About from "#lib/components/settings/About.svelte";
|
import About from "#lib/components/settings/About.svelte";
|
||||||
import Appearance from "#lib/components/settings/Appearance.svelte";
|
import Appearance from "#lib/components/settings/Appearance.svelte";
|
||||||
|
import CaptureLine from "#lib/components/settings/CaptureLine.svelte";
|
||||||
import Language from "#lib/components/settings/Language.svelte";
|
import Language from "#lib/components/settings/Language.svelte";
|
||||||
import MyAccount from "#lib/components/settings/MyAccount.svelte";
|
import MyAccount from "#lib/components/settings/MyAccount.svelte";
|
||||||
import SettingsItem from "#lib/components/settings/SettingsItem.svelte";
|
import SettingsItem from "#lib/components/settings/SettingsItem.svelte";
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
|
<CaptureLine />
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
icon="settings"
|
icon="settings"
|
||||||
label={t("settings.policy")}
|
label={t("settings.policy")}
|
||||||
@@ -83,6 +85,11 @@
|
|||||||
<AlertBadge inline />
|
<AlertBadge inline />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
<SettingsItem
|
||||||
|
icon="note"
|
||||||
|
label={t("notes.title")}
|
||||||
|
onclick={() => ui.openPanel("notes")}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<About />
|
<About />
|
||||||
|
|||||||
Reference in new issue
Block a user