atagen

malice

public
Star0
Watch0
973f8fbdaf80e2ef6679dd071233fe6b853344b2
2
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';
973f8fbdaf80atagen2026-09-28 09:10 UTC4
  import type { SendState, TimelineContent, TimelineEvent } from './types';
973f8fbdaf80atagen2026-09-28 09:10 UTC5
  import { senderLabel } from './types';
973f8fbdaf80atagen2026-09-28 09:10 UTC6
  import { formatClock, formatDay, initialsOf, senderHue } from './format';
973f8fbdaf80atagen2026-09-28 09:10 UTC7
  import { findUrls } from './urls';
973f8fbdaf80atagen2026-09-28 09:10 UTC8
973f8fbdaf80atagen2026-09-28 09:10 UTC9
  export let itemId: string;
973f8fbdaf80atagen2026-09-28 09:10 UTC10
  export let event: TimelineEvent;
973f8fbdaf80atagen2026-09-28 09:10 UTC11
  export let showSender: boolean;
973f8fbdaf80atagen2026-09-28 09:10 UTC12
  export let reacting: boolean;
973f8fbdaf80atagen2026-09-28 09:10 UTC13
  export let onReact: (eventId: string, key: string) => void;
973f8fbdaf80atagen2026-09-28 09:10 UTC14
  export let senderAvatar: string | null = null;
973f8fbdaf80atagen2026-09-28 09:10 UTC15
973f8fbdaf80atagen2026-09-28 09:10 UTC16
  const QUICK_KEYS = ['๐Ÿ‘', 'โค๏ธ', '๐Ÿ˜‚', '๐ŸŽ‰', '๐Ÿ˜ฎ', '๐Ÿ˜ข'];
973f8fbdaf80atagen2026-09-28 09:10 UTC17
973f8fbdaf80atagen2026-09-28 09:10 UTC18
  const MEMBERSHIP_LABELS: Record<string, string> = {
973f8fbdaf80atagen2026-09-28 09:10 UTC19
    Joined: 'joined the room',
973f8fbdaf80atagen2026-09-28 09:10 UTC20
    Left: 'left the room',
973f8fbdaf80atagen2026-09-28 09:10 UTC21
    Banned: 'was banned',
973f8fbdaf80atagen2026-09-28 09:10 UTC22
    Unbanned: 'was unbanned',
973f8fbdaf80atagen2026-09-28 09:10 UTC23
    Kicked: 'was kicked',
973f8fbdaf80atagen2026-09-28 09:10 UTC24
    KickedAndBanned: 'was kicked and banned',
973f8fbdaf80atagen2026-09-28 09:10 UTC25
    Invited: 'was invited',
973f8fbdaf80atagen2026-09-28 09:10 UTC26
    InvitationAccepted: 'accepted the invite',
973f8fbdaf80atagen2026-09-28 09:10 UTC27
    InvitationRejected: 'rejected the invite',
973f8fbdaf80atagen2026-09-28 09:10 UTC28
    InvitationRevoked: 'had the invite revoked',
973f8fbdaf80atagen2026-09-28 09:10 UTC29
    Knocked: 'requested to join',
973f8fbdaf80atagen2026-09-28 09:10 UTC30
    KnockAccepted: 'was let in',
973f8fbdaf80atagen2026-09-28 09:10 UTC31
    KnockRetracted: 'withdrew the request to join',
973f8fbdaf80atagen2026-09-28 09:10 UTC32
    KnockDenied: 'had the request to join denied'
973f8fbdaf80atagen2026-09-28 09:10 UTC33
  };
973f8fbdaf80atagen2026-09-28 09:10 UTC34
973f8fbdaf80atagen2026-09-28 09:10 UTC35
  $: name = senderLabel(event.sender);
973f8fbdaf80atagen2026-09-28 09:10 UTC36
  $: displayName = event.sender.name_ambiguous ? `${name} (${event.sender.user_id})` : name;
973f8fbdaf80atagen2026-09-28 09:10 UTC37
  $: interactive = event.content.kind === 'Message' || event.content.kind === 'Sticker' || event.content.kind === 'Poll';
973f8fbdaf80atagen2026-09-28 09:10 UTC38
  $: previews = event.content.kind === 'Message' ? findUrls(event.content.message.body) : [];
