atagen

malice

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/MessageRow.svelte

973f8fbdaf80atagen2026-09-28 09:10 UTC1
<script lang="ts">
973f8fbdaf80atagen2026-09-28 09:10 UTC2
  import MediaAttachmentView from './MediaAttachmentView.svelte';
973f8fbdaf80atagen2026-09-28 09:10 UTC3
  import MxcAvatar from './MxcAvatar.svelte';
544a170ead2catagen2026-10-20 11:47 UTC4
  import Emoji from './Emoji.svelte';
544a170ead2catagen2026-10-20 11:47 UTC5
  import EmojiText from './EmojiText.svelte';
df3c0f2b3da4atagen2026-10-22 13:07 UTC6
  import MessageText from './MessageText.svelte';
544a170ead2catagen2026-10-20 11:47 UTC7
  import ReactionPicker from './ReactionPicker.svelte';
b8ab00377c20atagen2026-10-22 05:13 UTC8
  import LinkCard from './LinkCard.svelte';
ecb8598b38afatagen2026-10-22 09:19 UTC9
  import { mediaUrl, messageMediaUrl } from './api';
df3c0f2b3da4atagen2026-10-22 13:07 UTC10
  import type { ImagePack, LinkPreview, Person, QuickReaction, ReplyReference, SendState, TimelineContent, TimelineEvent } from './types';
973f8fbdaf80atagen2026-09-28 09:10 UTC11
  import { senderLabel } from './types';
973f8fbdaf80atagen2026-09-28 09:10 UTC12
  import { formatClock, formatDay, initialsOf, senderHue } from './format';
973f8fbdaf80atagen2026-09-28 09:10 UTC13
  import { findUrls } from './urls';
787cdbbf0721atagen2026-10-24 06:50 UTC14
  import { mentionsFromHtml, resolveMentions, stripMarkdownPills } from './mentions';
973f8fbdaf80atagen2026-09-28 09:10 UTC15
973f8fbdaf80atagen2026-09-28 09:10 UTC16
  export let itemId: string;
973f8fbdaf80atagen2026-09-28 09:10 UTC17
  export let event: TimelineEvent;
973f8fbdaf80atagen2026-09-28 09:10 UTC18
  export let showSender: boolean;
973f8fbdaf80atagen2026-09-28 09:10 UTC19
  export let reacting: boolean;
ecb8598b38afatagen2026-10-22 09:19 UTC20
  export let onReact: (eventId: string, key: string, shortcode?: string) => void;
973f8fbdaf80atagen2026-09-28 09:10 UTC21
  export let senderAvatar: string | null = null;
544a170ead2catagen2026-10-20 11:47 UTC22
  export let quickReactions: QuickReaction[] = [];
544a170ead2catagen2026-10-20 11:47 UTC23
  export let packs: ImagePack[] = [];
544a170ead2catagen2026-10-20 11:47 UTC24
  export let onQuickChange: (list: QuickReaction[]) => void = () => {};
ecb8598b38afatagen2026-10-22 09:19 UTC25
  export let linkPreviews: Record<string, LinkPreview | null> = {};
ecb8598b38afatagen2026-10-22 09:19 UTC26
  export let loadPreview: ((url: string) => void) | null = null;
df3c0f2b3da4atagen2026-10-22 13:07 UTC27
  export let people: Record<string, Person> = {};
df3c0f2b3da4atagen2026-10-22 13:07 UTC28
  export let onReply: ((target: ReplyReference) => void) | null = null;
df3c0f2b3da4atagen2026-10-22 13:07 UTC29
  export let onJump: ((eventId: string) => void) | null = null;
cf23dbf4e77catagen2026-10-23 06:36 UTC30
  export let onEdit: ((eventId: string, body: string) => void) | null = null;
cf23dbf4e77catagen2026-10-23 06:36 UTC31
  export let editing = false;
df3c0f2b3da4atagen2026-10-22 13:07 UTC32
  export let showSent = true;
668ab5c30531atagen2026-10-25 03:13 UTC33
  export let readOnly = false;
705b1e1062c2atagen2026-10-23 09:05 UTC34
  export let self: string | null = null;
d8624c214c04atagen2026-10-24 08:24 UTC35
  export let onUser: ((userId: string, x: number, y: number, menu: boolean) => void) | null = null;
d095131eec0eatagen2026-10-24 08:39 UTC36
  export let onCloneSticker: ((eventId: string, body: string, x: number, y: number) => void) | null = null;
0380cd8c765catagen2026-10-24 09:08 UTC37
  export let onMore: ((event: TimelineEvent, x: number, y: number) => void) | null = null;
