Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

notif: Handle remove notification message #878

Merged
merged 7 commits into from
Aug 21, 2024

Conversation

rajveermalviya
Copy link
Collaborator

Fixes: #341

@rajveermalviya rajveermalviya added the buddy review GSoC buddy review needed. label Aug 11, 2024
Copy link
Collaborator

@sm-sayedi sm-sayedi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @rajveermalviya for working on this nice feature. Did a manual testing of the feature which works fine. LGTM. Moving on to the mentor review! cc @sumanthvrao @kenclary

@sm-sayedi sm-sayedi added mentor review GSoC mentor review needed. and removed buddy review GSoC buddy review needed. labels Aug 12, 2024
@rajveermalviya rajveermalviya added the maintainer review PR ready for review by Zulip maintainers label Aug 13, 2024
Copy link
Collaborator

@kenclary kenclary left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm too :)

Copy link
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Here's a light review, and I expect Greg may have more to say since he knows the Android-notifications system better than I do.

@@ -560,8 +560,8 @@ class FakeAndroidNotificationHostApi implements AndroidNotificationHostApi {

final Map<String, MessagingStyle?> _activeNotificationsMessagingStyle = {};

/// Clears all active notifications that have been created via [notify].
void clearActiveNotifications() {
/// Clears all messaginging style of active notifications that have been created via [notify].
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: spelling; also, instead of "messaging style", maybe this should say [MessagingStyle]? It's not intuitively clear what a "messaging style" is, and maybe Android could have chosen a better name for it, but using the identifier should at least shorten the reader's path to looking up what it means in the Android documentation.

// Don't act on the summary notification for the group.
if (statusBarNotification.tag == groupKey) continue;

final lastMessageIdStr = notification.extras[kExtraZulipMessageId];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an independent improvement, could you write a dartdoc for kExtraZulipMessageId? I found myself wondering what it was, while reading this.

@rajveermalviya rajveermalviya force-pushed the pr-notif-remove branch 2 times, most recently from 21c13ff to f5d05a1 Compare August 15, 2024 10:48
@rajveermalviya
Copy link
Collaborator Author

Thanks for the reviews @kenclary and @chrisbobbe!
Pushed a new revision @chrisbobbe, PTAL.

@rajveermalviya rajveermalviya added the integration review Added by maintainers when PR may be ready for integration label Aug 15, 2024
@rajveermalviya rajveermalviya requested a review from gnprice August 15, 2024 10:51
Copy link
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Comments below.

Comment on lines 258 to 268
/// The identifier key for the message-id of embedded in the
/// notification's extras bundle.
@visibleForTesting
static const kExtraZulipMessageId = 'zulipMessageId';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding this dartdoc. Does it have a missing word or something? I'm not parsing "[…] the message-id of embedded […]".

Also, "the notification" makes it sound like I should already know what kind of thing—and also which specific thing of that kind—is being referred to. Is it an instance of StatusBarNotification, or of Notification? Looking through the code, I think this string is used as a key in the Notification.extras map, so it seems helpful to include a reference to [Notification.extras].

I'm not as familiar with the Android-notifications code as you and Greg are, and one question I had was whether this constant's value is part of the Zulip API, or if it's purely internal to our code. It seems like the latter, but I think if the dartdoc were fleshed out a bit more to answer that question, I think it would be helpful.

Comment on lines 560 to 564
/// Clears all active notifications that have been created via [notify].
void clearActiveNotifications() {
/// Clears all [MessagingStyle] of active notifications that have been created via [notify].
void clearActiveNotificationsMessagingStyle() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_activeNotificationsMessagingStyle is a collection, so do we want to say [MessagingStyle]s, plural?

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @rajveermalviya, and thanks @sm-sayedi and @chrisbobbe for the previous reviews!

Generally this looks great. I like these test cases. Various small comments below, and I see there are a couple open from Chris's review today above.

Let's add one other test case: the RemoveFcmMessage shouldn't affect any notifications for other accounts. To make the test as sharp as possible, let's exercise the case where by sheer bad luck the other account's notification has the same message ID, and is to the same user ID — the only difference is the realm URL. (This isn't even totally improbable: if the accounts are in two different very small or new servers, our user could easily have the same very small user ID in both and then the two servers could even hit the same small message ID around the same time.)

I guess to be thorough let's make that two test cases (or perhaps two steps in one test case): different realm URL but same user ID, and same realm URL but different user ID. And in both cases the same message ID. (For the same realm and different user IDs, that's even less improbable: our user has two accounts in the realm, both got notified by some message, and now they've read the message from just one account. I guess for that scenario it would actually be a defensible product choice to remove both notifications, since it's truly the same message and not two unrelated messages that happen to have the same ID… but I like the behavior we have, so let's test it.)

I'll also want to look closer at the implementation of _onRemoveFcmMessage in comparison with the zulip-mobile version — it looks right, and most of the comments even look familiar, but I'll want to do a careful line-by-line comparison to be sure we understand any differences. (Skipping that in this round only because it's late evening here.)

Comment on lines 143 to 154
final String tag;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My draft had some comments here and on Notification about other properties that were included or not included, and why. Those comments were intended to include in the final commits (and I now realize that was ambiguous when they appeared in the context of a draft commit) — I think they'll be useful if we later return to these bindings and consider extending them, for implementing some future change to notifications.

Comment on lines 149 to 157
StatusBarNotification(
it.id.toLong(),
Notification(
it.notification.group,
desiredExtras
.associateWith { key -> it.notification.extras.getString(key) }
.filter { entry -> entry.value != null }
),
it.tag
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: put id and tag before the body of the notification — in general, short arguments that identify the name or location of something go before long multi-line arguments that provide its content

(probably it'd be good to reorder them that way in the class declaration, too)

Comment on lines +154 to +156
.associateWith { key -> it.notification.extras.getString(key) }
.filter { entry -> entry.value != null }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This semantics works fine since it covers the one use case we actually have — it's definitely simpler to think about than the one in my draft, and that's a good improvement.

It needs to be described in the method's doc, though (in pigeon/notification.dart). It looks to be:

  • Each requested extra is included if it's present with type string.
  • If it's absent, or not a string, it's skipped.

If not written down explicitly, that's the sort of thing that could lead to some confused time spent debugging when someone in the future tries to add a number or a boolean or something, and then it just doesn't show up — they'd wonder if they typoed the key, or if the extra didn't make it onto the notification for some reason, or what.

if (statusBarNotification.tag == groupKey) continue;

final lastMessageIdStr = notification.extras[kExtraZulipMessageId];
if (lastMessageIdStr == null) continue;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case shouldn't happen, right? It'd be a bug within this file, or at least in our understanding of how the Android SDK features we're using are supposed to work.

So let's make that explicit:

Suggested change
if (lastMessageIdStr == null) continue;
assert(lastMessageIdStr != null);
if (lastMessageIdStr == null) continue; // TODO(log)


final lastMessageIdStr = notification.extras[kExtraZulipMessageId];
if (lastMessageIdStr == null) continue;
final lastMessageId = int.parse(lastMessageIdStr, radix: 10);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, good use of radix: 10

Comment on lines +550 to +554
testBinding.firebaseMessaging.onMessage.add(
RemoteMessage(data: removeFcmMessage([message]).toJson()));
async.flushMicrotasks();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this can use receiveFcmMessage, right?

(and a few more places below)

Comment on lines 553 to 555
check(testBinding.androidNotificationHost.activeNotifications).isEmpty();

check(testBinding.androidNotificationHost.activeNotifications).isEmpty();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
check(testBinding.androidNotificationHost.activeNotifications).isEmpty();
check(testBinding.androidNotificationHost.activeNotifications).isEmpty();
check(testBinding.androidNotificationHost.activeNotifications).isEmpty();

these are close enough together the reader can be expected to remember the previous one 🙂

Comment on lines 554 to 566

check(testBinding.androidNotificationHost.activeNotifications).isEmpty();
await receiveFcmMessage(async, data);
check(testBinding.androidNotificationHost.activeNotifications).deepEquals(<Condition<Object?>>[
conditionActiveNotif(data, 'stream:${message.streamId}:${message.topic}'),
conditionSummaryActiveNotif(expectedGroupKey),
]);
testBinding.firebaseMessaging.onBackgroundMessage.add(
RemoteMessage(data: removeFcmMessage([message]).toJson()));
async.flushMicrotasks();
check(testBinding.androidNotificationHost.activeNotifications).isEmpty();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm but maybe just delete this whole stanza? seems like it's a copy of the one above. I don't think it's expected to get a MessageFcmMessage for the same Zulip message twice. If that's intentional and you think it's a scenario which should be tested, it needs at least a comment explaining the narrative of it.

Comment on lines 581 to 586
await receiveFcmMessage(async, data1);
await receiveFcmMessage(async, data2);
await receiveFcmMessage(async, data3);

check(testBinding.androidNotificationHost.activeNotifications).deepEquals(<Condition<Object?>>[
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
await receiveFcmMessage(async, data1);
await receiveFcmMessage(async, data2);
await receiveFcmMessage(async, data3);
check(testBinding.androidNotificationHost.activeNotifications).deepEquals(<Condition<Object?>>[
await receiveFcmMessage(async, data1);
await receiveFcmMessage(async, data2);
await receiveFcmMessage(async, data3);
check(testBinding.androidNotificationHost.activeNotifications).deepEquals(<Condition<Object?>>[

Generally when a test has code like "make this change, this change, this change; then check the state of the world", I like to group those into one stanza. Then a blank line separates the checks from the next round of making changes.

That way the stanzas, or the blank lines separating them, serve to group together those change/check pairs.

Comment on lines 590 to 593
testBinding.firebaseMessaging.onMessage.add(
RemoteMessage(data: removeFcmMessage([message1, message2]).toJson()));
async.flushMicrotasks();

check(testBinding.androidNotificationHost.activeNotifications).deepEquals(<Condition<Object?>>[
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could benefit from a brief comment telling the story of the test:

Suggested change
testBinding.firebaseMessaging.onMessage.add(
RemoteMessage(data: removeFcmMessage([message1, message2]).toJson()));
async.flushMicrotasks();
check(testBinding.androidNotificationHost.activeNotifications).deepEquals(<Condition<Object?>>[
// A RemoveFcmMessage for the first two messages; the notification stays.
testBinding.firebaseMessaging.onMessage.add(
RemoteMessage(data: removeFcmMessage([message1, message2]).toJson()));
async.flushMicrotasks();
check(testBinding.androidNotificationHost.activeNotifications).deepEquals(<Condition<Object?>>[

(and then a couple of nits above apply to other aspects of these lines)

Similarly later in this test case:

      // Then a RemoveFcmMessage for the last message; clear the notification.

The test name above already gives a good summary of the plot. But I think these are helpful to make sure the reader sees the point, and how the details connect to the idea that's being tested.

@rajveermalviya rajveermalviya force-pushed the pr-notif-remove branch 2 times, most recently from 65f9094 to 1152d94 Compare August 16, 2024 23:39
@rajveermalviya
Copy link
Collaborator Author

Thanks for the reviews @chrisbobbe and @gnprice! Pushed a new revision, PTAL.

@rajveermalviya rajveermalviya requested a review from gnprice August 16, 2024 23:49
@chrisbobbe
Copy link
Collaborator

Cool, thanks! I see my review has been addressed, so I'll remove the maintainer-review label. 🙂

@chrisbobbe chrisbobbe removed the maintainer review PR ready for review by Zulip maintainers label Aug 17, 2024
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the revision! This looks great.

One nit below, and then I've pushed a few added commits on top:
b9addf2 notif [nfc]: Make shorter local alias for ZulipBinding.instance.androidNotificationHost
f2c8e40 notif [nfc]: Keep more comments in remove-notif from zulip-mobile implementation
aa5c31d notif [nfc]: Clarify meaning of last-message-ID extra

I did that comparison to the zulip-mobile code which I mentioned above, and all the logic looks like a match but there were more comments I felt should come over too; that's the second commit above.

I also fixed the one nit. @chrisbobbe would you take a look at the new commits? In particular see if they answer the questions you had at #878 (comment) above. Then I think this is ready to merge.

Comment on lines 132 to 134
final Map<String?, String?> extras;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final Map<String?, String?> extras;
}
final Map<String?, String?> extras;
// Various other properties too; add them if needed.
}

(part of #878 (comment) )

Comment on lines +557 to +563
// Check on background event; onBackgroundMessage
await receiveFcmMessage(async, data);
check(testBinding.androidNotificationHost.activeNotifications).deepEquals(<Condition<Object?>>[
conditionActiveNotif(data, 'stream:${message.streamId}:${message.topic}'),
conditionSummaryActiveNotif(expectedGroupKey),
]);
testBinding.firebaseMessaging.onBackgroundMessage.add(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see the difference from the previous stanza now. 🙂 (continuing from #878 (comment) )

Having a test for this makes sense, good idea.

@gnprice gnprice added the maintainer review PR ready for review by Zulip maintainers label Aug 20, 2024
@chrisbobbe
Copy link
Collaborator

I also fixed the one nit. @chrisbobbe would you take a look at the new commits? In particular see if they answer the questions you had at #878 (comment) above. Then I think this is ready to merge.

Yep, it's clear now, thanks! Please merge at will.

rajveermalviya and others added 7 commits August 21, 2024 15:53
…onMessagingStyleByTag` non-growable

This better simulates real bindings behaviour.
…idNotificationHost

This is quite a mouthful and we repeat it a lot in this file.
A nice short local name also helps things fit more easily on a line.
…lementation

These comments in the corresponding code in zulip-mobile:
  https://github.com/zulip/zulip-mobile/blob/2217c858e207f9f092651dd853051843c3f04422/android/app/src/main/java/com/zulipmobile/notifications/NotificationUiManager.kt#L116-L168

are explaining the same logic we've copied into this code.  So the
comments are equally relevant for explaining the code here too.
Each notification can be showing several Zulip messages.  This extra
carries the message ID of specifically the latest one.
@gnprice
Copy link
Member

gnprice commented Aug 21, 2024

Thanks! Merged.

@gnprice gnprice merged commit b521c45 into zulip:main Aug 21, 2024
@rajveermalviya rajveermalviya deleted the pr-notif-remove branch August 22, 2024 07:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration review Added by maintainers when PR may be ready for integration maintainer review PR ready for review by Zulip maintainers mentor review GSoC mentor review needed.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Remove notifications when messages are read, on Android
5 participants