Skip to content

Commit 1aeee3e

Browse files
committed
notif: Ignore notifications for logged out accounts
Fixes: #1264 Notifications are ignored for accounts which were logged out but the request to stop receiving notifications failed due to some reason. Also when an account is logged out, any existing notifications for that account is removed from the UI.
1 parent 55cab94 commit 1aeee3e

File tree

4 files changed

+88
-6
lines changed

4 files changed

+88
-6
lines changed

lib/model/actions.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import 'dart:async';
22

3+
import 'package:flutter/foundation.dart';
4+
5+
import '../notifications/display.dart';
36
import '../notifications/receive.dart';
47
import 'store.dart';
58

@@ -11,6 +14,10 @@ Future<void> logOutAccount(GlobalStore globalStore, int accountId) async {
1114
// Unawaited, to not block removing the account on this request.
1215
unawaited(unregisterToken(globalStore, accountId));
1316

17+
if (defaultTargetPlatform == TargetPlatform.android) {
18+
unawaited(NotificationDisplayManager.removeNotificationsForAccount(account.realmUrl, account.userId));
19+
}
20+
1421
await globalStore.removeAccount(accountId);
1522
}
1623

lib/notifications/display.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,13 @@ class NotificationDisplayManager {
234234
final groupKey = _groupKey(data.realmUrl, data.userId);
235235
final conversationKey = _conversationKey(data, groupKey);
236236

237+
final globalStore = await ZulipBinding.instance.getGlobalStore();
238+
final account = globalStore.accounts.firstWhereOrNull((account) =>
239+
account.realmUrl.origin == data.realmUrl.origin && account.userId == data.userId);
240+
if (account == null) {
241+
return;
242+
}
243+
237244
final oldMessagingStyle = await _androidHost
238245
.getActiveNotificationMessagingStyleByTag(conversationKey);
239246

@@ -518,6 +525,16 @@ class NotificationDisplayManager {
518525
}
519526
return null;
520527
}
528+
529+
static Future<void> removeNotificationsForAccount(Uri realmUri, int userId) async {
530+
final groupKey = _groupKey(realmUri, userId);
531+
final activeNotifications = await _androidHost.getActiveNotifications(desiredExtras: [kExtraLastZulipMessageId]);
532+
for (final statusBarNotification in activeNotifications) {
533+
if (statusBarNotification.notification.group == groupKey) {
534+
await _androidHost.cancel(tag: statusBarNotification.tag, id: statusBarNotification.id);
535+
}
536+
}
537+
}
521538
}
522539

523540
/// The information contained in 'zulip://notification/…' internal

test/model/actions_test.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:checks/checks.dart';
2+
import 'package:firebase_messaging/firebase_messaging.dart';
23
import 'package:flutter/foundation.dart';
34
import 'package:flutter_test/flutter_test.dart';
45
import 'package:http/http.dart' as http;
@@ -12,6 +13,7 @@ import '../fake_async.dart';
1213
import '../model/binding.dart';
1314
import '../model/store_checks.dart';
1415
import '../model/test_store.dart';
16+
import '../notifications/display_test.dart';
1517
import '../stdlib_checks.dart';
1618
import 'store_test.dart';
1719

@@ -27,6 +29,11 @@ void main() {
2729
await testBinding.globalStore.add(selfAccount, eg.initialSnapshot());
2830
store = await testBinding.globalStore.perAccount(selfAccount.id);
2931
connection = store.connection as FakeApiConnection;
32+
33+
testBinding.firebaseMessagingInitialToken = '012abc';
34+
addTearDown(NotificationService.debugReset);
35+
NotificationService.debugBackgroundIsolateIsLive = false;
36+
await NotificationService.instance.start();
3037
}
3138

