atagen

malice

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

Blame: web/src/lib/MessageRow.svelte

d07bca5da4aeatagen2026-10-26 03:48 UTC1
<svelte:options immutable />
d07bca5da4aeatagen2026-10-26 03:48 UTC2
973f8fbdaf80atagen2026-09-28 09:10 UTC3
<script lang="ts">
973f8fbdaf80atagen2026-09-28 09:10 UTC4
  import MediaAttachmentView from './MediaAttachmentView.svelte';
973f8fbdaf80atagen2026-09-28 09:10 UTC5
  import MxcAvatar from './MxcAvatar.svelte';
544a170ead2catagen2026-10-20 11:47 UTC6
  import Emoji from './Emoji.svelte';
544a170ead2catagen2026-10-20 11:47 UTC7
  import EmojiText from './EmojiText.svelte';
df3c0f2b3da4atagen2026-10-22 13:07 UTC8
  import MessageText from './MessageText.svelte';
20fe182f3204atagen2026-10-26 05:50 UTC9
  import { longpress } from './longpress';
544a170ead2catagen2026-10-20 11:47 UTC10
  import ReactionPicker from './ReactionPicker.svelte';
20fe182f3204atagen2026-10-26 05:50 UTC11
  import { backStack, instanceLayerId } from './back-stack';
b8ab00377c20atagen2026-10-22 05:13 UTC12
  import LinkCard from './LinkCard.svelte';
d07bca5da4aeatagen2026-10-26 03:48 UTC13
  import { afterUpdate, onDestroy } from 'svelte';
d07bca5da4aeatagen2026-10-26 03:48 UTC14
  import { dissolve, expand, rowIn, rowOut, settleHeight } from './motion';
d07bca5da4aeatagen2026-10-26 03:48 UTC15
  import { selectedRow } from './selected-row';
ecb8598b38afatagen2026-10-22 09:19 UTC16
  import { mediaUrl, messageMediaUrl } from './api';
a4555a7eae2datagen2026-10-25 09:18 UTC17
  import { keywordMatcher } from './keywords';
5059ed753f8batagen2026-10-27 07:20 UTC18
  import type { ImagePack, LinkPreview, Person, QuickEntry, ReplyReference, SendState, TimelineContent, TimelineEvent } from './types';
8c81366a752fatagen2026-10-26 03:56 UTC19
  import { QUICK_REACTIONS_PER_ROW, senderLabel } from './types';
5059ed753f8batagen2026-10-27 07:20 UTC20
  import { quickColumns, quickGroups } from './quick-reactions-model';
973f8fbdaf80atagen2026-09-28 09:10 UTC21
  import { formatClock, formatDay, initialsOf, senderHue } from './format';
973f8fbdaf80atagen2026-09-28 09:10 UTC22
  import { findUrls } from './urls';
787cdbbf0721atagen2026-10-24 06:50 UTC23
  import { mentionsFromHtml, resolveMentions, stripMarkdownPills } from './mentions';
973f8fbdaf80atagen2026-09-28 09:10 UTC24
973f8fbdaf80atagen2026-09-28 09:10 UTC25
  export let itemId: string;
973f8fbdaf80atagen2026-09-28 09:10 UTC26
  export let event: TimelineEvent;
973f8fbdaf80atagen2026-09-28 09:10 UTC27
  export let showSender: boolean;
973f8fbdaf80atagen2026-09-28 09:10 UTC28
  export let reacting: boolean;
ecb8598b38afatagen2026-10-22 09:19 UTC29
  export let onReact: (eventId: string, key: string, shortcode?: string) => void;
973f8fbdaf80atagen2026-09-28 09:10 UTC30
  export let senderAvatar: string | null = null;
5059ed753f8batagen2026-10-27 07:20 UTC31
  export let quickReactions: QuickEntry[] = [];
544a170ead2catagen2026-10-20 11:47 UTC32
  export let packs: ImagePack[] = [];
5059ed753f8batagen2026-10-27 07:20 UTC33
  export let onQuickChange: (list: QuickEntry[]) => void = () => {};
ecb8598b38afatagen2026-10-22 09:19 UTC34
  export let linkPreviews: Record<string, LinkPreview | null> = {};
ecb8598b38afatagen2026-10-22 09:19 UTC35
  export let loadPreview: ((url: string) => void) | null = null;
df3c0f2b3da4atagen2026-10-22 13:07 UTC36
  export let people: Record<string, Person> = {};
df3c0f2b3da4atagen2026-10-22 13:07 UTC37
  export let onReply: ((target: ReplyReference) => void) | null = null;
df3c0f2b3da4atagen2026-10-22 13:07 UTC38
  export let onJump: ((eventId: string) => void) | null = null;
cf23dbf4e77catagen2026-10-23 06:36 UTC39
  export let onEdit: ((eventId: string, body: string) => void) | null = null;
cf23dbf4e77catagen2026-10-23 06:36 UTC40
  export let editing = false;
df3c0f2b3da4atagen2026-10-22 13:07 UTC41
  export let showSent = true;
668ab5c30531atagen2026-10-25 03:13 UTC42
  export let readOnly = false;
705b1e1062c2atagen2026-10-23 09:05 UTC43
  export let self: string | null = null;
d8624c214c04atagen2026-10-24 08:24 UTC44
  export let onUser: ((userId: string, x: number, y: number, menu: boolean) => void) | null = null;
d095131eec0eatagen2026-10-24 08:39 UTC45
  export let onCloneSticker: ((eventId: string, body: string, x: number, y: number) => void) | null = null;
0380cd8c765catagen2026-10-24 09:08 UTC46
  export let onMore: ((event: TimelineEvent, x: number, y: number) => void) | null = null;
7ebc2472a9e3atagen2026-10-25 09:53 UTC47
  export let onOpenThread: ((rootEventId: string) => void) | null = null;
d8624c214c04atagen2026-10-24 08:24 UTC48
d2a34d0f4861atagen2026-10-25 07:29 UTC49
  $: lag = lagLabel(event);
7ebc2472a9e3atagen2026-10-25 09:53 UTC50
  $: threadRoot = onOpenThread && interactive && event.event_id && !event.thread_root ? event.event_id : null;
7ebc2472a9e3atagen2026-10-25 09:53 UTC51
  $: threadReplies = event.thread_summary?.reply_count ?? 0;
d2a34d0f4861atagen2026-10-25 07:29 UTC52
d2a34d0f4861atagen2026-10-25 07:29 UTC53
  function lagLabel(event: TimelineEvent): string | null {
d2a34d0f4861atagen2026-10-25 07:29 UTC54
    const lag = event.delivery_lag_ms;
d2a34d0f4861atagen2026-10-25 07:29 UTC55
    if (!lag) return null;
d2a34d0f4861atagen2026-10-25 07:29 UTC56
    if (lag < 60 * 60 * 1000) return `${Math.max(1, Math.floor(lag / 60000))}m`;
d2a34d0f4861atagen2026-10-25 07:29 UTC57
    if (lag < 24 * 60 * 60 * 1000) return `${Math.floor(lag / 3600000)}h`;
d2a34d0f4861atagen2026-10-25 07:29 UTC58
    return `${Math.floor(lag / 86400000)}d`;
d2a34d0f4861atagen2026-10-25 07:29 UTC59
  }
