feat: 1-to-1 message render + web data-lake backend
This commit is contained in:
1 parent
f0afb7ec5b
commit
75425d1bee
110 files changed
+10199
-54
No files matched your search
@@ -0,0 +1,262 @@
|
||||
<script lang="ts">
|
||||
import type { MessageView } from "$lib/api/types";
|
||||
import EntityText from "$lib/components/EntityText.svelte";
|
||||
import ForwardHeader from "$lib/components/ForwardHeader.svelte";
|
||||
import MediaAlbum from "$lib/components/MediaAlbum.svelte";
|
||||
import MessageMedia from "$lib/components/MessageMedia.svelte";
|
||||
import MessageMeta from "$lib/components/MessageMeta.svelte";
|
||||
import ReplyHeader from "$lib/components/ReplyHeader.svelte";
|
||||
import Avatar from "$lib/components/ui/Avatar.svelte";
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import { accountName, peerColorIndex, peerName } from "$lib/format/peer";
|
||||
import { accounts } from "$lib/stores/accounts.svelte";
|
||||
import { peers } from "$lib/stores/peers.svelte";
|
||||
import { appear } from "$lib/transitions/appear";
|
||||
|
||||
interface Props {
|
||||
animate: boolean;
|
||||
firstInGroup: boolean;
|
||||
highlighted: boolean;
|
||||
isGroupChat: boolean;
|
||||
lastInGroup: boolean;
|
||||
message: MessageView;
|
||||
onjump: (messageId: number) => void;
|
||||
onmedia: (index: number) => void;
|
||||
onversions: () => void;
|
||||
own: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
message,
|
||||
own,
|
||||
isGroupChat,
|
||||
firstInGroup,
|
||||
lastInGroup,
|
||||
highlighted,
|
||||
animate,
|
||||
onjump,
|
||||
onmedia,
|
||||
onversions,
|
||||
}: Props = $props();
|
||||
|
||||
const deleted = $derived(message.deleted_at !== null);
|
||||
const hasText = $derived(Boolean(message.text));
|
||||
|
||||
const ownId = $derived(accounts.selected?.tg_user_id ?? null);
|
||||
const sender = $derived(
|
||||
message.sender_id === null ? undefined : peers.get(message.sender_id)
|
||||
);
|
||||
const senderName = $derived.by(() => {
|
||||
if (own) {
|
||||
return accounts.selected ? accountName(accounts.selected) : "You";
|
||||
}
|
||||
if (sender) {
|
||||
return peerName(sender);
|
||||
}
|
||||
return message.sender_id === null ? "" : String(message.sender_id);
|
||||
});
|
||||
const colorIndex = $derived(peerColorIndex(message.sender_id ?? 0));
|
||||
const showName = $derived(isGroupChat && !own && firstInGroup);
|
||||
|
||||
const avatarId = $derived(own ? ownId : message.sender_id);
|
||||
const avatarHas = $derived(
|
||||
own
|
||||
? ownId !== null && peers.get(ownId)?.has_avatar
|
||||
: (sender?.has_avatar ?? false)
|
||||
);
|
||||
</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}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<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.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 hasText}
|
||||
<div class="text">
|
||||
<EntityText
|
||||
text={message.text ?? ""}
|
||||
entities={message.entities}
|
||||
{own}
|
||||
/>
|
||||
</div>
|
||||
{:else if !(message.has_media || deleted)}
|
||||
<div class="text empty">(no text)</div>
|
||||
{/if}
|
||||
<MessageMeta {message} {onversions} />
|
||||
{#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>
|
||||
|
||||
<style lang="scss">
|
||||
.Message {
|
||||
--background-color: var(--color-background);
|
||||
--meta-color: var(--color-text-meta);
|
||||
|
||||
position: relative;
|
||||
transform-origin: bottom left;
|
||||
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0.4375rem;
|
||||
margin-bottom: 0.125rem;
|
||||
|
||||
&.last-in-group {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&.own {
|
||||
--background-color: var(--color-background-own);
|
||||
--meta-color: var(--color-message-meta-own);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-slot {
|
||||
flex-shrink: 0;
|
||||
width: 2.125rem;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
position: relative;
|
||||
|
||||
max-width: min(30rem, 75%);
|
||||
min-width: 3.5rem;
|
||||
padding: 0.3125rem 0.5rem 0.375rem;
|
||||
border-radius: var(--border-radius-messages);
|
||||
|
||||
font-size: 1rem;
|
||||
line-height: 1.3125;
|
||||
color: var(--color-text);
|
||||
|
||||
background-color: var(--background-color);
|
||||
box-shadow: 0 1px 2px var(--color-default-shadow);
|
||||
}
|
||||
|
||||
.Message.highlighted .message-content {
|
||||
animation: highlight-flash 1.6s ease;
|
||||
}
|
||||
|
||||
@keyframes highlight-flash {
|
||||
0%,
|
||||
60% {
|
||||
background-color: var(--color-primary-opacity);
|
||||
}
|
||||
|
||||
100% {
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.Message:not(.first-in-group) .message-content {
|
||||
border-top-left-radius: var(--border-radius-messages-small);
|
||||
}
|
||||
|
||||
.Message:not(.last-in-group) .message-content {
|
||||
border-bottom-left-radius: var(--border-radius-messages-small);
|
||||
}
|
||||
|
||||
.Message.last-in-group .message-content.has-appendix {
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.sender-name {
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.0625rem;
|
||||
|
||||
font-size: 0.875rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text {
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
|
||||
&.empty {
|
||||
font-style: italic;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.deleted .text {
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.deleted-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
font-size: 0.75rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.svg-appendix {
|
||||
position: absolute;
|
||||
bottom: -0.0625rem;
|
||||
left: -0.5rem;
|
||||
|
||||
width: 9px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.svg-appendix .corner {
|
||||
fill: var(--background-color);
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user