3239
/// Creates and caches a new [FakeApiConnection] in [TestGlobalStore].
@@ -79,6 +86,23 @@ void main() {
7986
final newConnection = separateConnection()
8087
..prepare(delay: unregisterDelay, json: {'msg': '', 'result': 'success'});
8188

89+
// Create a notification to check that it's removed after logout
90+
final message = eg.dmMessage(from: eg.otherUser, to: [eg.selfUser]);
91+
final data = messageFcmMessage(message);
92+
93+
testBinding.firebaseMessaging.onMessage.add(
94+
RemoteMessage(data: data.toJson()));
95+
async.flushMicrotasks();
96+
97+
testBinding.firebaseMessaging.onBackgroundMessage.add(
98+
RemoteMessage(data: data.toJson()));
99+
async.flushMicrotasks();
100+
101+
// NotificationDisplayManager.onFcmMessage(data, RemoteMessage(data: data.toJson()).data);
102+
103+
// Check that notifications were created
104+
check(testBinding.androidNotificationHost.activeNotifications).isNotEmpty();
105+
82106
final future = logOutAccount(testBinding.globalStore, eg.selfAccount.id);
83107
// Unregister-token request and account removal dispatched together
84108
checkSingleUnregisterRequest(newConnection);
@@ -94,6 +118,9 @@ void main() {
94118

95119
async.elapse(unregisterDelay - TestGlobalStore.removeAccountDuration);
96120
check(newConnection.isOpen).isFalse();
121+
122+
// Check that notifications were removed
123+
check(testBinding.androidNotificationHost.activeNotifications).isEmpty();
97124
}));
98125

99126
test('unregister request has an error', () => awaitFakeAsync((async) async {
@@ -120,6 +147,10 @@ void main() {
120147

121148
async.elapse(unregisterDelay - TestGlobalStore.removeAccountDuration);
122149
check(newConnection.isOpen).isFalse();
150+
151+
// Check that notifications were removed
152+
final activeNotifications = await testBinding.androidNotificationHost.getActiveNotifications(desiredExtras: []);
153+
check(activeNotifications).isEmpty();
123154
}));
124155
});
125156

test/notifications/display_test.dart

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ import 'package:zulip/widgets/message_list.dart';
2626
import 'package:zulip/widgets/page.dart';
2727
import 'package:zulip/widgets/theme.dart';
2828

29+
import '../example_data.dart' as eg;
2930
import '../fake_async.dart';
3031
import '../model/binding.dart';
31-
import '../example_data.dart' as eg;
3232
import '../model/narrow_checks.dart';
3333
import '../stdlib_checks.dart';
3434
import '../test_images.dart';
@@ -114,7 +114,10 @@ void main() {
114114
return http.runWithClient(callback, httpClientFactory ?? () => fakeHttpClientGivingSuccess);
115115
}
116116

