-
Notifications
You must be signed in to change notification settings - Fork 310
RecentDmConversationsPage: Show unread counts #334
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
a76787a
to
fbbf4ab
Compare
// When the count is zero, don't show an unread-count widget. Do keep a | ||
// "0" placeholder, to approximately match the width of a nonzero | ||
// one-digit unread count, given font-size settings etc. That's to | ||
// reduce layout jumping of the title text (which wraps and is | ||
// truncated) in the common case where a DM conversation goes between | ||
// zero and a small number of unreads. |
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.
There might be a way to test this without depending so much on the implementation (in particular the use of the Visibility
widget with specific params). But I don't yet know how that might look.
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 a lower-tech solution to this would be good: instead of having the whole unread-count marker widget tree be there when there are no unreads, just reserve some constant width in pixels. Can do it with ConstrainedBox
and BoxConstraints.minWidth
, so that it always reserves at least that much width even if the unread-count marker is slightly narrower.
One reason that would be good is that it avoids having a bunch of widgets there getting built when there's nothing we want to show.
But another is that that should be more effective at reducing jumping. In this version, the width consumed can actually get wider when all the messages get read — for example "0" is wider than "1" in this font (and basically any font that's not explicitly monospace or tabular-numerals).
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.
(Also it's fine if things sometimes jump slightly here — it can't happen as a consequence of anything the user does on this screen, and so it won't be common that it happens at all while the user is looking at it.)
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.
instead of having the whole unread-count marker widget tree be there when there are no unreads, just reserve some constant width in pixels.
Hmm. If it really is constant, then we can't avoid it being consistently wrong (maybe very wrong) for some users, right? The width should approximate the width of a single-digit unread counter, and that width can change dramatically with the user's choice of font size, and I assume it can change some with the user's choice of whether to force bold.
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.
If we can access the effective font size (accounting for the user's font-size setting), we can probably use a width with a component that's proportional to that font size. Ignoring the user's force-bold setting, I assume the width of a single digit will be proportional to the effective font size, and maybe we can find the ratio experimentally? It helps that we're providing the font ("Source Sans 3"); I think the user can't clobber that with their own choice of font but I actually don't know for sure.
I had enough uncertainty there that it seemed simplest to just do the layout but make the counter invisible. Since performance is a concern, would it be easy to do the layout work just once and reuse it for all the conversations with zero unreads?
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.
Well, I think most of all it comes down for me to the fact that it's fine if things sometimes jump slightly here (because it'll be rare for it to do so while the user is looking), and that reserving a constant width is much simpler than any solution involving a placeholder widget.
The placeholder widget doesn't completely eliminate jumping anyway, because the width varies between different values of the counter.
If the width of a counter is increased for a given user because they have a larger font size, that's actually a situation where it's probably best if we don't reserve space for the counter, and failing that we should at least not increase the space we reserve. That's because the increased font size means that space is at a premium for the text that actually is on the screen and they want to read. There's some discussion in a slightly different context here:
https://github.com/zulip/zulip-mobile/blob/main/docs/background/webview.md#why-mostly-px-and-not-rem
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.
If the width of a counter is increased for a given user because they have a larger font size, that's actually a situation where it's probably best if we don't reserve space for the counter, and failing that we should at least not increase the space we reserve. That's because the increased font size means that space is at a premium for the text that actually is on the screen and they want to read. There's some discussion in a slightly different context here:
https://github.com/zulip/zulip-mobile/blob/main/docs/background/webview.md#why-mostly-px-and-not-rem
Oh, good point. Also: one thing we haven't considered is just not reserving empty space for the counter in any case. That would depart from Vlad's design in Figma (so tagging @terpimost), but as you said:
it's fine if things sometimes jump slightly here (because it'll be rare for it to do so while the user is looking)
and it is nice to not cut off text that we have room to show.
@override | ||
void dispose() { | ||
model?.removeListener(_modelChanged); | ||
super.dispose(); | ||
} | ||
|
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.
RecentDmConversationsPage: Add missing dispose override to state
In theory this is possible to add a regression test for, but I wonder if it's more trouble than it's worth.
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! Generally this looks good; comments below.
Would you also take a screenshot of this and post it? That would help @terpimost take a look at the design. I'll give him a head start, though, by saying that I did look at the design in Figma (from the link in the issue) and I believe you've captured all the details faithfully from there 🙂
void dispose() { | ||
model?.removeListener(_modelChanged); | ||
unreadsModel?.removeListener(_unreadsModelChanged); | ||
super.dispose(); |
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.
Oops yeah good catch.
I just did a quick scan of the other onNewStore
overrides, and it looks like they have this covered.
Arguably it'd be helpful to add a reminder on the onNewStore
doc that one may want to have dispose
dispose of the state the onNewStore
maintains. … But State.didChangeDependencies
doesn't have any such admonition. Maybe it's best to just say that's a general pattern of writing a State
subclass, and an extra reminder would be clutter and not likely to get read at the right moment to help.
One nit: let's put dispose
after onNewStore
, similar to what we do elsewhere, so that the lifecycle methods are in rough order of when they're called.
void _unreadsModelChanged() { | ||
setState(() { | ||
// The actual state lives in [unreadsModel]. | ||
// This method was called because that just changed. | ||
}); | ||
} |
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.
Simpler to unify these, I think:
void _unreadsModelChanged() { | |
setState(() { | |
// The actual state lives in [unreadsModel]. | |
// This method was called because that just changed. | |
}); | |
} | |
void _modelChanged() { | |
setState(() { | |
// The actual state lives in [model] and [unreadsModel]. | |
// This method was called because one of those just changed. | |
}); | |
} |
final narrow = sorted[index]; | ||
return RecentDmConversationsItem( | ||
narrow: narrow, | ||
unreadCount: unreadDms[narrow]?.length ?? 0, |
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.
Let's factor this out as a method on Unreads
. Say int countInDmNarrow(DmNarrow narrow)
, and ordered after the fields and before the handleFoo methods.
Then for #130 (and the "N unreads" banner that goes with it) we'll naturally add similar methods like countInTopicNarrow
etc., and possibly a convenience wrapper like countInNarrow
that switch
es between them.
It occurs to me that it might be ideal for the pieces of the unreads model like dms
and streams
to be private, or @visibleForTesting
. But we can see whether that feels right as we add more things consuming this model.
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.
👀 on this, I've got sirpengi@237ea8f for my own usage in my work on #130
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.
It occurs to me that it might be ideal for the pieces of the unreads model like
dms
andstreams
to be private, or@visibleForTesting
. But we can see whether that feels right as we add more things consuming this model.
I think we'll want the unreads model to expose unread IDs so that UI code can do some filtering based on mute state, which will be tracked elsewhere.
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 we'll want the unreads model to expose unread IDs so that UI code can do some filtering based on mute state, which will be tracked elsewhere.
Hmm yeah, good point.
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.
For the counts that currently require traversing the model and doing sums (so, total unreads and unreads per stream), I wonder about instead adding properties that the model maintains, and updating them when the model handles events etc. Then UI-code callers don't have to worry about a lot of repetitive counting work being done when they request a count.
I've sent the draft #345 for one approach to tracking those numbers.
@@ -102,7 +139,27 @@ class RecentDmConversationsItem extends StatelessWidget { | |||
overflow: TextOverflow.ellipsis, | |||
title))), | |||
const SizedBox(width: 8), | |||
// TODO(#253): Unread count | |||
Visibility.maintain( |
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, fancy.
Reading the definition of what this does:
/// This is equivalent to the default [Visibility] constructor with all
/// "maintain" fields set to true. This constructor should be used in place of
and among those
/// Whether to maintain the semantics for the widget when it is hidden (e.g.
/// for accessibility).
///
/// To set this, [maintainSize] must also be set.
///
/// By default, with [maintainSemantics] set to false, the [child] is not
/// visible to accessibility tools when it is hidden from the user. If this
/// flag is set to true, then accessibility tools will report the widget as if
/// it was present.
final bool maintainSemantics;
it doesn't seem like that's something we want — we don't want to tell the user there's a "0" there for all the conversations with no unreads.
Probably also don't want maintainInteractivity
, though that one may have no effect here.
// When the count is zero, don't show an unread-count widget. Do keep a | ||
// "0" placeholder, to approximately match the width of a nonzero | ||
// one-digit unread count, given font-size settings etc. That's to | ||
// reduce layout jumping of the title text (which wraps and is | ||
// truncated) in the common case where a DM conversation goes between | ||
// zero and a small number of unreads. |
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 a lower-tech solution to this would be good: instead of having the whole unread-count marker widget tree be there when there are no unreads, just reserve some constant width in pixels. Can do it with ConstrainedBox
and BoxConstraints.minWidth
, so that it always reserves at least that much width even if the unread-count marker is slightly narrower.
One reason that would be good is that it avoids having a bunch of widgets there getting built when there's nothing we want to show.
But another is that that should be more effective at reducing jumping. In this version, the width consumed can actually get wider when all the messages get read — for example "0" is wider than "1" in this font (and basically any font that's not explicitly monospace or tabular-numerals).
unreadCount.toString()), | ||
)), | ||
), |
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:
unreadCount.toString()), | |
)), | |
), | |
unreadCount.toString())))), |
// When the count is zero, don't show an unread-count widget. Do keep a | ||
// "0" placeholder, to approximately match the width of a nonzero | ||
// one-digit unread count, given font-size settings etc. That's to | ||
// reduce layout jumping of the title text (which wraps and is | ||
// truncated) in the common case where a DM conversation goes between | ||
// zero and a small number of unreads. |
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.
(Also it's fine if things sometimes jump slightly here — it can't happen as a consequence of anything the user does on this screen, and so it won't be common that it happens at all while the user is looking at it.)
fontFamily: 'Source Sans 3', | ||
fontSize: 16, | ||
height: (18 / 16), | ||
fontFeatures: [FontFeature.enable('smcp')], // small caps |
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.
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.
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 I should give "dev mode" a try, then. I got a modal recently advertising it as a beta feature, but hadn't tried it out.
store.handleEvent( | ||
UpdateMessageFlagsAddEvent(id: message.id, flag: MessageFlag.read, all: false, messages: [message.id]), | ||
); |
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.
nits: line too long; and the event ID isn't related to a message ID. Here's a tweaked version:
store.handleEvent( | |
UpdateMessageFlagsAddEvent(id: message.id, flag: MessageFlag.read, all: false, messages: [message.id]), | |
); | |
store.handleEvent(UpdateMessageFlagsAddEvent( | |
id: 1, flag: MessageFlag.read, all: false, messages: [message.id])); |
Sure! Here, @terpimost: ![]() |
As Greg proposes: zulip#334 (comment)
fbbf4ab
to
e2a31c8
Compare
Thanks for the review! Revision pushed. About the subthread at #334 (comment): this revision has a simple |
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 revision! All looks good with a few small comments below, one of which is a small design/layout question which I'll raise in chat.
// TODO(i18n) formatting, which we might need the locale for. | ||
// For 9000, do we want 9000 or 9,000 or 9k or 9K or | ||
// (as in Figma) 999+? | ||
unreadCount: unreadsModel!.countInDmNarrow(narrow), |
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.
In web we give the exact figure, so I think we want the same here. In particular that way you can see it go down as you read things, even if the total is large. (Not super likely for a DM conversation in particular, but makes sense to be consistent.)
Specifically it looks like we say "9000". So we can just do the same here without a TODO. If we later decide to change that UI choice, we can come back to it (and make it locale-sensitive so it's "9.000" in some locales, etc.).
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.
bump — no need for a TODO comment for this
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.
Ah oops; forgot about this, thanks.
? Container( | ||
margin: const EdgeInsetsDirectional.only(start: 8, end: 16), |
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.
? Container( | |
margin: const EdgeInsetsDirectional.only(start: 8, end: 16), | |
? Padding( | |
padding: const EdgeInsetsDirectional.only(start: 8, end: 16), |
That's what Container.margin
boils down to — see the implementation of Container.build
. And I find it a lot clearer to understand this way, particularly because it avoids sounding like CSS's "margin", which is a lot more complicated.
color: Color(0xFF222222), | ||
).merge(weightVariableTextStyle(context)), | ||
unreadCount.toString())))) | ||
: const SizedBox(width: 12), // match whitespace at start of row |
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 yeah good question. I'll start a quick chat thread to get @terpimost's feedback on this bit — I don't see an answer for it in the Figma design.
→ thread here, as a continuation of the existing topic for this screen from before adding these unread counts.
I'll prefer for that to be structured in a way where this more-specific method is still exposed too:
So this is fine, and the work on #130 can build on that. |
As Greg proposes: zulip#334 (comment)
e2a31c8
to
33acff2
Compare
Thanks for the review! Revision pushed, and we can await Vlad's response in that CZO thread. |
33acff2
to
6c77b7c
Compare
And Vlad responded that the symmetric 12px is good, as in this revision. |
6c77b7c
to
387916d
Compare
Revision pushed, adjusting the gap between the right edge of the text and the counter, from the Figma's 8px to 12px, following Vlad's advice in chat. |
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 with one nit; then please go ahead and merge.
// TODO(i18n) formatting, which we might need the locale for. | ||
// For 9000, do we want 9000 or 9,000 or 9k or 9K or | ||
// (as in Figma) 999+? | ||
unreadCount: unreadsModel!.countInDmNarrow(narrow), |
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.
bump — no need for a TODO comment for this
There's about to be another kind of content to test for: a DM conversation's unread count.
As Greg proposes: zulip#334 (comment)
This follows Vlad's suggestion to adjust the gap between the right edge of the text and the counter, from the Figma's 8px to 12px: https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/design.3A.20DM-conversation.20list/near/1672155 Fixes: zulip#328
387916d
to
44e6642
Compare
As Greg proposes: #334 (comment)
Thanks! Merged, fixing that nit. |
Ah and for the thread: copy-pasting from CZO a screenshot with the layout we ended up with in this PR: ![]() |
Fixes: #328