-
Notifications
You must be signed in to change notification settings - Fork 321
Show read receipts #1706
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
base: main
Are you sure you want to change the base?
Show read receipts #1706
Conversation
color: designVariables.title, | ||
).merge(weightVariableTextStyle(context, wght: 600))), | ||
if (status == FetchStatus.success && receiptCount > 0) | ||
StyledText( |
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.
Very interesting, I didn't expect to be able to do this! Is this a solution to #1285?
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 the issue, I think this could be a solution for 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.
Hmm interesting.
This wouldn't be a satisfactory solution to all of #1285, because this appears to fire up a whole XML parser for each one of these widgets, which is going to be expensive. That's acceptable in this context, where there's only one of these at a time (and it isn't a very common part of the UI anyway). It wouldn't be OK for something that could appear in, say, message content, where you might scroll past a lot of them.
7c6c52c
to
d05ac9d
Compare
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! Here's comments from an initial skim.
} | ||
} | ||
|
||
class ViewReadReceiptsButton extends MessageActionSheetMenuItemButton { |
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.
An intermediate commit has:
@override void onPressed() {
// TODO: open read receipts sheet
}
So it shows the button but the button doesn't do anything. That's a buggy state, so let's avoid it.
That commit is quite small, so we can just squash it into the later commit that gives this button something to do.
lib/widgets/action_sheet.dart
Outdated
/// A plain text widget for a bottom sheet with a multiline UI string. | ||
/// | ||
/// Comes with built-in 16px horizontal padding. | ||
/// | ||
/// Figma: | ||
/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3481-26993&m=dev | ||
class BottomSheetPlainText extends StatelessWidget { |
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 name is very general. It's hard for me to tell what sort of situations this is meant for, or what it does.
If I want plain text, that's what Text
does, right? It's not clear how the bottom sheet interacts with that.
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.
What about the new name BottomSheetInfoText
?
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.
That does suggest more information. Can you explain a bit more in the widget's doc?
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.
Updated its doc.
lib/widgets/read_receipts.dart
Outdated
child: status == FetchStatus.loading | ||
? CircularProgressIndicator() | ||
: status == FetchStatus.error | ||
? BottomSheetPlainText( |
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 chain of ?:
would be best as a switch (status)
. That way it's clear we handle all the possible values.
final localizations = ZulipLocalizations.of(context); | ||
final designVariables = DesignVariables.of(context); | ||
|
||
return Center( |
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.
Does this Center
end up doing anything in the three cases other than CircularProgressIndicator
? I suspect it doesn't.
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 also centers the plain text in the sheet, especially vertically.🙂
lib/widgets/read_receipts.dart
Outdated
// TODO: deduplicate the code with [ViewReactionsUserItem] | ||
@visibleForTesting | ||
class ReadReceiptsUserItem extends StatelessWidget { | ||
const ReadReceiptsUserItem(this.pageContext, { |
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: widget parameters should always be named (with very rare exceptions like Text
)
lib/widgets/read_receipts.dart
Outdated
padding: EdgeInsets.symmetric(vertical: 8), | ||
itemCount: userIds.length, | ||
itemBuilder: (context, index) => | ||
ReadReceiptsUserItem(context, userId: userIds[index])))); |
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 child widget here calls this argument pageContext
, but the context we're passing looks like just its own parent's context. If this context works, then I expect the child's own context would work equally well.
(This is also an example of why it's good for widget parameters to be named — if this said pageContext: context
, then the contrast would jump out more.)
lib/widgets/read_receipts.dart
Outdated
/// https://zulip.com/api/get-read-receipts | ||
Future<GetReadReceiptsResult> getReadReceipts(ApiConnection connection, { | ||
required int messageId, | ||
}) { | ||
return connection.get('getReadReceipts', GetReadReceiptsResult.fromJson, | ||
'messages/$messageId/read_receipts', null); |
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 should go in lib/api/ :slightly_smiling_face:
color: designVariables.title, | ||
).merge(weightVariableTextStyle(context, wght: 600))), | ||
if (status == FetchStatus.success && receiptCount > 0) | ||
StyledText( |
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 interesting.
This wouldn't be a satisfactory solution to all of #1285, because this appears to fire up a whole XML parser for each one of these widgets, which is going to be expensive. That's acceptable in this context, where there's only one of these at a time (and it isn't a very common part of the UI anyway). It wouldn't be OK for something that could appear in, say, message content, where you might scroll past a lot of them.
2ed033d
to
2b769fd
Compare
Thanks @gnprice for the previous review. New changes pushed. Marked for Chris's review. |
This fixes the two inconsistencies flagged in discussion: https://chat.zulip.org/#narrow/channel/530-mobile-design/topic/bottom.20sheet.20.22Cancel.22.2F.22Close.22.20button/near/2216116 > I think it's reasonable to have both labels, but I think we should > choose them differently than now: > > - "Cancel" when the sheet is about doing an action: [etc.] > > - "Close" when the sheet just presents information or nav options: > [etc.]
…s test So we can add another test that uses it.
I experimented with using Semantics to help write human-centered tests, and I ended up adding some configuration that actually seemed to make a reasonable experience in the UI, at least in my testing with VoiceOver. Fixes zulip#740.
This way, it can be used for purposes other than being a header, such as in the next commits, when read receipts fail to load, we use this widget in the center of bottom sheet to give feedback to the user. Additional changes: - Adds a TextAlign property for controlling the alignment. - Removes vertical padding so callers can decide about it. But keeps the horizontal padding of 16 as it is the default padding of our bottoms sheets.
2b769fd
to
24a4e8e
Compare
Pushed a new revision with tests included. |
24a4e8e
to
961fcd8
Compare
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, this is exciting! Comments below, from reading all the commits except the last, which I've read only partially so far.
@@ -48,143 +48,146 @@ abstract final class ZulipIcons { | |||
/// The Zulip custom icon "check". | |||
static const IconData check = IconData(0xf108, fontFamily: "Zulip Icons"); | |||
|
|||
/// The Zulip custom icon "check_check". | |||
static const IconData check_check = IconData(0xf109, fontFamily: "Zulip Icons"); |
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.
icons: Add check_check icon, from Web Figma file
I started a discussion on this; I don't think there's a change to make right now, but please link to it in the commit message for context: #mobile-design > read-receipts icon in #F1706 @ 💬
lib/widgets/action_sheet.dart
Outdated
/// A header for a bottom sheet with a multiline UI string. | ||
/// A plain text widget for a bottom sheet with a multiline UI string. | ||
/// | ||
/// Assumes 8px padding below the top of the bottom sheet. | ||
/// Use it to present short, non-interactive explanatory messages to the user, | ||
/// such as an error message or other feedback. | ||
/// | ||
/// Comes with built-in 16px horizontal padding. | ||
/// | ||
/// Figma: | ||
/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3481-26993&m=dev |
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.
action_sheet: Rename BottomSheetHeaderPlainText to BottomSheetInfoText
This commit decreases the number of widgets that
- declare the job of following one specific source in the Figma,
- do that job correctly, and
- have an implementation that's easy to compare to that source. 🙂
Let's make a new widget for the new not-header thing, and keep this widget for a header. This would be an example of the principle "prefer duplication over the wrong abstraction", I think 🙂, which Greg taught me about a while ago: https://chat.zulip.org/#narrow/sender/2187-Greg-Price/search/wrong.20abstraction
aside
I could more easily imagine BottomSheetHeaderPlainText
growing a "title/body" variant, if that's helpful for following this part of the Figma—

; it would still have the "header" abstraction, which seems right. In fact, that might be helpful—the Figma's 18px horizontal padding kind of looks like a mistake to me; the plain-text version has 16px. A shared widget would be a natural place for a code comment that mentions both kinds of headers. We'd want to make sure the widget still has a clear, coherent interface, with a link to the Figma source for the new behavior.
"@actionSheetReadReceipts": { | ||
"description": "Title for the \"Read receipts\" bottom sheet." | ||
}, | ||
"actionSheetReadReceiptsReadCount": "This message has been <link href=\"https://chat.zulip.org/help/read-receipts\">read</link> by {count} {count, plural, =1{person} other{people}}:", |
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.
We shouldn't give translators control of the URL. If the Help Center page moves, we should only have to make a one-line change in our source code, instead of going through and changing language data for all the languages. 🙂
See web's string, can we do something like <z-link>
there?
{num_of_people, plural, one {This message has been <z-link>read</z-link> by {num_of_people} person:} other {This message has been <z-link>read</z-link> by {num_of_people} people:}}
Also I think we should link to https://zulip.com/help/read-receipts
instead of that page on CZO.
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 IIUC that plural syntax works, but I think we'd normally do
{count, plural, =1{1 person} other{{count} people}}
in case that's a more helpful model when translating into a language that varies the word order among the one/two/few/many/other cases? (I'm not actually sure if such languages exist actually.)
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, in fact best to have the whole sentence inside the plural block — like it is in that example from web.
That way if e.g. the form of the verb needs to change too, the translator has that flexibility.
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.
That way if e.g. the form of the verb needs to change too, the translator has that flexibility.
nit: IIUC the translator has that flexibility already; they can choose to write the translated string however they want, including by re-scoping the plural block (or even omitting it which would probably be wrong). But by putting the whole sentence in the plural block in English, we're potentially helping by minimizing the things a translator has to think through in order to make a correct translation for their language.
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.
True. So it's more that we're giving them a model that makes it easier to see how to apply that flexibility.
@@ -720,6 +721,7 @@ void showMessageActionSheet({required BuildContext context, required Message mes | |||
ReactionButtons(message: message, pageContext: pageContext), | |||
if (hasReactions) | |||
ViewReactionsButton(message: message, pageContext: pageContext), | |||
ViewReadReceiptsButton(message: message, pageContext: pageContext), |
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.
We should store and check the org's "Enable read receipts" setting before including the button; see realm_enable_read_receipts
in https://zulip.com/api/register-queue
overlayColor: WidgetStateColor.resolveWith((states) => | ||
states.any((e) => e == WidgetState.pressed) | ||
? designVariables.contextMenuItemBg.withFadedAlpha(0.20) | ||
: 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.
How about
overlayColor: WidgetStateColor.fromMap({
WidgetState.pressed: designVariables.contextMenuItemBg.withFadedAlpha(0.20)
}),
?
I updated my #1700 with that in response to #1700 (comment) 🙂 (there might be other things that would be helpful to bring over from ViewReactionsUserItem
in my current revision there)
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.
And here's a full review of that last commit. Generally looks great; small comments below.
@override | ||
Widget build(BuildContext context) { | ||
return SizedBox( | ||
height: 500, |
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 the view-reactions sheet, I used 400 with a TODO(design) tune
. I'd like to be consistent between the two places unless there's a reason not to, but I don't mind which value is used; 500 is fine if you think it's better.
@override | ||
void didChangeDependencies() { | ||
super.didChangeDependencies(); | ||
|
||
tryFetchReadReceipts(); | ||
} |
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 means we can end up fetching multiple times, so I think my ideal version would have generation
logic like in lib/model/message_list.dart, to invalidate stale fetches. If results arrive out of order, we don't want to get stuck showing old results when we know we have results from a fetch we made more recently.
I think that can be a followup though—I can write up a more detailed proposal later, or perhaps just send a draft to demonstrate, once this work is merged.
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.
What are the reasons we would want to do a new fetch because a dependency changed?
I think it may only be if there's a new store. So this could go in an onNewStore method instead.
Then the generation-like logic can just be: when getting the response, check if the store we had at the start of the request is still the current store (with identical
). If not, ignore the response.
}); | ||
} | ||
|
||
class ReadReceipts extends StatefulWidget { |
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 link to the Figma, so it's easier for readers to check the implementation against the spec.
final designVariables = DesignVariables.of(context); | ||
|
||
return Padding( | ||
padding: EdgeInsetsDirectional.fromSTEB(18, 16, 18, 8), |
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 this is a rare case where I disagree with the Figma (link). Can we change the horizontal padding to 16 and the bottom padding to 4, like in BottomSheetHeaderPlainText
(which in main
follows this in the Figma)?
Here's a before/after of my proposal (from the current revision):
Before | After |
---|---|
![]() |
![]() |
You can leave a code comment explaining that we prefer consistency with that other header, with a link to this GitHub comment.
|
||
@override | ||
Widget build(BuildContext context) { | ||
final localizations = ZulipLocalizations.of(context); |
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: we generally write zulipLocalizations
instead of localizations
; that way it's distinct from other localizations like MaterialLocalizations
.
style: TextStyle( | ||
decoration: TextDecoration.underline, | ||
color: designVariables.link, | ||
decorationColor: designVariables.link), |
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 Figma specifies some other details about the underline style. Let's follow any of those that we can (e.g. decorationThickness
, I think) and write TODO(upstream)
s for the others.
@@ -171,6 +171,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> { | |||
labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 0).toColor(), | |||
labelMenuButton: const Color(0xff222222), | |||
labelSearchPrompt: const Color(0xff000000).withValues(alpha: 0.5), | |||
labelTime: const Color(0x00000000).withValues(alpha: 0.49), | |||
link: const Color(0xff066bd0), |
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 didn't find this in the list of Figma variables that I usually look at.
Turns out it's defined in the "Zulip Web UI kit": https://www.figma.com/design/msWyAJ8cnMHgOMPxi7BUvA/Zulip-Web-UI-kit?node-id=0-1&p=f&vars=1&var-id=b9c211a7c212568bc84139413454dd8dd07321fb%2F833-56&m=dev

It has distinct light/dark values; we should use both. And maybe add a comment like
link: const Color(0xff066bd0), // From "Zulip Web UI kit"
Finder findUserItem(String fullName) => find.ancestor(of: find.text(fullName), | ||
matching: find.byType(ReadReceiptsUserItem)); |
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.
How about:
Finder findUserItem(String fullName) => find.ancestor(of: find.text(fullName), | |
matching: find.byType(ReadReceiptsUserItem)); | |
Finder findUserItem(String fullName) => | |
find.widgetWithText(ReadReceiptsUserItem, fullName); |
?
delay: transitionDurationObserver.transitionDuration | ||
+ const Duration(milliseconds: 100)); |
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 takes some nonlocal reasoning to work out which nav-transition this is about, and I notice that we're always passing the same duration here.
How about replacing prepareReceiptsResponse
with prepareReceiptsResponseSuccess(List<int> userIds)
and prepareReceiptsResponseError(Object? httpException)
, and control the delay in setupReceiptsSheet
?
check(find.byWidgetPredicate((widget) => switch(widget) { | ||
ProfilePage(userId: 1) => true, | ||
_ => false, | ||
})).findsOne(); |
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.
Can be two lines instead of four I think 🙂
check(find.byWidgetPredicate((widget) => widget is ProfilePage && widget.userId == 1))
.findsOne();
final result = await getReadReceipts(store.connection, messageId: widget.messageId); | ||
// TODO(i18n): add locale-aware sorting | ||
userIds = result.userIds.sortedByCompare( | ||
(id) => store.userDisplayName(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.
Relatedly: in general we never want to keep a reference to the store across an await and use it again after, because the old store might already have been disposed. Instead, after an await we should look up the store afresh using a context.
Support for viewing read receipts of a message.
Rebased on top of #1700 to borrow some code from. Main commits start from 87518f1 "icons: Add
check_check
icon, from Figma".Figma link: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=11367-20647&t=bpmAb3Fr9aJdMIHw-0
Screenshots
Screen recording
Read.receipts.-.Screen.recording.mov
Fixes: #667