fluffychat/lib/pangea/join_codes/knocked_rooms_model.dart
wcjord 6f32aab48b
fix: show "knock accepted" push notification body instead of "You have been invited" (#5823) (#5835)
* fix: show knock-accepted notification body when invite follows a knock (#5823)

When Synapse accepts a knock it sends an m.room.member invite event. The client
was displaying the generic 'You have been invited by X' push notification body
because the invite event alone doesn't carry prev_content on the push path.

Use KnockTracker (already used for auto-join) to detect knock-accepted invites
and show a dedicated 'Your join request was accepted!' notification body instead.

- Add knockAccepted string to intl_en.arb
- Extract condition into isKnockAcceptedInvite() pure util for testability
- Expose KnockTracker.getKnockedRoomIds() publicly (was _getKnockedRoomIds)
- Override notification body in push_helper.dart (background) and
  local_notifications_extension.dart (foreground/web)
- Unit tests for all isKnockAcceptedInvite() branches (9 tests)

* formatting

* fix up pangea comments

* fix: avoid race condition with knocked room account data updates and local push notification content

* translations

---------

Co-authored-by: ggurdin <ggurdin@gmail.com>
2026-02-27 12:29:50 -05:00

84 lines
2.5 KiB
Dart

import 'package:collection/collection.dart';
class KnockedRoomsModel {
final List<String> knockedRoomIds;
final List<String> acceptedInviteRoomIds;
const KnockedRoomsModel({
this.knockedRoomIds = const [],
this.acceptedInviteRoomIds = const [],
});
static const String _roomIdsField = 'room_ids';
static const String _acceptedInviteRoomIdsField = 'accepted_invite_room_ids';
bool hasEverKnocked(String roomId) {
return knockedRoomIds.contains(roomId) ||
acceptedInviteRoomIds.contains(roomId);
}
Map<String, dynamic> toJson() {
return {
_roomIdsField: knockedRoomIds,
_acceptedInviteRoomIdsField: acceptedInviteRoomIds,
};
}
factory KnockedRoomsModel.fromJson(Map<String, dynamic> json) {
final knockedIds = json[_roomIdsField];
final acceptedInviteIds = json[_acceptedInviteRoomIdsField];
return KnockedRoomsModel(
knockedRoomIds: knockedIds is List
? List<String>.from(knockedIds)
: <String>[],
acceptedInviteRoomIds: acceptedInviteIds is List
? List<String>.from(acceptedInviteIds)
: <String>[],
);
}
KnockedRoomsModel copyWithKnockedRoom(String roomId) {
final newKnockedRoomIds = List<String>.from(knockedRoomIds);
if (!newKnockedRoomIds.contains(roomId)) {
newKnockedRoomIds.add(roomId);
}
final newAcceptedInviteRoomIds = List<String>.from(acceptedInviteRoomIds)
..remove(roomId);
return KnockedRoomsModel(
knockedRoomIds: newKnockedRoomIds,
acceptedInviteRoomIds: newAcceptedInviteRoomIds,
);
}
KnockedRoomsModel copyWithAcceptedInviteRoom(String roomId) {
final newAcceptedInviteRoomIds = List<String>.from(acceptedInviteRoomIds);
if (!newAcceptedInviteRoomIds.contains(roomId)) {
newAcceptedInviteRoomIds.add(roomId);
}
final newKnockedRoomIds = List<String>.from(knockedRoomIds)..remove(roomId);
return KnockedRoomsModel(
knockedRoomIds: newKnockedRoomIds,
acceptedInviteRoomIds: newAcceptedInviteRoomIds,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is KnockedRoomsModel &&
ListEquality().equals(other.knockedRoomIds, knockedRoomIds) &&
ListEquality().equals(
other.acceptedInviteRoomIds,
acceptedInviteRoomIds,
);
}
@override
int get hashCode => Object.hash(
ListEquality().hash(knockedRoomIds),
ListEquality().hash(acceptedInviteRoomIds),
);
}