Compare commits

..
Author SHA1 Message Date
Lotus CIandClaude Opus 5.5 0d08ba0a55 fix(search): an edited message shows once, with its latest text (#288)
CI / Build & Quality Checks (pull_request) Successful in 2m57s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Playwright smoke (e2e) (pull_request) Waiting to run
CI / Secret scan (gitleaks) (pull_request) Successful in 14s
Server search also returns the m.replace events, so one message edited
twice came back as three rows. Fold edits into their message: one row
at the best-ranked position, showing the latest m.new_content and
pointing at the original event so Open jumps to the real message.

Fixes #288

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-17 20:14:09 -04:00
5 changed files with 100 additions and 18 deletions
+4 -4
View File
@@ -81,7 +81,7 @@
},
"devDependencies": {
"@axe-core/playwright": "4.13.0",
"@lotusguild/element-call-embedded": "0.25.0-lotus.23",
"@lotusguild/element-call-embedded": "0.25.0-lotus.22",
"@playwright/test": "1.63.0",
"@rollup/plugin-inject": "5.0.5",
"@rollup/plugin-wasm": "6.2.2",
@@ -2695,9 +2695,9 @@
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
},
"node_modules/@lotusguild/element-call-embedded": {
"version": "0.25.0-lotus.23",
"resolved": "https://code.lotusguild.org/api/packages/LotusGuild/npm/%40lotusguild%2Felement-call-embedded/-/0.25.0-lotus.23/element-call-embedded-0.25.0-lotus.23.tgz",
"integrity": "sha512-vRmcWeJcU0Accyl2x3mqNEzrIe8djE5BekyYbYOSCsMflC98rZgD1mO7KOe7bA1cX7gparRf6sAt1fotHoU+bA==",
"version": "0.25.0-lotus.22",
"resolved": "https://code.lotusguild.org/api/packages/LotusGuild/npm/%40lotusguild%2Felement-call-embedded/-/0.25.0-lotus.22/element-call-embedded-0.25.0-lotus.22.tgz",
"integrity": "sha512-AimDKZA4xUKEEyl2XUFUSUDl03tBKFv88T/3igKyH2VqNSPMGnqcQJC5HXIkHLTWrOmg8kLcJ//FFbqlUZiLcA==",
"dev": true
},
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
+1 -1
View File
@@ -108,7 +108,7 @@
},
"devDependencies": {
"@axe-core/playwright": "4.13.0",
"@lotusguild/element-call-embedded": "0.25.0-lotus.23",
"@lotusguild/element-call-embedded": "0.25.0-lotus.22",
"@playwright/test": "1.63.0",
"@rollup/plugin-inject": "5.0.5",
"@rollup/plugin-wasm": "6.2.2",
@@ -1,6 +1,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
foldEdits,
filterGroupsByDateRange,
filterGroupsByMsgType,
filterGroupsByPinned,
@@ -109,3 +110,47 @@ test('filterGroupsByPinned: keeps pinned items and drops empty groups', () => {
['$2'],
);
});
const sr = (eventId: string, ts: number, content: Record<string, unknown>) =>
({
rank: 1,
result: {
event_id: eventId,
room_id: '!r:x',
origin_server_ts: ts,
type: 'm.room.message',
content,
},
}) as unknown as Parameters<typeof foldEdits>[0][number];
test('foldEdits: one row per message, showing the latest edit (#288)', () => {
const edit = (id: string, ts: number, body: string) =>
sr(id, ts, {
body: `* ${body}`,
'm.new_content': { body },
'm.relates_to': { rel_type: 'm.replace', event_id: '$orig' },
});
const out = foldEdits([
edit('$e2', 30, 'Tuesday 11am room B'),
sr('$orig', 10, { body: 'Monday 10am room A' }),
edit('$e1', 20, 'Tuesday 10am room A'),
sr('$other', 5, { body: 'meeting notes' }),
]);
assert.deepEqual(
out.map((r) => r.result.event_id),
['$orig', '$other'],
);
assert.deepEqual(out[0].result.content['m.new_content'], { body: 'Tuesday 11am room B' });
assert.equal(out[0].result.content.body, 'Monday 10am room A');
});
test('foldEdits: an edit whose original did not match stands in for it', () => {
const out = foldEdits([
sr('$e1', 20, {
'm.new_content': { body: 'x' },
'm.relates_to': { rel_type: 'm.replace', event_id: '$orig' },
}),
]);
assert.equal(out.length, 1);
assert.equal(out[0].result.event_id, '$orig');
});
@@ -96,10 +96,58 @@ export const filterGroupsByDateRange = (
.filter((group) => group.items.length > 0);
};
/**
* [Gitea #288] Server search also returns `m.replace` events, so an edited
* message showed once per version. Fold each edit into its message: one row,
* at the first position any version ranked, showing the latest text and
* pointing at the original event (so "jump" lands on the real message).
*/
export const foldEdits = (results: ISearchResult[]): ISearchResult[] => {
const targetOf = (r: ISearchResult): string => {
const rel = r.result.content?.['m.relates_to'];
return rel?.rel_type === 'm.replace' && typeof rel.event_id === 'string'
? rel.event_id
: r.result.event_id;
};
const isEdit = (r: ISearchResult) => targetOf(r) !== r.result.event_id;
const order: string[] = [];
const originals = new Map<string, ISearchResult>();
const latestEdit = new Map<string, ISearchResult>();
results.forEach((r) => {
const target = targetOf(r);
if (!originals.has(target) && !latestEdit.has(target)) order.push(target);
if (!isEdit(r)) {
originals.set(target, r);
return;
}
const prev = latestEdit.get(target);
if (!prev || r.result.origin_server_ts > prev.result.origin_server_ts)
latestEdit.set(target, r);
});
return order.map((target) => {
const original = originals.get(target);
const edit = latestEdit.get(target);
const newContent = edit?.result.content?.['m.new_content'];
if (original && !newContent) return original;
if (original) {
return {
...original,
result: {
...original.result,
content: { ...original.result.content, 'm.new_content': newContent },
},
};
}
// Only an edit matched: show it as its message.
const only = edit as ISearchResult;
return { ...only, result: { ...only.result, event_id: target } };
});
};
const groupSearchResult = (results: ISearchResult[]): ResultGroup[] => {
const groups: ResultGroup[] = [];
results.forEach((item) => {
foldEdits(results).forEach((item) => {
const roomId = item.result.room_id;
const resultItem: ResultItem = {
rank: item.rank,
+1 -12
View File
@@ -31,7 +31,6 @@ import { verifyDenoiseAssets } from './denoiseSmokeCheck';
import { canDelegateCapability } from './utils';
import { getCallPageUrl } from './callPageUrl';
import { restrictWidgetMessages } from '../widgetTransport';
import { isRingableRoom } from '../../utils/callRing';
// Maximum time to wait for the embedded Element Call iframe to progress from
// initial load to a ready/joined state. If it hasn't by then, we assume the
@@ -252,18 +251,8 @@ export class CallEmbed {
verifyDenoiseAssets(denoiseModel).catch(() => undefined);
}
// [Gitea #295] Only ask Element Call to notify where a call can ring at all
// (DMs, invite-only groups outside a space). In a voice channel or space
// room the notification carries an @room mention, so every member got a
// highlighted ping (and phone push) whenever a moderator hopped in, and
// other clients showed it as a missed call.
// `none` needs element-call-embedded ≥ 0.25.0-lotus.23 (older builds ignore it).
if (CallEmbed.startingCall(intent)) {
let notify = 'none';
if (isRingableRoom(room, CallEmbed.dmCall(intent))) {
notify = CallEmbed.dmCall(intent) ? 'ring' : 'notification';
}
params.append('sendNotificationType', notify);
params.append('sendNotificationType', CallEmbed.dmCall(intent) ? 'ring' : 'notification');
}
// [Gitea #43] On its own origin when config.json sets elementCallUrl.