Commit fe847416ebd8
tarball7 files changed+10-139
@@ -2250,7 +2250,7 @@ checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e"
22502250
[[package]]22512251
name = "macltok"22522252
version = "0.1.0"2253
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/macltok?rev=aced11e80b47de7a887296b10eedff310df5360f#aced11e80b47de7a887296b10eedff310df5360f"2253
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/macltok?rev=3eee54c8ed63154fa8fe3d292e7556b4c19379fa#3eee54c8ed63154fa8fe3d292e7556b4c19379fa"22542254
dependencies = [22552255
"futures-util",22562256
"getrandom 0.2.17",@@ -2283,7 +2283,7 @@ dependencies = [
22832283
[[package]]22842284
name = "macltok-push-gateway"22852285
version = "0.1.0"2286
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/macltok?rev=aced11e80b47de7a887296b10eedff310df5360f#aced11e80b47de7a887296b10eedff310df5360f"2286
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/macltok?rev=3eee54c8ed63154fa8fe3d292e7556b4c19379fa#3eee54c8ed63154fa8fe3d292e7556b4c19379fa"22872287
dependencies = [22882288
"ruma-common",22892289
"serde",@@ -3,8 +3,8 @@ resolver = "3"
33
members = ["crates/malice-core", "crates/malice-push", "crates/malice-server", "crates/malice-static"]44
55
[workspace.dependencies]6
-macltok = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/macltok", rev = "aced11e80b47de7a887296b10eedff310df5360f" }7
-macltok-push-gateway = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/macltok", rev = "aced11e80b47de7a887296b10eedff310df5360f" }6
+macltok = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/macltok", rev = "3eee54c8ed63154fa8fe3d292e7556b4c19379fa" }7
+macltok-push-gateway = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/macltok", rev = "3eee54c8ed63154fa8fe3d292e7556b4c19379fa" }88
ruma = { version = "0.16", default-features = false }99
serde = { version = "1", features = ["derive"] }1010
serde_json = "1"@@ -1999,7 +1999,7 @@ checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e"
19991999
[[package]]20002000
name = "macltok"20012001
version = "0.1.0"2002
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/macltok?rev=aced11e80b47de7a887296b10eedff310df5360f#aced11e80b47de7a887296b10eedff310df5360f"2002
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/macltok?rev=3eee54c8ed63154fa8fe3d292e7556b4c19379fa#3eee54c8ed63154fa8fe3d292e7556b4c19379fa"20032003
dependencies = [20042004
"futures-util",20052005
"getrandom 0.2.17",@@ -14,7 +14,7 @@ publish = false
1414
crate-type = ["cdylib", "rlib"]1515
1616
[target.'cfg(target_family = "wasm")'.dependencies]17
-macltok = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/macltok", rev = "aced11e80b47de7a887296b10eedff310df5360f", features = ["indexeddb", "serde", "mirror"] }17
+macltok = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/macltok", rev = "3eee54c8ed63154fa8fe3d292e7556b4c19379fa", features = ["indexeddb", "serde", "mirror"] }1818
malice-core = { path = "../malice-core" }1919
ruma = { version = "0.16", default-features = false }2020
futures-util = { version = "0.3", default-features = false, features = ["std"] }@@ -1,7 +1,7 @@
11
<script lang="ts">22
import { get } from 'svelte/store';33
import { onDestroy, onMount, tick } from 'svelte';4
- import { getMembers, mentionContext, mentionsPage, recoverMentions, retryMentionContext, setMentionsRead, markMentionsReadThrough } from './api';4
+ import { mentionContext, mentionsPage, recoverMentions, retryMentionContext, setMentionsRead, markMentionsReadThrough } from './api';55
import ContextTimeline from './ContextTimeline.svelte';66
import EmojiText from './EmojiText.svelte';77
import MessageRow from './MessageRow.svelte';@@ -10,8 +10,6 @@
1010
import { ApiError } from './transport';1111
import type { TimelineItem } from './types';1212
import { mentionKey } from './mentions-model';13
- import { contextWithProfiles, roomsLackingProfiles, withProfiles } from './mention-profiles';14
- import type { RoomProfiles } from './mention-profiles';1513
import type { MentionEntry, MentionsPage, SavedMentionContext } from './mentions-model';1614
1715
export let self: string;@@ -42,16 +40,10 @@
4240
let queuedContext: MentionEntry | null = null;4341
let pendingExplicitOpen = false;4442
let savedListTop = 0;45
- let roomProfiles: RoomProfiles = {};46
- let profileRequests = new Set<string>();4743
$: unreadCount = $mentionsSnapshot?.boundary.account === self ? $mentionsSnapshot.unread_count : page?.unread_count ?? 0;4844
$: joinedRooms = new Map($rooms.filter((room) => room.membership === 'Joined').map((room) => [room.room_id, room]));4945
$: selectedRoom = selected ? joinedRooms.get(selected.key.room_id) : undefined;5046
$: contextCanPaginate = !!selectedRoom;51
- $: requestProfiles([52
- ...entries.map((entry) => ({ roomId: entry.key.room_id, item: entry.event })),53
- ...(selected && saved?.context ? [...saved.context.events_before, saved.context.event, ...saved.context.events_after].map((item) => ({ roomId: selected!.key.room_id, item })) : [])54
- ], joinedRooms);5547
5648
function itemEvent(item: TimelineItem) {5749
return item.kind === 'event' ? item.event : null;@@ -83,17 +75,6 @@
8375
return state.kind === 'Ready' ? 'Context saved' : state.kind === 'Partial' ? 'Partial context saved' : state.kind === 'Pending' ? 'Saving context' : 'Context unavailable';8476
}8577
function keyOf(entry: MentionEntry): string { return mentionKey(entry.key); }86
- function requestProfiles(items: { roomId: string; item: TimelineItem }[], joined: Map<string, unknown>): void {87
- const account = self;88
- const requests = profileRequests;89
- for (const roomId of roomsLackingProfiles(items)) {90
- if (requests.has(roomId) || !joined.has(roomId)) continue;91
- requests.add(roomId);92
- getMembers(roomId).then(({ members }) => {93
- if (currentAccount(account) && requests === profileRequests) roomProfiles = { ...roomProfiles, [roomId]: Object.fromEntries(members.map((member) => [member.user_id, member])) };94
- }, () => requests.delete(roomId));95
- }96
- }9778
function bindViewport(node: HTMLDivElement) {9879
list = node;9980
viewport?.releaseScroll();@@ -413,8 +394,6 @@
413394
pendingExplicitOpen = false;414395
pendingRevision = 0;415396
savedListTop = 0;416
- roomProfiles = {};417
- profileRequests = new Set();418397
if (refreshTimer) clearTimeout(refreshTimer);419398
renderedAccount = self;420399
if (mounted) void fetchRange(1, generation, self);@@ -439,7 +418,7 @@
439418
{#if selected}440419
{#if error}<div class="error" role="alert"><EmojiText text={error} /><button disabled={loadingContext} onclick={() => void loadContext(selected!)}>Retry</button></div>{/if}441420
{#if saved?.context}442
- <ContextTimeline context={contextWithProfiles(saved.context, roomProfiles[selected.key.room_id])} roomId={selected.key.room_id} eventId={selected.key.event_id} roomName={roomName(selected)} {self} canPaginate={contextCanPaginate} onClose={backToList} {onLive} />421
+ <ContextTimeline context={saved.context} roomId={selected.key.room_id} eventId={selected.key.event_id} roomName={roomName(selected)} {self} canPaginate={contextCanPaginate} onClose={backToList} {onLive} />443422
{#if selected.capture.kind === 'Partial' || selected.capture.kind === 'Failed' || selected.capture.kind === 'Unavailable' || saved.state.kind === 'Partial' || saved.state.kind === 'Failed'}<button class="capture-retry" disabled={!selectedRoom || actionBusy} onclick={retryCapture}>Retry context capture</button>{/if}444423
{#if !contextCanPaginate}<p class="status">This room is no longer joined. Saved context remains available.</p>{/if}445424
{#if saved.state.kind === 'Partial'}<p class="status">This saved context is partial.</p>{/if}@@ -452,7 +431,7 @@
452431
{:else if saved?.state.kind === 'Unavailable'}<p class="status">Saved context is unavailable.</p>453432
{:else if saved?.state.kind === 'Partial'}<p class="status">Saved context is partial and currently unavailable.</p>454433
{:else}<p class="status">Saved context is not available yet.</p>{/if}455
- {@const preview = itemEvent(withProfiles(selected.event, roomProfiles[selected.key.room_id]))}434
+ {@const preview = itemEvent(selected.event)}456435
{#if preview}<MessageRow itemId={preview.item_id} event={preview} showSender={true} reacting={false} onReact={() => {}} readOnly={true} showSent={false} {self} senderAvatar={preview.sender.avatar} />{/if}457436
{#if selected.capture.kind === 'Failed' || selected.capture.kind === 'Partial' || selected.capture.kind === 'Unavailable'}458437
<button disabled={!selectedRoom || actionBusy} onclick={retryCapture}>Retry context capture</button>@@ -479,7 +458,7 @@
479458
<div class="entries" use:bindViewport onscroll={() => { if (!viewport?.consumeScroll()) viewport?.capture(); }}>480459
<div class="entry-content">481460
{#each entries as entry (keyOf(entry))}482
- {@const event = itemEvent(withProfiles(entry.event, roomProfiles[entry.key.room_id]))}461
+ {@const event = itemEvent(entry.event)}483462
<article class:unread={!entry.read}>484463
<div class="entry-head"><button class="room" onclick={() => openContext(entry)}><EmojiText text={roomName(entry)} /></button><time>{new Date(entry.timestamp_ms).toLocaleString()}</time><span>{entry.read ? 'Read' : 'Unread'}</span></div>485464
<div class="labels">{#each entry.reasons as reason}<span>{reasonLabel(reason)}</span>{/each}<span>{captureLabel(entry.capture)}</span>{#if entry.notification_suppressed}<span>Notifications suppressed</span>{/if}</div>@@ -1,45 +0,0 @@
1
-import type { EventContext, MemberSummary, Sender, TimelineItem } from './types';2
-3
-export type RoomProfiles = Record<string, Record<string, MemberSummary>>;4
-5
-export function lacksProfile(sender: Sender): boolean {6
- return !sender.display_name && !sender.avatar;7
-}8
-9
-/** Rooms holding a sender or reply sender that came back with no name and no avatar. */10
-export function roomsLackingProfiles(items: { roomId: string; item: TimelineItem }[]): Set<string> {11
- const rooms = new Set<string>();12
- for (const { roomId, item } of items) {13
- if (item.kind !== 'event') continue;14
- const reply = item.event.reply_to?.details;15
- if (lacksProfile(item.event.sender) || (reply?.kind === 'Ready' && lacksProfile(reply.sender))) rooms.add(roomId);16
- }17
- return rooms;18
-}19
-20
-function filled(sender: Sender, members: Record<string, MemberSummary> | undefined): Sender {21
- const member = members?.[sender.user_id];22
- if (!member || !lacksProfile(sender) || (!member.display_name && !member.avatar)) return sender;23
- return { ...sender, display_name: member.display_name, avatar: member.avatar, name_ambiguous: member.name_ambiguous };24
-}25
-26
-export function withProfiles(item: TimelineItem, members: Record<string, MemberSummary> | undefined): TimelineItem {27
- if (item.kind !== 'event' || !members) return item;28
- const { reply_to: reply } = item.event;29
- const details = reply?.details;30
- const sender = filled(item.event.sender, members);31
- const replySender = details?.kind === 'Ready' ? filled(details.sender, members) : null;32
- if (sender === item.event.sender && (details?.kind !== 'Ready' || replySender === details.sender)) return item;33
- const reply_to = reply && details?.kind === 'Ready' && replySender ? { ...reply, details: { ...details, sender: replySender } } : reply;34
- return { ...item, event: { ...item.event, sender, reply_to } };35
-}36
-37
-export function contextWithProfiles(context: EventContext, members: Record<string, MemberSummary> | undefined): EventContext {38
- if (!members) return context;39
- return {40
- ...context,41
- events_before: context.events_before.map((item) => withProfiles(item, members)),42
- event: withProfiles(context.event, members),43
- events_after: context.events_after.map((item) => withProfiles(item, members))44
- };45
-}@@ -1,63 +0,0 @@
1
-import assert from 'node:assert/strict';2
-import { test } from 'node:test';3
-import { contextWithProfiles, roomsLackingProfiles, withProfiles } from '../src/lib/mention-profiles.ts';4
-import type { MemberSummary, Sender, TimelineItem } from '../src/lib/types.ts';5
-6
-const bare = (user_id: string): Sender => ({ user_id, display_name: null, avatar: null, name_ambiguous: false });7
-8
-function message(sender: Sender, replySender: Sender | null = null): TimelineItem {9
- return {10
- kind: 'event',11
- event: {12
- item_id: `i-${sender.user_id}`,13
- event_id: `$e-${sender.user_id}`,14
- transaction_id: null,15
- sender,16
- timestamp_ms: 1,17
- own: false,18
- send_state: { kind: 'Sent' },19
- content: { kind: 'Redacted' },20
- reply_to: replySender ? { event_id: '$target', details: { kind: 'Ready', sender: replySender, content: { kind: 'Redacted' }, timestamp_ms: 0 } } : null,21
- thread_root: null,22
- thread_summary: null,23
- reactions: [],24
- read_by: [],25
- editable: false26
- }27
- };28
-}29
-30
-function member(user_id: string, display_name: string | null, avatar: string | null = null): MemberSummary {31
- return { user_id, name: display_name ?? user_id, display_name, avatar, membership: 'Join', own: false, name_ambiguous: false, ignored: false, service: false };32
-}33
-34
-const members = { '@carol:x': member('@carol:x', 'Carol', 'mxc://x/c'), '@dave:x': member('@dave:x', 'Dave') };35
-36
-test('lists only rooms with a sender or reply sender missing both name and avatar', () => {37
- const named = { ...bare('@carol:x'), display_name: 'Carol' };38
- const rooms = roomsLackingProfiles([39
- { roomId: '!a:x', item: message(named) },40
- { roomId: '!b:x', item: message(bare('@carol:x')) },41
- { roomId: '!c:x', item: message(named, bare('@dave:x')) },42
- { roomId: '!d:x', item: { kind: 'readMarker' } }43
- ]);44
- assert.deepEqual([...rooms].sort(), ['!b:x', '!c:x']);45
-});46
-47
-test('fills the sender and reply sender from the room members', () => {48
- const filled = withProfiles(message(bare('@carol:x'), bare('@dave:x')), members);49
- assert.equal(filled.kind, 'event');50
- if (filled.kind !== 'event') return;51
- assert.equal(filled.event.sender.display_name, 'Carol');52
- assert.equal(filled.event.sender.avatar, 'mxc://x/c');53
- assert.equal(filled.event.reply_to?.details.kind === 'Ready' && filled.event.reply_to.details.sender.display_name, 'Dave');54
-});55
-56
-test('keeps an engine profile and the same item when nothing changes', () => {57
- const named = message({ ...bare('@carol:x'), display_name: 'Carol from the engine' });58
- assert.equal(withProfiles(named, members), named);59
- const unknown = message(bare('@erin:x'));60
- assert.equal(withProfiles(unknown, members), unknown);61
- const context = { events_before: [unknown], event: named, events_after: [], start_token: null, end_token: null };62
- assert.equal(contextWithProfiles(context, undefined), context);63
-});