feat: add annotations, user profiles, watchers, stories, search and more
This commit is contained in:
1 parent
ed469ba8dd
commit
2465bcd184
47 files changed
+5009
-242
No files matched your search
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import type { MessageView } from "$lib/api/types";
|
||||
import Contact from "$lib/components/Contact.svelte";
|
||||
import EntityText from "$lib/components/EntityText.svelte";
|
||||
@@ -12,11 +13,15 @@
|
||||
import Reactions from "$lib/components/Reactions.svelte";
|
||||
import ReplyHeader from "$lib/components/ReplyHeader.svelte";
|
||||
import Avatar from "$lib/components/ui/Avatar.svelte";
|
||||
import ContextMenu from "$lib/components/ui/ContextMenu.svelte";
|
||||
import ContextMenuItem from "$lib/components/ui/ContextMenuItem.svelte";
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import WebPage from "$lib/components/WebPage.svelte";
|
||||
import { accountName, peerColorIndex, peerName } from "$lib/format/peer";
|
||||
import { accounts } from "$lib/stores/accounts.svelte";
|
||||
import { peers } from "$lib/stores/peers.svelte";
|
||||
import { toasts } from "$lib/stores/toasts.svelte";
|
||||
import { ui } from "$lib/stores/ui.svelte";
|
||||
import { appear } from "$lib/transitions/appear";
|
||||
|
||||
interface Props {
|
||||
@@ -75,100 +80,210 @@
|
||||
? ownId !== null && peers.get(ownId)?.has_avatar
|
||||
: (sender?.has_avatar ?? false)
|
||||
);
|
||||
|
||||
const hasCallbacks = $derived(
|
||||
message.inline_buttons.some((row) =>
|
||||
row.some((button) => button.kind === "callback" && button.data)
|
||||
)
|
||||
);
|
||||
const linkCount = $derived.by(() => {
|
||||
const count = message.entities.filter(
|
||||
(entity) => entity.type === "url" || entity.type === "text_link"
|
||||
).length;
|
||||
if (count > 0) {
|
||||
return count;
|
||||
}
|
||||
return message.web_page ? 1 : 0;
|
||||
});
|
||||
|
||||
function openReactions() {
|
||||
ui.openMessagePanel("reactions", message.message_id);
|
||||
}
|
||||
function openCallbacks() {
|
||||
ui.openMessagePanel("callbacks", message.message_id);
|
||||
}
|
||||
function openLinks() {
|
||||
ui.openMessagePanel("links", message.message_id);
|
||||
}
|
||||
function openAnnotations() {
|
||||
ui.openMessagePanel("annotations", message.message_id);
|
||||
}
|
||||
function jumpToReply() {
|
||||
if (message.reply?.message_id != null) {
|
||||
onjump(message.reply.message_id);
|
||||
}
|
||||
}
|
||||
async function copyText() {
|
||||
await navigator.clipboard.writeText(message.text ?? "");
|
||||
toasts.success("Текст скопирован");
|
||||
}
|
||||
async function copyId() {
|
||||
await navigator.clipboard.writeText(String(message.message_id));
|
||||
toasts.success("ID скопирован");
|
||||
}
|
||||
function openSenderProfile() {
|
||||
if (message.sender_id !== null) {
|
||||
goto(`/app/${message.sender_id}`);
|
||||
ui.openPanel("profile");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="Message"
|
||||
class:own
|
||||
class:deleted
|
||||
class:highlighted
|
||||
class:first-in-group={firstInGroup}
|
||||
class:last-in-group={lastInGroup}
|
||||
class:with-avatar={isGroupChat}
|
||||
data-message-id={message.message_id}
|
||||
in:appear={{ disabled: !animate }}
|
||||
>
|
||||
{#if isGroupChat}
|
||||
<div class="avatar-slot">
|
||||
{#if lastInGroup && avatarId !== null}
|
||||
<Avatar
|
||||
name={senderName}
|
||||
colorKey={avatarId}
|
||||
size={2.125}
|
||||
avatar={{ kind: "peer", id: avatarId }}
|
||||
hasAvatar={avatarHas}
|
||||
/>
|
||||
<ContextMenu>
|
||||
{#snippet children({ props })}
|
||||
<div
|
||||
{...props}
|
||||
class="Message"
|
||||
class:own
|
||||
class:deleted
|
||||
class:highlighted
|
||||
class:first-in-group={firstInGroup}
|
||||
class:last-in-group={lastInGroup}
|
||||
class:with-avatar={isGroupChat}
|
||||
data-message-id={message.message_id}
|
||||
in:appear={{ disabled: !animate }}
|
||||
>
|
||||
{#if isGroupChat}
|
||||
<div class="avatar-slot">
|
||||
{#if lastInGroup && avatarId !== null}
|
||||
<Avatar
|
||||
name={senderName}
|
||||
colorKey={avatarId}
|
||||
size={2.125}
|
||||
avatar={{ kind: "peer", id: avatarId }}
|
||||
hasAvatar={avatarHas}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="message-content" class:has-appendix={lastInGroup}>
|
||||
{#if showName}
|
||||
<div class="sender-name peer-color-{colorIndex}">{senderName}</div>
|
||||
<div class="message-content" class:has-appendix={lastInGroup}>
|
||||
{#if showName}
|
||||
<div class="sender-name peer-color-{colorIndex}">{senderName}</div>
|
||||
{/if}
|
||||
{#if message.forward}
|
||||
<ForwardHeader forward={message.forward} />
|
||||
{/if}
|
||||
{#if message.reply}
|
||||
<ReplyHeader reply={message.reply} {onjump} />
|
||||
{/if}
|
||||
{#if deleted}
|
||||
<span class="deleted-tag">
|
||||
<Icon name="delete" size="0.875rem" />
|
||||
deleted
|
||||
</span>
|
||||
{/if}
|
||||
{#if message.poll}
|
||||
<Poll poll={message.poll} />
|
||||
{/if}
|
||||
{#if message.contact}
|
||||
<Contact contact={message.contact} />
|
||||
{/if}
|
||||
{#if message.location}
|
||||
<Location location={message.location} />
|
||||
{/if}
|
||||
{#if !special}
|
||||
{#if message.media.length > 1}
|
||||
<MediaAlbum
|
||||
media={message.media}
|
||||
chatId={message.chat_id}
|
||||
onopen={onmedia}
|
||||
/>
|
||||
{:else if message.has_media}
|
||||
<MessageMedia {message} {own} onopen={() => onmedia(0)} />
|
||||
{/if}
|
||||
{/if}
|
||||
{#if hasText}
|
||||
<div class="text">
|
||||
<EntityText
|
||||
text={message.text ?? ""}
|
||||
entities={message.entities}
|
||||
{own}
|
||||
/>
|
||||
</div>
|
||||
{:else if !(message.has_media || deleted || special)}
|
||||
<div class="text empty">(no text)</div>
|
||||
{/if}
|
||||
{#if message.web_page}
|
||||
<WebPage {message} {own} {onmedia} />
|
||||
{/if}
|
||||
{#if message.reactions.length}
|
||||
<Reactions
|
||||
reactions={message.reactions}
|
||||
{own}
|
||||
onclick={openReactions}
|
||||
/>
|
||||
{/if}
|
||||
{#if linkCount > 0}
|
||||
<button type="button" class="detail-chip" onclick={openLinks}>
|
||||
<Icon name="link" size="0.875rem" />
|
||||
{linkCount}
|
||||
{linkCount === 1 ? "ссылка" : "ссылки"}
|
||||
</button>
|
||||
{/if}
|
||||
<MessageMeta {message} {own} {onversions} />
|
||||
{#if message.inline_buttons.length}
|
||||
<InlineButtons rows={message.inline_buttons} />
|
||||
{/if}
|
||||
{#if hasCallbacks}
|
||||
<button type="button" class="detail-chip" onclick={openCallbacks}>
|
||||
<Icon name="bot-command" size="0.875rem" />
|
||||
Callback-данные
|
||||
</button>
|
||||
{/if}
|
||||
{#if lastInGroup}
|
||||
<svg aria-hidden="true" class="svg-appendix" height="20" width="9">
|
||||
<path
|
||||
class="corner"
|
||||
d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet menu()}
|
||||
<ContextMenuItem icon="note" onselect={openAnnotations}
|
||||
>Заметки</ContextMenuItem
|
||||
>
|
||||
{#if message.reactions.length}
|
||||
<ContextMenuItem icon="heart" onselect={openReactions}
|
||||
>Реакции</ContextMenuItem
|
||||
>
|
||||
{/if}
|
||||
{#if message.forward}
|
||||
<ForwardHeader forward={message.forward} />
|
||||
{#if linkCount > 0}
|
||||
<ContextMenuItem icon="link" onselect={openLinks}>Ссылки</ContextMenuItem>
|
||||
{/if}
|
||||
{#if message.reply}
|
||||
<ReplyHeader reply={message.reply} {onjump} />
|
||||
{#if hasCallbacks}
|
||||
<ContextMenuItem icon="bot-command" onselect={openCallbacks}
|
||||
>Callback-данные</ContextMenuItem
|
||||
>
|
||||
{/if}
|
||||
{#if deleted}
|
||||
<span class="deleted-tag">
|
||||
<Icon name="delete" size="0.875rem" />
|
||||
deleted
|
||||
</span>
|
||||
{#if message.edited_at}
|
||||
<ContextMenuItem icon="edit" onselect={onversions}
|
||||
>Версии</ContextMenuItem
|
||||
>
|
||||
{/if}
|
||||
{#if message.poll}
|
||||
<Poll poll={message.poll} />
|
||||
{/if}
|
||||
{#if message.contact}
|
||||
<Contact contact={message.contact} />
|
||||
{/if}
|
||||
{#if message.location}
|
||||
<Location location={message.location} />
|
||||
{/if}
|
||||
{#if !special}
|
||||
{#if message.media.length > 1}
|
||||
<MediaAlbum
|
||||
media={message.media}
|
||||
chatId={message.chat_id}
|
||||
onopen={onmedia}
|
||||
/>
|
||||
{:else if message.has_media}
|
||||
<MessageMedia {message} {own} onopen={() => onmedia(0)} />
|
||||
{/if}
|
||||
{#if message.reply?.message_id != null}
|
||||
<ContextMenuItem icon="reply" onselect={jumpToReply}
|
||||
>Перейти к ответу</ContextMenuItem
|
||||
>
|
||||
{/if}
|
||||
{#if hasText}
|
||||
<div class="text">
|
||||
<EntityText
|
||||
text={message.text ?? ""}
|
||||
entities={message.entities}
|
||||
{own}
|
||||
/>
|
||||
</div>
|
||||
{:else if !(message.has_media || deleted || special)}
|
||||
<div class="text empty">(no text)</div>
|
||||
<ContextMenuItem icon="copy" onselect={copyText}
|
||||
>Копировать текст</ContextMenuItem
|
||||
>
|
||||
{/if}
|
||||
{#if message.web_page}
|
||||
<WebPage {message} {own} {onmedia} />
|
||||
<ContextMenuItem icon="hashtag" onselect={copyId}
|
||||
>Копировать ID</ContextMenuItem
|
||||
>
|
||||
{#if isGroupChat && !own && message.sender_id !== null}
|
||||
<ContextMenuItem icon="info" onselect={openSenderProfile}
|
||||
>Профиль автора</ContextMenuItem
|
||||
>
|
||||
{/if}
|
||||
{#if message.reactions.length}
|
||||
<Reactions reactions={message.reactions} {own} />
|
||||
{/if}
|
||||
<MessageMeta {message} {own} {onversions} />
|
||||
{#if message.inline_buttons.length}
|
||||
<InlineButtons rows={message.inline_buttons} />
|
||||
{/if}
|
||||
{#if lastInGroup}
|
||||
<svg aria-hidden="true" class="svg-appendix" height="20" width="9">
|
||||
<path
|
||||
class="corner"
|
||||
d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ContextMenu>
|
||||
|
||||
<style lang="scss">
|
||||
.Message {
|
||||
@@ -278,6 +393,30 @@
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.detail-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
align-self: flex-start;
|
||||
|
||||
margin-top: 0.25rem;
|
||||
padding: 0.125rem 0.5rem 0.125rem 0.375rem;
|
||||
border: 0;
|
||||
border-radius: 1rem;
|
||||
|
||||
font-family: inherit;
|
||||
font-size: 0.75rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
cursor: pointer;
|
||||
background-color: var(--color-message-reaction);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.svg-appendix {
|
||||
position: absolute;
|
||||
bottom: -0.0625rem;
|
||||
|
||||
Reference in new issue
Block a user