-
Notifications
You must be signed in to change notification settings - Fork 306
msglist: Display typing indicators on typing. #790
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
2b24264
to
fc9db66
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! We'll get to a full review after #783.
On a quick skim, though, here's a pair of initial comments. And one more:
-
Please add a screenshot of what the typing indicators look like when visible. … It looks like we don't currently mention that in our guide to reviewable PRs, but I've seen @alya mention in a couple of threads in the past that that's helpful for reviewing the UI, even when there's also a video, because it makes it easier to look closely at the key moments in the UI's behavior.
(That guide does have a couple of other good tips on posting screenshots and videos, though.)
assets/l10n/app_en.arb
Outdated
@@ -479,5 +479,14 @@ | |||
"senderFullName": {"type": "String", "example": "Alice"}, | |||
"numOthers": {"type": "int", "example": "4"} | |||
} | |||
}, | |||
"typingIndicator": "{numPeople, plural, =1{{typist} is typing} =2{{typist} and {otherTypist} are typing} other{Several people are typing}}", |
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 "plural" feature in i18n is meant for handling the grammatical effects of a number. It shouldn't be used for trying to handle a number's other effects in the program logic, because its behavior is governed by the language's grammar.
In particular the names like "=1" can be misleading: they're pointers to the relevant number categories in the grammar, which don't always match the names' literal meaning.
Concretely, here, I believe that with this version:
- In English, only the "=1" and "other" forms will be used — if the number is 2, that's "other"
- In Chinese, only the "other" form will be used, regardless of number
- In Russian, the "=1" form will be used not only for 1 but for 21, 31, and many larger numbers
Some useful background reading (the first link is in a JS context, but the underlying concepts are the same as in i18n APIs used in Dart and many other languages):
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules
https://cldr.unicode.org/index/cldr-spec/plural-rules
https://www.unicode.org/cldr/charts/45/supplemental/language_plural_rules.html
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! Those are some interesting distinctions. =2
seems to be used for English after modifying the test case.
We can split this into 3 separate strings (or just two strings equivalent to the "=1" and "other" here, respectively) without the "plural" feature.
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.
=2
seems to be used for English after modifying the test case.
Huh, surprising.
In that case I'd guess that in Chinese it'd use the =1
and =2
forms too, if present… but a translator UI, like that in Transifex, generally wouldn't ask the Chinese translators for such forms, because in the normal grammatical behavior of the language (more precisely, in its behavior as recorded in the CLDR database, shown in that giant table at my third link) there's no distinction that would call for such forms. So we'd end up with only an "other" form in our translations.
And I'd expect that in Russian it'd still behave the way I predicted above, using the =1
form more broadly than this bit of program logic intends.
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.
Cross-posted a version of the above to #mobile-team, and then added further findings:
https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/i18n.20plurals.20.2F.20grammatical.20number/near/1848780
test/widgets/message_list_test.dart
Outdated
.data.equals(zulipLocalizations.typingIndicator( | ||
expected.length, expected[0].fullName, otherTypist?.fullName ?? '')); |
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 light of my other comment, let's have these tests use concrete strings for expectations, instead of zulipLocalizations
results 🙂 Try changing the tests first, and please confirm whether my prediction above is correct (at least about English).
I think just testing in English will suffice to make the point and confirm that the logic is happening outside of the i18n plural rules.
That's an old version of the documentation. I think it's more clear in the current version. |
Ah indeed, thanks — sorry for the confusion. I thought I remembered seeing such a writeup, so I was a bit puzzled not to find it; I didn't notice I'd landed on the "stable" version of the docs 🙂 |
I think we should include the |
cd62fea
to
617d7a7
Compare
Works for me! |
d3767d4
to
1a9d547
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! Small comments below.
lib/widgets/message_list.dart
Outdated
}; | ||
|
||
return Padding( | ||
padding: const EdgeInsets.only(left: 16, top: 2), |
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.
Since left and right aren't equal, this looks like a place to use EdgeInsetsDirectional
.
lib/widgets/message_list.dart
Outdated
return Padding( | ||
padding: const EdgeInsets.only(left: 16, top: 2), | ||
child: Text(text, | ||
textAlign: TextAlign.left, |
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.
TextAlign.start
, I think.
test/widgets/message_list_test.dart
Outdated
.data.equals(expected); | ||
} | ||
|
||
testTyping(WidgetTester tester, SendableNarrow narrow) async { |
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 only normally use the name testFoo
when the function itself calls test
/testWidgets
. I see that checkTyping
is already taken, though, by the helper just above this. Hmm.
Could find a different name, or maybe not use a helper and just cover the dm and topic cases with something like this (untested):
for (final (description, narrow, message) in [
('dm', …, …),
('topic', …, …),
]) {
testWidgets(description, …
lib/widgets/message_list.dart
Outdated
|
||
final typistNames = model!.typistIdsInNarrow(narrow) | ||
.where((id) => id != store.selfUserId) | ||
.map((id) => store.users[id]!.fullName) |
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 technically this should have an "(unknown user)" fallback, for #716? I acknowledge that it would be rare and silly for a nonexistent user to show up as typing, though.
assets/l10n/app_en.arb
Outdated
@@ -483,5 +483,24 @@ | |||
"notifSelfUser": "You", | |||
"@notifSelfUser": { | |||
"description": "Display name for the user themself, to show after replying in an Android notification" | |||
}, | |||
"onePersonTyping": "{typist} is typing...", |
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 probably want the "…" character instead of "...", right?
fd00b9e
to
3bf08e1
Compare
Updated the PR. Thanks for the review @chrisbobbe! |
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! Small comments below, and I'll mark this for Greg's review.
lib/widgets/message_list.dart
Outdated
@@ -530,6 +534,68 @@ class MarkAsReadWidget extends StatelessWidget { | |||
} | |||
} | |||
|
|||
class _TypingStatusState extends State<TypingStatusWidget> with PerAccountStoreAwareStateMixin<TypingStatusWidget> { |
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.
Normally we organize stateful-widget code by putting the widget first and then the state.
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, follow the normal pattern for the relationship between widget and state classes' names: widget Foo has state _FooState.
lib/widgets/message_list.dart
Outdated
color: HslColor(0, 0, 53), fontStyle: FontStyle.italic)), | ||
); |
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.
color: HslColor(0, 0, 53), fontStyle: FontStyle.italic)), | |
); | |
color: HslColor(0, 0, 53), fontStyle: FontStyle.italic))); |
lib/widgets/message_list.dart
Outdated
Widget build(BuildContext context) { | ||
final store = PerAccountStoreWidget.of(context); | ||
final narrow = widget.narrow; | ||
const placeholder = SizedBox(height: 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.
When the typing status is not shown, this puts an extra 8px between the last message and the mark-as-read button. If that's not part of the design, let's avoid 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.
Came across https://chat.zulip.org/#narrow/stream/48-mobile/topic/messages.20shift.20on.20mark-as-read, and I think the typing indicators have the same issue of causing a message shift. I wonder if we want to do something similar to #562.
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 follow-up discussion is here: https://chat.zulip.org/#narrow/stream/48-mobile/topic/messages.20shift.20on.20typing.20indicator/near/1925986
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 @PIG208 for implementing this, and @chrisbobbe for the previous review!
Generally this looks good. Comments below, in addition to those from Chris above.
'stream_typing_notifications': false, // TODO implement | ||
'stream_typing_notifications': 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 should get its own commit and own commit message, since it's changing what we get from the server (whereas the rest of the commit has a very different character, adding UI).
I think the effect is that it's just causing us to get more TypingEvent
s, and our code already handles the new kind?
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.
Yes, that's correct.
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.
lib/widgets/message_list.dart
Outdated
@@ -530,6 +534,68 @@ class MarkAsReadWidget extends StatelessWidget { | |||
} | |||
} | |||
|
|||
class _TypingStatusState extends State<TypingStatusWidget> with PerAccountStoreAwareStateMixin<TypingStatusWidget> { |
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, follow the normal pattern for the relationship between widget and state classes' names: widget Foo has state _FooState.
lib/widgets/message_list.dart
Outdated
const placeholder = SizedBox(height: 8); | ||
if (narrow is! SendableNarrow) return placeholder; | ||
|
||
final localization = 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: plural, not singular — this is an object that contains a whole set of localizations
assets/l10n/app_en.arb
Outdated
}, | ||
"onePersonTyping": "{typist} is typing…", | ||
"@onePersonTyping": { | ||
"description": "Text to display when there is a user typing.", |
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:
"description": "Text to display when there is a user typing.", | |
"description": "Text to display when there is one user typing.", |
In the two other cases below, I'd also say "there is a user typing".
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.
Do you mean "there is one user typing"? Or "there are two users typing" and 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.
Ah sorry, that was unclear: the text below is fine for those other cases. What I mean is, I would say all three of these situations are accurately described with "there is a user typing". So the text for this case should be more specific in order to distinguish it.
lib/widgets/message_list.dart
Outdated
|
||
final localization = ZulipLocalizations.of(context); | ||
final typistNames = model!.typistIdsInNarrow(narrow) | ||
.where((id) => id != store.selfUserId) |
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 we do this filtering on the model instead? That will simplify this build method's code, I think, as well as mean less work for it to do (by helping it skip allocating a new list).
test/widgets/message_list_test.dart
Outdated
@@ -981,4 +981,70 @@ void main() { | |||
}); | |||
}); | |||
}); | |||
|
|||
group('TypingStatus', () { |
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.
separate nit: where possible we try to keep tests in the same order as the code they're testing, to help with navigating between tests and code under test
Here MarkAsReadWidget is already out of order, so moving that would be a good small prep commit. Then this can go next to that.
lib/widgets/message_list.dart
Outdated
} | ||
} | ||
|
||
class TypingStatusWidget 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.
nit (a thought prompted by looking at the order of the tests): it'd be most logical for this to come just above MarkAsReadWidget, rather than just below — since that matches how it appears in the UI
(That would be the opposite order from their call sites, but that's because the code at the call sites happens to be written in a context where it's counting from the bottom.)
test/widgets/message_list_test.dart
Outdated
final dmMessage = eg.dmMessage(from: eg.otherUser, to: [eg.selfUser]); | ||
final dmNarrow = DmNarrow.withUsers( | ||
users.map((u) => u.userId).toList(), | ||
selfUserId: eg.selfUser.userId); |
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 message doesn't appear in this narrow. For that, the to
argument should have the other users. (The code below assumes the message is in the narrow, so this affects the realism of the test data.)
Then once you do that, there's a handy constructor DmNarrow.ofMessage
.
test/widgets/message_list_test.dart
Outdated
matching: find.byType(Text) | ||
); | ||
|
||
checkTyping(WidgetTester tester, TypingEvent event, {required String expected}) async { |
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: function definition should have explicit return type, so it doesn't look like a function call
(hmm, we should really look to see if there's a lint rule for that, and turn it on — seems eminently lintable)
test/widgets/message_list_test.dart
Outdated
check(finder.evaluate()).single.has((x) => x.widget, 'widget').isA<Text>() | ||
.data.equals(expected); |
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.
tester.widget
can simplify this:
check(finder.evaluate()).single.has((x) => x.widget, 'widget').isA<Text>() | |
.data.equals(expected); | |
check(tester.widget<Text>(finder)).data.equals(expected); |
Thanks for the reviews! Updated the PR and left some questions. |
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! Comments below.
lib/model/typing_status.dart
Outdated
Iterable<int> typistIdsInNarrow(SendableNarrow narrow) => | ||
_timerMapsByNarrow[narrow]?.keys ?? []; | ||
_timerMapsByNarrow[narrow]?.keys.where((id) => id != selfUserId) ?? []; |
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, in #790 (comment) I didn't just mean doing the filtering in the model code rather than the UI code — I meant doing so in the model data structure itself. That is, just never have an entry here where the key is the self-user.
That way we get to avoid the where
. Which in turn gets us out of scanning through the whole list if there are more than two.
lib/model/typing_status.dart
Outdated
allRecipientIds: event.recipientIds!..sort(), selfUserId: selfUserId), | ||
allRecipientIds: event.recipientIds!.toList()..sort(), selfUserId: selfUserId), |
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.
No need for the copy (i.e. this toList
call). In principle it's not ideal to be mutating the list because in principle something else could be looking at it… but (a) nothing else actually does look at this, because it's a specific feature that this model is entirely responsible for, and (b) really it ought to be sorted anyway, so this is just doing a public service for anything else that might be using the data.
(The ideal version of this would be that we sort at deserialization time.)
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 DmNarrow.ofMessage factory creates a unmodifiable list, which motivates the toList call here, because we can't mutate the list.
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 might not be using DmNarrow.ofMessage in the application code, so tweaking the tests can be a solution. But it would be helpful if there is a way to statically discover issues like 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.
Resolved by only sorting them when deserializing TypingEvent, and skipping the sort call from TypingStatus.
'stream_typing_notifications': false, // TODO implement | ||
'stream_typing_notifications': 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.
lib/widgets/message_list.dart
Outdated
final typistIds = model!.typistIdsInNarrow(narrow).toList(); | ||
if (typistIds.isEmpty) return const SizedBox(); | ||
final String text = switch (typistIds) { | ||
[final firstTypist] => |
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.
#790 (comment) (a reply I made yesterday to a question after your revision — echoing here just so it remains visible in this review)
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.
Rewritten that part of code. It ended up looking a bit verbose, but it does save the .toList
call or unnecessary .map
's.
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.
Great. It may look a little verbose… but I think in actual line count (for this build method) it's the shortest version yet 🙂 Sometimes just writing things very concretely pays off.
test/widgets/message_list_test.dart
Outdated
final dmMessage = eg.dmMessage(from: eg.selfUser, to: [eg.otherUser]); | ||
final dmNarrow = DmNarrow.ofMessage(dmMessage, selfUserId: eg.selfUser.userId); |
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 keep test data realistic — so if we're going to have eg.thirdUser and eg.fourthUser typing in this DM narrow, they should be members of it. (following up on #790 (comment) )
Updated the PR. Thanks! |
Rebased to resolve a conflict with 0d577a0 |
pushed to rebase and address #790 (comment) |
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, in the current revision it doesn't look like my comments at #790 (review) have been addressed. Perhaps you rebased a different version of the branch than you intended?
assets/l10n/app_en.arb
Outdated
"twoPeopleTyping": "{typist} and {otherTypist} are typing…", | ||
"@twoPeopleTyping": { | ||
"description": "Text to display when there is one user typing.", |
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 description doesn't match the text; it's also the same as the description of a different string, which makes it unhelpful for translators.
I guess #790 (comment) left things still confusing? The previous revision was fine on this file.
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.
Right, I thought #790 (comment) meant to have all three use this same description string.
I think a previous update was lost to a hard reset. Digging my reflog right now. |
This should be fixed now. |
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! This all looks great — just nits in this round.
lib/api/model/events.dart
Outdated
@@ -900,16 +901,16 @@ class TypingEvent extends Event { | |||
required this.recipientIds, | |||
required this.streamId, | |||
required this.topic, | |||
}); | |||
}) : assert(isSortedWithoutDuplicates(recipientIds ?? [])); |
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:
}) : assert(isSortedWithoutDuplicates(recipientIds ?? [])); | |
}) : assert(recipientIds == null || isSortedWithoutDuplicates(recipientIds)); |
I think that's a bit clearer, because in the situation where recipientIds
is null it's not that that's a way of expressing that the list of recipients is empty — rather it means it doesn't make sense to talk about recipients (because it's a stream/channel message).
lib/api/model/events.dart
Outdated
|
||
static Object? _readSenderId(Map<Object?, Object?> json, String key) { | ||
return (json['sender'] as Map<String, dynamic>)['user_id']; | ||
} | ||
|
||
static List<int>? _recipientIdsFromJson(Object? json) { | ||
if (json == null) return null; | ||
return (json as List<Object?>).map( | ||
(item) => (item as Map<String, Object?>)['user_id'] as int).toList(); | ||
return (json as Iterable<Object?>).map( |
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:
return (json as Iterable<Object?>).map( | |
return (json as List<Object?>).map( |
That keeps this code resembling the generated deserialization code when it comes to boring parts like casting pieces of the data to their expected types. There isn't some expected situation where this would be a non-list iterable, right?
@@ -3,6 +3,7 @@ import 'dart:async'; | |||
import 'package:flutter/foundation.dart'; | |||
|
|||
import '../api/model/events.dart'; | |||
import '../log.dart'; |
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: this happens a commit sooner than it should
(so the linter would warn at that commit)
return (json as List<Object?>).map( | ||
(item) => (item as Map<String, Object?>)['user_id'] as int).toList(); | ||
return (json as Iterable<Object?>).map( | ||
(item) => (item as Map<String, Object?>)['user_id'] as int).toList()..sort(); |
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.
commit-message nit:
events: Sort recipientIds for TypingEvent.
The prefix we've used for changes here is api:
, so let's stay consistent with that.
A command like git log --oneline lib/api/model/events.dart
is very handy for looking to see what precedents are for this kind of question.
@@ -38,6 +39,10 @@ class TypingStatus extends ChangeNotifier { | |||
} | |||
|
|||
bool _addTypist(SendableNarrow narrow, int typistUserId) { | |||
if (typistUserId == selfUserId) { |
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.
commit-message nit:
model: Do not allow adding self as typist.
For files in lib/model/, we normally make the prefix more specific than this — here, typing_status:
would be good.
(I'd be tempted to shorten it to typing:
, but that's too confusing with the concept of the type system and type checker.)
@@ -843,227 +1130,4 @@ void main() { | |||
..status.equals(AnimationStatus.dismissed); | |||
}); | |||
}); | |||
|
|||
group('MarkAsReadWidget', () { |
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.
commit-message nit:
test [nfc]: Reorder MarkAsReadWidget test group.
Prefix should be more specific: "msglist test [nfc]:".
(Same story as at #787 (comment) .)
lib/widgets/message_list.dart
Outdated
final typistIds = model!.typistIdsInNarrow(narrow).toList(); | ||
if (typistIds.isEmpty) return const SizedBox(); | ||
final String text = switch (typistIds) { | ||
[final firstTypist] => |
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.
Great. It may look a little verbose… but I think in actual line count (for this build method) it's the shortest version yet 🙂 Sometimes just writing things very concretely pays off.
So that its location within the test groups matches where its implementation is defined. Signed-off-by: Zixuan James Li <[email protected]>
We added support for stream typing notifications, and this change ensures that we get TypingEvents for them. Signed-off-by: Zixuan James Li <[email protected]>
DmNarrow expects them to be sorted, so doing it early during deserialization helps. Because of this TypingStatus no longer needs to modify the recipientIds list. Signed-off-by: Zixuan James Li <[email protected]>
The server usually does not send events notifying the user themself typing. But having this check gives us a stronger guarantee that the maps do not contain the self user. Signed-off-by: Zixuan James Li <[email protected]>
Because we don't have a Figma design yet, this revision supports a basic design similar to the web app when there are people typing. Fixes zulip#665. Signed-off-by: Zixuan James Li <[email protected]>
Updated the PR. Thanks for the review! |
Thanks! Looks good — merging. |
Display a typing indicator when there are people typing in a
SendableNarrow
such asDmNarrow
andTopicNarrow
, in a fashion similar to the web app.This is stacked on top of #783 and fixes #665.