feat(frontend): profiles chip in the feed shows name, username, photo and title changes
This commit is contained in:
1 parent
8e0f828dd4
commit
98dda99c5c
9 files changed
+180
-13
No files matched your search
@@ -222,7 +222,7 @@ export interface MessageVersion {
|
|||||||
text: string | null;
|
text: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FeedKind = "all" | "deleted" | "edited";
|
export type FeedKind = "all" | "deleted" | "edited" | "profile";
|
||||||
export type FeedChatKind = "dm" | "group" | "channel";
|
export type FeedChatKind = "dm" | "group" | "channel";
|
||||||
|
|
||||||
export interface FeedChat {
|
export interface FeedChat {
|
||||||
@@ -237,6 +237,7 @@ export interface FeedItem {
|
|||||||
at: string;
|
at: string;
|
||||||
before: string | null;
|
before: string | null;
|
||||||
chat: FeedChat;
|
chat: FeedChat;
|
||||||
|
detail: Record<string, [string | null, string | null]> | null;
|
||||||
edit_date: string | null;
|
edit_date: string | null;
|
||||||
has_before: boolean;
|
has_before: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { FeedKind } from "#lib/api/types.js";
|
import type { FeedItem, FeedKind } from "#lib/api/types.js";
|
||||||
import FeedFilters from "#lib/components/feed/FeedFilters.svelte";
|
import FeedFilters from "#lib/components/feed/FeedFilters.svelte";
|
||||||
import FeedList from "#lib/components/feed/FeedList.svelte";
|
import FeedList from "#lib/components/feed/FeedList.svelte";
|
||||||
import { ui } from "#lib/stores/ui.svelte.js";
|
import { ui } from "#lib/stores/ui.svelte.js";
|
||||||
@@ -16,10 +16,14 @@
|
|||||||
kind = next;
|
kind = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
function open(messageId: number) {
|
function open(item: FeedItem, messageId: number) {
|
||||||
if (chatId === null) {
|
if (chatId === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (item.kind === "profile" && !messageId) {
|
||||||
|
ui.openPanel("profile");
|
||||||
|
return;
|
||||||
|
}
|
||||||
ui.requestJump(chatId, messageId);
|
ui.requestJump(chatId, messageId);
|
||||||
if (isMobile()) {
|
if (isMobile()) {
|
||||||
ui.closePanel();
|
ui.closePanel();
|
||||||
@@ -30,6 +34,6 @@
|
|||||||
{#if chatId !== null}
|
{#if chatId !== null}
|
||||||
<FeedFilters {kind} onkind={pick} />
|
<FeedFilters {kind} onkind={pick} />
|
||||||
{#key chatId}
|
{#key chatId}
|
||||||
<FeedList {chatId} {kind} onopen={(_, messageId) => open(messageId)} />
|
<FeedList {chatId} {kind} onopen={open} />
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ripple } from "#lib/actions/ripple.js";
|
import { ripple } from "#lib/actions/ripple.js";
|
||||||
import type { FeedItem, MessageView } from "#lib/api/types.js";
|
import type { FeedItem, MessageView } from "#lib/api/types.js";
|
||||||
import FeedBulk from "#lib/components/feed/FeedBulk.svelte";
|
import FeedBulk from "#lib/components/feed/FeedBulk.svelte";
|
||||||
|
import FeedPhoto from "#lib/components/feed/FeedPhoto.svelte";
|
||||||
import FeedText from "#lib/components/feed/FeedText.svelte";
|
import FeedText from "#lib/components/feed/FeedText.svelte";
|
||||||
import Avatar from "#lib/components/ui/Avatar.svelte";
|
import Avatar from "#lib/components/ui/Avatar.svelte";
|
||||||
import Icon from "#lib/components/ui/Icon.svelte";
|
import Icon from "#lib/components/ui/Icon.svelte";
|
||||||
@@ -20,6 +21,14 @@
|
|||||||
|
|
||||||
let { item, onopen, inChat = false }: Props = $props();
|
let { item, onopen, inChat = false }: Props = $props();
|
||||||
|
|
||||||
|
const FIELD_KEYS = {
|
||||||
|
name: "feed.field.name",
|
||||||
|
photo: "feed.field.photo",
|
||||||
|
title: "feed.field.title",
|
||||||
|
username: "feed.field.username",
|
||||||
|
} as const;
|
||||||
|
const FIELD_ORDER = ["name", "title", "username", "photo"] as const;
|
||||||
|
|
||||||
let expanded = $state(false);
|
let expanded = $state(false);
|
||||||
|
|
||||||
const ownId = $derived(accounts.selected?.tg_user_id ?? null);
|
const ownId = $derived(accounts.selected?.tg_user_id ?? null);
|
||||||
@@ -67,8 +76,14 @@
|
|||||||
? t("chat.deletedAccount")
|
? t("chat.deletedAccount")
|
||||||
: t("chat.fallbackTitle", { id: item.chat.chat_id }))
|
: t("chat.fallbackTitle", { id: item.chat.chat_id }))
|
||||||
);
|
);
|
||||||
|
const profile = $derived(item.kind === "profile");
|
||||||
|
const fields = $derived(
|
||||||
|
FIELD_ORDER.filter((field) => item.detail?.[field]).map(
|
||||||
|
(field) => [field, item.detail?.[field] ?? [null, null]] as const
|
||||||
|
)
|
||||||
|
);
|
||||||
const title = $derived.by(() => {
|
const title = $derived.by(() => {
|
||||||
if (!inChat) {
|
if (!inChat || profile) {
|
||||||
return chatTitle;
|
return chatTitle;
|
||||||
}
|
}
|
||||||
if (bulk) {
|
if (bulk) {
|
||||||
@@ -77,7 +92,7 @@
|
|||||||
return senderName(item.sender_id, item.sender_name);
|
return senderName(item.sender_id, item.sender_name);
|
||||||
});
|
});
|
||||||
const avatar = $derived.by(() => {
|
const avatar = $derived.by(() => {
|
||||||
if (!inChat) {
|
if (!inChat || profile) {
|
||||||
return {
|
return {
|
||||||
has: item.chat.has_avatar,
|
has: item.chat.has_avatar,
|
||||||
id: item.chat.chat_id,
|
id: item.chat.chat_id,
|
||||||
@@ -92,7 +107,7 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
const prefix = $derived.by(() => {
|
const prefix = $derived.by(() => {
|
||||||
if (inChat) {
|
if (inChat || profile) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
if (item.outgoing) {
|
if (item.outgoing) {
|
||||||
@@ -107,6 +122,13 @@
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function shown(field: string, value: string | null): string {
|
||||||
|
if (!value) {
|
||||||
|
return t("feed.noText");
|
||||||
|
}
|
||||||
|
return field === "username" ? `@${value}` : value;
|
||||||
|
}
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
if (bulk) {
|
if (bulk) {
|
||||||
expanded = !expanded;
|
expanded = !expanded;
|
||||||
@@ -142,7 +164,27 @@
|
|||||||
{formatListDate(item.at)}
|
{formatListDate(item.at)}
|
||||||
</time>
|
</time>
|
||||||
</span>
|
</span>
|
||||||
{#if bulk}
|
{#if profile}
|
||||||
|
{#each fields as [field, pair] (field)}
|
||||||
|
<span class="change now">
|
||||||
|
<span class="tag">{t(FIELD_KEYS[field])}</span>
|
||||||
|
{#if field === "photo"}
|
||||||
|
<FeedPhoto ownerId={item.chat.chat_id} uniqueId={pair[0]} />
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<FeedPhoto ownerId={item.chat.chat_id} uniqueId={pair[1]} />
|
||||||
|
{:else}
|
||||||
|
<span class="was">{shown(field, pair[0])}</span>
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
{shown(field, pair[1])}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
{#if item.message_count > fields.length}
|
||||||
|
<span class="count">
|
||||||
|
{t("feed.profileCount", { count: item.message_count })}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{:else if bulk}
|
||||||
<span class="mark">
|
<span class="mark">
|
||||||
<Icon name="delete" size="0.875rem" />
|
<Icon name="delete" size="0.875rem" />
|
||||||
{inChat
|
{inChat
|
||||||
@@ -360,4 +402,18 @@
|
|||||||
.missing {
|
.missing {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.was {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
margin-inline: 0.25rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
all: "feed.filter.all",
|
all: "feed.filter.all",
|
||||||
deleted: "feed.filter.deleted",
|
deleted: "feed.filter.deleted",
|
||||||
edited: "feed.filter.edited",
|
edited: "feed.filter.edited",
|
||||||
|
profile: "feed.filter.profile",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const CHAT_KEYS = {
|
const CHAT_KEYS = {
|
||||||
@@ -69,7 +70,7 @@
|
|||||||
|
|
||||||
.group {
|
.group {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
all: "feed.empty.all",
|
all: "feed.empty.all",
|
||||||
deleted: "feed.empty.deleted",
|
deleted: "feed.empty.deleted",
|
||||||
edited: "feed.empty.edited",
|
edited: "feed.empty.edited",
|
||||||
|
profile: "feed.empty.profile",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
let items = $state<FeedItem[]>([]);
|
let items = $state<FeedItem[]>([]);
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { loadAvatarVariant } from "#lib/api/avatars.js";
|
||||||
|
import Icon from "#lib/components/ui/Icon.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
ownerId: number;
|
||||||
|
uniqueId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { ownerId, uniqueId }: Props = $props();
|
||||||
|
|
||||||
|
let url = $state<string | null>(null);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
url = null;
|
||||||
|
if (uniqueId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let active = true;
|
||||||
|
loadAvatarVariant(ownerId > 0 ? "peer" : "chat", ownerId, uniqueId).then(
|
||||||
|
(resolved) => {
|
||||||
|
if (active) {
|
||||||
|
url = resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="feed-photo">
|
||||||
|
{#if url}
|
||||||
|
<img alt="" src={url}>
|
||||||
|
{:else}
|
||||||
|
<Icon name="photo" size="0.875rem" />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.feed-photo {
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
color: var(--color-white);
|
||||||
|
|
||||||
|
background-color: var(--color-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
feed,
|
feed,
|
||||||
messageHref,
|
messageHref,
|
||||||
} from "#lib/stores/feed.svelte.js";
|
} from "#lib/stores/feed.svelte.js";
|
||||||
|
import { ui } from "#lib/stores/ui.svelte.js";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
|
|
||||||
@@ -20,8 +21,11 @@
|
|||||||
|
|
||||||
let { filter }: Props = $props();
|
let { filter }: Props = $props();
|
||||||
|
|
||||||
function open(item: FeedItem, messageId: number) {
|
async function open(item: FeedItem, messageId: number) {
|
||||||
goto(messageHref(page.url, item.chat.chat_id, messageId));
|
await goto(messageHref(page.url, item.chat.chat_id, messageId));
|
||||||
|
if (item.kind === "profile" && !messageId) {
|
||||||
|
ui.openPanel("profile");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|||||||
@@ -76,13 +76,22 @@ export const feed = {
|
|||||||
en: "No edited messages yet",
|
en: "No edited messages yet",
|
||||||
ru: "Изменённых пока нет",
|
ru: "Изменённых пока нет",
|
||||||
},
|
},
|
||||||
|
"feed.empty.profile": {
|
||||||
|
en: "No profile changes yet",
|
||||||
|
ru: "Профили пока не менялись",
|
||||||
|
},
|
||||||
"feed.error": {
|
"feed.error": {
|
||||||
en: "Couldn't load the feed",
|
en: "Couldn't load the feed",
|
||||||
ru: "Не удалось загрузить ленту",
|
ru: "Не удалось загрузить ленту",
|
||||||
},
|
},
|
||||||
|
"feed.field.name": { en: "Name", ru: "Имя" },
|
||||||
|
"feed.field.photo": { en: "Photo", ru: "Фото" },
|
||||||
|
"feed.field.title": { en: "Title", ru: "Название" },
|
||||||
|
"feed.field.username": { en: "Username", ru: "Юзернейм" },
|
||||||
"feed.filter.all": { en: "All", ru: "Все" },
|
"feed.filter.all": { en: "All", ru: "Все" },
|
||||||
"feed.filter.deleted": { en: "Deleted", ru: "Удалённые" },
|
"feed.filter.deleted": { en: "Deleted", ru: "Удалённые" },
|
||||||
"feed.filter.edited": { en: "Edited", ru: "Изменённые" },
|
"feed.filter.edited": { en: "Edited", ru: "Изменённые" },
|
||||||
|
"feed.filter.profile": { en: "Profiles", ru: "Профили" },
|
||||||
"feed.lostText": {
|
"feed.lostText": {
|
||||||
en: "The earlier text wasn't saved",
|
en: "The earlier text wasn't saved",
|
||||||
ru: "Прежний текст не сохранился",
|
ru: "Прежний текст не сохранился",
|
||||||
@@ -91,6 +100,15 @@ export const feed = {
|
|||||||
"feed.mediaChanged": { en: "Media replaced", ru: "Медиа заменено" },
|
"feed.mediaChanged": { en: "Media replaced", ru: "Медиа заменено" },
|
||||||
"feed.more": { en: "Show more", ru: "Показать ещё" },
|
"feed.more": { en: "Show more", ru: "Показать ещё" },
|
||||||
"feed.noText": { en: "No text", ru: "Без текста" },
|
"feed.noText": { en: "No text", ru: "Без текста" },
|
||||||
|
"feed.profileCount": {
|
||||||
|
en: { one: "{count} change", other: "{count} changes" },
|
||||||
|
ru: {
|
||||||
|
few: "{count} изменения",
|
||||||
|
many: "{count} изменений",
|
||||||
|
one: "{count} изменение",
|
||||||
|
other: "{count} изменения",
|
||||||
|
},
|
||||||
|
},
|
||||||
"feed.retry": { en: "Retry", ru: "Повторить" },
|
"feed.retry": { en: "Retry", ru: "Повторить" },
|
||||||
"feed.title": { en: "Feed", ru: "Лента" },
|
"feed.title": { en: "Feed", ru: "Лента" },
|
||||||
"feed.unseen": {
|
"feed.unseen": {
|
||||||
@@ -127,6 +145,20 @@ export const feed = {
|
|||||||
"panel.search": { en: "Search", ru: "Поиск" },
|
"panel.search": { en: "Search", ru: "Поиск" },
|
||||||
"panel.trash": { en: "Trash", ru: "Корзина" },
|
"panel.trash": { en: "Trash", ru: "Корзина" },
|
||||||
"panel.versions": { en: "Versions", ru: "Версии" },
|
"panel.versions": { en: "Versions", ru: "Версии" },
|
||||||
|
"thread.copiedId": { en: "ID copied", ru: "ID скопирован" },
|
||||||
|
"thread.copiedText": { en: "Text copied", ru: "Текст скопирован" },
|
||||||
|
"thread.copyId": { en: "Copy ID", ru: "Копировать ID" },
|
||||||
|
"thread.copyText": { en: "Copy text", ru: "Копировать текст" },
|
||||||
|
"thread.deleted": { en: "deleted", ru: "удалено" },
|
||||||
|
"thread.links": {
|
||||||
|
en: { one: "{count} link", other: "{count} links" },
|
||||||
|
ru: {
|
||||||
|
few: "{count} ссылки",
|
||||||
|
many: "{count} ссылок",
|
||||||
|
one: "{count} ссылка",
|
||||||
|
other: "{count} ссылки",
|
||||||
|
},
|
||||||
|
},
|
||||||
"thread.loadFailed": {
|
"thread.loadFailed": {
|
||||||
en: "Couldn't load messages",
|
en: "Couldn't load messages",
|
||||||
ru: "Не удалось загрузить сообщения",
|
ru: "Не удалось загрузить сообщения",
|
||||||
@@ -135,5 +167,7 @@ export const feed = {
|
|||||||
en: "This message isn't in the archive",
|
en: "This message isn't in the archive",
|
||||||
ru: "Этого сообщения нет в архиве",
|
ru: "Этого сообщения нет в архиве",
|
||||||
},
|
},
|
||||||
|
"thread.senderProfile": { en: "Sender's profile", ru: "Профиль автора" },
|
||||||
"thread.toLatest": { en: "Jump to latest", ru: "К последним сообщениям" },
|
"thread.toLatest": { en: "Jump to latest", ru: "К последним сообщениям" },
|
||||||
|
"thread.toReply": { en: "Go to the reply", ru: "Перейти к ответу" },
|
||||||
} satisfies Messages;
|
} satisfies Messages;
|
||||||
@@ -10,7 +10,7 @@ const KIND_PARAM = "feed";
|
|||||||
const CHATS_PARAM = "chats";
|
const CHATS_PARAM = "chats";
|
||||||
const MESSAGE_PARAM = "message";
|
const MESSAGE_PARAM = "message";
|
||||||
|
|
||||||
export const FEED_KINDS: FeedKind[] = ["all", "deleted", "edited"];
|
export const FEED_KINDS: FeedKind[] = ["all", "deleted", "edited", "profile"];
|
||||||
export const FEED_CHAT_KINDS: FeedChatKind[] = ["dm", "group", "channel"];
|
export const FEED_CHAT_KINDS: FeedChatKind[] = ["dm", "group", "channel"];
|
||||||
|
|
||||||
export interface FeedFilter {
|
export interface FeedFilter {
|
||||||
@@ -59,11 +59,13 @@ function href(url: UrlLike): string {
|
|||||||
export function messageHref(
|
export function messageHref(
|
||||||
url: UrlLike,
|
url: UrlLike,
|
||||||
chatId: number,
|
chatId: number,
|
||||||
messageId: number
|
messageId?: number
|
||||||
): string {
|
): string {
|
||||||
const next = withFilter(url, feedFilterOf(url));
|
const next = withFilter(url, feedFilterOf(url));
|
||||||
next.pathname = `/app/${chatId}`;
|
next.pathname = `/app/${chatId}`;
|
||||||
|
if (messageId) {
|
||||||
next.searchParams.set(MESSAGE_PARAM, String(messageId));
|
next.searchParams.set(MESSAGE_PARAM, String(messageId));
|
||||||
|
}
|
||||||
return href(next);
|
return href(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user