973f8fbdaf80atagen2026-09-28 09:10 UTC39
973f8fbdaf80atagen2026-09-28 09:10 UTC40
  function sendLabel(state: SendState): string {
973f8fbdaf80atagen2026-09-28 09:10 UTC41
    if (state.kind === 'Failed') return state.will_retry ? 'failed โ€” will retry' : 'failed โ€” not delivered';
973f8fbdaf80atagen2026-09-28 09:10 UTC42
    if (state.kind === 'Sending') return 'sendingโ€ฆ';
973f8fbdaf80atagen2026-09-28 09:10 UTC43
    if (event.own && state.kind === 'Sent' && event.event_id) return 'โœ“ sent';
973f8fbdaf80atagen2026-09-28 09:10 UTC44
    return '';
973f8fbdaf80atagen2026-09-28 09:10 UTC45
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC46
973f8fbdaf80atagen2026-09-28 09:10 UTC47
  function actorLabel(userId: string, name: string | null): string {
973f8fbdaf80atagen2026-09-28 09:10 UTC48
    return name ?? userId;
973f8fbdaf80atagen2026-09-28 09:10 UTC49
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC50
973f8fbdaf80atagen2026-09-28 09:10 UTC51
  function replyPreview(content: TimelineContent): string {
973f8fbdaf80atagen2026-09-28 09:10 UTC52
    if (content.kind === 'Message') return content.message.body;
973f8fbdaf80atagen2026-09-28 09:10 UTC53
    if (content.kind === 'Sticker') return content.body || 'Sticker';
973f8fbdaf80atagen2026-09-28 09:10 UTC54
    if (content.kind === 'Poll') return `Poll: ${content.question}`;
973f8fbdaf80atagen2026-09-28 09:10 UTC55
    if (content.kind === 'Redacted') return 'Message deleted';
973f8fbdaf80atagen2026-09-28 09:10 UTC56
    if (content.kind === 'UnableToDecrypt') return 'Unable to decrypt';
973f8fbdaf80atagen2026-09-28 09:10 UTC57
    return systemLine(content);
973f8fbdaf80atagen2026-09-28 09:10 UTC58
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC59
973f8fbdaf80atagen2026-09-28 09:10 UTC60
  function systemLine(content: TimelineContent): string {
973f8fbdaf80atagen2026-09-28 09:10 UTC61
    switch (content.kind) {
973f8fbdaf80atagen2026-09-28 09:10 UTC62
      case 'Redacted':
973f8fbdaf80atagen2026-09-28 09:10 UTC63
        return 'Message deleted';
973f8fbdaf80atagen2026-09-28 09:10 UTC64
      case 'UnableToDecrypt':
973f8fbdaf80atagen2026-09-28 09:10 UTC65
        return `Message from ${displayName} could not be decrypted`;
973f8fbdaf80atagen2026-09-28 09:10 UTC66
      case 'Membership': {
973f8fbdaf80atagen2026-09-28 09:10 UTC67
        const who = actorLabel(content.user_id, content.display_name);
973f8fbdaf80atagen2026-09-28 09:10 UTC68
        return `${who} ${MEMBERSHIP_LABELS[content.change] ?? 'updated their membership'}`;
973f8fbdaf80atagen2026-09-28 09:10 UTC69
      }
973f8fbdaf80atagen2026-09-28 09:10 UTC70
      case 'Profile': {
973f8fbdaf80atagen2026-09-28 09:10 UTC71
        const who = actorLabel(content.user_id, null);
973f8fbdaf80atagen2026-09-28 09:10 UTC72
        const parts: string[] = [];
973f8fbdaf80atagen2026-09-28 09:10 UTC73
        if (content.display_name) {
973f8fbdaf80atagen2026-09-28 09:10 UTC74
          parts.push(
973f8fbdaf80atagen2026-09-28 09:10 UTC75
            content.display_name.new
973f8fbdaf80atagen2026-09-28 09:10 UTC76
              ? `changed their display name to "${content.display_name.new}"`
973f8fbdaf80atagen2026-09-28 09:10 UTC77
              : 'removed their display name'
973f8fbdaf80atagen2026-09-28 09:10 UTC78
          );
973f8fbdaf80atagen2026-09-28 09:10 UTC79
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC80
        if (content.avatar) parts.push('changed their avatar');
973f8fbdaf80atagen2026-09-28 09:10 UTC81
        return `${who} ${parts.length > 0 ? parts.join(' and ') : 'updated their profile'}`;
973f8fbdaf80atagen2026-09-28 09:10 UTC82
      }
973f8fbdaf80atagen2026-09-28 09:10 UTC83
      case 'RoomName':
973f8fbdaf80atagen2026-09-28 09:10 UTC84
        return content.name
973f8fbdaf80atagen2026-09-28 09:10 UTC85
          ? `${displayName} changed the room name to "${content.name}"`
973f8fbdaf80atagen2026-09-28 09:10 UTC86
          : `${displayName} removed the room name`;
973f8fbdaf80atagen2026-09-28 09:10 UTC87
      case 'RoomTopic':
973f8fbdaf80atagen2026-09-28 09:10 UTC88
        return content.topic
973f8fbdaf80atagen2026-09-28 09:10 UTC89
          ? `${displayName} changed the topic to "${content.topic}"`
973f8fbdaf80atagen2026-09-28 09:10 UTC90
          : `${displayName} removed the topic`;
973f8fbdaf80atagen2026-09-28 09:10 UTC91
      case 'RoomAvatar':
973f8fbdaf80atagen2026-09-28 09:10 UTC92
        return content.avatar ? `${displayName} changed the room avatar` : `${displayName} removed the room avatar`;
973f8fbdaf80atagen2026-09-28 09:10 UTC93
      case 'EncryptionEnabled':
973f8fbdaf80atagen2026-09-28 09:10 UTC94
        return `${displayName} enabled encryption in this room`;
973f8fbdaf80atagen2026-09-28 09:10 UTC95
      case 'RoomCreated':
973f8fbdaf80atagen2026-09-28 09:10 UTC96
        return `${displayName} created the room`;
973f8fbdaf80atagen2026-09-28 09:10 UTC97
      case 'RoomUpgraded':
973f8fbdaf80atagen2026-09-28 09:10 UTC98
        return `${displayName} upgraded the room to a new version`;
973f8fbdaf80atagen2026-09-28 09:10 UTC99
      case 'OtherState':
973f8fbdaf80atagen2026-09-28 09:10 UTC100
        return `${displayName} sent a state event (${content.event_type})`;
973f8fbdaf80atagen2026-09-28 09:10 UTC101
      case 'Other':
973f8fbdaf80atagen2026-09-28 09:10 UTC102
        return `${displayName} sent an event (${content.event_type})`;
973f8fbdaf80atagen2026-09-28 09:10 UTC103
      default:
973f8fbdaf80atagen2026-09-28 09:10 UTC104
        return displayName;
973f8fbdaf80atagen2026-09-28 09:10 UTC105
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC106
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC107
</script>
973f8fbdaf80atagen2026-09-28 09:10 UTC108
973f8fbdaf80atagen2026-09-28 09:10 UTC109
<div class="msg" class:own={event.own} class:system={!interactive} data-item={itemId}>
973f8fbdaf80atagen2026-09-28 09:10 UTC110
  {#if showSender}
973f8fbdaf80atagen2026-09-28 09:10 UTC111
    {#if senderAvatar}
973f8fbdaf80atagen2026-09-28 09:10 UTC112
      <MxcAvatar mxc={senderAvatar} label={name} size={34} />
973f8fbdaf80atagen2026-09-28 09:10 UTC113
    {:else}
973f8fbdaf80atagen2026-09-28 09:10 UTC114
      <div
973f8fbdaf80atagen2026-09-28 09:10 UTC115
        class="avatar"
973f8fbdaf80atagen2026-09-28 09:10 UTC116
        aria-hidden="true"
973f8fbdaf80atagen2026-09-28 09:10 UTC117
        style={`background:hsl(${senderHue(event.sender.user_id)} 35% 32%)`}
973f8fbdaf80atagen2026-09-28 09:10 UTC118
      >
973f8fbdaf80atagen2026-09-28 09:10 UTC119
        {initialsOf(name)}
973f8fbdaf80atagen2026-09-28 09:10 UTC120
      </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC121
    {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC122
  {:else}
973f8fbdaf80atagen2026-09-28 09:10 UTC123
    <div class="avatar-spacer" aria-hidden="true">
973f8fbdaf80atagen2026-09-28 09:10 UTC124
      <span class="hover-time">{formatClock(event.timestamp_ms)}</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC125
    </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC126
  {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC127
  <div class="msg-body">
973f8fbdaf80atagen2026-09-28 09:10 UTC128
    {#if showSender}
973f8fbdaf80atagen2026-09-28 09:10 UTC129
      <div class="msg-head">
973f8fbdaf80atagen2026-09-28 09:10 UTC130
        <span class="sender" style={`color:hsl(${senderHue(event.sender.user_id)} 70% 72%)`}>
973f8fbdaf80atagen2026-09-28 09:10 UTC131
          {displayName}
973f8fbdaf80atagen2026-09-28 09:10 UTC132
        </span>
973f8fbdaf80atagen2026-09-28 09:10 UTC133
        <time class="time" datetime={new Date(event.timestamp_ms).toISOString()}>
973f8fbdaf80atagen2026-09-28 09:10 UTC134
          {formatDay(event.timestamp_ms)} {formatClock(event.timestamp_ms)}
973f8fbdaf80atagen2026-09-28 09:10 UTC135
        </time>
973f8fbdaf80atagen2026-09-28 09:10 UTC136
        {#if event.content.kind === 'Message' && event.content.message.edited}
973f8fbdaf80atagen2026-09-28 09:10 UTC137
          <span class="edited" title="Edited">(edited)</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC138
        {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC139
      </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC140
    {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC141
    {#if event.reply_to}
973f8fbdaf80atagen2026-09-28 09:10 UTC142
      <div class="reply-quote">
973f8fbdaf80atagen2026-09-28 09:10 UTC143
        {#if event.reply_to.details.kind === 'Ready'}
973f8fbdaf80atagen2026-09-28 09:10 UTC144
          Replying to {senderLabel(event.reply_to.details.sender)}: {replyPreview(event.reply_to.details.content)}
973f8fbdaf80atagen2026-09-28 09:10 UTC145
        {:else if event.reply_to.details.kind === 'Pending'}
973f8fbdaf80atagen2026-09-28 09:10 UTC146
          Loading replyโ€ฆ
973f8fbdaf80atagen2026-09-28 09:10 UTC147
        {:else}
973f8fbdaf80atagen2026-09-28 09:10 UTC148
          Reply unavailable
973f8fbdaf80atagen2026-09-28 09:10 UTC149
        {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC150
      </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC151
    {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC152
973f8fbdaf80atagen2026-09-28 09:10 UTC153
    {#if event.content.kind === 'Message'}
973f8fbdaf80atagen2026-09-28 09:10 UTC154
      {@const m = event.content.message}
973f8fbdaf80atagen2026-09-28 09:10 UTC155
      <p class="text" class:emote={m.kind === 'Emote'} class:notice={m.kind === 'Notice' || m.kind === 'ServerNotice'}>
973f8fbdaf80atagen2026-09-28 09:10 UTC156
        {#if m.kind === 'Emote'}* {displayName} {/if}{m.body}
973f8fbdaf80atagen2026-09-28 09:10 UTC157
      </p>
973f8fbdaf80atagen2026-09-28 09:10 UTC158
      {#if m.media}
973f8fbdaf80atagen2026-09-28 09:10 UTC159
        <MediaAttachmentView attachment={m.media} />
973f8fbdaf80atagen2026-09-28 09:10 UTC160
      {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC161
      {#if previews.length > 0}
973f8fbdaf80atagen2026-09-28 09:10 UTC162
        <div class="link-previews">
973f8fbdaf80atagen2026-09-28 09:10 UTC163
          {#each previews as preview (preview.href)}
973f8fbdaf80atagen2026-09-28 09:10 UTC164
            <a class="link-card" href={preview.href} target="_blank" rel="noopener noreferrer">
973f8fbdaf80atagen2026-09-28 09:10 UTC165
              <span class="link-host">{preview.hostname}</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC166
              {#if preview.path}<span class="link-path">{preview.path}</span>{/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC167
              <span class="link-url">{preview.display}</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC168
            </a>
973f8fbdaf80atagen2026-09-28 09:10 UTC169
          {/each}
973f8fbdaf80atagen2026-09-28 09:10 UTC170
        </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC171
      {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC172
    {:else if event.content.kind === 'Sticker'}
973f8fbdaf80atagen2026-09-28 09:10 UTC173
      {#if event.content.body}<p class="text">{event.content.body}</p>{/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC174
      {#if event.content.media}
973f8fbdaf80atagen2026-09-28 09:10 UTC175
        <MediaAttachmentView attachment={event.content.media} />
973f8fbdaf80atagen2026-09-28 09:10 UTC176
      {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC177
    {:else if event.content.kind === 'Poll'}
973f8fbdaf80atagen2026-09-28 09:10 UTC178
      <p class="text">๐Ÿ“Š {event.content.question}</p>
973f8fbdaf80atagen2026-09-28 09:10 UTC179
    {:else}
973f8fbdaf80atagen2026-09-28 09:10 UTC180
      <p class="system-line">{systemLine(event.content)}</p>
973f8fbdaf80atagen2026-09-28 09:10 UTC181
    {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC182
973f8fbdaf80atagen2026-09-28 09:10 UTC183
    {#if interactive && event.reactions.length > 0}
973f8fbdaf80atagen2026-09-28 09:10 UTC184
      <div class="reactions" aria-label="Reactions">
973f8fbdaf80atagen2026-09-28 09:10 UTC185
        {#each event.reactions as r (r.key)}
973f8fbdaf80atagen2026-09-28 09:10 UTC186
          <button
973f8fbdaf80atagen2026-09-28 09:10 UTC187
            type="button"
973f8fbdaf80atagen2026-09-28 09:10 UTC188
            class="reaction"
973f8fbdaf80atagen2026-09-28 09:10 UTC189
            class:mine={r.own}
973f8fbdaf80atagen2026-09-28 09:10 UTC190
            title={r.own ? 'You reacted โ€” activate to remove' : `${r.count} reaction${r.count === 1 ? '' : 's'}`}
973f8fbdaf80atagen2026-09-28 09:10 UTC191
            disabled={!event.event_id || reacting}
973f8fbdaf80atagen2026-09-28 09:10 UTC192
            on:click={() => event.event_id && onReact(event.event_id, r.key)}
973f8fbdaf80atagen2026-09-28 09:10 UTC193
          >
973f8fbdaf80atagen2026-09-28 09:10 UTC194
            <span aria-hidden="true">{r.key}</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC195
            <span class="count">{r.count}</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC196
          </button>
973f8fbdaf80atagen2026-09-28 09:10 UTC197
        {/each}
973f8fbdaf80atagen2026-09-28 09:10 UTC198
      </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC199
    {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC200
    {#if interactive}
973f8fbdaf80atagen2026-09-28 09:10 UTC201
      <div class="msg-foot">
973f8fbdaf80atagen2026-09-28 09:10 UTC202
        {#if sendLabel(event.send_state)}
973f8fbdaf80atagen2026-09-28 09:10 UTC203
          <span class="sendstate" class:failed={event.send_state.kind === 'Failed'}>{sendLabel(event.send_state)}</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC204
        {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC205
        {#if event.read_by.length > 0}
973f8fbdaf80atagen2026-09-28 09:10 UTC206
          <span class="readby" title={event.read_by.join(', ')}>โœ“โœ“ {event.read_by.length}</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC207
        {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC208
        {#if event.event_id}
973f8fbdaf80atagen2026-09-28 09:10 UTC209
          <span class="quick-react" role="group" aria-label={`React to message from ${displayName}`}>
973f8fbdaf80atagen2026-09-28 09:10 UTC210
            {#each QUICK_KEYS as key (key)}
973f8fbdaf80atagen2026-09-28 09:10 UTC211
              <button
973f8fbdaf80atagen2026-09-28 09:10 UTC212
                type="button"
973f8fbdaf80atagen2026-09-28 09:10 UTC213
                title={`React ${key}`}
973f8fbdaf80atagen2026-09-28 09:10 UTC214
                aria-label={`React ${key} to message from ${displayName}`}
973f8fbdaf80atagen2026-09-28 09:10 UTC215
                disabled={reacting}
973f8fbdaf80atagen2026-09-28 09:10 UTC216
                on:click={() => onReact(event.event_id ?? '', key)}
973f8fbdaf80atagen2026-09-28 09:10 UTC217
              >
973f8fbdaf80atagen2026-09-28 09:10 UTC218
                {key}
973f8fbdaf80atagen2026-09-28 09:10 UTC219
              </button>
973f8fbdaf80atagen2026-09-28 09:10 UTC220
            {/each}
973f8fbdaf80atagen2026-09-28 09:10 UTC221
          </span>
973f8fbdaf80atagen2026-09-28 09:10 UTC222
        {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC223
      </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC224
    {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC225
  </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC226
</div>
973f8fbdaf80atagen2026-09-28 09:10 UTC227
973f8fbdaf80atagen2026-09-28 09:10 UTC228
<style>
973f8fbdaf80atagen2026-09-28 09:10 UTC229
  .msg { display: flex; gap: 10px; padding: 3px 0; }
973f8fbdaf80atagen2026-09-28 09:10 UTC230
  .msg:hover { background: rgba(255, 255, 255, 0.018); }
973f8fbdaf80atagen2026-09-28 09:10 UTC231
  .msg:hover .hover-time { opacity: 1; }
973f8fbdaf80atagen2026-09-28 09:10 UTC232
  .msg-foot { display: flex; align-items: center; gap: 8px; min-height: 18px; }
973f8fbdaf80atagen2026-09-28 09:10 UTC233
  .sendstate { font-size: 11px; color: var(--muted); }
973f8fbdaf80atagen2026-09-28 09:10 UTC234
  .sendstate.failed { color: var(--danger); }
973f8fbdaf80atagen2026-09-28 09:10 UTC235
  .link-previews { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; max-width: 420px; }
973f8fbdaf80atagen2026-09-28 09:10 UTC236
  .link-card {
973f8fbdaf80atagen2026-09-28 09:10 UTC237
    display: flex; flex-direction: column; gap: 1px;
973f8fbdaf80atagen2026-09-28 09:10 UTC238
    border: 1px solid var(--border); border-radius: 8px;
973f8fbdaf80atagen2026-09-28 09:10 UTC239
    background: var(--bg-soft); padding: 8px 10px;
973f8fbdaf80atagen2026-09-28 09:10 UTC240
    text-decoration: none; color: var(--text);
973f8fbdaf80atagen2026-09-28 09:10 UTC241
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC242
  .link-card:hover { border-color: var(--accent); }
973f8fbdaf80atagen2026-09-28 09:10 UTC243
  .link-host { font-size: 12.5px; font-weight: 650; }
973f8fbdaf80atagen2026-09-28 09:10 UTC244
  .link-path { font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
973f8fbdaf80atagen2026-09-28 09:10 UTC245
  .link-url { font-size: 11.5px; color: var(--accent); overflow-wrap: anywhere; }
973f8fbdaf80atagen2026-09-28 09:10 UTC246
  .avatar {
973f8fbdaf80atagen2026-09-28 09:10 UTC247
    flex: none;
973f8fbdaf80atagen2026-09-28 09:10 UTC248
    width: 34px; height: 34px;
973f8fbdaf80atagen2026-09-28 09:10 UTC249
    border-radius: 50%;
973f8fbdaf80atagen2026-09-28 09:10 UTC250
    display: grid; place-items: center;
973f8fbdaf80atagen2026-09-28 09:10 UTC251
    font-size: 12px; font-weight: 700; color: #fff;
973f8fbdaf80atagen2026-09-28 09:10 UTC252
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC253
  .avatar-spacer { flex: none; width: 34px; padding-top: 2px; text-align: center; }
973f8fbdaf80atagen2026-09-28 09:10 UTC254
  .hover-time { opacity: 0; font-size: 10.5px; color: var(--muted); }
973f8fbdaf80atagen2026-09-28 09:10 UTC255
  .msg-body { min-width: 0; flex: 1; }
973f8fbdaf80atagen2026-09-28 09:10 UTC256
  .msg-head { display: flex; align-items: baseline; gap: 8px; }
973f8fbdaf80atagen2026-09-28 09:10 UTC257
  .sender { font-weight: 650; font-size: 13.5px; }
973f8fbdaf80atagen2026-09-28 09:10 UTC258
  .time, .edited { color: var(--muted); font-size: 11.5px; }
973f8fbdaf80atagen2026-09-28 09:10 UTC259
  .reply-quote {
973f8fbdaf80atagen2026-09-28 09:10 UTC260
    border-left: 2px solid var(--accent);
973f8fbdaf80atagen2026-09-28 09:10 UTC261
    padding: 2px 8px;
973f8fbdaf80atagen2026-09-28 09:10 UTC262
    margin: 2px 0 4px;
973f8fbdaf80atagen2026-09-28 09:10 UTC263
    color: var(--muted);
973f8fbdaf80atagen2026-09-28 09:10 UTC264
    font-size: 12.5px;
973f8fbdaf80atagen2026-09-28 09:10 UTC265
    background: var(--bg-soft);
973f8fbdaf80atagen2026-09-28 09:10 UTC266
    border-radius: 0 6px 6px 0;
973f8fbdaf80atagen2026-09-28 09:10 UTC267
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC268
  .text {
973f8fbdaf80atagen2026-09-28 09:10 UTC269
    margin: 1px 0 2px;
973f8fbdaf80atagen2026-09-28 09:10 UTC270
    font-size: 14px;
973f8fbdaf80atagen2026-09-28 09:10 UTC271
    line-height: 1.5;
973f8fbdaf80atagen2026-09-28 09:10 UTC272
    white-space: pre-wrap;
973f8fbdaf80atagen2026-09-28 09:10 UTC273
    overflow-wrap: anywhere;
973f8fbdaf80atagen2026-09-28 09:10 UTC274
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC275
  .text.emote { font-style: italic; color: var(--muted); }
973f8fbdaf80atagen2026-09-28 09:10 UTC276
  .text.notice { color: var(--muted); }
973f8fbdaf80atagen2026-09-28 09:10 UTC277
  .system-line { color: var(--muted); font-size: 12.5px; font-style: italic; margin: 1px 0; }
973f8fbdaf80atagen2026-09-28 09:10 UTC278
  .msg.system { opacity: 0.85; }
973f8fbdaf80atagen2026-09-28 09:10 UTC279
  .reactions { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
973f8fbdaf80atagen2026-09-28 09:10 UTC280
  .reaction {
973f8fbdaf80atagen2026-09-28 09:10 UTC281
    display: inline-flex; align-items: center; gap: 4px;
973f8fbdaf80atagen2026-09-28 09:10 UTC282
    background: var(--bg-soft);
973f8fbdaf80atagen2026-09-28 09:10 UTC283
    border: 1px solid var(--border);
973f8fbdaf80atagen2026-09-28 09:10 UTC284
    border-radius: 999px;
973f8fbdaf80atagen2026-09-28 09:10 UTC285
    padding: 1px 8px;
973f8fbdaf80atagen2026-09-28 09:10 UTC286
    font-size: 13px;
973f8fbdaf80atagen2026-09-28 09:10 UTC287
    color: var(--text);
973f8fbdaf80atagen2026-09-28 09:10 UTC288
    cursor: pointer;
973f8fbdaf80atagen2026-09-28 09:10 UTC289
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC290
  .reaction.mine { border-color: var(--accent); background: rgba(88, 101, 242, 0.16); }
973f8fbdaf80atagen2026-09-28 09:10 UTC291
  .reaction:disabled { cursor: default; opacity: 0.85; }
973f8fbdaf80atagen2026-09-28 09:10 UTC292
  .msg-foot { display: flex; align-items: center; gap: 8px; min-height: 18px; }
973f8fbdaf80atagen2026-09-28 09:10 UTC293
  .readby { font-size: 11px; color: var(--muted); }
973f8fbdaf80atagen2026-09-28 09:10 UTC294
  .msg:hover .quick-react, .msg:focus-within .quick-react { display: inline-flex; }
973f8fbdaf80atagen2026-09-28 09:10 UTC295
  .quick-react button {
973f8fbdaf80atagen2026-09-28 09:10 UTC296
    background: transparent;
973f8fbdaf80atagen2026-09-28 09:10 UTC297
    border: 1px solid transparent;
973f8fbdaf80atagen2026-09-28 09:10 UTC298
    border-radius: 6px;
973f8fbdaf80atagen2026-09-28 09:10 UTC299
    cursor: pointer;
973f8fbdaf80atagen2026-09-28 09:10 UTC300
    font-size: 13px;
973f8fbdaf80atagen2026-09-28 09:10 UTC301
    padding: 0 3px;
973f8fbdaf80atagen2026-09-28 09:10 UTC302
    opacity: 0.75;
973f8fbdaf80atagen2026-09-28 09:10 UTC303
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC304
  .quick-react button:hover:not(:disabled) { opacity: 1; border-color: var(--border); background: var(--bg-soft); }
973f8fbdaf80atagen2026-09-28 09:10 UTC305
  .quick-react button:disabled { opacity: 0.4; cursor: default; }
973f8fbdaf80atagen2026-09-28 09:10 UTC306
</style>