fluffychat/lib/pangea/join_codes/knock_room_extension.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

92 lines
2.6 KiB
Dart

import 'dart:async';
import 'package:matrix/matrix.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/join_codes/knocked_rooms_model.dart';
extension KnockRoomExtension on Room {
bool get hasKnocked => client.hasKnockedRoom(id);
Future<void> joinKnockedRoom() async {
await join();
await client.onJoinKnockedRoom(id);
}
}
extension KnockClientExtension on Client {
KnockedRoomsModel get _knockedRooms {
final data = accountData[PangeaEventTypes.knockedRooms];
if (data != null) {
return KnockedRoomsModel.fromJson(data.content);
}
return const KnockedRoomsModel();
}
Future<void> _setKnockedRooms(KnockedRoomsModel model) async {
final prevModel = _knockedRooms;
if (model == prevModel) {
Logs().w('Knocked rooms model is the same as previous, skipping write.');
Logs().w('Model: ${model.toJson()}');
Logs().w('Previous Model: ${prevModel.toJson()}');
return;
}
await setAccountData(
userID!,
PangeaEventTypes.knockedRooms,
model.toJson(),
);
final updatedModel = _knockedRooms;
if (model == updatedModel) {
try {
await onSync.stream
.firstWhere((sync) => sync.accountData != null)
.timeout(Duration(seconds: 10));
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
'client_user_id': userID,
'expected_ids': model.toJson(),
'updated_ids': updatedModel.toJson(),
},
level: e is TimeoutException
? SentryLevel.warning
: SentryLevel.error,
);
}
}
}
Future<String> knockAndRecordRoom(
String roomIdOrAlias, {
List<String>? via,
String? reason,
}) async {
final resp = await knockRoom(roomIdOrAlias, via: via, reason: reason);
final updatedModel = _knockedRooms.copyWithKnockedRoom(roomIdOrAlias);
await _setKnockedRooms(updatedModel);
return resp;
}
Future<void> onJoinKnockedRoom(String roomId) async {
final updatedModel = _knockedRooms.copyWithAcceptedInviteRoom(roomId);
await _setKnockedRooms(updatedModel);
}
bool hasKnockedRoom(String roomId) {
return _knockedRooms.knockedRoomIds.contains(roomId);
}
bool hasEverKnockedRoom(String roomId) {
return hasKnockedRoom(roomId) ||
_knockedRooms.acceptedInviteRoomIds.contains(roomId);
}
List<String> get knockedRoomIds => _knockedRooms.knockedRoomIds;
}