-
Notifications
You must be signed in to change notification settings - Fork 320
Set user status #1701
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?
Set user status #1701
Conversation
0d852c1
to
242f877
Compare
Sure, on it. |
23cdd3a
to
f67c992
Compare
assets/l10n/app_en.arb
Outdated
"@setStatus": { | ||
"description": "The status button label in self-user profile page when status is not set." | ||
}, | ||
"noStatusText": "Not status text", |
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.
"noStatusText": "Not status text", | |
"noStatusText": "No status text", |
The screenshots look good to me, other than the small note above! |
1405501
to
1ca2ceb
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 for building this! Comments below; and I see your PR description has a TODO for tests, which I believe is the only reason this is currently marked as a draft.
I've read through the first 5 commits:
a604e22 button: Add ZulipMenuItemButton.subLabel
e3b091a profile: Add button for setting/showing user status in self-user profile
a90f83e model [nfc]: Add UserStatusChange.copyWith method
0104cd1 content: Add emoji
property to UserStatusEmoji widget
649232a emoji: Make emoji picker return the selected emoji, for reuse
and part of the 6th:
1ca2ceb user-status: Add page for setting own user status
lib/widgets/button.dart
Outdated
children: [ | ||
Text(label, | ||
style: const TextStyle(fontSize: 20, height: 24 / 20) | ||
.merge(weightVariableTextStyle(context, wght: _labelWght()))), | ||
if (subLabel != null) | ||
Text.rich(subLabel!, |
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 happens when there isn't room for both of these? (Always a question with a 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.
Thanks for catching. They would overflow. Fixed in the new revision, with both of them taking half the space and ellipsized.
lib/widgets/profile.dart
Outdated
class _SetStatusButton extends StatelessWidget { | ||
const _SetStatusButton({required this.userId}); | ||
|
||
final int 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 is always the self-user ID, right? (Otherwise, it wouldn't make sense to set status.)
Seems clearest to not take it as a parameter, then; this widget's build method can look it up for itself.
assets/l10n/app_en.arb
Outdated
@@ -809,6 +809,62 @@ | |||
"@userRoleUnknown": { | |||
"description": "Label for UserRole.unknown" | |||
}, | |||
"status": "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.
This key is too general — there could easily be several places where we use the same string "Status" in English, and then the right translations might not be the same in some languages.
Instead, can say:
"status": "Status", | |
"statusButton": "Status", |
assets/l10n/app_en.arb
Outdated
"setStatus": "Set status", | ||
"@setStatus": { | ||
"description": "The status button label in self-user profile page when status is not set. Also, title for the page where user status is set." |
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 "also" is a good sign that this should be two separate strings 🙂 even though they happen to have the same value in English.
For the first… perhaps statusButtonAction
.
For the second, setStatusPageTitle
. (See examples of other "…PageTitle" strings.)
assets/l10n/app_en.arb
Outdated
"userStatusBusy": "Busy", | ||
"@userStatusBusy": { | ||
"description": "Label for one of the suggested user statuses with status text 'Busy', in setting user status page." |
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 isn't so much a label as a suggested actual value for the status. (If the user chooses it, it'll set their status text to the value of this string, right? Not to the English "Busy".)
So:
"userStatusBusy": "Busy", | |
"@userStatusBusy": { | |
"description": "Label for one of the suggested user statuses with status text 'Busy', in setting user status page." | |
"userStatusBusy": "Busy", | |
"@userStatusBusy": { | |
"description": "A suggested user status text, 'Busy'." |
lib/widgets/set_status.dart
Outdated
class _SetStatusPageState extends State<SetStatusPage> { | ||
List<UserStatus> _statusSuggestions(ZulipLocalizations localizations) => [ |
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: put this method down below the state fields and the lifecycle methods; it's basically part of the work of the build method, so put it next to that
lib/widgets/set_status.dart
Outdated
padding: const EdgeInsets.only( | ||
// In Figma design, this is 16px, but we compensate for that in | ||
// the icon button below. | ||
left: 8, | ||
top: 8, right: 10, |
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.
left/right should be start/end (so it works correctly in RTL locales)
lib/widgets/set_status.dart
Outdated
: icon!; | ||
}, | ||
child: Icon(ZulipIcons.smile, size: 24)), |
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 icon doesn't really play the role of "child", since it commonly won't even appear at all.
The small optimization that comes from using ValueListenableBuilder.child here can be matched (and exceeded) by using const
:
: icon!; | |
}, | |
child: Icon(ZulipIcons.smile, size: 24)), | |
: const Icon(ZulipIcons.smile, size: 24); | |
}), |
Expanded(child: TextField( | ||
controller: statusTextController, | ||
minLines: 1, | ||
maxLines: 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.
Is this different from omitting minLines
?
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.
Omitting minLines
will make the field take maxLines
height.
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 see, interesting. I looked at the doc on minLines
before making my comment, and it seemed pretty unclear on this point, but didn't see those bits of the doc on maxLines
.
It looks like the key logic in the implementation is this:
https://github.com/flutter/flutter/blob/e2441683275879df1ac0311e02ff868410599ab1/packages/flutter/lib/src/rendering/editable.dart#L2425-L2433
final double preferredHeight = switch (maxLines) {
null => math.max(_textPainter.height, preferredLineHeight * (minLines ?? 0)),
1 => _textPainter.height,
final int maxLines => clampDouble(
_textPainter.height,
preferredLineHeight * (minLines ?? maxLines),
preferredLineHeight * maxLines,
),
};
So when maxLines
is non-null, the effective default for minLines
is that it equals maxLines
. When maxLines
is null, though, the effective default for minLines
is 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.
Interesting piece of code. However, I think that, at least for me, it didn't make sense the first time to know that I had to specify minLines
, for maxLines
to work as the maximum number of lines.😀
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 agreed — the behavior is a bit nonintuitive, and the docs aren't super clear about it.
lib/widgets/set_status.dart
Outdated
final emoji = switch(change.emoji) { | ||
OptionNone<StatusEmoji?>() => oldStatus.emoji, | ||
OptionSome<StatusEmoji?>(:var value) => value, | ||
}; |
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:
final emoji = switch(change.emoji) { | |
OptionNone<StatusEmoji?>() => oldStatus.emoji, | |
OptionSome<StatusEmoji?>(:var value) => value, | |
}; | |
final emoji = change.emoji.or(oldStatus.emoji); |
That's equivalent, 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.
Ahh, indeed; thanks!
764b856
to
ea1d711
Compare
Thanks @gnprice for the review. New revision pushed. Please have a 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.
Thanks for the revision! Comments below.
Like in the previous round above, I've read the first N-1 commits:
88f8299 button: Add ZulipMenuItemButton.subLabel
03d3999 button [nfc]: Make ZulipMenuItemButton.onPressed optional
f65ee25 profile: Add button for setting/showing user status in self-user profile
5d50ee5 model [nfc]: Add UserStatusChange.copyWith method
c9b5972 content: Add emoji
property to UserStatusEmoji widget
4feb05b emoji [nfc]: Make emoji picker return the selected emoji, for reuse
and part of the last:
ea1d711 user-status: Add page for setting own user status
@@ -393,13 +395,29 @@ class ZulipMenuItemButton extends StatelessWidget { | |||
foregroundColor: _labelColor(designVariables), | |||
splashFactory: NoSplash.splashFactory, | |||
).copyWith(backgroundColor: _backgroundColor(designVariables)), | |||
overflowAxis: Axis.vertical, |
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. What's the effect of this? I'm finding its doc a bit opaque 🙂
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, the child widget of MenuItemButton
would overflow horizontally, even if it's a simple widget like Text
with a longer string. Setting overflowAxis: Axis.vertical
wraps the child in an Expanded
widget internally, thus preventing the overflow.
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 would "overflow horizontally" mean concretely?
I guess the other part I'm trying to understand is: the argument sounds like it means the button now overflows vertically, instead of horizontally. What does that look like?
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.
By "overflow horizontally", I meant that it shows that striped pattern:
And when overflowAxis: Axis.vertical
is set, it will expand vertically:
Now Text.overflow
will also work:
For more context, here's the upstream PR that added MenuItemButton.overflowAxis
: flutter/flutter#143932
Expanded(child: TextField( | ||
controller: statusTextController, | ||
minLines: 1, | ||
maxLines: 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.
Yeah agreed — the behavior is a bit nonintuitive, and the docs aren't super clear about it.
lib/widgets/set_status.dart
Outdated
final values = [ | ||
(localizations.userStatusBusy, '1f6e0', 'working_on_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.
If the data we have from the server is missing one of these emoji, I think better to leave it out from the suggestions than to try to fall back to a hard-coded name. It's likely in that case that the hard-coded name won't work either.
(continuing from #1701 (comment))
lib/widgets/set_status.dart
Outdated
emojiName: store.allEmojiCandidates() | ||
.firstWhereOrNull((e) => e.emojiCode == emojiCode) |
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, this iterates through the whole list of all known emoji, repeatedly for each of the suggestions we want to offer. Let's avoid doing that. 🙂
Instead, have a prep commit add a little method to EmojiStore to offer the API we need in order to do this efficiently. I think that can look like String? getUnicodeEmojiNameByCode(String emojiCode)
.
controller: statusTextController, | ||
minLines: 1, | ||
maxLines: 2, | ||
maxLength: 60, |
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 interesting. Where does this max length come from?
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.
Note: The limit on the size of the message is 60 characters.
It's in the updateStatus API docs, and I forgot to include it in the previous revisions.😀
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.
Aha, I see. I guess let's have a short comment here with that link. That way it's clear why that's there and has the value it has; and, crucially, it's easy for someone reading in the future to check whether 60 is still the right answer then.
lib/widgets/set_status.dart
Outdated
textCapitalization: TextCapitalization.sentences, | ||
style: TextStyle(fontSize: 19, height: 24 / 19), | ||
decoration: InputDecoration( | ||
counterText: '', // TODO: should we show counter? |
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.
Sounds like a question for #mobile-design
:-)
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.
Cool, thanks. This comment can link to that, then.
lib/widgets/set_status.dart
Outdated
physics: AlwaysScrollableScrollPhysics(), // TODO: necessary? | ||
padding: EdgeInsets.symmetric(vertical: 6), | ||
child: Column(children: [ | ||
for (final status in statusSuggestions(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.
Let's pull this out as a local variable: final suggestions = statusSuggestions(context);
, and then this iterates through that local.
That way the call to this helper method is a bit more visible when reading this build method's logic.
/// https://zulip.com/api/update-status | ||
Future<void> updateStatus(ApiConnection connection, { |
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: add API bindings in a separate commit
lib/api/route/users.dart
Outdated
/// https://zulip.com/api/update-status | ||
Future<void> updateStatus(ApiConnection connection, { | ||
required UserStatus 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.
In the server API, both the text and the emoji are optional — either can be omitted and then only the other one will be set. So UserStatusChange
seems like a closer fit for that.
Status emojis are only shown for self-1:1 and 1:1 conversation items. They're ignored for group conversations as that's what the Web does.
In self-user profile page, this also removes the user status information where shown in regular (non-self-user) profile page, as the newly-added button already shows the same information.
This is useful when we want to show a status emoji that we already know about, instead of relying on `userId` to get the emoji for the user. For example in the next commits, in setting user status page, where a list of status suggestions are shown.
Instead of using the selected emoji deep down the widget tree, simply return it where the emoji picker sheet is opened, to use it for different purposes.
ea1d711
to
60c9baf
Compare
Thanks @gnprice for the review. New revision pushed. |
60c9baf
to
bef6e76
Compare
Support for setting the user status.
Rebased on top of #1702, starting from: fd7320f button: Add ZulipMenuItemButton.subLabel
Figma design: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=5000-52611&t=ZkYDTwsnzsgZJRYr-0
TODOs:
Profile Page
Set Status Page
Screen recordings
Set status - Success
Set.status.-.Success.mov
Set status - Failure
Set.status.-.Error.mov
Fixes: #198