fix(chat): dedupe messages on display, not just in the store

Duplicated bubbles came from the cubit emitting every inbox event to the
screen regardless of whether it was already shown. On opening a chat the
saved message loads from history AND the live subscription hands back the
same stored gift wrap the relay re-delivers — two bubbles. (The store was
already idempotent, but the cubit ignored that for display.)

MessagesCubit now keeps a seen-set (sender+timestamp+text): a re-delivered
or already-loaded message is skipped, and pre-existing duplicates in old
saved history collapse on load too. Arrival order preserved (no resort).
Tests: re-delivered wrap shown once; history-raced-by-live shown once.
This commit is contained in:
vjrj 2026-07-10 17:02:40 +02:00
parent 73ee98206f
commit 33d8b2a4d7
2 changed files with 52 additions and 3 deletions

View file

@ -110,6 +110,38 @@ void main() {
await reopened.close();
});
test('a re-delivered wrap is shown once (relay resends stored events)',
() async {
final transport = FakeMessageTransport();
final cubit =
MessagesCubit(transport, peerPubkey: peer, selfPubkey: me)..start();
final same = msg(peer, 'hola'); // identical sender+timestamp+text
transport.receive(same);
transport.receive(same); // relay re-delivery on the same subscription
await pumpEventQueue();
expect(cubit.state.messages, hasLength(1));
await cubit.close();
});
test('history already surfaced live is not shown twice', () async {
// The stored message is ALSO handed back by the live subscription on open.
final store = MessageStore(InMemorySecretStore());
final m = msg(peer, 'hola');
await store.append(peer, m);
final transport = FakeMessageTransport();
final cubit = MessagesCubit(transport,
peerPubkey: peer, selfPubkey: me, store: store);
unawaited(cubit.start());
transport.receive(m); // live redelivery races the history load
await pumpEventQueue();
expect(cubit.state.messages.map((m) => m.text), ['hola']);
await cubit.close();
});
test('offline (no transport) never throws', () async {
final cubit = MessagesCubit(null, peerPubkey: peer, selfPubkey: me)..start();
expect(cubit.isOnline, isFalse);