Skip to content

Conversation

chrisbobbe
Copy link
Collaborator

TODO tests, but here's a draft for presence!

Screenshots coming soon.

Fixes: #196
Fixes: #1607

@chrisbobbe chrisbobbe requested a review from gnprice June 23, 2025 23:12
@chrisbobbe chrisbobbe added the integration review Added by maintainers when PR may be ready for integration label Jun 23, 2025
@chrisbobbe
Copy link
Collaborator Author

On the profile page:

TODO (followup): The "offline" status should be represented with an open circle, like on web, not a solid circle, like here. That's in order to show meaning by shape, not just color, for color-blind users.

Light Dark
image image
image image
image image

@chrisbobbe
Copy link
Collaborator Author

Direct-messages screen. See that "Chris Bobbe (Test Account)" is online, "Alya Abbott" is idle, and everyone else is offline.

Light Dark
image image

@chrisbobbe
Copy link
Collaborator Author

Message list:

Light Dark
image image
image image

Comment on lines +90 to +105
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);
Copy link
Member

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

#mobile-team > presence @ 💬

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.

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 swift implementation! Generally all looks great. Comments below.

}) async {
if (realmPresenceDisabled) return;

late UpdatePresenceResult result;
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
late UpdatePresenceResult result;
final UpdatePresenceResult result;

right? (I.e., the analyzer should be able to verify this is always initialized before it gets used.)

),
typingStartedExpiryPeriod: Duration(milliseconds: initialSnapshot.serverTypingStartedExpiryPeriodMilliseconds)),
presence: Presence(core: core,
serverPresencePingIntervalSeconds: Duration(seconds: initialSnapshot.serverPresencePingIntervalSeconds),
Copy link
Member

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:

Suggested change
serverPresencePingIntervalSeconds: Duration(seconds: initialSnapshot.serverPresencePingIntervalSeconds),
serverPresencePingInterval: Duration(seconds: initialSnapshot.serverPresencePingIntervalSeconds),

(cf typingStartedExpiryPeriod / serverTypingStartedExpiryPeriodMilliseconds above)

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);
Copy link
Member

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.

@@ -1662,17 +1664,23 @@ class Avatar extends StatelessWidget {
required this.userId,
required this.size,
required this.borderRadius,
this.backgroundColor,
this.omitPresenceStatus = false,
Copy link
Member

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

Suggested change
this.omitPresenceStatus = false,
this.showPresence = true,

Comment on lines 1667 to 1669
this.backgroundColor,
this.omitPresenceStatus = false,
});
Copy link
Member

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?
Copy link
Member

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.

Copy link
Member

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.


/// The green or orange-gradient circle representing [PresenceStatus].
///
/// [backgroundColor] is required and must not be [Colors.transparent].
Copy link
Member

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)));
Copy link
Member

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.

Copy link
Member

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.

Comment on lines 1856 to 1868
Color? color;
LinearGradient? gradient;

switch (status) {
Copy link
Member

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

Suggested change
Color? color;
LinearGradient? gradient;
switch (status) {
Color? color;
LinearGradient? gradient;
switch (status) {

});

final int userId;
final double size;
final double borderRadius;
final Color? backgroundColor;
final bool omitPresenceStatus;
Copy link
Member

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

@gnprice
Copy link
Member

gnprice commented Jun 24, 2025

CI shows some test failures, in test/model/store_test.dart .

@chrisbobbe
Copy link
Collaborator Author

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.

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! 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.

Comment on lines 179 to 180
static void debugReset() {
_debugEnable = true;
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
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.

@gnprice
Copy link
Member

gnprice commented Jun 24, 2025

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.

@chrisbobbe
Copy link
Collaborator Author

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
@gnprice
Copy link
Member

gnprice commented Jun 24, 2025

Thanks! Looks good; merging.

Tweaked one bit in the commit messages:

-    We plan to write tests for this as a followup: #1620
+    We plan to write tests for this as a followup: #1620.
 
     Fixes: #1607

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 🙂

@gnprice gnprice merged commit f11c52f into zulip:main Jun 24, 2025
1 check passed
@chrisbobbe chrisbobbe deleted the pr-presence branch June 24, 2025 01:06
gnprice added a commit to gnprice/zulip-flutter that referenced this pull request Jul 19, 2025
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.
chrisbobbe pushed a commit that referenced this pull request Jul 22, 2025
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.
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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Show presence (active/inactive) in the app Presence data, and report presence
2 participants