-
Notifications
You must be signed in to change notification settings - Fork 330
Store, report, and display presence! #1619
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
Conversation
case AppLifecycleState.resumed: | ||
// > […] the default running mode for a running application that has | ||
// > input focus and is visible. | ||
result = await updatePresence(connection, | ||
pingOnly: pingOnly, | ||
status: PresenceStatus.active, | ||
newUserInput: true); | ||
case AppLifecycleState.inactive: | ||
// > At least one view of the application is visible, but none have | ||
// > input focus. The application is otherwise running normally. | ||
// For example, we expect this state when the user is selecting a file | ||
// to upload. | ||
result = await updatePresence(connection, | ||
pingOnly: pingOnly, | ||
status: PresenceStatus.active, | ||
newUserInput: false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- The newUserInput param is now usually true instead of always
false. This seems more correct to me, and the change seems
low-stakes (the doc says it's used to implement usage statistics);
see the doc:
https://zulip.com/api/update-presence#parameter-new_user_input
This sounds reasonable to me, but probably good to check with Tim in case he has thoughts based on how that's actually used on the server — perhaps @-mention him in the #mobile-team thread.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quoting Tim:
Probably true is a better value if one wants to do something quick. Most accurately, you should be passing true if any only if the user has interacted with the app at all since the last presence request.
I think on a mobile device, always true is a fairly good simulation of that. But I'd definitely an M6 issue for refining it to implement that detail on the spec fully and then move on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the swift implementation! Generally all looks great. Comments below.
lib/model/presence.dart
Outdated
}) async { | ||
if (realmPresenceDisabled) return; | ||
|
||
late UpdatePresenceResult result; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
late UpdatePresenceResult result; | |
final UpdatePresenceResult result; |
right? (I.e., the analyzer should be able to verify this is always initialized before it gets used.)
lib/model/store.dart
Outdated
), | ||
typingStartedExpiryPeriod: Duration(milliseconds: initialSnapshot.serverTypingStartedExpiryPeriodMilliseconds)), | ||
presence: Presence(core: core, | ||
serverPresencePingIntervalSeconds: Duration(seconds: initialSnapshot.serverPresencePingIntervalSeconds), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: once it's a Duration rather than a plain number, the units no longer belong in the name:
serverPresencePingIntervalSeconds: Duration(seconds: initialSnapshot.serverPresencePingIntervalSeconds), | |
serverPresencePingInterval: Duration(seconds: initialSnapshot.serverPresencePingIntervalSeconds), |
(cf typingStartedExpiryPeriod
/ serverTypingStartedExpiryPeriodMilliseconds
above)
lib/model/presence.dart
Outdated
while (true) { | ||
// We put the wait upfront because we already have data when [start] is | ||
// called; it comes from /register. | ||
await Future<void>.delayed(serverPresencePingIntervalSeconds); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually the spot that prompted the comment just above about this field's name — this line looks wrong, as it's taking what looks like a number of seconds but then using it in a way that doesn't appear to be about seconds.
lib/widgets/content.dart
Outdated
@@ -1662,17 +1664,23 @@ class Avatar extends StatelessWidget { | |||
required this.userId, | |||
required this.size, | |||
required this.borderRadius, | |||
this.backgroundColor, | |||
this.omitPresenceStatus = false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the negative feels more confusing to read; and I think "status" is redundant here
this.omitPresenceStatus = false, | |
this.showPresence = true, |
lib/widgets/content.dart
Outdated
this.backgroundColor, | ||
this.omitPresenceStatus = false, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The backgroundColor is only meaningful if presence will be shown, right? Probably worth an assert here to avoid passing it when it wouldn't do anything.
(and/or on AvatarShape)
Widget result = child; | ||
|
||
if (userIdForPresence != null) { | ||
final presenceCircleSize = size / 4; // TODO(design) is this right? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, good question. Probably good to raise in #mobile-design, as a follow-up.
Given we don't show them on the one place where an avatar is a drastically different size from elsewhere (namely the top of the profile page), this way seems fine to ship.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think one assumption this layout makes is that borderRadius is no bigger than presenceCircleSize. If bigger, it looks like it would cut off part of the circle.
Could make that assumption explicit with an assert… or could also move the ClipRRect to apply only to the child, not this whole Stack. Then the presence circle in that case would stick out a little past the avatar's rounded corner, which I think would be fine.
lib/widgets/content.dart
Outdated
|
||
/// The green or orange-gradient circle representing [PresenceStatus]. | ||
/// | ||
/// [backgroundColor] is required and must not be [Colors.transparent]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This bit appears to not match the code — that parameter is optional.
userId: userId, | ||
size: size, | ||
backgroundColor: backgroundColor, | ||
explicitOffline: true))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This flag seems orthogonal to being used as a WidgetSpan. Probably just make it a parameter of this method, and have the one caller specify it explicitly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
… hmm, I guess the padding would look odd if there's nothing there.
Maybe just mention in this method's doc, then.
lib/widgets/content.dart
Outdated
Color? color; | ||
LinearGradient? gradient; | ||
|
||
switch (status) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: join these to the switch that sets them
Color? color; | |
LinearGradient? gradient; | |
switch (status) { | |
Color? color; | |
LinearGradient? gradient; | |
switch (status) { |
lib/widgets/content.dart
Outdated
}); | ||
|
||
final int userId; | ||
final double size; | ||
final double borderRadius; | ||
final Color? backgroundColor; | ||
final bool omitPresenceStatus; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at other call sites of the Avatar constructor, and thinking about whether they should set this flag to omit presence.
- autocomplete → keep
- "My profile" in main menu → probably omit? seems like there's no useful information there; and pending presence: Handle
presence
events #1618, could be confusing by suggesting you're offline - lightbox → keep
- msglist senders → see other comment
- new DMs, two places → keep
- top of profile → skip, as this version already does
- users in custom profile fields → keep
CI shows some test failures, in test/model/store_test.dart . |
Thanks for the review! Revision pushed. This one omits the presence circle on sender rows in the message list, to match zulip-mobile and web. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! All looks good except a nit below.
This one omits the presence circle on sender rows in the message list, to match zulip-mobile and web.
To be explicit for the thread: it also omits it on the "My profile" item in the main menu.
lib/model/presence.dart
Outdated
static void debugReset() { | ||
_debugEnable = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
static void debugReset() { | |
_debugEnable = true; | |
static void debugReset() { | |
debugEnable = true; |
That way it goes through the setter above, so it's clear to the compiler that this field never gets written outside debug mode.
Then I'll want to go ahead and merge, to simplify release management. So I guess let's file one more follow-up issue calling for tests. |
Thanks! Done (#1620), and fixed that nit; PTAL. |
We plan to write tests for this as a followup: zulip#1620. Notable differences from zulip-mobile: - Here, we make report-presence requests more frequently: our "app state" listener triggers a request immediately, instead of scheduling it when the "ping interval" expires. This approach anticipates the requests being handled much more efficiently, with presence_last_update_id (zulip#1611) -- but it shouldn't regress on performance now, because these immediate requests are done (for now) as "ping only", i.e., asking the server not to compute a presence data payload. - The newUserInput param is now usually true instead of always false. This seems more correct to me, and the change seems low-stakes (the doc says it's used to implement usage statistics); see the doc: https://zulip.com/api/update-presence#parameter-new_user_input Fixes: zulip#196
We plan to write tests for this as a followup: zulip#1620. Fixes: zulip#1607
Thanks! Looks good; merging. Tweaked one bit in the commit messages:
because it was looking almost like an analogue of the "Fixes:" lines, and then it was bugging me because the name had spaces in it 🙂 |
I happened to notice this message getting printed repeatedly in the debug logs (reformatted a bit): [ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: NetworkException: HTTP request failed. Client is already closed. (ClientException: HTTP request failed. Client is already closed., uri=https://chat.zulip.org/api/v1/users/me/presence) #0 ApiConnection.send (package:zulip/api/core.dart:175) <asynchronous suspension> zulip#1 Presence._maybePingAndRecordResponse (package:zulip/model/presence.dart:93) <asynchronous suspension> zulip#2 Presence._poll (package:zulip/model/presence.dart:121) <asynchronous suspension> That'd be a symptom of an old Presence continuing to run its polling loop after the ApiConnection has been closed, which happens when the PerAccountStore is disposed. Looks like when we introduced Presence in 5d43df2 (zulip#1619), we forgot to call its `dispose` method. Fix that now. The presence model doesn't currently have any tests. So rather than try to add a test for just this, we'll leave it as something to include when we write those tests, zulip#1620.
I happened to notice this message getting printed repeatedly in the debug logs (reformatted a bit): [ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: NetworkException: HTTP request failed. Client is already closed. (ClientException: HTTP request failed. Client is already closed., uri=https://chat.zulip.org/api/v1/users/me/presence) #0 ApiConnection.send (package:zulip/api/core.dart:175) <asynchronous suspension> #1 Presence._maybePingAndRecordResponse (package:zulip/model/presence.dart:93) <asynchronous suspension> #2 Presence._poll (package:zulip/model/presence.dart:121) <asynchronous suspension> That'd be a symptom of an old Presence continuing to run its polling loop after the ApiConnection has been closed, which happens when the PerAccountStore is disposed. Looks like when we introduced Presence in 5d43df2 (#1619), we forgot to call its `dispose` method. Fix that now. The presence model doesn't currently have any tests. So rather than try to add a test for just this, we'll leave it as something to include when we write those tests, #1620.
TODO tests, but here's a draft for presence!
Screenshots coming soon.
Fixes: #196
Fixes: #1607