d8624c214c04atagen2026-10-24 08:24 UTC38
d8624c214c04atagen2026-10-24 08:24 UTC39
  function openUser(ev: MouseEvent, menu: boolean): void {
d8624c214c04atagen2026-10-24 08:24 UTC40
    if (!onUser) return;
d8624c214c04atagen2026-10-24 08:24 UTC41
    const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
d8624c214c04atagen2026-10-24 08:24 UTC42
    onUser(event.sender.user_id, menu ? ev.clientX : rect.left, menu ? ev.clientY : rect.bottom + 4, menu);
d8624c214c04atagen2026-10-24 08:24 UTC43
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC44
544a170ead2catagen2026-10-20 11:47 UTC45
  const PICKER_WIDTH = 320;
544a170ead2catagen2026-10-20 11:47 UTC46
  const PICKER_HEIGHT = 380;
544a170ead2catagen2026-10-20 11:47 UTC47
  const PICKER_MARGIN = 8;
df3c0f2b3da4atagen2026-10-22 13:07 UTC48
  const MAX_READERS = 3;
05a48e9e7942atagen2026-10-24 09:32 UTC49
  const STATE_LABELS: Record<string, string> = {
05a48e9e7942atagen2026-10-24 09:32 UTC50
    'm.room.pinned_events': 'changed the pinned messages',
05a48e9e7942atagen2026-10-24 09:32 UTC51
    'm.room.power_levels': 'changed the room permissions',
05a48e9e7942atagen2026-10-24 09:32 UTC52
    'm.room.join_rules': 'changed who can join',
05a48e9e7942atagen2026-10-24 09:32 UTC53
    'm.room.history_visibility': 'changed who can read history',
05a48e9e7942atagen2026-10-24 09:32 UTC54
    'm.room.guest_access': 'changed guest access',
05a48e9e7942atagen2026-10-24 09:32 UTC55
    'm.room.canonical_alias': 'changed the room address',
05a48e9e7942atagen2026-10-24 09:32 UTC56
    'm.room.server_acl': 'changed the server access list',
05a48e9e7942atagen2026-10-24 09:32 UTC57
    'm.space.child': 'changed the rooms in this space',
05a48e9e7942atagen2026-10-24 09:32 UTC58
    'm.space.parent': 'changed the parent space',
05a48e9e7942atagen2026-10-24 09:32 UTC59
    'im.ponies.room_emotes': 'updated a sticker pack'
05a48e9e7942atagen2026-10-24 09:32 UTC60
  };
544a170ead2catagen2026-10-20 11:47 UTC61
544a170ead2catagen2026-10-20 11:47 UTC62
  let row: HTMLDivElement;
544a170ead2catagen2026-10-20 11:47 UTC63
  let pickerPop: HTMLDivElement | undefined;
544a170ead2catagen2026-10-20 11:47 UTC64
  let selected = false;
544a170ead2catagen2026-10-20 11:47 UTC65
  let pickerOpen = false;
544a170ead2catagen2026-10-20 11:47 UTC66
  let pickerStyle = '';
973f8fbdaf80atagen2026-09-28 09:10 UTC67
973f8fbdaf80atagen2026-09-28 09:10 UTC68
  const MEMBERSHIP_LABELS: Record<string, string> = {
973f8fbdaf80atagen2026-09-28 09:10 UTC69
    Joined: 'joined the room',
973f8fbdaf80atagen2026-09-28 09:10 UTC70
    Left: 'left the room',
973f8fbdaf80atagen2026-09-28 09:10 UTC71
    Banned: 'was banned',
973f8fbdaf80atagen2026-09-28 09:10 UTC72
    Unbanned: 'was unbanned',
973f8fbdaf80atagen2026-09-28 09:10 UTC73
    Kicked: 'was kicked',
973f8fbdaf80atagen2026-09-28 09:10 UTC74
    KickedAndBanned: 'was kicked and banned',
973f8fbdaf80atagen2026-09-28 09:10 UTC75
    Invited: 'was invited',
973f8fbdaf80atagen2026-09-28 09:10 UTC76
    InvitationAccepted: 'accepted the invite',
973f8fbdaf80atagen2026-09-28 09:10 UTC77
    InvitationRejected: 'rejected the invite',
973f8fbdaf80atagen2026-09-28 09:10 UTC78
    InvitationRevoked: 'had the invite revoked',
973f8fbdaf80atagen2026-09-28 09:10 UTC79
    Knocked: 'requested to join',
973f8fbdaf80atagen2026-09-28 09:10 UTC80
    KnockAccepted: 'was let in',
973f8fbdaf80atagen2026-09-28 09:10 UTC81
    KnockRetracted: 'withdrew the request to join',
973f8fbdaf80atagen2026-09-28 09:10 UTC82
    KnockDenied: 'had the request to join denied'
973f8fbdaf80atagen2026-09-28 09:10 UTC83
  };
973f8fbdaf80atagen2026-09-28 09:10 UTC84
f604ef6df0c4atagen2026-10-24 10:08 UTC85
  $: name = event.sender.display_name ?? people[event.sender.user_id]?.name ?? event.sender.user_id;
973f8fbdaf80atagen2026-09-28 09:10 UTC86
  $: displayName = event.sender.name_ambiguous ? `${name} (${event.sender.user_id})` : name;
df3c0f2b3da4atagen2026-10-22 13:07 UTC87
  $: readers = event.read_by.map((id) => people[id] ?? { user_id: id, name: id, avatar: null });
973f8fbdaf80atagen2026-09-28 09:10 UTC88
  $: interactive = event.content.kind === 'Message' || event.content.kind === 'Sticker' || event.content.kind === 'Poll';
705b1e1062c2atagen2026-10-23 09:05 UTC89
  $: message = event.content.kind === 'Message' ? event.content.message : null;
705b1e1062c2atagen2026-10-23 09:05 UTC90
  $: stripped = stripMarkdownPills(message ? (message.media ? message.media.caption : message.body) ?? '' : '');
787cdbbf0721atagen2026-10-24 06:50 UTC91
  $: linked = message ? [...mentionsFromHtml(message.formatted_body), ...stripped.mentions] : [];
787cdbbf0721atagen2026-10-24 06:50 UTC92
  $: mentions = resolveMentions(stripped.text, linked);
787cdbbf0721atagen2026-10-24 06:50 UTC93
  $: mentionsMe = self !== null && [...linked, ...mentions].some((m) => m.user_id === self);
705b1e1062c2atagen2026-10-23 09:05 UTC94
  $: previews = findUrls(stripped.text);
705b1e1062c2atagen2026-10-23 09:05 UTC95
  $: bundled = message ? message.link_previews : [];
705b1e1062c2atagen2026-10-23 09:05 UTC96
  $: cards = previews
705b1e1062c2atagen2026-10-23 09:05 UTC97
    .map((link) => ({ link, preview: bundled.find((b) => b.url === link.href) ?? linkPreviews[link.href] ?? null }))
705b1e1062c2atagen2026-10-23 09:05 UTC98
    .filter((c) => c.preview && (c.preview.title || c.preview.description || c.preview.image));
ecb8598b38afatagen2026-10-22 09:19 UTC99
  $: if (loadPreview) {
ecb8598b38afatagen2026-10-22 09:19 UTC100
    for (const link of previews) {
ecb8598b38afatagen2026-10-22 09:19 UTC101
      if (!bundled.some((b) => b.url === link.href) && !(link.href in linkPreviews)) loadPreview(link.href);
ecb8598b38afatagen2026-10-22 09:19 UTC102
    }
ecb8598b38afatagen2026-10-22 09:19 UTC103
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC104
f604ef6df0c4atagen2026-10-24 10:08 UTC105
  $: sent = interactive && showSent && event.own && event.send_state.kind === 'Sent' && event.event_id !== null;
f604ef6df0c4atagen2026-10-24 10:08 UTC106
973f8fbdaf80atagen2026-09-28 09:10 UTC107
  function sendLabel(state: SendState): string {
973f8fbdaf80atagen2026-09-28 09:10 UTC108
    if (state.kind === 'Failed') return state.will_retry ? 'failed — will retry' : 'failed — not delivered';
973f8fbdaf80atagen2026-09-28 09:10 UTC109
    if (state.kind === 'Sending') return 'sending…';
973f8fbdaf80atagen2026-09-28 09:10 UTC110
    return '';
973f8fbdaf80atagen2026-09-28 09:10 UTC111
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC112
544a170ead2catagen2026-10-20 11:47 UTC113
  function toggleSelected(e: MouseEvent) {
668ab5c30531atagen2026-10-25 03:13 UTC114
    if (readOnly || !interactive || (e.target as Element).closest('a, button, input, video, audio, .picker-pop')) return;
544a170ead2catagen2026-10-20 11:47 UTC115
    selected = !selected;
544a170ead2catagen2026-10-20 11:47 UTC116
  }
544a170ead2catagen2026-10-20 11:47 UTC117
544a170ead2catagen2026-10-20 11:47 UTC118
  function deselectOutside(e: PointerEvent) {
544a170ead2catagen2026-10-20 11:47 UTC119
    if (selected && !pickerOpen && !row.contains(e.target as Node)) selected = false;
544a170ead2catagen2026-10-20 11:47 UTC120
  }
544a170ead2catagen2026-10-20 11:47 UTC121
544a170ead2catagen2026-10-20 11:47 UTC122
  function openPicker(e: MouseEvent) {
544a170ead2catagen2026-10-20 11:47 UTC123
    if (pickerOpen) {
544a170ead2catagen2026-10-20 11:47 UTC124
      pickerOpen = false;
544a170ead2catagen2026-10-20 11:47 UTC125
      return;
544a170ead2catagen2026-10-20 11:47 UTC126
    }
544a170ead2catagen2026-10-20 11:47 UTC127
    const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
544a170ead2catagen2026-10-20 11:47 UTC128
    const height = Math.min(PICKER_HEIGHT, window.innerHeight - 2 * PICKER_MARGIN);
544a170ead2catagen2026-10-20 11:47 UTC129
    const below = window.innerHeight - rect.bottom - PICKER_MARGIN >= height;
544a170ead2catagen2026-10-20 11:47 UTC130
    const top = below ? rect.bottom + 4 : Math.max(PICKER_MARGIN, rect.top - height - 4);
544a170ead2catagen2026-10-20 11:47 UTC131
    const maxLeft = window.innerWidth - PICKER_WIDTH - PICKER_MARGIN;
544a170ead2catagen2026-10-20 11:47 UTC132
    const left = Math.max(PICKER_MARGIN, Math.min(rect.right - PICKER_WIDTH, maxLeft));
544a170ead2catagen2026-10-20 11:47 UTC133
    pickerStyle = `top:${top}px;left:${left}px;height:${height}px`;
544a170ead2catagen2026-10-20 11:47 UTC134
    selected = true;
544a170ead2catagen2026-10-20 11:47 UTC135
    pickerOpen = true;
544a170ead2catagen2026-10-20 11:47 UTC136
  }
544a170ead2catagen2026-10-20 11:47 UTC137
544a170ead2catagen2026-10-20 11:47 UTC138
  function closePicker() {
544a170ead2catagen2026-10-20 11:47 UTC139
    pickerOpen = false;
544a170ead2catagen2026-10-20 11:47 UTC140
  }
544a170ead2catagen2026-10-20 11:47 UTC141
ecb8598b38afatagen2026-10-22 09:19 UTC142
  function isImageKey(key: string): boolean {
ecb8598b38afatagen2026-10-22 09:19 UTC143
    return key.startsWith('mxc://') || packs.some((p) => p.images.some((i) => i.reaction_key === key));
ecb8598b38afatagen2026-10-22 09:19 UTC144
  }
ecb8598b38afatagen2026-10-22 09:19 UTC145
ecb8598b38afatagen2026-10-22 09:19 UTC146
  function react(key: string, shortcode: string | null) {
ecb8598b38afatagen2026-10-22 09:19 UTC147
    if (shortcode && isImageKey(key)) onReact(event.event_id ?? '', key, shortcode);
ecb8598b38afatagen2026-10-22 09:19 UTC148
    else onReact(event.event_id ?? '', key);
ecb8598b38afatagen2026-10-22 09:19 UTC149
  }
ecb8598b38afatagen2026-10-22 09:19 UTC150
df3c0f2b3da4atagen2026-10-22 13:07 UTC151
  function reply() {
df3c0f2b3da4atagen2026-10-22 13:07 UTC152
    if (!event.event_id || !onReply) return;
df3c0f2b3da4atagen2026-10-22 13:07 UTC153
    onReply({
df3c0f2b3da4atagen2026-10-22 13:07 UTC154
      event_id: event.event_id,
df3c0f2b3da4atagen2026-10-22 13:07 UTC155
      details: { kind: 'Ready', sender: event.sender, content: event.content, timestamp_ms: event.timestamp_ms }
df3c0f2b3da4atagen2026-10-22 13:07 UTC156
    });
df3c0f2b3da4atagen2026-10-22 13:07 UTC157
  }
df3c0f2b3da4atagen2026-10-22 13:07 UTC158
cf23dbf4e77catagen2026-10-23 06:36 UTC159
  $: editableBody =
cf23dbf4e77catagen2026-10-23 06:36 UTC160
    event.own && event.editable && event.event_id && event.content.kind === 'Message' && event.content.message.kind === 'Text'
cf23dbf4e77catagen2026-10-23 06:36 UTC161
      ? event.content.message.body
cf23dbf4e77catagen2026-10-23 06:36 UTC162
      : null;
cf23dbf4e77catagen2026-10-23 06:36 UTC163
cf23dbf4e77catagen2026-10-23 06:36 UTC164
  function edit() {
cf23dbf4e77catagen2026-10-23 06:36 UTC165
    if (!event.event_id || editableBody === null || !onEdit) return;
cf23dbf4e77catagen2026-10-23 06:36 UTC166
    onEdit(event.event_id, editableBody);
cf23dbf4e77catagen2026-10-23 06:36 UTC167
  }
cf23dbf4e77catagen2026-10-23 06:36 UTC168
cf23dbf4e77catagen2026-10-23 06:36 UTC169
  function reactorNames(senders: string[]): string {
cf23dbf4e77catagen2026-10-23 06:36 UTC170
    const names = senders.map((id) => people[id]?.name ?? id);
cf23dbf4e77catagen2026-10-23 06:36 UTC171
    if (names.length <= 12) return names.join(', ');
cf23dbf4e77catagen2026-10-23 06:36 UTC172
    return `${names.slice(0, 12).join(', ')} and ${names.length - 12} more`;
cf23dbf4e77catagen2026-10-23 06:36 UTC173
  }
cf23dbf4e77catagen2026-10-23 06:36 UTC174
ecb8598b38afatagen2026-10-22 09:19 UTC175
  function pick(key: string, shortcode: string | null) {
544a170ead2catagen2026-10-20 11:47 UTC176
    pickerOpen = false;
ecb8598b38afatagen2026-10-22 09:19 UTC177
    react(key, shortcode);
544a170ead2catagen2026-10-20 11:47 UTC178
  }
544a170ead2catagen2026-10-20 11:47 UTC179
544a170ead2catagen2026-10-20 11:47 UTC180
  function closeOnScroll(e: Event) {
544a170ead2catagen2026-10-20 11:47 UTC181
    if (pickerOpen && !pickerPop?.contains(e.target as Node)) pickerOpen = false;
544a170ead2catagen2026-10-20 11:47 UTC182
  }
544a170ead2catagen2026-10-20 11:47 UTC183
973f8fbdaf80atagen2026-09-28 09:10 UTC184
  function actorLabel(userId: string, name: string | null): string {
f604ef6df0c4atagen2026-10-24 10:08 UTC185
    return name ?? people[userId]?.name ?? userId;
973f8fbdaf80atagen2026-09-28 09:10 UTC186
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC187
973f8fbdaf80atagen2026-09-28 09:10 UTC188
  function replyPreview(content: TimelineContent): string {
705b1e1062c2atagen2026-10-23 09:05 UTC189
    if (content.kind === 'Message') return stripMarkdownPills(content.message.body).text;
973f8fbdaf80atagen2026-09-28 09:10 UTC190
    if (content.kind === 'Sticker') return content.body || 'Sticker';
973f8fbdaf80atagen2026-09-28 09:10 UTC191
    if (content.kind === 'Poll') return `Poll: ${content.question}`;
973f8fbdaf80atagen2026-09-28 09:10 UTC192
    if (content.kind === 'Redacted') return 'Message deleted';
973f8fbdaf80atagen2026-09-28 09:10 UTC193
    if (content.kind === 'UnableToDecrypt') return 'Unable to decrypt';
973f8fbdaf80atagen2026-09-28 09:10 UTC194
    return systemLine(content);
973f8fbdaf80atagen2026-09-28 09:10 UTC195
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC196
f604ef6df0c4atagen2026-10-24 10:08 UTC197
  function systemLine(content: TimelineContent, actor = displayName): string {
973f8fbdaf80atagen2026-09-28 09:10 UTC198
    switch (content.kind) {
973f8fbdaf80atagen2026-09-28 09:10 UTC199
      case 'Redacted':
973f8fbdaf80atagen2026-09-28 09:10 UTC200
        return 'Message deleted';
973f8fbdaf80atagen2026-09-28 09:10 UTC201
      case 'UnableToDecrypt':
f604ef6df0c4atagen2026-10-24 10:08 UTC202
        return `Message from ${actor} could not be decrypted`;
973f8fbdaf80atagen2026-09-28 09:10 UTC203
      case 'Membership': {
973f8fbdaf80atagen2026-09-28 09:10 UTC204
        const who = actorLabel(content.user_id, content.display_name);
973f8fbdaf80atagen2026-09-28 09:10 UTC205
        return `${who} ${MEMBERSHIP_LABELS[content.change] ?? 'updated their membership'}`;
973f8fbdaf80atagen2026-09-28 09:10 UTC206
      }
973f8fbdaf80atagen2026-09-28 09:10 UTC207
      case 'Profile': {
973f8fbdaf80atagen2026-09-28 09:10 UTC208
        const who = actorLabel(content.user_id, null);
973f8fbdaf80atagen2026-09-28 09:10 UTC209
        const parts: string[] = [];
973f8fbdaf80atagen2026-09-28 09:10 UTC210
        if (content.display_name) {
973f8fbdaf80atagen2026-09-28 09:10 UTC211
          parts.push(
973f8fbdaf80atagen2026-09-28 09:10 UTC212
            content.display_name.new
973f8fbdaf80atagen2026-09-28 09:10 UTC213
              ? `changed their display name to "${content.display_name.new}"`
973f8fbdaf80atagen2026-09-28 09:10 UTC214
              : 'removed their display name'
973f8fbdaf80atagen2026-09-28 09:10 UTC215
          );
973f8fbdaf80atagen2026-09-28 09:10 UTC216
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC217
        if (content.avatar) parts.push('changed their avatar');
973f8fbdaf80atagen2026-09-28 09:10 UTC218
        return `${who} ${parts.length > 0 ? parts.join(' and ') : 'updated their profile'}`;
973f8fbdaf80atagen2026-09-28 09:10 UTC219
      }
973f8fbdaf80atagen2026-09-28 09:10 UTC220
      case 'RoomName':
973f8fbdaf80atagen2026-09-28 09:10 UTC221
        return content.name
f604ef6df0c4atagen2026-10-24 10:08 UTC222
          ? `${actor} changed the room name to "${content.name}"`
f604ef6df0c4atagen2026-10-24 10:08 UTC223
          : `${actor} removed the room name`;
973f8fbdaf80atagen2026-09-28 09:10 UTC224
      case 'RoomTopic':
973f8fbdaf80atagen2026-09-28 09:10 UTC225
        return content.topic
f604ef6df0c4atagen2026-10-24 10:08 UTC226
          ? `${actor} changed the topic to "${content.topic}"`
f604ef6df0c4atagen2026-10-24 10:08 UTC227
          : `${actor} removed the topic`;
973f8fbdaf80atagen2026-09-28 09:10 UTC228
      case 'RoomAvatar':
f604ef6df0c4atagen2026-10-24 10:08 UTC229
        return content.avatar ? `${actor} changed the room avatar` : `${actor} removed the room avatar`;
973f8fbdaf80atagen2026-09-28 09:10 UTC230
      case 'EncryptionEnabled':
f604ef6df0c4atagen2026-10-24 10:08 UTC231
        return `${actor} enabled encryption in this room`;
973f8fbdaf80atagen2026-09-28 09:10 UTC232
      case 'RoomCreated':
f604ef6df0c4atagen2026-10-24 10:08 UTC233
        return `${actor} created the room`;
973f8fbdaf80atagen2026-09-28 09:10 UTC234
      case 'RoomUpgraded':
f604ef6df0c4atagen2026-10-24 10:08 UTC235
        return `${actor} upgraded the room to a new version`;
973f8fbdaf80atagen2026-09-28 09:10 UTC236
      case 'OtherState':
f604ef6df0c4atagen2026-10-24 10:08 UTC237
        return `${actor} ${STATE_LABELS[content.event_type] ?? `sent a state event (${content.event_type})`}`;
973f8fbdaf80atagen2026-09-28 09:10 UTC238
      case 'Other':
f604ef6df0c4atagen2026-10-24 10:08 UTC239
        return `${actor} sent an event (${content.event_type})`;
973f8fbdaf80atagen2026-09-28 09:10 UTC240
      default:
f604ef6df0c4atagen2026-10-24 10:08 UTC241
        return actor;
973f8fbdaf80atagen2026-09-28 09:10 UTC242
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC243
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC244
</script>
973f8fbdaf80atagen2026-09-28 09:10 UTC245
544a170ead2catagen2026-10-20 11:47 UTC246
<svelte:window
544a170ead2catagen2026-10-20 11:47 UTC247
  on:pointerdown={deselectOutside}
544a170ead2catagen2026-10-20 11:47 UTC248
  on:keydown={(e) => e.key === 'Escape' && (selected = false)}
544a170ead2catagen2026-10-20 11:47 UTC249
  on:scroll|capture={closeOnScroll}
544a170ead2catagen2026-10-20 11:47 UTC250
  on:resize={closePicker}
544a170ead2catagen2026-10-20 11:47 UTC251
/>
544a170ead2catagen2026-10-20 11:47 UTC252
544a170ead2catagen2026-10-20 11:47 UTC253
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
544a170ead2catagen2026-10-20 11:47 UTC254
<div
544a170ead2catagen2026-10-20 11:47 UTC255
  class="msg"
544a170ead2catagen2026-10-20 11:47 UTC256
  class:own={event.own}
cf23dbf4e77catagen2026-10-23 06:36 UTC257
  class:editing
705b1e1062c2atagen2026-10-23 09:05 UTC258
  class:mentioned={mentionsMe}
544a170ead2catagen2026-10-20 11:47 UTC259
  class:system={!interactive}
df3c0f2b3da4atagen2026-10-22 13:07 UTC260
  class:has-readers={interactive && readers.length > 0}
544a170ead2catagen2026-10-20 11:47 UTC261
  class:selected
544a170ead2catagen2026-10-20 11:47 UTC262
  data-item={itemId}
df3c0f2b3da4atagen2026-10-22 13:07 UTC263
  data-event={event.event_id}
544a170ead2catagen2026-10-20 11:47 UTC264
  bind:this={row}
544a170ead2catagen2026-10-20 11:47 UTC265
  on:click={toggleSelected}
544a170ead2catagen2026-10-20 11:47 UTC266
>
668ab5c30531atagen2026-10-25 03:13 UTC267
  {#if !readOnly && interactive && event.event_id}
544a170ead2catagen2026-10-20 11:47 UTC268
    <div class="actions" role="toolbar" aria-label={`Actions for message from ${displayName}`}>
544a170ead2catagen2026-10-20 11:47 UTC269
      {#each quickReactions as r (r.key)}
544a170ead2catagen2026-10-20 11:47 UTC270
        {@const label = r.shortcode ? `:${r.shortcode}:` : r.key}
544a170ead2catagen2026-10-20 11:47 UTC271
        <button
544a170ead2catagen2026-10-20 11:47 UTC272
          type="button"
544a170ead2catagen2026-10-20 11:47 UTC273
          title={`React ${label}`}
544a170ead2catagen2026-10-20 11:47 UTC274
          aria-label={`React ${label} to message from ${displayName}`}
544a170ead2catagen2026-10-20 11:47 UTC275
          disabled={reacting}
ecb8598b38afatagen2026-10-22 09:19 UTC276
          on:click={() => react(r.key, r.shortcode)}
544a170ead2catagen2026-10-20 11:47 UTC277
        >
544a170ead2catagen2026-10-20 11:47 UTC278
          {#if r.key.startsWith('mxc://')}
544a170ead2catagen2026-10-20 11:47 UTC279
            <img class="emote" src={mediaUrl(r.key) ?? ''} alt="" />
544a170ead2catagen2026-10-20 11:47 UTC280
          {:else}
544a170ead2catagen2026-10-20 11:47 UTC281
            <Emoji emoji={r.key} size={18} />
544a170ead2catagen2026-10-20 11:47 UTC282
          {/if}
544a170ead2catagen2026-10-20 11:47 UTC283
        </button>
544a170ead2catagen2026-10-20 11:47 UTC284
      {/each}
544a170ead2catagen2026-10-20 11:47 UTC285
      <button
544a170ead2catagen2026-10-20 11:47 UTC286
        type="button"
544a170ead2catagen2026-10-20 11:47 UTC287
        title="Add reaction"
544a170ead2catagen2026-10-20 11:47 UTC288
        aria-label={`Add reaction to message from ${displayName}`}
544a170ead2catagen2026-10-20 11:47 UTC289
        aria-haspopup="dialog"
544a170ead2catagen2026-10-20 11:47 UTC290
        aria-expanded={pickerOpen}
544a170ead2catagen2026-10-20 11:47 UTC291
        on:pointerdown|stopPropagation
544a170ead2catagen2026-10-20 11:47 UTC292
        on:click={openPicker}
544a170ead2catagen2026-10-20 11:47 UTC293
      >
544a170ead2catagen2026-10-20 11:47 UTC294
        ☺+
544a170ead2catagen2026-10-20 11:47 UTC295
      </button>
d095131eec0eatagen2026-10-24 08:39 UTC296
      {#if onCloneSticker && event.content.kind === 'Sticker'}
d095131eec0eatagen2026-10-24 08:39 UTC297
        {@const stickerBody = event.content.body}
d095131eec0eatagen2026-10-24 08:39 UTC298
        {@const stickerId = event.event_id}
d095131eec0eatagen2026-10-24 08:39 UTC299
        <button
d095131eec0eatagen2026-10-24 08:39 UTC300
          type="button"
d095131eec0eatagen2026-10-24 08:39 UTC301
          title="Clone sticker"
d095131eec0eatagen2026-10-24 08:39 UTC302
          aria-label="Clone sticker to one of your packs"
d095131eec0eatagen2026-10-24 08:39 UTC303
          on:click|stopPropagation={(ev) => onCloneSticker?.(stickerId, stickerBody, ev.clientX, ev.clientY)}
d095131eec0eatagen2026-10-24 08:39 UTC304
        >
d095131eec0eatagen2026-10-24 08:39 UTC305
          ⧉
d095131eec0eatagen2026-10-24 08:39 UTC306
        </button>
d095131eec0eatagen2026-10-24 08:39 UTC307
      {/if}
df3c0f2b3da4atagen2026-10-22 13:07 UTC308
      {#if onReply}
df3c0f2b3da4atagen2026-10-22 13:07 UTC309
        <button
df3c0f2b3da4atagen2026-10-22 13:07 UTC310
          type="button"
df3c0f2b3da4atagen2026-10-22 13:07 UTC311
          title="Reply"
df3c0f2b3da4atagen2026-10-22 13:07 UTC312
          aria-label={`Reply to message from ${displayName}`}
df3c0f2b3da4atagen2026-10-22 13:07 UTC313
          on:click|stopPropagation={reply}
df3c0f2b3da4atagen2026-10-22 13:07 UTC314
        >
df3c0f2b3da4atagen2026-10-22 13:07 UTC315
          ↩
df3c0f2b3da4atagen2026-10-22 13:07 UTC316
        </button>
df3c0f2b3da4atagen2026-10-22 13:07 UTC317
      {/if}
cf23dbf4e77catagen2026-10-23 06:36 UTC318
      {#if onEdit && editableBody !== null}
cf23dbf4e77catagen2026-10-23 06:36 UTC319
        <button type="button" title="Edit" aria-label="Edit your message" on:click|stopPropagation={edit}>
cf23dbf4e77catagen2026-10-23 06:36 UTC320
          ✎
cf23dbf4e77catagen2026-10-23 06:36 UTC321
        </button>
cf23dbf4e77catagen2026-10-23 06:36 UTC322
      {/if}
0380cd8c765catagen2026-10-24 09:08 UTC323
      {#if onMore}
0380cd8c765catagen2026-10-24 09:08 UTC324
        <button
0380cd8c765catagen2026-10-24 09:08 UTC325
          type="button"
0380cd8c765catagen2026-10-24 09:08 UTC326
          title="More"
0380cd8c765catagen2026-10-24 09:08 UTC327
          aria-label={`More actions for message from ${displayName}`}
0380cd8c765catagen2026-10-24 09:08 UTC328
          aria-haspopup="menu"
0380cd8c765catagen2026-10-24 09:08 UTC329
          on:click|stopPropagation={(ev) => onMore?.(event, ev.clientX, ev.clientY)}
0380cd8c765catagen2026-10-24 09:08 UTC330
        >
0380cd8c765catagen2026-10-24 09:08 UTC331
          ⋯
0380cd8c765catagen2026-10-24 09:08 UTC332
        </button>
0380cd8c765catagen2026-10-24 09:08 UTC333
      {/if}
544a170ead2catagen2026-10-20 11:47 UTC334
    </div>
544a170ead2catagen2026-10-20 11:47 UTC335
    {#if pickerOpen}
544a170ead2catagen2026-10-20 11:47 UTC336
      <div class="picker-pop" style={pickerStyle} bind:this={pickerPop}>
ecb8598b38afatagen2026-10-22 09:19 UTC337
        <ReactionPicker {packs} quick={quickReactions} onPick={pick} {onQuickChange} onClose={closePicker} />
544a170ead2catagen2026-10-20 11:47 UTC338
      </div>
544a170ead2catagen2026-10-20 11:47 UTC339
    {/if}
544a170ead2catagen2026-10-20 11:47 UTC340
  {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC341
  {#if showSender}
d8624c214c04atagen2026-10-24 08:24 UTC342
    <button
d8624c214c04atagen2026-10-24 08:24 UTC343
      type="button"
d8624c214c04atagen2026-10-24 08:24 UTC344
      class="user-hit"
668ab5c30531atagen2026-10-25 03:13 UTC345
      disabled={!onUser}
d8624c214c04atagen2026-10-24 08:24 UTC346
      tabindex="-1"
d8624c214c04atagen2026-10-24 08:24 UTC347
      aria-hidden="true"
d8624c214c04atagen2026-10-24 08:24 UTC348
      on:click|stopPropagation={(ev) => openUser(ev, false)}
d8624c214c04atagen2026-10-24 08:24 UTC349
      on:contextmenu|preventDefault|stopPropagation={(ev) => openUser(ev, true)}
d8624c214c04atagen2026-10-24 08:24 UTC350
    >
d8624c214c04atagen2026-10-24 08:24 UTC351
      {#if senderAvatar}
d8624c214c04atagen2026-10-24 08:24 UTC352
        <MxcAvatar mxc={senderAvatar} label={name} size={34} />
d8624c214c04atagen2026-10-24 08:24 UTC353
      {:else}
d8624c214c04atagen2026-10-24 08:24 UTC354
        <span class="avatar" style={`background:hsl(${senderHue(event.sender.user_id)} 35% 32%)`}>
445bf199a9cfatagen2026-10-25 02:56 UTC355
          <span><EmojiText text={initialsOf(name)} /></span>
d8624c214c04atagen2026-10-24 08:24 UTC356
        </span>
d8624c214c04atagen2026-10-24 08:24 UTC357
      {/if}
d8624c214c04atagen2026-10-24 08:24 UTC358
    </button>
973f8fbdaf80atagen2026-09-28 09:10 UTC359
  {:else}
973f8fbdaf80atagen2026-09-28 09:10 UTC360
    <div class="avatar-spacer" aria-hidden="true">
973f8fbdaf80atagen2026-09-28 09:10 UTC361
      <span class="hover-time">{formatClock(event.timestamp_ms)}</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC362
    </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC363
  {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC364
  <div class="msg-body">
973f8fbdaf80atagen2026-09-28 09:10 UTC365
    {#if showSender}
973f8fbdaf80atagen2026-09-28 09:10 UTC366
      <div class="msg-head">
d8624c214c04atagen2026-10-24 08:24 UTC367
        <button
d8624c214c04atagen2026-10-24 08:24 UTC368
          type="button"
d8624c214c04atagen2026-10-24 08:24 UTC369
          class="sender"
668ab5c30531atagen2026-10-25 03:13 UTC370
          disabled={!onUser}
d8624c214c04atagen2026-10-24 08:24 UTC371
          style={`color:hsl(${senderHue(event.sender.user_id)} 70% var(--name-lightness))`}
d8624c214c04atagen2026-10-24 08:24 UTC372
          on:click|stopPropagation={(ev) => openUser(ev, false)}
d8624c214c04atagen2026-10-24 08:24 UTC373
          on:contextmenu|preventDefault|stopPropagation={(ev) => openUser(ev, true)}
d8624c214c04atagen2026-10-24 08:24 UTC374
        >
445bf199a9cfatagen2026-10-25 02:56 UTC375
          <EmojiText text={displayName} />
d8624c214c04atagen2026-10-24 08:24 UTC376
        </button>
973f8fbdaf80atagen2026-09-28 09:10 UTC377
        <time class="time" datetime={new Date(event.timestamp_ms).toISOString()}>
973f8fbdaf80atagen2026-09-28 09:10 UTC378
          {formatDay(event.timestamp_ms)} {formatClock(event.timestamp_ms)}
973f8fbdaf80atagen2026-09-28 09:10 UTC379
        </time>
973f8fbdaf80atagen2026-09-28 09:10 UTC380
        {#if event.content.kind === 'Message' && event.content.message.edited}
973f8fbdaf80atagen2026-09-28 09:10 UTC381
          <span class="edited" title="Edited">(edited)</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC382
        {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC383
      </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC384
    {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC385
    {#if event.reply_to}
df3c0f2b3da4atagen2026-10-22 13:07 UTC386
      {@const target = event.reply_to.event_id}
df3c0f2b3da4atagen2026-10-22 13:07 UTC387
      <button
df3c0f2b3da4atagen2026-10-22 13:07 UTC388
        type="button"
df3c0f2b3da4atagen2026-10-22 13:07 UTC389
        class="reply-quote"
df3c0f2b3da4atagen2026-10-22 13:07 UTC390
        disabled={!onJump}
df3c0f2b3da4atagen2026-10-22 13:07 UTC391
        title="Jump to the original message"
df3c0f2b3da4atagen2026-10-22 13:07 UTC392
        on:click|stopPropagation={() => onJump?.(target)}
df3c0f2b3da4atagen2026-10-22 13:07 UTC393
      >
973f8fbdaf80atagen2026-09-28 09:10 UTC394
        {#if event.reply_to.details.kind === 'Ready'}
445bf199a9cfatagen2026-10-25 02:56 UTC395
          <span class="reply-sender"><EmojiText text={event.reply_to.details.sender.display_name ?? people[event.reply_to.details.sender.user_id]?.name ?? senderLabel(event.reply_to.details.sender)} /></span>
df3c0f2b3da4atagen2026-10-22 13:07 UTC396
          <span class="reply-text"><EmojiText text={replyPreview(event.reply_to.details.content)} /></span>
973f8fbdaf80atagen2026-09-28 09:10 UTC397
        {:else if event.reply_to.details.kind === 'Pending'}
973f8fbdaf80atagen2026-09-28 09:10 UTC398
          Loading reply…
973f8fbdaf80atagen2026-09-28 09:10 UTC399
        {:else}
973f8fbdaf80atagen2026-09-28 09:10 UTC400
          Reply unavailable
973f8fbdaf80atagen2026-09-28 09:10 UTC401
        {/if}
df3c0f2b3da4atagen2026-10-22 13:07 UTC402
      </button>
973f8fbdaf80atagen2026-09-28 09:10 UTC403
    {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC404
973f8fbdaf80atagen2026-09-28 09:10 UTC405
    {#if event.content.kind === 'Message'}
973f8fbdaf80atagen2026-09-28 09:10 UTC406
      {@const m = event.content.message}
705b1e1062c2atagen2026-10-23 09:05 UTC407
      {#if stripped.text}
445bf199a9cfatagen2026-10-25 02:56 UTC408
        <div class="text" class:emote={m.kind === 'Emote'} class:notice={m.kind === 'Notice' || m.kind === 'ServerNotice'}>{#if m.kind === 'Emote'}* <EmojiText text={displayName} /> {/if}<MessageText text={stripped.text} html={m.formatted_body} jumbo={m.kind === 'Text'} {mentions} {self} nameOf={(id) => people[id]?.name ?? null} />{#if m.edited && !showSender}<span class="edited" title="Edited">(edited)</span>{/if}</div>
df3c0f2b3da4atagen2026-10-22 13:07 UTC409
      {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC410
      {#if m.media}
973f8fbdaf80atagen2026-09-28 09:10 UTC411
        <MediaAttachmentView attachment={m.media} />
973f8fbdaf80atagen2026-09-28 09:10 UTC412
      {/if}
705b1e1062c2atagen2026-10-23 09:05 UTC413
      {#if cards.length > 0}
973f8fbdaf80atagen2026-09-28 09:10 UTC414
        <div class="link-previews">
705b1e1062c2atagen2026-10-23 09:05 UTC415
          {#each cards as card (card.link.href)}
705b1e1062c2atagen2026-10-23 09:05 UTC416
            <LinkCard link={card.link} preview={card.preview} />
973f8fbdaf80atagen2026-09-28 09:10 UTC417
          {/each}
973f8fbdaf80atagen2026-09-28 09:10 UTC418
        </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC419
      {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC420
    {:else if event.content.kind === 'Sticker'}
973f8fbdaf80atagen2026-09-28 09:10 UTC421
      {#if event.content.media}
973f8fbdaf80atagen2026-09-28 09:10 UTC422
        <MediaAttachmentView attachment={event.content.media} />
dfd1feaae855atagen2026-10-24 10:54 UTC423
      {:else if event.content.body}<p class="text"><EmojiText text={event.content.body} /></p>{/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC424
    {:else if event.content.kind === 'Poll'}
445bf199a9cfatagen2026-10-25 02:56 UTC425
      <p class="text"><EmojiText text={`📊 ${event.content.question}`} /></p>
973f8fbdaf80atagen2026-09-28 09:10 UTC426
    {:else}
445bf199a9cfatagen2026-10-25 02:56 UTC427
      <p class="system-line"><EmojiText text={systemLine(event.content, displayName)} /></p>
973f8fbdaf80atagen2026-09-28 09:10 UTC428
    {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC429
973f8fbdaf80atagen2026-09-28 09:10 UTC430
    {#if interactive && event.reactions.length > 0}
973f8fbdaf80atagen2026-09-28 09:10 UTC431
      <div class="reactions" aria-label="Reactions">
973f8fbdaf80atagen2026-09-28 09:10 UTC432
        {#each event.reactions as r (r.key)}
973f8fbdaf80atagen2026-09-28 09:10 UTC433
          <button
973f8fbdaf80atagen2026-09-28 09:10 UTC434
            type="button"
973f8fbdaf80atagen2026-09-28 09:10 UTC435
            class="reaction"
973f8fbdaf80atagen2026-09-28 09:10 UTC436
            class:mine={r.own}
d4c0d65f690aatagen2026-10-24 12:50 UTC437
            aria-label={`${r.shortcode ? `:${r.shortcode}:` : r.key.startsWith('mxc://') ? 'custom emote' : r.key}, ${r.count} reaction${r.count === 1 ? '' : 's'}`}
668ab5c30531atagen2026-10-25 03:13 UTC438
            disabled={readOnly || !event.event_id || reacting}
973f8fbdaf80atagen2026-09-28 09:10 UTC439
            on:click={() => event.event_id && onReact(event.event_id, r.key)}
973f8fbdaf80atagen2026-09-28 09:10 UTC440
          >
ecb8598b38afatagen2026-10-22 09:19 UTC441
            {#if r.image}
ecb8598b38afatagen2026-10-22 09:19 UTC442
              <img class="emote" src={messageMediaUrl(r.image, 'Thumbnail')} alt={r.shortcode ?? ''} title={r.shortcode ?? ''} />
ecb8598b38afatagen2026-10-22 09:19 UTC443
            {:else if r.key.startsWith('mxc://')}
544a170ead2catagen2026-10-20 11:47 UTC444
              <img class="emote" src={mediaUrl(r.key) ?? ''} alt="" />
544a170ead2catagen2026-10-20 11:47 UTC445
            {:else}
544a170ead2catagen2026-10-20 11:47 UTC446
              <Emoji emoji={r.key} size={16} />
544a170ead2catagen2026-10-20 11:47 UTC447
            {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC448
            <span class="count">{r.count}</span>
cf23dbf4e77catagen2026-10-23 06:36 UTC449
            <span class="reactors" role="tooltip" id={`${itemId}-reactors-${r.key}`}>
2021154f9e16atagen2026-10-25 02:37 UTC450
              <span class="reactors-key"><EmojiText text={r.shortcode ? `:${r.shortcode}:` : r.key.startsWith('mxc://') ? 'custom emote' : r.key} /></span>
2021154f9e16atagen2026-10-25 02:37 UTC451
              <EmojiText text={reactorNames(r.senders)} />
cf23dbf4e77catagen2026-10-23 06:36 UTC452
            </span>
973f8fbdaf80atagen2026-09-28 09:10 UTC453
          </button>
973f8fbdaf80atagen2026-09-28 09:10 UTC454
        {/each}
973f8fbdaf80atagen2026-09-28 09:10 UTC455
      </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC456
    {/if}
f604ef6df0c4atagen2026-10-24 10:08 UTC457
    {#if sent}<span class="sent-tick" title="Sent" aria-label="Sent">✓</span>{/if}
df3c0f2b3da4atagen2026-10-22 13:07 UTC458
    {#if interactive && sendLabel(event.send_state)}
973f8fbdaf80atagen2026-09-28 09:10 UTC459
      <div class="msg-foot">
df3c0f2b3da4atagen2026-10-22 13:07 UTC460
        <span class="sendstate" class:failed={event.send_state.kind === 'Failed'}>{sendLabel(event.send_state)}</span>
df3c0f2b3da4atagen2026-10-22 13:07 UTC461
      </div>
df3c0f2b3da4atagen2026-10-22 13:07 UTC462
    {/if}
df3c0f2b3da4atagen2026-10-22 13:07 UTC463
    {#if interactive && readers.length > 0}
df3c0f2b3da4atagen2026-10-22 13:07 UTC464
      {@const seenBy = `Seen by ${readers.map((r) => r.name).join(', ')}`}
df3c0f2b3da4atagen2026-10-22 13:07 UTC465
      <div class="readby" title={seenBy} aria-label={seenBy} role="img">
df3c0f2b3da4atagen2026-10-22 13:07 UTC466
        {#each readers.slice(0, MAX_READERS) as r (r.user_id)}
df3c0f2b3da4atagen2026-10-22 13:07 UTC467
          <MxcAvatar mxc={r.avatar} label={r.name} size={16} />
df3c0f2b3da4atagen2026-10-22 13:07 UTC468
        {/each}
df3c0f2b3da4atagen2026-10-22 13:07 UTC469
        {#if readers.length > MAX_READERS}<span class="more">+{readers.length - MAX_READERS}</span>{/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC470
      </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC471
    {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC472
  </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC473
</div>
973f8fbdaf80atagen2026-09-28 09:10 UTC474
973f8fbdaf80atagen2026-09-28 09:10 UTC475
<style>
df3c0f2b3da4atagen2026-10-22 13:07 UTC476
  .msg { position: relative; display: flex; gap: 10px; padding: var(--msg-gap, 3px) 0; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC477
  .msg:hover, .msg.selected { background: var(--hover-faint); }
544a170ead2catagen2026-10-20 11:47 UTC478
  .msg:hover .hover-time, .msg.selected .hover-time { opacity: 1; }
544a170ead2catagen2026-10-20 11:47 UTC479
  .msg-foot { display: flex; align-items: center; gap: 8px; }
973f8fbdaf80atagen2026-09-28 09:10 UTC480
  .sendstate { font-size: 11px; color: var(--muted); }
f604ef6df0c4atagen2026-10-24 10:08 UTC481
  .sent-tick { position: absolute; right: 12px; bottom: 4px; font-size: 11px; color: var(--muted); }
973f8fbdaf80atagen2026-09-28 09:10 UTC482
  .sendstate.failed { color: var(--danger); }
973f8fbdaf80atagen2026-09-28 09:10 UTC483
  .link-previews { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; max-width: 420px; }
973f8fbdaf80atagen2026-09-28 09:10 UTC484
  .avatar {
973f8fbdaf80atagen2026-09-28 09:10 UTC485
    flex: none;
973f8fbdaf80atagen2026-09-28 09:10 UTC486
    width: 34px; height: 34px;
973f8fbdaf80atagen2026-09-28 09:10 UTC487
    border-radius: 50%;
973f8fbdaf80atagen2026-09-28 09:10 UTC488
    display: grid; place-items: center;
973f8fbdaf80atagen2026-09-28 09:10 UTC489
    font-size: 12px; font-weight: 700; color: #fff;
973f8fbdaf80atagen2026-09-28 09:10 UTC490
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC491
  .avatar-spacer { flex: none; width: 34px; padding-top: 2px; text-align: center; }
973f8fbdaf80atagen2026-09-28 09:10 UTC492
  .hover-time { opacity: 0; font-size: 10.5px; color: var(--muted); }
973f8fbdaf80atagen2026-09-28 09:10 UTC493
  .msg-body { min-width: 0; flex: 1; }
973f8fbdaf80atagen2026-09-28 09:10 UTC494
  .msg-head { display: flex; align-items: baseline; gap: 8px; }
d8624c214c04atagen2026-10-24 08:24 UTC495
  .sender { font-weight: 650; font-size: 13.5px; padding: 0; border: none; background: none; font-family: inherit; cursor: pointer; }
d8624c214c04atagen2026-10-24 08:24 UTC496
  .sender:hover { text-decoration: underline; }
d8624c214c04atagen2026-10-24 08:24 UTC497
  .user-hit { flex: none; align-self: flex-start; padding: 0; border: none; background: none; border-radius: 50%; cursor: pointer; }
973f8fbdaf80atagen2026-09-28 09:10 UTC498
  .time, .edited { color: var(--muted); font-size: 11.5px; }
cf23dbf4e77catagen2026-10-23 06:36 UTC499
  .text .edited { margin-left: 6px; }
cf23dbf4e77catagen2026-10-23 06:36 UTC500
  .msg.editing { background: color-mix(in srgb, var(--accent) 10%, transparent); }
705b1e1062c2atagen2026-10-23 09:05 UTC501
  .msg.mentioned { background: color-mix(in srgb, var(--warn) 8%, transparent); box-shadow: inset 2px 0 0 var(--warn); }
973f8fbdaf80atagen2026-09-28 09:10 UTC502
  .reply-quote {
df3c0f2b3da4atagen2026-10-22 13:07 UTC503
    display: flex; gap: 6px; align-items: baseline;
df3c0f2b3da4atagen2026-10-22 13:07 UTC504
    max-width: min(100%, 520px);
df3c0f2b3da4atagen2026-10-22 13:07 UTC505
    border: none;
973f8fbdaf80atagen2026-09-28 09:10 UTC506
    border-left: 2px solid var(--accent);
973f8fbdaf80atagen2026-09-28 09:10 UTC507
    padding: 2px 8px;
973f8fbdaf80atagen2026-09-28 09:10 UTC508
    margin: 2px 0 4px;
973f8fbdaf80atagen2026-09-28 09:10 UTC509
    color: var(--muted);
973f8fbdaf80atagen2026-09-28 09:10 UTC510
    font-size: 12.5px;
df3c0f2b3da4atagen2026-10-22 13:07 UTC511
    text-align: left;
973f8fbdaf80atagen2026-09-28 09:10 UTC512
    background: var(--bg-soft);
05a48e9e7942atagen2026-10-24 09:32 UTC513
    border-radius: 0 var(--r-sm) var(--r-sm) 0;
df3c0f2b3da4atagen2026-10-22 13:07 UTC514
    cursor: pointer;
973f8fbdaf80atagen2026-09-28 09:10 UTC515
  }
df3c0f2b3da4atagen2026-10-22 13:07 UTC516
  .reply-quote:disabled { cursor: default; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC517
  .reply-quote:hover:not(:disabled) { color: var(--text); }
df3c0f2b3da4atagen2026-10-22 13:07 UTC518
  .reply-sender { flex: none; font-weight: 650; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC519
  .reply-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC520
  :global(.msg.flash) { animation: reply-flash 1.2s ease-out; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC521
  @keyframes -global-reply-flash { from { background: color-mix(in srgb, var(--accent) 22%, transparent); } }
973f8fbdaf80atagen2026-09-28 09:10 UTC522
  .text {
973f8fbdaf80atagen2026-09-28 09:10 UTC523
    margin: 1px 0 2px;
973f8fbdaf80atagen2026-09-28 09:10 UTC524
    font-size: 14px;
df3c0f2b3da4atagen2026-10-22 13:07 UTC525
    line-height: var(--msg-line-height, 1.5);
973f8fbdaf80atagen2026-09-28 09:10 UTC526
    white-space: pre-wrap;
973f8fbdaf80atagen2026-09-28 09:10 UTC527
    overflow-wrap: anywhere;
973f8fbdaf80atagen2026-09-28 09:10 UTC528
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC529
  .text.emote { font-style: italic; color: var(--muted); }
973f8fbdaf80atagen2026-09-28 09:10 UTC530
  .text.notice { color: var(--muted); }
973f8fbdaf80atagen2026-09-28 09:10 UTC531
  .system-line { color: var(--muted); font-size: 12.5px; font-style: italic; margin: 1px 0; }
f604ef6df0c4atagen2026-10-24 10:08 UTC532
  .msg.system { opacity: 0.85; --msg-gap: 1.5px; }
f604ef6df0c4atagen2026-10-24 10:08 UTC533
  .msg.system .avatar-spacer { height: 0; padding-top: 0; }
dfd1feaae855atagen2026-10-24 10:54 UTC534
  .reactions { display: flex; flex-wrap: wrap; gap: 4px; margin-top: max(4px, var(--msg-gap, 3px)); }
973f8fbdaf80atagen2026-09-28 09:10 UTC535
  .reaction {
973f8fbdaf80atagen2026-09-28 09:10 UTC536
    display: inline-flex; align-items: center; gap: 4px;
973f8fbdaf80atagen2026-09-28 09:10 UTC537
    background: var(--bg-soft);
973f8fbdaf80atagen2026-09-28 09:10 UTC538
    border: 1px solid var(--border);
05a48e9e7942atagen2026-10-24 09:32 UTC539
    border-radius: var(--r-sm);
973f8fbdaf80atagen2026-09-28 09:10 UTC540
    padding: 1px 8px;
973f8fbdaf80atagen2026-09-28 09:10 UTC541
    font-size: 13px;
973f8fbdaf80atagen2026-09-28 09:10 UTC542
    color: var(--text);
973f8fbdaf80atagen2026-09-28 09:10 UTC543
    cursor: pointer;
973f8fbdaf80atagen2026-09-28 09:10 UTC544
  }
cf23dbf4e77catagen2026-10-23 06:36 UTC545
  .reaction { position: relative; }
cf23dbf4e77catagen2026-10-23 06:36 UTC546
  .reactors {
cf23dbf4e77catagen2026-10-23 06:36 UTC547
    position: absolute;
cf23dbf4e77catagen2026-10-23 06:36 UTC548
    left: 0;
cf23dbf4e77catagen2026-10-23 06:36 UTC549
    bottom: calc(100% + 6px);
cf23dbf4e77catagen2026-10-23 06:36 UTC550
    z-index: 15;
cf23dbf4e77catagen2026-10-23 06:36 UTC551
    display: none;
cf23dbf4e77catagen2026-10-23 06:36 UTC552
    width: max-content;
cf23dbf4e77catagen2026-10-23 06:36 UTC553
    max-width: 260px;
cf23dbf4e77catagen2026-10-23 06:36 UTC554
    padding: 6px 9px;
cf23dbf4e77catagen2026-10-23 06:36 UTC555
    border: 1px solid var(--border);
05a48e9e7942atagen2026-10-24 09:32 UTC556
    border-radius: var(--r-md);
cf23dbf4e77catagen2026-10-23 06:36 UTC557
    background: var(--panel-2);
cf23dbf4e77catagen2026-10-23 06:36 UTC558
    box-shadow: 0 6px 18px rgb(0 0 0 / 0.3);
cf23dbf4e77catagen2026-10-23 06:36 UTC559
    color: var(--text);
cf23dbf4e77catagen2026-10-23 06:36 UTC560
    font-size: 12px;
cf23dbf4e77catagen2026-10-23 06:36 UTC561
    line-height: 1.35;
cf23dbf4e77catagen2026-10-23 06:36 UTC562
    text-align: left;
cf23dbf4e77catagen2026-10-23 06:36 UTC563
    white-space: normal;
cf23dbf4e77catagen2026-10-23 06:36 UTC564
    pointer-events: none;
cf23dbf4e77catagen2026-10-23 06:36 UTC565
  }
cf23dbf4e77catagen2026-10-23 06:36 UTC566
  .reactors-key { display: block; color: var(--muted); font-size: 11px; }
cf23dbf4e77catagen2026-10-23 06:36 UTC567
  .reaction:hover .reactors, .reaction:focus-visible .reactors { display: block; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC568
  .reaction.mine { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 16%, transparent); }
973f8fbdaf80atagen2026-09-28 09:10 UTC569
  .reaction:disabled { cursor: default; opacity: 0.85; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC570
  .msg.has-readers .msg-body { padding-right: 72px; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC571
  .readby { position: absolute; right: 12px; bottom: 4px; display: flex; align-items: center; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC572
  .readby > :global(* + *) { margin-left: -4px; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC573
  .readby :global(.mxc-avatar), .readby :global(.mxc-fallback) { box-shadow: 0 0 0 1.5px var(--bg); }
df3c0f2b3da4atagen2026-10-22 13:07 UTC574
  .readby .more { margin-left: 3px; font-size: 10.5px; color: var(--muted); }
544a170ead2catagen2026-10-20 11:47 UTC575
  .actions {
05a48e9e7942atagen2026-10-24 09:32 UTC576
    position: absolute; top: -12px; right: 10px; z-index: 2;
544a170ead2catagen2026-10-20 11:47 UTC577
    display: flex; gap: 2px;
544a170ead2catagen2026-10-20 11:47 UTC578
    padding: 2px;
544a170ead2catagen2026-10-20 11:47 UTC579
    background: var(--bg-soft);
544a170ead2catagen2026-10-20 11:47 UTC580
    border: 1px solid var(--border);
05a48e9e7942atagen2026-10-24 09:32 UTC581
    border-radius: var(--r-md);
544a170ead2catagen2026-10-20 11:47 UTC582
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
544a170ead2catagen2026-10-20 11:47 UTC583
    opacity: 0;
d4c0d65f690aatagen2026-10-24 12:50 UTC584
    visibility: hidden;
544a170ead2catagen2026-10-20 11:47 UTC585
    pointer-events: none;
544a170ead2catagen2026-10-20 11:47 UTC586
  }
d4c0d65f690aatagen2026-10-24 12:50 UTC587
  .msg:hover .actions, .msg:focus-within .actions, .msg.selected .actions { opacity: 1; visibility: visible; pointer-events: auto; }
d4c0d65f690aatagen2026-10-24 12:50 UTC588
  :global(.timeline-inner:has(.msg.selected)) .msg:not(.selected) .actions { opacity: 0; visibility: hidden; pointer-events: none; }
544a170ead2catagen2026-10-20 11:47 UTC589
  .actions button {
973f8fbdaf80atagen2026-09-28 09:10 UTC590
    background: transparent;
973f8fbdaf80atagen2026-09-28 09:10 UTC591
    border: 1px solid transparent;
05a48e9e7942atagen2026-10-24 09:32 UTC592
    border-radius: var(--r-sm);
973f8fbdaf80atagen2026-09-28 09:10 UTC593
    cursor: pointer;
544a170ead2catagen2026-10-20 11:47 UTC594
    font-size: 14px;
544a170ead2catagen2026-10-20 11:47 UTC595
    padding: 1px 5px;
973f8fbdaf80atagen2026-09-28 09:10 UTC596
  }
df3c0f2b3da4atagen2026-10-22 13:07 UTC597
  .actions button:hover:not(:disabled) { border-color: var(--border); background: var(--hover); }
544a170ead2catagen2026-10-20 11:47 UTC598
  .actions button:disabled { opacity: 0.4; cursor: default; }
544a170ead2catagen2026-10-20 11:47 UTC599
  .emote { width: 1.3em; height: 1.3em; object-fit: contain; vertical-align: middle; }
05a48e9e7942atagen2026-10-24 09:32 UTC600
  .picker-pop { position: fixed; z-index: 60; width: 320px; max-width: calc(100vw - 16px); animation: pop-in var(--pop); }
973f8fbdaf80atagen2026-09-28 09:10 UTC601
</style>