feat(frontend): feed of deleted and edited messages above the chat list and per chat in the changes panel
This commit is contained in:
1 parent
010fa29ce0
commit
9f16e2aa56
23 files changed
+1915
-71
No files matched your search
@@ -0,0 +1,216 @@
|
||||
<script lang="ts">
|
||||
import { ripple } from "#lib/actions/ripple.js";
|
||||
import type { FeedItem } from "#lib/api/types.js";
|
||||
import Icon from "#lib/components/ui/Icon.svelte";
|
||||
import { formatListDate } from "#lib/format/datetime.js";
|
||||
import { mediaKindLabel } from "#lib/format/media.js";
|
||||
import { locale, t } from "#lib/i18n/locale.svelte.js";
|
||||
import { accounts } from "#lib/stores/accounts.svelte.js";
|
||||
import { feed } from "#lib/stores/feed.svelte.js";
|
||||
import { page } from "$app/state";
|
||||
|
||||
const BADGE_LIMIT = 10_000;
|
||||
|
||||
const latest = $derived(feed.unseen?.latest ?? null);
|
||||
const count = $derived(feed.unseen?.count ?? 0);
|
||||
const badge = $derived(
|
||||
count < BADGE_LIMIT
|
||||
? String(count)
|
||||
: new Intl.NumberFormat(locale.tag, { notation: "compact" }).format(count)
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (accounts.selectedId !== null) {
|
||||
feed.sync();
|
||||
}
|
||||
});
|
||||
|
||||
function chatTitle(item: FeedItem): string {
|
||||
return (
|
||||
item.chat.title ??
|
||||
(item.chat.chat_id > 0
|
||||
? t("chat.deletedAccount")
|
||||
: t("chat.fallbackTitle", { id: item.chat.chat_id }))
|
||||
);
|
||||
}
|
||||
|
||||
function summary(item: FeedItem): string {
|
||||
if (item.kind === "deleted" && item.message_count > 1) {
|
||||
return t("feed.bulk", { count: item.message_count });
|
||||
}
|
||||
const text = item.kind === "edited" ? item.after : item.message?.text;
|
||||
const media = item.message?.media[0]?.kind ?? null;
|
||||
return text?.trim() || mediaKindLabel(media) || t("feed.noText");
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="feed-row"
|
||||
onclick={() => feed.open(page.url)}
|
||||
type="button"
|
||||
use:ripple
|
||||
>
|
||||
<span class="avatar"><Icon name="recent" /></span>
|
||||
<span class="info">
|
||||
<span class="info-row">
|
||||
<span class="title">{t("feed.title")}</span>
|
||||
{#if latest}
|
||||
<span class="date">{formatListDate(latest.at)}</span>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="subtitle">
|
||||
<span class="preview">
|
||||
{#if latest}
|
||||
<Icon
|
||||
class="kind"
|
||||
name={latest.kind === "deleted" ? "delete" : "edit"}
|
||||
size="0.875rem"
|
||||
/>
|
||||
<span class="chat">{chatTitle(latest)}:</span>
|
||||
<span
|
||||
class:struck={latest.kind === "deleted" &&
|
||||
latest.message_count === 1}
|
||||
>{summary(latest)}</span
|
||||
>
|
||||
{:else}
|
||||
{t("feed.empty.all")}
|
||||
{/if}
|
||||
</span>
|
||||
{#if count > 0}
|
||||
<span class="badge">{badge}</span>
|
||||
{/if}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<style lang="scss">
|
||||
.feed-row {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
gap: 0.625rem;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
height: 4.5rem;
|
||||
padding: 0.5625rem 0.5rem;
|
||||
border: 0;
|
||||
border-radius: 0.625rem;
|
||||
|
||||
font: inherit;
|
||||
text-align: start;
|
||||
color: var(--color-text);
|
||||
|
||||
background-color: transparent;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 3.375rem;
|
||||
height: 3.375rem;
|
||||
border-radius: 50%;
|
||||
|
||||
font-size: 1.75rem;
|
||||
color: var(--color-white);
|
||||
|
||||
background-color: var(--color-gray);
|
||||
}
|
||||
|
||||
.info {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
|
||||
font-size: 1rem;
|
||||
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);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.preview {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
:global(.kind) {
|
||||
margin-inline-end: 0.125rem;
|
||||
vertical-align: -0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.chat {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.struck {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.badge {
|
||||
flex-shrink: 0;
|
||||
|
||||
min-width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0 0.4375rem;
|
||||
border-radius: 0.75rem;
|
||||
|
||||
font-size: 0.875rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.5rem;
|
||||
color: var(--color-white);
|
||||
text-align: center;
|
||||
|
||||
background-color: var(--color-gray);
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user