117-
Future<void> init() async {
117+
Future<void> init({bool addSelfAccount = true}) async {
118+
if (addSelfAccount) {
119+
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
120+
}
118121
addTearDown(testBinding.reset);
119122
testBinding.firebaseMessagingInitialToken = '012abc';
120123
addTearDown(NotificationService.debugReset);
@@ -481,7 +484,8 @@ void main() {
481484
await init();
482485
final stream = eg.stream();
483486
final message = eg.streamMessage(stream: stream);
484-
await checkNotifications(async, messageFcmMessage(message, streamName: stream.name),
487+
await checkNotifications(async,
488+
messageFcmMessage(message, streamName: stream.name),
485489
expectedIsGroupConversation: true,
486490
expectedTitle: '#${stream.name} > ${message.topic}',
487491
expectedTagComponent: 'stream:${message.streamId}:${message.topic}');
@@ -613,7 +617,8 @@ void main() {
613617
await init();
614618
final stream = eg.stream();
615619
final message = eg.streamMessage(stream: stream);
616-
await checkNotifications(async, messageFcmMessage(message, streamName: null),
620+
await checkNotifications(async,
621+
messageFcmMessage(message, streamName: null),
617622
expectedIsGroupConversation: true,
618623
expectedTitle: '#(unknown channel) > ${message.topic}',
619624
expectedTagComponent: 'stream:${message.streamId}:${message.topic}');
@@ -730,6 +735,7 @@ void main() {
730735
await init();
731736
final message = eg.dmMessage(from: eg.otherUser, to: [eg.selfUser]);
732737
final data = messageFcmMessage(message);
738+
733739
receiveFcmMessage(async, data);
734740
checkNotification(data,
735741
messageStyleMessages: [data],
@@ -746,6 +752,7 @@ void main() {
746752
await init();
747753
final message = eg.dmMessage(from: eg.otherUser, to: [eg.selfUser]);
748754
final data = messageFcmMessage(message);
755+
749756
receiveFcmMessage(async, data);
750757
checkNotification(data,
751758
messageStyleMessages: [data],
@@ -872,7 +879,8 @@ void main() {
872879
})));
873880

874881
test('remove: different realm URLs but same user-ids and same message-ids', () => runWithHttpClient(() => awaitFakeAsync((async) async {
875-
await init();
882+
await init(addSelfAccount: false);
883+
876884
final stream = eg.stream();
877885
const topic = 'Some Topic';
878886
final conversationKey = 'stream:${stream.streamId}:some topic';
@@ -881,6 +889,7 @@ void main() {
881889
realmUrl: Uri.parse('https://1.chat.example'),
882890
id: 1001,
883891
user: eg.user(userId: 1001));
892+
await testBinding.globalStore.add(account1, eg.initialSnapshot());
884893
final message1 = eg.streamMessage(id: 1000, stream: stream, topic: topic);
885894
final data1 =
886895
messageFcmMessage(message1, account: account1, streamName: stream.name);
@@ -890,6 +899,7 @@ void main() {
890899
realmUrl: Uri.parse('https://2.chat.example'),
891900
id: 1002,
892901
user: eg.user(userId: 1001));
902+
await testBinding.globalStore.add(account2, eg.initialSnapshot());
893903
final message2 = eg.streamMessage(id: 1000, stream: stream, topic: topic);
894904
final data2 =
895905
messageFcmMessage(message2, account: account2, streamName: stream.name);
@@ -917,19 +927,21 @@ void main() {
917927
})));
918928

919929
test('remove: different user-ids but same realm URL and same message-ids', () => runWithHttpClient(() => awaitFakeAsync((async) async {
920-
await init();
930+
await init(addSelfAccount: false);
921931
final realmUrl = eg.realmUrl;
922932
final stream = eg.stream();
923933
const topic = 'Some Topic';
924934
final conversationKey = 'stream:${stream.streamId}:some topic';
925935

926936
final account1 = eg.account(id: 1001, user: eg.user(userId: 1001), realmUrl: realmUrl);
937+
await testBinding.globalStore.add(account1, eg.initialSnapshot());
927938
final message1 = eg.streamMessage(id: 1000, stream: stream, topic: topic);
928939
final data1 =
929940
messageFcmMessage(message1, account: account1, streamName: stream.name);
930941
final groupKey1 = '${account1.realmUrl}|${account1.userId}';
931942

932943
final account2 = eg.account(id: 1002, user: eg.user(userId: 1002), realmUrl: realmUrl);
944+
await testBinding.globalStore.add(account2, eg.initialSnapshot());
933945
final message2 = eg.streamMessage(id: 1000, stream: stream, topic: topic);
934946
final data2 =
935947
messageFcmMessage(message2, account: account2, streamName: stream.name);
@@ -955,6 +967,21 @@ void main() {
955967
receiveFcmMessage(async, removeFcmMessage([message2], account: account2));
956968
check(testBinding.androidNotificationHost.activeNotifications).isEmpty();
957969
})));
970+
971+
test('removeNotificationsForAccount removes notifications', () => runWithHttpClient(() => awaitFakeAsync((async) async {
972+
await init();
973+
final message = eg.dmMessage(from: eg.otherUser, to: [eg.selfUser]);
974+
975+
await checkNotifications(async, messageFcmMessage(message),
976+
expectedIsGroupConversation: false,
977+
expectedTitle: eg.otherUser.fullName,
978+
expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}');
979+
980+
check(testBinding.androidNotificationHost.activeNotifications).isNotEmpty();
981+
await NotificationDisplayManager.removeNotificationsForAccount(
982+
eg.selfAccount.realmUrl, eg.selfAccount.userId);
983+
check(testBinding.androidNotificationHost.activeNotifications).isEmpty();
984+
})));
958985
});
959986

960987
group('NotificationDisplayManager open', () {

0 commit comments

Comments
 (0)