feat(#383): track who reacted, not just the count
Reactions stored only a Map<String,int> of emoji to count, so "who reacted" was unanswerable and MeshCore One's tap-to-see-who had nothing to show against. This captures the reactor for every reaction, both our own r: format and the PocketMesh / MeshCore One format. Data layer only. The tap-a-badge-to-see-who UI is a separate follow-on; this makes the data available and does not change any screen. Additive by design. A new reactionSenders field (Map<String,List<String>>, emoji -> reactor names) sits alongside the existing count map, serialised under a new JSON key. Both maps are always written. An older build reading a newer store ignores the unknown key and still gets correct counts from reactions; it never hits the hard `value as int` cast that would fail the whole message-list load (the #355 data-loss shape). Records written before this field load with an empty sender map, so their counts survive with names simply absent. - ChannelMessage + Message: new reactionSenders field, constructor, copyWith - both stores: serialise the new key; deserialise via a shared ReactionHelper.reactionSendersFromJson that returns empty for a missing key and skips malformed entries rather than throwing - applyReaction: records the reactor and dedups per reactor per emoji. This is persistent dedup (survives restart), unlike the connector's in-memory processed-set. A pre-existing count with no sender list is incremented from its stored value, not recomputed from the partial list, so old counts are preserved. - connector: threads the reactor name into all reaction paths. Channel uses the frame sender; a room resolves the author via fourByteRoomContactKey; an outgoing reaction is attributed to self. - pending queue: retry now hands the stored reactor to its callback Tests: reactor capture, two-reactor count, same-reactor no-double-count, pre-#383 count preservation, and JSON round-trip + backward-compat + malformed-entry handling for the new field. Full suite 604 passing. Epic #376. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>pull/419/head
parent
646dc79d86
commit
e452ab4805
@ -0,0 +1,44 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_open/helpers/reaction_helper.dart';
|
||||
|
||||
void main() {
|
||||
group('ReactionHelper.reactionSendersFromJson (#383)', () {
|
||||
test('round-trips a real sender map through JSON', () {
|
||||
final original = <String, List<String>>{
|
||||
'\u{1F44D}': ['Alice', 'Bob'],
|
||||
'\u{2764}\u{FE0F}': ['Carol'],
|
||||
};
|
||||
final decoded = ReactionHelper.reactionSendersFromJson(
|
||||
jsonDecode(jsonEncode(original)),
|
||||
);
|
||||
expect(decoded, original);
|
||||
});
|
||||
|
||||
test('an old store with no reactionSenders key degrades to empty', () {
|
||||
// This is the backward-compat guarantee: a record written before #383
|
||||
// has no such key, so the field is absent from the decoded JSON.
|
||||
expect(ReactionHelper.reactionSendersFromJson(null), isEmpty);
|
||||
});
|
||||
|
||||
test('a non-map value is ignored rather than thrown', () {
|
||||
// A single bad entry must never fail the whole message-list load (#355).
|
||||
expect(ReactionHelper.reactionSendersFromJson('garbage'), isEmpty);
|
||||
expect(ReactionHelper.reactionSendersFromJson(42), isEmpty);
|
||||
expect(ReactionHelper.reactionSendersFromJson(['a', 'b']), isEmpty);
|
||||
});
|
||||
|
||||
test('malformed entries within a map are skipped, valid ones kept', () {
|
||||
final decoded = ReactionHelper.reactionSendersFromJson({
|
||||
'\u{1F44D}': ['Alice', 42, null, 'Bob'], // non-strings dropped
|
||||
'\u{1F525}': 'not a list', // whole entry dropped
|
||||
'\u{1F389}': ['Dave'],
|
||||
});
|
||||
expect(decoded, {
|
||||
'\u{1F44D}': ['Alice', 'Bob'],
|
||||
'\u{1F389}': ['Dave'],
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in new issue