d2a34d0f4861atagen2026-10-25 07:29 UTC60
20fe182f3204atagen2026-10-26 05:50 UTC61
  function openUser(ev: Pick<MouseEvent, 'clientX' | 'clientY' | 'currentTarget'>, menu: boolean): void {
d8624c214c04atagen2026-10-24 08:24 UTC62
    if (!onUser) return;
d8624c214c04atagen2026-10-24 08:24 UTC63
    const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
d8624c214c04atagen2026-10-24 08:24 UTC64
    onUser(event.sender.user_id, menu ? ev.clientX : rect.left, menu ? ev.clientY : rect.bottom + 4, menu);
d8624c214c04atagen2026-10-24 08:24 UTC65
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC66
544a170ead2catagen2026-10-20 11:47 UTC67
  const PICKER_WIDTH = 320;
544a170ead2catagen2026-10-20 11:47 UTC68
  const PICKER_HEIGHT = 380;
544a170ead2catagen2026-10-20 11:47 UTC69
  const PICKER_MARGIN = 8;
df3c0f2b3da4atagen2026-10-22 13:07 UTC70
  const MAX_READERS = 3;
05a48e9e7942atagen2026-10-24 09:32 UTC71
  const STATE_LABELS: Record<string, string> = {
05a48e9e7942atagen2026-10-24 09:32 UTC72
    'm.room.pinned_events': 'changed the pinned messages',
05a48e9e7942atagen2026-10-24 09:32 UTC73
    'm.room.power_levels': 'changed the room permissions',
05a48e9e7942atagen2026-10-24 09:32 UTC74
    'm.room.join_rules': 'changed who can join',
05a48e9e7942atagen2026-10-24 09:32 UTC75
    'm.room.history_visibility': 'changed who can read history',
05a48e9e7942atagen2026-10-24 09:32 UTC76
    'm.room.guest_access': 'changed guest access',
05a48e9e7942atagen2026-10-24 09:32 UTC77
    'm.room.canonical_alias': 'changed the room address',
05a48e9e7942atagen2026-10-24 09:32 UTC78
    'm.room.server_acl': 'changed the server access list',
05a48e9e7942atagen2026-10-24 09:32 UTC79
    'm.space.child': 'changed the rooms in this space',
05a48e9e7942atagen2026-10-24 09:32 UTC80
    'm.space.parent': 'changed the parent space',
05a48e9e7942atagen2026-10-24 09:32 UTC81
    'im.ponies.room_emotes': 'updated a sticker pack'
05a48e9e7942atagen2026-10-24 09:32 UTC82
  };
544a170ead2catagen2026-10-20 11:47 UTC83
d07bca5da4aeatagen2026-10-26 03:48 UTC84
  const owner = Symbol();
544a170ead2catagen2026-10-20 11:47 UTC85
  let row: HTMLDivElement;
544a170ead2catagen2026-10-20 11:47 UTC86
  let pickerPop: HTMLDivElement | undefined;
d07bca5da4aeatagen2026-10-26 03:48 UTC87
  let hovered = false;
d07bca5da4aeatagen2026-10-26 03:48 UTC88
  let focused = false;
d07bca5da4aeatagen2026-10-26 03:48 UTC89
  let reactorKey: string | null = null;
20fe182f3204atagen2026-10-26 05:50 UTC90
  let reactorPinned = false;
544a170ead2catagen2026-10-20 11:47 UTC91
  let pickerOpen = false;
544a170ead2catagen2026-10-20 11:47 UTC92
  let pickerStyle = '';
973f8fbdaf80atagen2026-09-28 09:10 UTC93
973f8fbdaf80atagen2026-09-28 09:10 UTC94
  const MEMBERSHIP_LABELS: Record<string, string> = {
973f8fbdaf80atagen2026-09-28 09:10 UTC95
    Joined: 'joined the room',
973f8fbdaf80atagen2026-09-28 09:10 UTC96
    Left: 'left the room',
973f8fbdaf80atagen2026-09-28 09:10 UTC97
    Banned: 'was banned',
973f8fbdaf80atagen2026-09-28 09:10 UTC98
    Unbanned: 'was unbanned',
973f8fbdaf80atagen2026-09-28 09:10 UTC99
    Kicked: 'was kicked',
973f8fbdaf80atagen2026-09-28 09:10 UTC100
    KickedAndBanned: 'was kicked and banned',
973f8fbdaf80atagen2026-09-28 09:10 UTC101
    Invited: 'was invited',
973f8fbdaf80atagen2026-09-28 09:10 UTC102
    InvitationAccepted: 'accepted the invite',
973f8fbdaf80atagen2026-09-28 09:10 UTC103
    InvitationRejected: 'rejected the invite',
973f8fbdaf80atagen2026-09-28 09:10 UTC104
    InvitationRevoked: 'had the invite revoked',
973f8fbdaf80atagen2026-09-28 09:10 UTC105
    Knocked: 'requested to join',
973f8fbdaf80atagen2026-09-28 09:10 UTC106
    KnockAccepted: 'was let in',
973f8fbdaf80atagen2026-09-28 09:10 UTC107
    KnockRetracted: 'withdrew the request to join',
973f8fbdaf80atagen2026-09-28 09:10 UTC108
    KnockDenied: 'had the request to join denied'
973f8fbdaf80atagen2026-09-28 09:10 UTC109
  };
973f8fbdaf80atagen2026-09-28 09:10 UTC110
f604ef6df0c4atagen2026-10-24 10:08 UTC111
  $: name = event.sender.display_name ?? people[event.sender.user_id]?.name ?? event.sender.user_id;
973f8fbdaf80atagen2026-09-28 09:10 UTC112
  $: displayName = event.sender.name_ambiguous ? `${name} (${event.sender.user_id})` : name;
df3c0f2b3da4atagen2026-10-22 13:07 UTC113
  $: readers = event.read_by.map((id) => people[id] ?? { user_id: id, name: id, avatar: null });
973f8fbdaf80atagen2026-09-28 09:10 UTC114
  $: interactive = event.content.kind === 'Message' || event.content.kind === 'Sticker' || event.content.kind === 'Poll';
705b1e1062c2atagen2026-10-23 09:05 UTC115
  $: message = event.content.kind === 'Message' ? event.content.message : null;
d07bca5da4aeatagen2026-10-26 03:48 UTC116
  $: rawText = message ? (message.media ? message.media.caption : message.body) ?? '' : '';
d07bca5da4aeatagen2026-10-26 03:48 UTC117
  $: formattedBody = message?.formatted_body ?? null;
d07bca5da4aeatagen2026-10-26 03:48 UTC118
  $: hasMessage = message !== null;
d07bca5da4aeatagen2026-10-26 03:48 UTC119
  $: stripped = stripMarkdownPills(rawText);
d07bca5da4aeatagen2026-10-26 03:48 UTC120
  $: linked = hasMessage ? [...mentionsFromHtml(formattedBody), ...stripped.mentions] : [];
d07bca5da4aeatagen2026-10-26 03:48 UTC121
  $: nameOf = (id: string) => people[id]?.name ?? null;
787cdbbf0721atagen2026-10-24 06:50 UTC122
  $: mentions = resolveMentions(stripped.text, linked);
787cdbbf0721atagen2026-10-24 06:50 UTC123
  $: mentionsMe = self !== null && [...linked, ...mentions].some((m) => m.user_id === self);
d07bca5da4aeatagen2026-10-26 03:48 UTC124
  $: own = event.own;
d07bca5da4aeatagen2026-10-26 03:48 UTC125
  $: highlighted = event.highlight === true;
d07bca5da4aeatagen2026-10-26 03:48 UTC126
  $: keywordHit = !own && (highlighted || (hasMessage && ($keywordMatcher?.test(stripped.text) ?? false)));
705b1e1062c2atagen2026-10-23 09:05 UTC127
  $: previews = findUrls(stripped.text);
705b1e1062c2atagen2026-10-23 09:05 UTC128
  $: bundled = message ? message.link_previews : [];
705b1e1062c2atagen2026-10-23 09:05 UTC129
  $: cards = previews
705b1e1062c2atagen2026-10-23 09:05 UTC130
    .map((link) => ({ link, preview: bundled.find((b) => b.url === link.href) ?? linkPreviews[link.href] ?? null }))
705b1e1062c2atagen2026-10-23 09:05 UTC131
    .filter((c) => c.preview && (c.preview.title || c.preview.description || c.preview.image));
ecb8598b38afatagen2026-10-22 09:19 UTC132
  $: if (loadPreview) {
ecb8598b38afatagen2026-10-22 09:19 UTC133
    for (const link of previews) {
ecb8598b38afatagen2026-10-22 09:19 UTC134
      if (!bundled.some((b) => b.url === link.href) && !(link.href in linkPreviews)) loadPreview(link.href);
ecb8598b38afatagen2026-10-22 09:19 UTC135
    }
ecb8598b38afatagen2026-10-22 09:19 UTC136
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC137
f604ef6df0c4atagen2026-10-24 10:08 UTC138
  $: sent = interactive && showSent && event.own && event.send_state.kind === 'Sent' && event.event_id !== null;
f604ef6df0c4atagen2026-10-24 10:08 UTC139
d07bca5da4aeatagen2026-10-26 03:48 UTC140
  let lastShape = '';
d07bca5da4aeatagen2026-10-26 03:48 UTC141
  let heightBefore: number | null = null;
d07bca5da4aeatagen2026-10-26 03:48 UTC142
  $: noteShape(`${event.content.kind}|${rawText}|${formattedBody}|${message?.edited}|${event.reply_to?.details.kind}|${showSender}`);
d07bca5da4aeatagen2026-10-26 03:48 UTC143
d07bca5da4aeatagen2026-10-26 03:48 UTC144
  function noteShape(shape: string): void {
d07bca5da4aeatagen2026-10-26 03:48 UTC145
    if (row && lastShape && shape !== lastShape) heightBefore = row.offsetHeight;
d07bca5da4aeatagen2026-10-26 03:48 UTC146
    lastShape = shape;
d07bca5da4aeatagen2026-10-26 03:48 UTC147
  }
d07bca5da4aeatagen2026-10-26 03:48 UTC148
d07bca5da4aeatagen2026-10-26 03:48 UTC149
  afterUpdate(() => {
d07bca5da4aeatagen2026-10-26 03:48 UTC150
    if (heightBefore === null) return;
d07bca5da4aeatagen2026-10-26 03:48 UTC151
    const from = heightBefore;
d07bca5da4aeatagen2026-10-26 03:48 UTC152
    heightBefore = null;
d07bca5da4aeatagen2026-10-26 03:48 UTC153
    requestAnimationFrame(() => row && settleHeight(row, from));
d07bca5da4aeatagen2026-10-26 03:48 UTC154
  });
d07bca5da4aeatagen2026-10-26 03:48 UTC155
973f8fbdaf80atagen2026-09-28 09:10 UTC156
  function sendLabel(state: SendState): string {
973f8fbdaf80atagen2026-09-28 09:10 UTC157
    if (state.kind === 'Failed') return state.will_retry ? 'failed — will retry' : 'failed — not delivered';
973f8fbdaf80atagen2026-09-28 09:10 UTC158
    return '';
973f8fbdaf80atagen2026-09-28 09:10 UTC159
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC160
d07bca5da4aeatagen2026-10-26 03:48 UTC161
  $: selected = $selectedRow === owner;
d07bca5da4aeatagen2026-10-26 03:48 UTC162
  $: showActions = !readOnly && interactive && !!event.event_id && (selected || pickerOpen || ($selectedRow === null && (hovered || focused)));
d07bca5da4aeatagen2026-10-26 03:48 UTC163
d07bca5da4aeatagen2026-10-26 03:48 UTC164
  $: syncSelectionListeners(selected);
d07bca5da4aeatagen2026-10-26 03:48 UTC165
  $: syncPickerListeners(pickerOpen);
d07bca5da4aeatagen2026-10-26 03:48 UTC166
20fe182f3204atagen2026-10-26 05:50 UTC167
  const pickerLayer = instanceLayerId('reaction-picker');
20fe182f3204atagen2026-10-26 05:50 UTC168
  const dismissPicker = () => (pickerOpen = false);
20fe182f3204atagen2026-10-26 05:50 UTC169
  $: backStack.sync(pickerLayer, pickerOpen, dismissPicker);
20fe182f3204atagen2026-10-26 05:50 UTC170
  onDestroy(() => backStack.sync(pickerLayer, false, dismissPicker));
20fe182f3204atagen2026-10-26 05:50 UTC171
d07bca5da4aeatagen2026-10-26 03:48 UTC172
  function onEscape(e: KeyboardEvent) {
d07bca5da4aeatagen2026-10-26 03:48 UTC173
    if (e.key === 'Escape') select(false);
d07bca5da4aeatagen2026-10-26 03:48 UTC174
  }
d07bca5da4aeatagen2026-10-26 03:48 UTC175
d07bca5da4aeatagen2026-10-26 03:48 UTC176
  let selectionAttached = false;
d07bca5da4aeatagen2026-10-26 03:48 UTC177
  let pickerAttached = false;
d07bca5da4aeatagen2026-10-26 03:48 UTC178
d07bca5da4aeatagen2026-10-26 03:48 UTC179
  function syncSelectionListeners(on: boolean) {
d07bca5da4aeatagen2026-10-26 03:48 UTC180
    if (on === selectionAttached) return;
d07bca5da4aeatagen2026-10-26 03:48 UTC181
    selectionAttached = on;
d07bca5da4aeatagen2026-10-26 03:48 UTC182
    if (on) {
d07bca5da4aeatagen2026-10-26 03:48 UTC183
      window.addEventListener('pointerdown', deselectOutside);
d07bca5da4aeatagen2026-10-26 03:48 UTC184
      window.addEventListener('keydown', onEscape);
d07bca5da4aeatagen2026-10-26 03:48 UTC185
    } else {
d07bca5da4aeatagen2026-10-26 03:48 UTC186
      window.removeEventListener('pointerdown', deselectOutside);
d07bca5da4aeatagen2026-10-26 03:48 UTC187
      window.removeEventListener('keydown', onEscape);
d07bca5da4aeatagen2026-10-26 03:48 UTC188
    }
d07bca5da4aeatagen2026-10-26 03:48 UTC189
  }
d07bca5da4aeatagen2026-10-26 03:48 UTC190
d07bca5da4aeatagen2026-10-26 03:48 UTC191
  function syncPickerListeners(on: boolean) {
d07bca5da4aeatagen2026-10-26 03:48 UTC192
    if (on === pickerAttached) return;
d07bca5da4aeatagen2026-10-26 03:48 UTC193
    pickerAttached = on;
d07bca5da4aeatagen2026-10-26 03:48 UTC194
    const op = on ? 'addEventListener' : 'removeEventListener';
d07bca5da4aeatagen2026-10-26 03:48 UTC195
    window[op]('scroll', closeOnScroll, true);
d07bca5da4aeatagen2026-10-26 03:48 UTC196
    window[op]('resize', closePicker);
d07bca5da4aeatagen2026-10-26 03:48 UTC197
  }
d07bca5da4aeatagen2026-10-26 03:48 UTC198
d07bca5da4aeatagen2026-10-26 03:48 UTC199
  onDestroy(() => {
d07bca5da4aeatagen2026-10-26 03:48 UTC200
    syncSelectionListeners(false);
d07bca5da4aeatagen2026-10-26 03:48 UTC201
    syncPickerListeners(false);
d07bca5da4aeatagen2026-10-26 03:48 UTC202
    selectedRow.update((o) => (o === owner ? null : o));
d07bca5da4aeatagen2026-10-26 03:48 UTC203
  });
d07bca5da4aeatagen2026-10-26 03:48 UTC204
20fe182f3204atagen2026-10-26 05:50 UTC205
  function pinReactors(key: string) {
20fe182f3204atagen2026-10-26 05:50 UTC206
    reactorPinned = true;
20fe182f3204atagen2026-10-26 05:50 UTC207
    reactorKey = key;
20fe182f3204atagen2026-10-26 05:50 UTC208
  }
20fe182f3204atagen2026-10-26 05:50 UTC209
20fe182f3204atagen2026-10-26 05:50 UTC210
  function unpinReactors() {
20fe182f3204atagen2026-10-26 05:50 UTC211
    if (!reactorPinned) return;
20fe182f3204atagen2026-10-26 05:50 UTC212
    reactorPinned = false;
20fe182f3204atagen2026-10-26 05:50 UTC213
    reactorKey = null;
20fe182f3204atagen2026-10-26 05:50 UTC214
  }
20fe182f3204atagen2026-10-26 05:50 UTC215
20fe182f3204atagen2026-10-26 05:50 UTC216
  function holdMore(p: { clientX: number; clientY: number }) {
20fe182f3204atagen2026-10-26 05:50 UTC217
    if (readOnly || !interactive || !event.event_id) return;
20fe182f3204atagen2026-10-26 05:50 UTC218
    select(true);
20fe182f3204atagen2026-10-26 05:50 UTC219
    onMore?.(event, p.clientX, p.clientY);
20fe182f3204atagen2026-10-26 05:50 UTC220
  }
20fe182f3204atagen2026-10-26 05:50 UTC221
984f84f9bf6catagen2026-10-27 09:43 UTC222
  function contextMore(ev: MouseEvent) {
984f84f9bf6catagen2026-10-27 09:43 UTC223
    if (ev.defaultPrevented || ev.shiftKey || !onMore || readOnly || !interactive || !event.event_id) return;
984f84f9bf6catagen2026-10-27 09:43 UTC224
    if ((ev.target as Element).closest('a[href], img, video, audio, input, textarea, .picker-pop')) return;
984f84f9bf6catagen2026-10-27 09:43 UTC225
    const selection = window.getSelection();
984f84f9bf6catagen2026-10-27 09:43 UTC226
    if (selection && !selection.isCollapsed && row.contains(selection.anchorNode)) return;
984f84f9bf6catagen2026-10-27 09:43 UTC227
    ev.preventDefault();
984f84f9bf6catagen2026-10-27 09:43 UTC228
    holdMore(ev);
984f84f9bf6catagen2026-10-27 09:43 UTC229
  }
984f84f9bf6catagen2026-10-27 09:43 UTC230
d07bca5da4aeatagen2026-10-26 03:48 UTC231
  function select(on: boolean) {
d07bca5da4aeatagen2026-10-26 03:48 UTC232
    selectedRow.update((o) => (on ? owner : o === owner ? null : o));
d07bca5da4aeatagen2026-10-26 03:48 UTC233
  }
d07bca5da4aeatagen2026-10-26 03:48 UTC234
544a170ead2catagen2026-10-20 11:47 UTC235
  function toggleSelected(e: MouseEvent) {
668ab5c30531atagen2026-10-25 03:13 UTC236
    if (readOnly || !interactive || (e.target as Element).closest('a, button, input, video, audio, .picker-pop')) return;
d07bca5da4aeatagen2026-10-26 03:48 UTC237
    select(!selected);
d07bca5da4aeatagen2026-10-26 03:48 UTC238
  }
d07bca5da4aeatagen2026-10-26 03:48 UTC239
d07bca5da4aeatagen2026-10-26 03:48 UTC240
  function onRowKeydown(e: KeyboardEvent) {
d07bca5da4aeatagen2026-10-26 03:48 UTC241
    if (e.target !== row || readOnly || !interactive) return;
d07bca5da4aeatagen2026-10-26 03:48 UTC242
    if (e.key === 'Enter' || e.key === ' ') {
d07bca5da4aeatagen2026-10-26 03:48 UTC243
      e.preventDefault();
d07bca5da4aeatagen2026-10-26 03:48 UTC244
      select(!selected);
d07bca5da4aeatagen2026-10-26 03:48 UTC245
    }
d07bca5da4aeatagen2026-10-26 03:48 UTC246
  }
d07bca5da4aeatagen2026-10-26 03:48 UTC247
d07bca5da4aeatagen2026-10-26 03:48 UTC248
  function onFocusOut(e: FocusEvent) {
d07bca5da4aeatagen2026-10-26 03:48 UTC249
    if (!row.contains(e.relatedTarget as Node | null)) focused = false;
544a170ead2catagen2026-10-20 11:47 UTC250
  }
544a170ead2catagen2026-10-20 11:47 UTC251
544a170ead2catagen2026-10-20 11:47 UTC252
  function deselectOutside(e: PointerEvent) {
d07bca5da4aeatagen2026-10-26 03:48 UTC253
    if (selected && !pickerOpen && !row.contains(e.target as Node)) select(false);
544a170ead2catagen2026-10-20 11:47 UTC254
  }
544a170ead2catagen2026-10-20 11:47 UTC255
544a170ead2catagen2026-10-20 11:47 UTC256
  function openPicker(e: MouseEvent) {
544a170ead2catagen2026-10-20 11:47 UTC257
    if (pickerOpen) {
544a170ead2catagen2026-10-20 11:47 UTC258
      pickerOpen = false;
544a170ead2catagen2026-10-20 11:47 UTC259
      return;
544a170ead2catagen2026-10-20 11:47 UTC260
    }
544a170ead2catagen2026-10-20 11:47 UTC261
    const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
544a170ead2catagen2026-10-20 11:47 UTC262
    const height = Math.min(PICKER_HEIGHT, window.innerHeight - 2 * PICKER_MARGIN);
544a170ead2catagen2026-10-20 11:47 UTC263
    const below = window.innerHeight - rect.bottom - PICKER_MARGIN >= height;
544a170ead2catagen2026-10-20 11:47 UTC264
    const top = below ? rect.bottom + 4 : Math.max(PICKER_MARGIN, rect.top - height - 4);
544a170ead2catagen2026-10-20 11:47 UTC265
    const maxLeft = window.innerWidth - PICKER_WIDTH - PICKER_MARGIN;
544a170ead2catagen2026-10-20 11:47 UTC266
    const left = Math.max(PICKER_MARGIN, Math.min(rect.right - PICKER_WIDTH, maxLeft));
544a170ead2catagen2026-10-20 11:47 UTC267
    pickerStyle = `top:${top}px;left:${left}px;height:${height}px`;
d07bca5da4aeatagen2026-10-26 03:48 UTC268
    select(true);
544a170ead2catagen2026-10-20 11:47 UTC269
    pickerOpen = true;
544a170ead2catagen2026-10-20 11:47 UTC270
  }
544a170ead2catagen2026-10-20 11:47 UTC271
544a170ead2catagen2026-10-20 11:47 UTC272
  function closePicker() {
544a170ead2catagen2026-10-20 11:47 UTC273
    pickerOpen = false;
544a170ead2catagen2026-10-20 11:47 UTC274
  }
544a170ead2catagen2026-10-20 11:47 UTC275
ecb8598b38afatagen2026-10-22 09:19 UTC276
  function isImageKey(key: string): boolean {
ecb8598b38afatagen2026-10-22 09:19 UTC277
    return key.startsWith('mxc://') || packs.some((p) => p.images.some((i) => i.reaction_key === key));
ecb8598b38afatagen2026-10-22 09:19 UTC278
  }
ecb8598b38afatagen2026-10-22 09:19 UTC279
ecb8598b38afatagen2026-10-22 09:19 UTC280
  function react(key: string, shortcode: string | null) {
ecb8598b38afatagen2026-10-22 09:19 UTC281
    if (shortcode && isImageKey(key)) onReact(event.event_id ?? '', key, shortcode);
ecb8598b38afatagen2026-10-22 09:19 UTC282
    else onReact(event.event_id ?? '', key);
e590609b6343atagen2026-10-27 10:33 UTC283
    select(false);
ecb8598b38afatagen2026-10-22 09:19 UTC284
  }
ecb8598b38afatagen2026-10-22 09:19 UTC285
df3c0f2b3da4atagen2026-10-22 13:07 UTC286
  function reply() {
df3c0f2b3da4atagen2026-10-22 13:07 UTC287
    if (!event.event_id || !onReply) return;
df3c0f2b3da4atagen2026-10-22 13:07 UTC288
    onReply({
df3c0f2b3da4atagen2026-10-22 13:07 UTC289
      event_id: event.event_id,
df3c0f2b3da4atagen2026-10-22 13:07 UTC290
      details: { kind: 'Ready', sender: event.sender, content: event.content, timestamp_ms: event.timestamp_ms }
df3c0f2b3da4atagen2026-10-22 13:07 UTC291
    });
e590609b6343atagen2026-10-27 10:33 UTC292
    select(false);
df3c0f2b3da4atagen2026-10-22 13:07 UTC293
  }
df3c0f2b3da4atagen2026-10-22 13:07 UTC294
cf23dbf4e77catagen2026-10-23 06:36 UTC295
  $: editableBody =
cf23dbf4e77catagen2026-10-23 06:36 UTC296
    event.own && event.editable && event.event_id && event.content.kind === 'Message' && event.content.message.kind === 'Text'
cf23dbf4e77catagen2026-10-23 06:36 UTC297
      ? event.content.message.body
cf23dbf4e77catagen2026-10-23 06:36 UTC298
      : null;
cf23dbf4e77catagen2026-10-23 06:36 UTC299
cf23dbf4e77catagen2026-10-23 06:36 UTC300
  function edit() {
cf23dbf4e77catagen2026-10-23 06:36 UTC301
    if (!event.event_id || editableBody === null || !onEdit) return;
cf23dbf4e77catagen2026-10-23 06:36 UTC302
    onEdit(event.event_id, editableBody);
e590609b6343atagen2026-10-27 10:33 UTC303
    select(false);
cf23dbf4e77catagen2026-10-23 06:36 UTC304
  }
cf23dbf4e77catagen2026-10-23 06:36 UTC305
cf23dbf4e77catagen2026-10-23 06:36 UTC306
  function reactorNames(senders: string[]): string {
cf23dbf4e77catagen2026-10-23 06:36 UTC307
    const names = senders.map((id) => people[id]?.name ?? id);
cf23dbf4e77catagen2026-10-23 06:36 UTC308
    if (names.length <= 12) return names.join(', ');
cf23dbf4e77catagen2026-10-23 06:36 UTC309
    return `${names.slice(0, 12).join(', ')} and ${names.length - 12} more`;
cf23dbf4e77catagen2026-10-23 06:36 UTC310
  }
cf23dbf4e77catagen2026-10-23 06:36 UTC311
ecb8598b38afatagen2026-10-22 09:19 UTC312
  function pick(key: string, shortcode: string | null) {
544a170ead2catagen2026-10-20 11:47 UTC313
    pickerOpen = false;
ecb8598b38afatagen2026-10-22 09:19 UTC314
    react(key, shortcode);
544a170ead2catagen2026-10-20 11:47 UTC315
  }
544a170ead2catagen2026-10-20 11:47 UTC316
544a170ead2catagen2026-10-20 11:47 UTC317
  function closeOnScroll(e: Event) {
544a170ead2catagen2026-10-20 11:47 UTC318
    if (pickerOpen && !pickerPop?.contains(e.target as Node)) pickerOpen = false;
544a170ead2catagen2026-10-20 11:47 UTC319
  }
544a170ead2catagen2026-10-20 11:47 UTC320
973f8fbdaf80atagen2026-09-28 09:10 UTC321
  function actorLabel(userId: string, name: string | null): string {
f604ef6df0c4atagen2026-10-24 10:08 UTC322
    return name ?? people[userId]?.name ?? userId;
973f8fbdaf80atagen2026-09-28 09:10 UTC323
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC324
973f8fbdaf80atagen2026-09-28 09:10 UTC325
  function replyPreview(content: TimelineContent): string {
705b1e1062c2atagen2026-10-23 09:05 UTC326
    if (content.kind === 'Message') return stripMarkdownPills(content.message.body).text;
973f8fbdaf80atagen2026-09-28 09:10 UTC327
    if (content.kind === 'Sticker') return content.body || 'Sticker';
973f8fbdaf80atagen2026-09-28 09:10 UTC328
    if (content.kind === 'Poll') return `Poll: ${content.question}`;
973f8fbdaf80atagen2026-09-28 09:10 UTC329
    if (content.kind === 'Redacted') return 'Message deleted';
973f8fbdaf80atagen2026-09-28 09:10 UTC330
    if (content.kind === 'UnableToDecrypt') return 'Unable to decrypt';
973f8fbdaf80atagen2026-09-28 09:10 UTC331
    return systemLine(content);
973f8fbdaf80atagen2026-09-28 09:10 UTC332
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC333
f604ef6df0c4atagen2026-10-24 10:08 UTC334
  function systemLine(content: TimelineContent, actor = displayName): string {
973f8fbdaf80atagen2026-09-28 09:10 UTC335
    switch (content.kind) {
973f8fbdaf80atagen2026-09-28 09:10 UTC336
      case 'Redacted':
973f8fbdaf80atagen2026-09-28 09:10 UTC337
        return 'Message deleted';
973f8fbdaf80atagen2026-09-28 09:10 UTC338
      case 'UnableToDecrypt':
f604ef6df0c4atagen2026-10-24 10:08 UTC339
        return `Message from ${actor} could not be decrypted`;
973f8fbdaf80atagen2026-09-28 09:10 UTC340
      case 'Membership': {
973f8fbdaf80atagen2026-09-28 09:10 UTC341
        const who = actorLabel(content.user_id, content.display_name);
973f8fbdaf80atagen2026-09-28 09:10 UTC342
        return `${who} ${MEMBERSHIP_LABELS[content.change] ?? 'updated their membership'}`;
973f8fbdaf80atagen2026-09-28 09:10 UTC343
      }
973f8fbdaf80atagen2026-09-28 09:10 UTC344
      case 'Profile': {
973f8fbdaf80atagen2026-09-28 09:10 UTC345
        const who = actorLabel(content.user_id, null);
973f8fbdaf80atagen2026-09-28 09:10 UTC346
        const parts: string[] = [];
973f8fbdaf80atagen2026-09-28 09:10 UTC347
        if (content.display_name) {
973f8fbdaf80atagen2026-09-28 09:10 UTC348
          parts.push(
973f8fbdaf80atagen2026-09-28 09:10 UTC349
            content.display_name.new
973f8fbdaf80atagen2026-09-28 09:10 UTC350
              ? `changed their display name to "${content.display_name.new}"`
973f8fbdaf80atagen2026-09-28 09:10 UTC351
              : 'removed their display name'
973f8fbdaf80atagen2026-09-28 09:10 UTC352
          );
973f8fbdaf80atagen2026-09-28 09:10 UTC353
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC354
        if (content.avatar) parts.push('changed their avatar');
973f8fbdaf80atagen2026-09-28 09:10 UTC355
        return `${who} ${parts.length > 0 ? parts.join(' and ') : 'updated their profile'}`;
973f8fbdaf80atagen2026-09-28 09:10 UTC356
      }
973f8fbdaf80atagen2026-09-28 09:10 UTC357
      case 'RoomName':
973f8fbdaf80atagen2026-09-28 09:10 UTC358
        return content.name
f604ef6df0c4atagen2026-10-24 10:08 UTC359
          ? `${actor} changed the room name to "${content.name}"`
f604ef6df0c4atagen2026-10-24 10:08 UTC360
          : `${actor} removed the room name`;
973f8fbdaf80atagen2026-09-28 09:10 UTC361
      case 'RoomTopic':
973f8fbdaf80atagen2026-09-28 09:10 UTC362
        return content.topic
f604ef6df0c4atagen2026-10-24 10:08 UTC363
          ? `${actor} changed the topic to "${content.topic}"`
f604ef6df0c4atagen2026-10-24 10:08 UTC364
          : `${actor} removed the topic`;
973f8fbdaf80atagen2026-09-28 09:10 UTC365
      case 'RoomAvatar':
f604ef6df0c4atagen2026-10-24 10:08 UTC366
        return content.avatar ? `${actor} changed the room avatar` : `${actor} removed the room avatar`;
973f8fbdaf80atagen2026-09-28 09:10 UTC367
      case 'EncryptionEnabled':
f604ef6df0c4atagen2026-10-24 10:08 UTC368
        return `${actor} enabled encryption in this room`;
973f8fbdaf80atagen2026-09-28 09:10 UTC369
      case 'RoomCreated':
f604ef6df0c4atagen2026-10-24 10:08 UTC370
        return `${actor} created the room`;
973f8fbdaf80atagen2026-09-28 09:10 UTC371
      case 'RoomUpgraded':
f604ef6df0c4atagen2026-10-24 10:08 UTC372
        return `${actor} upgraded the room to a new version`;
973f8fbdaf80atagen2026-09-28 09:10 UTC373
      case 'OtherState':
f604ef6df0c4atagen2026-10-24 10:08 UTC374
        return `${actor} ${STATE_LABELS[content.event_type] ?? `sent a state event (${content.event_type})`}`;
973f8fbdaf80atagen2026-09-28 09:10 UTC375
      case 'Other':
f604ef6df0c4atagen2026-10-24 10:08 UTC376
        return `${actor} sent an event (${content.event_type})`;
973f8fbdaf80atagen2026-09-28 09:10 UTC377
      default:
f604ef6df0c4atagen2026-10-24 10:08 UTC378
        return actor;
973f8fbdaf80atagen2026-09-28 09:10 UTC379
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC380
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC381
</script>
973f8fbdaf80atagen2026-09-28 09:10 UTC382
d07bca5da4aeatagen2026-10-26 03:48 UTC383
<!-- svelte-ignore a11y_no_noninteractive_tabindex a11y_no_noninteractive_element_interactions -->
544a170ead2catagen2026-10-20 11:47 UTC384
<div
544a170ead2catagen2026-10-20 11:47 UTC385
  class="msg"
d07bca5da4aeatagen2026-10-26 03:48 UTC386
  role="group"
d07bca5da4aeatagen2026-10-26 03:48 UTC387
  tabindex={interactive && !readOnly ? 0 : undefined}
544a170ead2catagen2026-10-20 11:47 UTC388
  class:own={event.own}
cf23dbf4e77catagen2026-10-23 06:36 UTC389
  class:editing
a4555a7eae2datagen2026-10-25 09:18 UTC390
  class:mentioned={mentionsMe || keywordHit}
544a170ead2catagen2026-10-20 11:47 UTC391
  class:system={!interactive}
544a170ead2catagen2026-10-20 11:47 UTC392
  class:selected
544a170ead2catagen2026-10-20 11:47 UTC393
  data-item={itemId}
df3c0f2b3da4atagen2026-10-22 13:07 UTC394
  data-event={event.event_id}
d07bca5da4aeatagen2026-10-26 03:48 UTC395
  in:rowIn|global
d07bca5da4aeatagen2026-10-26 03:48 UTC396
  out:rowOut|global
544a170ead2catagen2026-10-20 11:47 UTC397
  bind:this={row}
544a170ead2catagen2026-10-20 11:47 UTC398
  on:click={toggleSelected}
984f84f9bf6catagen2026-10-27 09:43 UTC399
  on:contextmenu={contextMore}
20fe182f3204atagen2026-10-26 05:50 UTC400
  use:longpress={{ on: holdMore, allowSelection: true, ignore: 'a, button, input, img, video, audio, .text, .picker-pop' }}
d07bca5da4aeatagen2026-10-26 03:48 UTC401
  on:keydown={onRowKeydown}
20fe182f3204atagen2026-10-26 05:50 UTC402
  on:pointerenter={(e) => (hovered = e.pointerType === 'mouse')}
d07bca5da4aeatagen2026-10-26 03:48 UTC403
  on:pointerleave={() => (hovered = false)}
d07bca5da4aeatagen2026-10-26 03:48 UTC404
  on:focusin={() => (focused = true)}
d07bca5da4aeatagen2026-10-26 03:48 UTC405
  on:focusout={onFocusOut}
544a170ead2catagen2026-10-20 11:47 UTC406
>
d07bca5da4aeatagen2026-10-26 03:48 UTC407
  {#if showActions && event.event_id}
5059ed753f8batagen2026-10-27 07:20 UTC408
    {@const quickRows = quickGroups(quickReactions)}
544a170ead2catagen2026-10-20 11:47 UTC409
    <div class="actions" role="toolbar" aria-label={`Actions for message from ${displayName}`}>
5059ed753f8batagen2026-10-27 07:20 UTC410
      <div class="quick" style={`--quick-cols:${quickColumns(quickRows, QUICK_REACTIONS_PER_ROW)}`}>
5059ed753f8batagen2026-10-27 07:20 UTC411
      {#each quickRows as group, g}
5059ed753f8batagen2026-10-27 07:20 UTC412
        {#each group as r, i (r.key)}
5059ed753f8batagen2026-10-27 07:20 UTC413
          {@const label = r.shortcode ? `:${r.shortcode}:` : r.key}
5059ed753f8batagen2026-10-27 07:20 UTC414
          <button
5059ed753f8batagen2026-10-27 07:20 UTC415
            type="button"
5059ed753f8batagen2026-10-27 07:20 UTC416
            class:group-start={g > 0 && i === 0}
5059ed753f8batagen2026-10-27 07:20 UTC417
            title={`React ${label}`}
5059ed753f8batagen2026-10-27 07:20 UTC418
            aria-label={`React ${label} to message from ${displayName}`}
5059ed753f8batagen2026-10-27 07:20 UTC419
            disabled={reacting}
5059ed753f8batagen2026-10-27 07:20 UTC420
            on:click={() => react(r.key, r.shortcode)}
5059ed753f8batagen2026-10-27 07:20 UTC421
          >
5059ed753f8batagen2026-10-27 07:20 UTC422
            {#if r.key.startsWith('mxc://')}
5059ed753f8batagen2026-10-27 07:20 UTC423
              <img class="emote" src={mediaUrl(r.key, 64) ?? ''} alt="" />
5059ed753f8batagen2026-10-27 07:20 UTC424
            {:else}
5059ed753f8batagen2026-10-27 07:20 UTC425
              <Emoji emoji={r.key} size={18} />
5059ed753f8batagen2026-10-27 07:20 UTC426
            {/if}
5059ed753f8batagen2026-10-27 07:20 UTC427
          </button>
5059ed753f8batagen2026-10-27 07:20 UTC428
        {/each}
544a170ead2catagen2026-10-20 11:47 UTC429
      {/each}
8c81366a752fatagen2026-10-26 03:56 UTC430
      </div>
544a170ead2catagen2026-10-20 11:47 UTC431
      <button
544a170ead2catagen2026-10-20 11:47 UTC432
        type="button"
544a170ead2catagen2026-10-20 11:47 UTC433
        title="Add reaction"
544a170ead2catagen2026-10-20 11:47 UTC434
        aria-label={`Add reaction to message from ${displayName}`}
544a170ead2catagen2026-10-20 11:47 UTC435
        aria-haspopup="dialog"
544a170ead2catagen2026-10-20 11:47 UTC436
        aria-expanded={pickerOpen}
544a170ead2catagen2026-10-20 11:47 UTC437
        on:pointerdown|stopPropagation
544a170ead2catagen2026-10-20 11:47 UTC438
        on:click={openPicker}
544a170ead2catagen2026-10-20 11:47 UTC439
      >
544a170ead2catagen2026-10-20 11:47 UTC440
        ☺+
544a170ead2catagen2026-10-20 11:47 UTC441
      </button>
d095131eec0eatagen2026-10-24 08:39 UTC442
      {#if onCloneSticker && event.content.kind === 'Sticker'}
d095131eec0eatagen2026-10-24 08:39 UTC443
        {@const stickerBody = event.content.body}
d095131eec0eatagen2026-10-24 08:39 UTC444
        {@const stickerId = event.event_id}
d095131eec0eatagen2026-10-24 08:39 UTC445
        <button
d095131eec0eatagen2026-10-24 08:39 UTC446
          type="button"
d095131eec0eatagen2026-10-24 08:39 UTC447
          title="Clone sticker"
d095131eec0eatagen2026-10-24 08:39 UTC448
          aria-label="Clone sticker to one of your packs"
d095131eec0eatagen2026-10-24 08:39 UTC449
          on:click|stopPropagation={(ev) => onCloneSticker?.(stickerId, stickerBody, ev.clientX, ev.clientY)}
d095131eec0eatagen2026-10-24 08:39 UTC450
        >
d095131eec0eatagen2026-10-24 08:39 UTC451
          ⧉
d095131eec0eatagen2026-10-24 08:39 UTC452
        </button>
d095131eec0eatagen2026-10-24 08:39 UTC453
      {/if}
df3c0f2b3da4atagen2026-10-22 13:07 UTC454
      {#if onReply}
df3c0f2b3da4atagen2026-10-22 13:07 UTC455
        <button
df3c0f2b3da4atagen2026-10-22 13:07 UTC456
          type="button"
df3c0f2b3da4atagen2026-10-22 13:07 UTC457
          title="Reply"
df3c0f2b3da4atagen2026-10-22 13:07 UTC458
          aria-label={`Reply to message from ${displayName}`}
df3c0f2b3da4atagen2026-10-22 13:07 UTC459
          on:click|stopPropagation={reply}
df3c0f2b3da4atagen2026-10-22 13:07 UTC460
        >
df3c0f2b3da4atagen2026-10-22 13:07 UTC461
          ↩
df3c0f2b3da4atagen2026-10-22 13:07 UTC462
        </button>
df3c0f2b3da4atagen2026-10-22 13:07 UTC463
      {/if}
7ebc2472a9e3atagen2026-10-25 09:53 UTC464
      {#if threadRoot}
7ebc2472a9e3atagen2026-10-25 09:53 UTC465
        {@const root = threadRoot}
7ebc2472a9e3atagen2026-10-25 09:53 UTC466
        <button
7ebc2472a9e3atagen2026-10-25 09:53 UTC467
          type="button"
7ebc2472a9e3atagen2026-10-25 09:53 UTC468
          title="Reply in thread"
7ebc2472a9e3atagen2026-10-25 09:53 UTC469
          aria-label={`Reply in thread to message from ${displayName}`}
7ebc2472a9e3atagen2026-10-25 09:53 UTC470
          on:click|stopPropagation={() => onOpenThread?.(root)}
7ebc2472a9e3atagen2026-10-25 09:53 UTC471
        >
7ebc2472a9e3atagen2026-10-25 09:53 UTC472
          <svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true"><path d="M2.5 3h11v7h-6l-3 3v-3h-2z" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M5 6h6M5 8h4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
7ebc2472a9e3atagen2026-10-25 09:53 UTC473
        </button>
7ebc2472a9e3atagen2026-10-25 09:53 UTC474
      {/if}
cf23dbf4e77catagen2026-10-23 06:36 UTC475
      {#if onEdit && editableBody !== null}
cf23dbf4e77catagen2026-10-23 06:36 UTC476
        <button type="button" title="Edit" aria-label="Edit your message" on:click|stopPropagation={edit}>
cf23dbf4e77catagen2026-10-23 06:36 UTC477
          ✎
cf23dbf4e77catagen2026-10-23 06:36 UTC478
        </button>
cf23dbf4e77catagen2026-10-23 06:36 UTC479
      {/if}
0380cd8c765catagen2026-10-24 09:08 UTC480
      {#if onMore}
0380cd8c765catagen2026-10-24 09:08 UTC481
        <button
0380cd8c765catagen2026-10-24 09:08 UTC482
          type="button"
0380cd8c765catagen2026-10-24 09:08 UTC483
          title="More"
0380cd8c765catagen2026-10-24 09:08 UTC484
          aria-label={`More actions for message from ${displayName}`}
0380cd8c765catagen2026-10-24 09:08 UTC485
          aria-haspopup="menu"
0380cd8c765catagen2026-10-24 09:08 UTC486
          on:click|stopPropagation={(ev) => onMore?.(event, ev.clientX, ev.clientY)}
0380cd8c765catagen2026-10-24 09:08 UTC487
        >
0380cd8c765catagen2026-10-24 09:08 UTC488
          ⋯
0380cd8c765catagen2026-10-24 09:08 UTC489
        </button>
0380cd8c765catagen2026-10-24 09:08 UTC490
      {/if}
544a170ead2catagen2026-10-20 11:47 UTC491
    </div>
544a170ead2catagen2026-10-20 11:47 UTC492
    {#if pickerOpen}
544a170ead2catagen2026-10-20 11:47 UTC493
      <div class="picker-pop" style={pickerStyle} bind:this={pickerPop}>
ecb8598b38afatagen2026-10-22 09:19 UTC494
        <ReactionPicker {packs} quick={quickReactions} onPick={pick} {onQuickChange} onClose={closePicker} />
544a170ead2catagen2026-10-20 11:47 UTC495
      </div>
544a170ead2catagen2026-10-20 11:47 UTC496
    {/if}
544a170ead2catagen2026-10-20 11:47 UTC497
  {/if}
d07bca5da4aeatagen2026-10-26 03:48 UTC498
  {#if event.reply_to}
d07bca5da4aeatagen2026-10-26 03:48 UTC499
    {@const target = event.reply_to.event_id}
d07bca5da4aeatagen2026-10-26 03:48 UTC500
    <div class="reply-line" class:linked={showSender}>
d07bca5da4aeatagen2026-10-26 03:48 UTC501
      <button
d07bca5da4aeatagen2026-10-26 03:48 UTC502
        type="button"
d07bca5da4aeatagen2026-10-26 03:48 UTC503
        class="reply-quote"
d07bca5da4aeatagen2026-10-26 03:48 UTC504
        disabled={!onJump}
d07bca5da4aeatagen2026-10-26 03:48 UTC505
        title="Jump to the original message"
d07bca5da4aeatagen2026-10-26 03:48 UTC506
        on:click|stopPropagation={() => onJump?.(target)}
d07bca5da4aeatagen2026-10-26 03:48 UTC507
      >
d07bca5da4aeatagen2026-10-26 03:48 UTC508
        {#if event.reply_to.details.kind === 'Ready'}
d07bca5da4aeatagen2026-10-26 03:48 UTC509
          <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>
d07bca5da4aeatagen2026-10-26 03:48 UTC510
          <span class="reply-text"><EmojiText text={replyPreview(event.reply_to.details.content)} /></span>
d07bca5da4aeatagen2026-10-26 03:48 UTC511
        {:else if event.reply_to.details.kind === 'Pending'}
d07bca5da4aeatagen2026-10-26 03:48 UTC512
          Loading reply…
d07bca5da4aeatagen2026-10-26 03:48 UTC513
        {:else}
d07bca5da4aeatagen2026-10-26 03:48 UTC514
          Reply unavailable
d07bca5da4aeatagen2026-10-26 03:48 UTC515
        {/if}
d07bca5da4aeatagen2026-10-26 03:48 UTC516
      </button>
d07bca5da4aeatagen2026-10-26 03:48 UTC517
    </div>
d07bca5da4aeatagen2026-10-26 03:48 UTC518
  {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC519
  {#if showSender}
d8624c214c04atagen2026-10-24 08:24 UTC520
    <button
d8624c214c04atagen2026-10-24 08:24 UTC521
      type="button"
d8624c214c04atagen2026-10-24 08:24 UTC522
      class="user-hit"
668ab5c30531atagen2026-10-25 03:13 UTC523
      disabled={!onUser}
d8624c214c04atagen2026-10-24 08:24 UTC524
      tabindex="-1"
d8624c214c04atagen2026-10-24 08:24 UTC525
      aria-hidden="true"
d8624c214c04atagen2026-10-24 08:24 UTC526
      on:click|stopPropagation={(ev) => openUser(ev, false)}
d8624c214c04atagen2026-10-24 08:24 UTC527
      on:contextmenu|preventDefault|stopPropagation={(ev) => openUser(ev, true)}
20fe182f3204atagen2026-10-26 05:50 UTC528
      use:longpress={(ev) => openUser(ev, true)}
d8624c214c04atagen2026-10-24 08:24 UTC529
    >
d8624c214c04atagen2026-10-24 08:24 UTC530
      {#if senderAvatar}
d07bca5da4aeatagen2026-10-26 03:48 UTC531
        <MxcAvatar mxc={senderAvatar} label={name} size={34} eager />
d8624c214c04atagen2026-10-24 08:24 UTC532
      {:else}
d8624c214c04atagen2026-10-24 08:24 UTC533
        <span class="avatar" style={`background:hsl(${senderHue(event.sender.user_id)} 35% 32%)`}>
445bf199a9cfatagen2026-10-25 02:56 UTC534
          <span><EmojiText text={initialsOf(name)} /></span>
d8624c214c04atagen2026-10-24 08:24 UTC535
        </span>
d8624c214c04atagen2026-10-24 08:24 UTC536
      {/if}
d8624c214c04atagen2026-10-24 08:24 UTC537
    </button>
973f8fbdaf80atagen2026-09-28 09:10 UTC538
  {:else}
973f8fbdaf80atagen2026-09-28 09:10 UTC539
    <div class="avatar-spacer" aria-hidden="true">
973f8fbdaf80atagen2026-09-28 09:10 UTC540
      <span class="hover-time">{formatClock(event.timestamp_ms)}</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC541
    </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC542
  {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC543
  <div class="msg-body">
973f8fbdaf80atagen2026-09-28 09:10 UTC544
    {#if showSender}
973f8fbdaf80atagen2026-09-28 09:10 UTC545
      <div class="msg-head">
d8624c214c04atagen2026-10-24 08:24 UTC546
        <button
d8624c214c04atagen2026-10-24 08:24 UTC547
          type="button"
d8624c214c04atagen2026-10-24 08:24 UTC548
          class="sender"
668ab5c30531atagen2026-10-25 03:13 UTC549
          disabled={!onUser}
d8624c214c04atagen2026-10-24 08:24 UTC550
          style={`color:hsl(${senderHue(event.sender.user_id)} 70% var(--name-lightness))`}
d8624c214c04atagen2026-10-24 08:24 UTC551
          on:click|stopPropagation={(ev) => openUser(ev, false)}
d8624c214c04atagen2026-10-24 08:24 UTC552
          on:contextmenu|preventDefault|stopPropagation={(ev) => openUser(ev, true)}
20fe182f3204atagen2026-10-26 05:50 UTC553
          use:longpress={(ev) => openUser(ev, true)}
d8624c214c04atagen2026-10-24 08:24 UTC554
        >
445bf199a9cfatagen2026-10-25 02:56 UTC555
          <EmojiText text={displayName} />
d8624c214c04atagen2026-10-24 08:24 UTC556
        </button>
d2a34d0f4861atagen2026-10-25 07:29 UTC557
        {#if lag}
d2a34d0f4861atagen2026-10-25 07:29 UTC558
          <span class="federation-lag" title="Possibly delayed by federation" aria-label={`Arrived ${lag} late, possibly delayed by federation`}>
d2a34d0f4861atagen2026-10-25 07:29 UTC559
            <span aria-hidden="true">◷</span><span>{lag}</span>
d2a34d0f4861atagen2026-10-25 07:29 UTC560
          </span>
d2a34d0f4861atagen2026-10-25 07:29 UTC561
        {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC562
        <time class="time" datetime={new Date(event.timestamp_ms).toISOString()}>
973f8fbdaf80atagen2026-09-28 09:10 UTC563
          {formatDay(event.timestamp_ms)} {formatClock(event.timestamp_ms)}
973f8fbdaf80atagen2026-09-28 09:10 UTC564
        </time>
973f8fbdaf80atagen2026-09-28 09:10 UTC565
        {#if event.content.kind === 'Message' && event.content.message.edited}
d07bca5da4aeatagen2026-10-26 03:48 UTC566
          <span class="edited" title="Edited" transition:dissolve>(edited)</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC567
        {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC568
      </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC569
    {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC570
973f8fbdaf80atagen2026-09-28 09:10 UTC571
    {#if event.content.kind === 'Message'}
973f8fbdaf80atagen2026-09-28 09:10 UTC572
      {@const m = event.content.message}
705b1e1062c2atagen2026-10-23 09:05 UTC573
      {#if stripped.text}
d07bca5da4aeatagen2026-10-26 03:48 UTC574
        <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} />{#if m.edited && !showSender}<span class="edited" title="Edited" transition:dissolve>(edited)</span>{/if}</div>
df3c0f2b3da4atagen2026-10-22 13:07 UTC575
      {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC576
      {#if m.media}
973f8fbdaf80atagen2026-09-28 09:10 UTC577
        <MediaAttachmentView attachment={m.media} />
973f8fbdaf80atagen2026-09-28 09:10 UTC578
      {/if}
705b1e1062c2atagen2026-10-23 09:05 UTC579
      {#if cards.length > 0}
85334efe0e3aatagen2026-10-27 05:55 UTC580
        <div class="link-previews" in:dissolve>
705b1e1062c2atagen2026-10-23 09:05 UTC581
          {#each cards as card (card.link.href)}
85334efe0e3aatagen2026-10-27 05:55 UTC582
            <div in:dissolve><LinkCard link={card.link} preview={card.preview} /></div>
973f8fbdaf80atagen2026-09-28 09:10 UTC583
          {/each}
973f8fbdaf80atagen2026-09-28 09:10 UTC584
        </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC585
      {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC586
    {:else if event.content.kind === 'Sticker'}
973f8fbdaf80atagen2026-09-28 09:10 UTC587
      {#if event.content.media}
973f8fbdaf80atagen2026-09-28 09:10 UTC588
        <MediaAttachmentView attachment={event.content.media} />
dfd1feaae855atagen2026-10-24 10:54 UTC589
      {:else if event.content.body}<p class="text"><EmojiText text={event.content.body} /></p>{/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC590
    {:else if event.content.kind === 'Poll'}
445bf199a9cfatagen2026-10-25 02:56 UTC591
      <p class="text"><EmojiText text={`📊 ${event.content.question}`} /></p>
973f8fbdaf80atagen2026-09-28 09:10 UTC592
    {:else}
445bf199a9cfatagen2026-10-25 02:56 UTC593
      <p class="system-line"><EmojiText text={systemLine(event.content, displayName)} /></p>
973f8fbdaf80atagen2026-09-28 09:10 UTC594
    {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC595
973f8fbdaf80atagen2026-09-28 09:10 UTC596
    {#if interactive && event.reactions.length > 0}
d07bca5da4aeatagen2026-10-26 03:48 UTC597
      <div class="reactions" aria-label="Reactions" transition:expand>
973f8fbdaf80atagen2026-09-28 09:10 UTC598
        {#each event.reactions as r (r.key)}
973f8fbdaf80atagen2026-09-28 09:10 UTC599
          <button
973f8fbdaf80atagen2026-09-28 09:10 UTC600
            type="button"
973f8fbdaf80atagen2026-09-28 09:10 UTC601
            class="reaction"
d07bca5da4aeatagen2026-10-26 03:48 UTC602
            transition:expand={{ axis: 'x' }}
973f8fbdaf80atagen2026-09-28 09:10 UTC603
            class:mine={r.own}
d4c0d65f690aatagen2026-10-24 12:50 UTC604
            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 UTC605
            disabled={readOnly || !event.event_id || reacting}
d07bca5da4aeatagen2026-10-26 03:48 UTC606
            aria-describedby={reactorKey === r.key ? `${itemId}-reactors-${r.key}` : undefined}
20fe182f3204atagen2026-10-26 05:50 UTC607
            on:pointerenter={(e) => e.pointerType === 'mouse' && (reactorKey = r.key)}
20fe182f3204atagen2026-10-26 05:50 UTC608
            on:pointerleave={() => !reactorPinned && (reactorKey = null)}
20fe182f3204atagen2026-10-26 05:50 UTC609
            use:longpress={() => pinReactors(r.key)}
d07bca5da4aeatagen2026-10-26 03:48 UTC610
            on:focus={() => (reactorKey = r.key)}
20fe182f3204atagen2026-10-26 05:50 UTC611
            on:blur={() => !reactorPinned && (reactorKey = null)}
973f8fbdaf80atagen2026-09-28 09:10 UTC612
            on:click={() => event.event_id && onReact(event.event_id, r.key)}
973f8fbdaf80atagen2026-09-28 09:10 UTC613
          >
ecb8598b38afatagen2026-10-22 09:19 UTC614
            {#if r.image}
ecb8598b38afatagen2026-10-22 09:19 UTC615
              <img class="emote" src={messageMediaUrl(r.image, 'Thumbnail')} alt={r.shortcode ?? ''} title={r.shortcode ?? ''} />
ecb8598b38afatagen2026-10-22 09:19 UTC616
            {:else if r.key.startsWith('mxc://')}
7643577c76bbatagen2026-10-25 07:37 UTC617
              <img class="emote" src={mediaUrl(r.key, 64) ?? ''} alt="" />
544a170ead2catagen2026-10-20 11:47 UTC618
            {:else}
544a170ead2catagen2026-10-20 11:47 UTC619
              <Emoji emoji={r.key} size={16} />
544a170ead2catagen2026-10-20 11:47 UTC620
            {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC621
            <span class="count">{r.count}</span>
d07bca5da4aeatagen2026-10-26 03:48 UTC622
            {#if reactorKey === r.key}
cf23dbf4e77catagen2026-10-23 06:36 UTC623
            <span class="reactors" role="tooltip" id={`${itemId}-reactors-${r.key}`}>
2021154f9e16atagen2026-10-25 02:37 UTC624
              <span class="reactors-key"><EmojiText text={r.shortcode ? `:${r.shortcode}:` : r.key.startsWith('mxc://') ? 'custom emote' : r.key} /></span>
2021154f9e16atagen2026-10-25 02:37 UTC625
              <EmojiText text={reactorNames(r.senders)} />
cf23dbf4e77catagen2026-10-23 06:36 UTC626
            </span>
d07bca5da4aeatagen2026-10-26 03:48 UTC627
            {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC628
          </button>
973f8fbdaf80atagen2026-09-28 09:10 UTC629
        {/each}
973f8fbdaf80atagen2026-09-28 09:10 UTC630
      </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC631
    {/if}
7ebc2472a9e3atagen2026-10-25 09:53 UTC632
    {#if threadRoot && threadReplies > 0}
7ebc2472a9e3atagen2026-10-25 09:53 UTC633
      {@const root = threadRoot}
7ebc2472a9e3atagen2026-10-25 09:53 UTC634
      {@const latest = event.thread_summary?.latest_event_timestamp_ms ?? null}
d07bca5da4aeatagen2026-10-26 03:48 UTC635
      <button type="button" class="thread-chip" transition:expand on:click|stopPropagation={() => onOpenThread?.(root)}>
7ebc2472a9e3atagen2026-10-25 09:53 UTC636
        <span class="thread-count">{threadReplies} {threadReplies === 1 ? 'reply' : 'replies'}</span>
7ebc2472a9e3atagen2026-10-25 09:53 UTC637
        {#if latest}<time datetime={new Date(latest).toISOString()}>{formatDay(latest)} {formatClock(latest)}</time>{/if}
7ebc2472a9e3atagen2026-10-25 09:53 UTC638
      </button>
7ebc2472a9e3atagen2026-10-25 09:53 UTC639
    {/if}
d07bca5da4aeatagen2026-10-26 03:48 UTC640
    {#if sent}<span class="sent-tick" title="Sent" aria-label="Sent" transition:dissolve>✓</span>{/if}
df3c0f2b3da4atagen2026-10-22 13:07 UTC641
    {#if interactive && sendLabel(event.send_state)}
d07bca5da4aeatagen2026-10-26 03:48 UTC642
      <div class="msg-foot" transition:expand>
df3c0f2b3da4atagen2026-10-22 13:07 UTC643
        <span class="sendstate" class:failed={event.send_state.kind === 'Failed'}>{sendLabel(event.send_state)}</span>
df3c0f2b3da4atagen2026-10-22 13:07 UTC644
      </div>
df3c0f2b3da4atagen2026-10-22 13:07 UTC645
    {/if}
df3c0f2b3da4atagen2026-10-22 13:07 UTC646
    {#if interactive && readers.length > 0}
df3c0f2b3da4atagen2026-10-22 13:07 UTC647
      {@const seenBy = `Seen by ${readers.map((r) => r.name).join(', ')}`}
d07bca5da4aeatagen2026-10-26 03:48 UTC648
      <div class="readby" transition:dissolve title={seenBy} aria-label={seenBy} role="img">
df3c0f2b3da4atagen2026-10-22 13:07 UTC649
        {#each readers.slice(0, MAX_READERS) as r (r.user_id)}
df3c0f2b3da4atagen2026-10-22 13:07 UTC650
          <MxcAvatar mxc={r.avatar} label={r.name} size={16} />
df3c0f2b3da4atagen2026-10-22 13:07 UTC651
        {/each}
df3c0f2b3da4atagen2026-10-22 13:07 UTC652
        {#if readers.length > MAX_READERS}<span class="more">+{readers.length - MAX_READERS}</span>{/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC653
      </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC654
    {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC655
  </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC656
</div>
973f8fbdaf80atagen2026-09-28 09:10 UTC657
20fe182f3204atagen2026-10-26 05:50 UTC658
<svelte:window on:pointerdown={unpinReactors} />
20fe182f3204atagen2026-10-26 05:50 UTC659
973f8fbdaf80atagen2026-09-28 09:10 UTC660
<style>
d07bca5da4aeatagen2026-10-26 03:48 UTC661
  .msg { position: relative; display: flex; flex-wrap: wrap; gap: 0 10px; padding: var(--msg-gap, 3px) 0; }
d07bca5da4aeatagen2026-10-26 03:48 UTC662
  .reply-line { position: relative; flex: 0 0 100%; min-width: 0; padding-left: 44px; }
d07bca5da4aeatagen2026-10-26 03:48 UTC663
  .reply-line.linked::before { content: ''; position: absolute; left: 16px; top: 50%; bottom: -3px; width: 24px; border-left: 2px solid var(--border); border-top: 2px solid var(--border); border-top-left-radius: 8px; pointer-events: none; }
20fe182f3204atagen2026-10-26 05:50 UTC664
  .msg.selected { background: var(--hover-faint); }
20fe182f3204atagen2026-10-26 05:50 UTC665
  .msg.selected .hover-time { opacity: 1; }
20fe182f3204atagen2026-10-26 05:50 UTC666
  @media (hover: hover) {
20fe182f3204atagen2026-10-26 05:50 UTC667
    .msg:hover { background: var(--hover-faint); }
20fe182f3204atagen2026-10-26 05:50 UTC668
    .msg:hover .hover-time { opacity: 1; }
20fe182f3204atagen2026-10-26 05:50 UTC669
  }
544a170ead2catagen2026-10-20 11:47 UTC670
  .msg-foot { display: flex; align-items: center; gap: 8px; }
7ebc2472a9e3atagen2026-10-25 09:53 UTC671
  .thread-chip { display: inline-flex; align-items: baseline; gap: 8px; margin-top: 4px; padding: 3px 8px; border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--bg-soft); color: var(--text); font: inherit; font-size: 12px; cursor: pointer; transition: border-color var(--pop), background var(--pop); }
7ebc2472a9e3atagen2026-10-25 09:53 UTC672
  .thread-chip:hover { border-color: var(--accent); background: var(--hover-faint); }
7ebc2472a9e3atagen2026-10-25 09:53 UTC673
  .thread-count { color: var(--accent); font-weight: 650; }
7ebc2472a9e3atagen2026-10-25 09:53 UTC674
  .thread-chip time { color: var(--muted); font-size: 11px; }
973f8fbdaf80atagen2026-09-28 09:10 UTC675
  .sendstate { font-size: 11px; color: var(--muted); }
f604ef6df0c4atagen2026-10-24 10:08 UTC676
  .sent-tick { position: absolute; right: 12px; bottom: 4px; font-size: 11px; color: var(--muted); }
973f8fbdaf80atagen2026-09-28 09:10 UTC677
  .sendstate.failed { color: var(--danger); }
973f8fbdaf80atagen2026-09-28 09:10 UTC678
  .link-previews { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; max-width: 420px; }
973f8fbdaf80atagen2026-09-28 09:10 UTC679
  .avatar {
973f8fbdaf80atagen2026-09-28 09:10 UTC680
    flex: none;
973f8fbdaf80atagen2026-09-28 09:10 UTC681
    width: 34px; height: 34px;
973f8fbdaf80atagen2026-09-28 09:10 UTC682
    border-radius: 50%;
973f8fbdaf80atagen2026-09-28 09:10 UTC683
    display: grid; place-items: center;
973f8fbdaf80atagen2026-09-28 09:10 UTC684
    font-size: 12px; font-weight: 700; color: #fff;
973f8fbdaf80atagen2026-09-28 09:10 UTC685
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC686
  .avatar-spacer { flex: none; width: 34px; padding-top: 2px; text-align: center; }
973f8fbdaf80atagen2026-09-28 09:10 UTC687
  .hover-time { opacity: 0; font-size: 10.5px; color: var(--muted); }
31606c86aa9fatagen2026-10-27 09:48 UTC688
  .msg-body { min-width: 0; flex: 1; }
973f8fbdaf80atagen2026-09-28 09:10 UTC689
  .msg-head { display: flex; align-items: baseline; gap: 8px; }
d2a34d0f4861atagen2026-10-25 07:29 UTC690
  .federation-lag { display: inline-flex; align-items: center; gap: 3px; color: var(--danger); font-size: 11px; font-weight: 650; }
d2a34d0f4861atagen2026-10-25 07:29 UTC691
  .federation-lag > span:last-child { display: inline-grid; place-items: center; min-width: 24px; height: 18px; padding: 0 4px; border: 1px solid currentColor; border-radius: 999px; }
20fe182f3204atagen2026-10-26 05:50 UTC692
  .sender { font-weight: 650; font-size: 13.5px; padding: 0; border: none; background: none; font-family: inherit; text-align: left; cursor: pointer; }
d8624c214c04atagen2026-10-24 08:24 UTC693
  .sender:hover { text-decoration: underline; }
d8624c214c04atagen2026-10-24 08:24 UTC694
  .user-hit { flex: none; align-self: flex-start; padding: 0; border: none; background: none; border-radius: 50%; cursor: pointer; }
973f8fbdaf80atagen2026-09-28 09:10 UTC695
  .time, .edited { color: var(--muted); font-size: 11.5px; }
cf23dbf4e77catagen2026-10-23 06:36 UTC696
  .text .edited { margin-left: 6px; }
cf23dbf4e77catagen2026-10-23 06:36 UTC697
  .msg.editing { background: color-mix(in srgb, var(--accent) 10%, transparent); }
705b1e1062c2atagen2026-10-23 09:05 UTC698
  .msg.mentioned { background: color-mix(in srgb, var(--warn) 8%, transparent); box-shadow: inset 2px 0 0 var(--warn); }
973f8fbdaf80atagen2026-09-28 09:10 UTC699
  .reply-quote {
df3c0f2b3da4atagen2026-10-22 13:07 UTC700
    display: flex; gap: 6px; align-items: baseline;
df3c0f2b3da4atagen2026-10-22 13:07 UTC701
    max-width: min(100%, 520px);
df3c0f2b3da4atagen2026-10-22 13:07 UTC702
    border: none;
973f8fbdaf80atagen2026-09-28 09:10 UTC703
    border-left: 2px solid var(--accent);
973f8fbdaf80atagen2026-09-28 09:10 UTC704
    padding: 2px 8px;
973f8fbdaf80atagen2026-09-28 09:10 UTC705
    margin: 2px 0 4px;
973f8fbdaf80atagen2026-09-28 09:10 UTC706
    color: var(--muted);
973f8fbdaf80atagen2026-09-28 09:10 UTC707
    font-size: 12.5px;
df3c0f2b3da4atagen2026-10-22 13:07 UTC708
    text-align: left;
973f8fbdaf80atagen2026-09-28 09:10 UTC709
    background: var(--bg-soft);
05a48e9e7942atagen2026-10-24 09:32 UTC710
    border-radius: 0 var(--r-sm) var(--r-sm) 0;
df3c0f2b3da4atagen2026-10-22 13:07 UTC711
    cursor: pointer;
973f8fbdaf80atagen2026-09-28 09:10 UTC712
  }
df3c0f2b3da4atagen2026-10-22 13:07 UTC713
  .reply-quote:disabled { cursor: default; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC714
  .reply-quote:hover:not(:disabled) { color: var(--text); }
df3c0f2b3da4atagen2026-10-22 13:07 UTC715
  .reply-sender { flex: none; font-weight: 650; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC716
  .reply-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC717
  :global(.msg.flash) { animation: reply-flash 1.2s ease-out; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC718
  @keyframes -global-reply-flash { from { background: color-mix(in srgb, var(--accent) 22%, transparent); } }
973f8fbdaf80atagen2026-09-28 09:10 UTC719
  .text {
973f8fbdaf80atagen2026-09-28 09:10 UTC720
    margin: 1px 0 2px;
973f8fbdaf80atagen2026-09-28 09:10 UTC721
    font-size: 14px;
df3c0f2b3da4atagen2026-10-22 13:07 UTC722
    line-height: var(--msg-line-height, 1.5);
973f8fbdaf80atagen2026-09-28 09:10 UTC723
    white-space: pre-wrap;
973f8fbdaf80atagen2026-09-28 09:10 UTC724
    overflow-wrap: anywhere;
973f8fbdaf80atagen2026-09-28 09:10 UTC725
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC726
  .text.emote { font-style: italic; color: var(--muted); }
973f8fbdaf80atagen2026-09-28 09:10 UTC727
  .text.notice { color: var(--muted); }
973f8fbdaf80atagen2026-09-28 09:10 UTC728
  .system-line { color: var(--muted); font-size: 12.5px; font-style: italic; margin: 1px 0; }
f604ef6df0c4atagen2026-10-24 10:08 UTC729
  .msg.system { opacity: 0.85; --msg-gap: 1.5px; }
f604ef6df0c4atagen2026-10-24 10:08 UTC730
  .msg.system .avatar-spacer { height: 0; padding-top: 0; }
dfd1feaae855atagen2026-10-24 10:54 UTC731
  .reactions { display: flex; flex-wrap: wrap; gap: 4px; margin-top: max(4px, var(--msg-gap, 3px)); }
973f8fbdaf80atagen2026-09-28 09:10 UTC732
  .reaction {
973f8fbdaf80atagen2026-09-28 09:10 UTC733
    display: inline-flex; align-items: center; gap: 4px;
973f8fbdaf80atagen2026-09-28 09:10 UTC734
    background: var(--bg-soft);
973f8fbdaf80atagen2026-09-28 09:10 UTC735
    border: 1px solid var(--border);
05a48e9e7942atagen2026-10-24 09:32 UTC736
    border-radius: var(--r-sm);
973f8fbdaf80atagen2026-09-28 09:10 UTC737
    padding: 1px 8px;
973f8fbdaf80atagen2026-09-28 09:10 UTC738
    font-size: 13px;
973f8fbdaf80atagen2026-09-28 09:10 UTC739
    color: var(--text);
973f8fbdaf80atagen2026-09-28 09:10 UTC740
    cursor: pointer;
973f8fbdaf80atagen2026-09-28 09:10 UTC741
  }
cf23dbf4e77catagen2026-10-23 06:36 UTC742
  .reaction { position: relative; }
cf23dbf4e77catagen2026-10-23 06:36 UTC743
  .reactors {
cf23dbf4e77catagen2026-10-23 06:36 UTC744
    position: absolute;
cf23dbf4e77catagen2026-10-23 06:36 UTC745
    left: 0;
cf23dbf4e77catagen2026-10-23 06:36 UTC746
    bottom: calc(100% + 6px);
cf23dbf4e77catagen2026-10-23 06:36 UTC747
    z-index: 15;
d07bca5da4aeatagen2026-10-26 03:48 UTC748
    display: block;
cf23dbf4e77catagen2026-10-23 06:36 UTC749
    width: max-content;
cf23dbf4e77catagen2026-10-23 06:36 UTC750
    max-width: 260px;
cf23dbf4e77catagen2026-10-23 06:36 UTC751
    padding: 6px 9px;
cf23dbf4e77catagen2026-10-23 06:36 UTC752
    border: 1px solid var(--border);
05a48e9e7942atagen2026-10-24 09:32 UTC753
    border-radius: var(--r-md);
cf23dbf4e77catagen2026-10-23 06:36 UTC754
    background: var(--panel-2);
cf23dbf4e77catagen2026-10-23 06:36 UTC755
    box-shadow: 0 6px 18px rgb(0 0 0 / 0.3);
cf23dbf4e77catagen2026-10-23 06:36 UTC756
    color: var(--text);
cf23dbf4e77catagen2026-10-23 06:36 UTC757
    font-size: 12px;
cf23dbf4e77catagen2026-10-23 06:36 UTC758
    line-height: 1.35;
cf23dbf4e77catagen2026-10-23 06:36 UTC759
    text-align: left;
cf23dbf4e77catagen2026-10-23 06:36 UTC760
    white-space: normal;
cf23dbf4e77catagen2026-10-23 06:36 UTC761
    pointer-events: none;
cf23dbf4e77catagen2026-10-23 06:36 UTC762
  }
cf23dbf4e77catagen2026-10-23 06:36 UTC763
  .reactors-key { display: block; color: var(--muted); font-size: 11px; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC764
  .reaction.mine { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 16%, transparent); }
973f8fbdaf80atagen2026-09-28 09:10 UTC765
  .reaction:disabled { cursor: default; opacity: 0.85; }
31606c86aa9fatagen2026-10-27 09:48 UTC766
  .msg:not(.system) .msg-body { padding-right: 72px; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC767
  .readby { position: absolute; right: 12px; bottom: 4px; display: flex; align-items: center; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC768
  .readby > :global(* + *) { margin-left: -4px; }
df3c0f2b3da4atagen2026-10-22 13:07 UTC769
  .readby :global(.mxc-avatar), .readby :global(.mxc-fallback) { box-shadow: 0 0 0 1.5px var(--bg); }
df3c0f2b3da4atagen2026-10-22 13:07 UTC770
  .readby .more { margin-left: 3px; font-size: 10.5px; color: var(--muted); }
544a170ead2catagen2026-10-20 11:47 UTC771
  .actions {
31606c86aa9fatagen2026-10-27 09:48 UTC772
    position: absolute; bottom: calc(100% - 18px); right: 10px; z-index: 2;
8c81366a752fatagen2026-10-26 03:56 UTC773
    display: flex; align-items: flex-start; gap: 2px;
544a170ead2catagen2026-10-20 11:47 UTC774
    padding: 2px;
544a170ead2catagen2026-10-20 11:47 UTC775
    background: var(--bg-soft);
544a170ead2catagen2026-10-20 11:47 UTC776
    border: 1px solid var(--border);
05a48e9e7942atagen2026-10-24 09:32 UTC777
    border-radius: var(--r-md);
544a170ead2catagen2026-10-20 11:47 UTC778
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
544a170ead2catagen2026-10-20 11:47 UTC779
  }
8c81366a752fatagen2026-10-26 03:56 UTC780
  .quick { display: grid; grid-template-columns: repeat(var(--quick-cols, 8), auto); gap: 2px; }
8c81366a752fatagen2026-10-26 03:56 UTC781
  .quick:empty { display: none; }
5059ed753f8batagen2026-10-27 07:20 UTC782
  .quick .group-start { grid-column-start: 1; }
544a170ead2catagen2026-10-20 11:47 UTC783
  .actions button {
973f8fbdaf80atagen2026-09-28 09:10 UTC784
    background: transparent;
973f8fbdaf80atagen2026-09-28 09:10 UTC785
    border: 1px solid transparent;
05a48e9e7942atagen2026-10-24 09:32 UTC786
    border-radius: var(--r-sm);
973f8fbdaf80atagen2026-09-28 09:10 UTC787
    cursor: pointer;
544a170ead2catagen2026-10-20 11:47 UTC788
    font-size: 14px;
544a170ead2catagen2026-10-20 11:47 UTC789
    padding: 1px 5px;
973f8fbdaf80atagen2026-09-28 09:10 UTC790
  }
df3c0f2b3da4atagen2026-10-22 13:07 UTC791
  .actions button:hover:not(:disabled) { border-color: var(--border); background: var(--hover); }
544a170ead2catagen2026-10-20 11:47 UTC792
  .actions button:disabled { opacity: 0.4; cursor: default; }
544a170ead2catagen2026-10-20 11:47 UTC793
  .emote { width: 1.3em; height: 1.3em; object-fit: contain; vertical-align: middle; }
20fe182f3204atagen2026-10-26 05:50 UTC794
  @media (pointer: coarse) {
20fe182f3204atagen2026-10-26 05:50 UTC795
    .actions button { min-width: 36px; min-height: 40px; font-size: 16px; }
20fe182f3204atagen2026-10-26 05:50 UTC796
    .actions { top: auto; bottom: calc(100% - 18px); right: 6px; flex-wrap: wrap; justify-content: flex-end; max-width: calc(100% - 12px); }
20fe182f3204atagen2026-10-26 05:50 UTC797
    .reaction::after, .sender::after, .thread-chip::after { content: ''; position: absolute; inset: -9px -2px; }
20fe182f3204atagen2026-10-26 05:50 UTC798
    .sender, .thread-chip { position: relative; }
20fe182f3204atagen2026-10-26 05:50 UTC799
  }
05a48e9e7942atagen2026-10-24 09:32 UTC800
  .picker-pop { position: fixed; z-index: 60; width: 320px; max-width: calc(100vw - 16px); animation: pop-in var(--pop); }
973f8fbdaf80atagen2026-09-28 09:10 UTC801
</style>