Skip to content

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

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft

Set user status #1701

wants to merge 17 commits into from

Conversation

sm-sayedi
Copy link
Collaborator

@sm-sayedi sm-sayedi commented Jul 10, 2025

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:

  • Tests

Profile Page

No status set Full status set
Only emoji set Only text set

Set Status Page

No status set - Light No status set - Dark
Full status set - Light Full status set - Dark

Screen recordings

Set status - Success

Set.status.-.Success.mov

Set status - Failure

Set.status.-.Error.mov

Fixes: #198

@sm-sayedi sm-sayedi force-pushed the 198-set-user-status branch from 0d852c1 to 242f877 Compare July 10, 2025 01:15
@gnprice
Copy link
Member

gnprice commented Jul 10, 2025

Great!

Let's make this two PRs: one for the remaining portion of #1629 (i.e. same commits as #1699), and one for setting status. That will help keep the reviews more organized and will help us merge the first part earlier.

@sm-sayedi
Copy link
Collaborator Author

Sure, on it.

@sm-sayedi sm-sayedi force-pushed the 198-set-user-status branch 3 times, most recently from 23cdd3a to f67c992 Compare July 10, 2025 02:42
"@setStatus": {
"description": "The status button label in self-user profile page when status is not set."
},
"noStatusText": "Not status text",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
"noStatusText": "Not status text",
"noStatusText": "No status text",

@alya
Copy link
Collaborator

alya commented Jul 10, 2025

The screenshots look good to me, other than the small note above!

@sm-sayedi sm-sayedi marked this pull request as draft July 10, 2025 14:01
@sm-sayedi sm-sayedi force-pushed the 198-set-user-status branch 4 times, most recently from 1405501 to 1ca2ceb Compare July 16, 2025 18:00
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

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

Thanks for 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

Comment on lines 405 to 410
children: [
Text(label,
style: const TextStyle(fontSize: 20, height: 24 / 20)
.merge(weightVariableTextStyle(context, wght: _labelWght()))),
if (subLabel != null)
Text.rich(subLabel!,
Copy link
Member

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

Copy link
Collaborator Author

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.

class _SetStatusButton extends StatelessWidget {
const _SetStatusButton({required this.userId});

final int userId;
Copy link
Member

Choose a reason for hiding this comment

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

This is 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.

@@ -809,6 +809,62 @@
"@userRoleUnknown": {
"description": "Label for UserRole.unknown"
},
"status": "Status",
Copy link
Member

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:

Suggested change
"status": "Status",
"statusButton": "Status",

Comment on lines 816 to 818
"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."
Copy link
Member

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

Comment on lines 836 to 838
"userStatusBusy": "Busy",
"@userStatusBusy": {
"description": "Label for one of the suggested user statuses with status text 'Busy', in setting user status page."
Copy link
Member

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:

Suggested change
"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'."

Comment on lines 40 to 41
class _SetStatusPageState extends State<SetStatusPage> {
List<UserStatus> _statusSuggestions(ZulipLocalizations localizations) => [
Copy link
Member

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

Comment on lines 180 to 184
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,
Copy link
Member

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)

Comment on lines 212 to 214
: icon!;
},
child: Icon(ZulipIcons.smile, size: 24)),
Copy link
Member

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:

Suggested change
: icon!;
},
child: Icon(ZulipIcons.smile, size: 24)),
: const Icon(ZulipIcons.smile, size: 24);
}),

Comment on lines +219 to +230
Expanded(child: TextField(
controller: statusTextController,
minLines: 1,
maxLines: 2,
Copy link
Member

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Member

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.

Copy link
Collaborator Author

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

Copy link
Member

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.

Comment on lines 206 to 209
final emoji = switch(change.emoji) {
OptionNone<StatusEmoji?>() => oldStatus.emoji,
OptionSome<StatusEmoji?>(:var value) => value,
};
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ahh, indeed; thanks!

@sm-sayedi sm-sayedi force-pushed the 198-set-user-status branch 2 times, most recently from 764b856 to ea1d711 Compare July 17, 2025 23:24
@sm-sayedi
Copy link
Collaborator Author

Thanks @gnprice for the review. New revision pushed. Please have a look.

@sm-sayedi sm-sayedi requested a review from gnprice July 17, 2025 23:34
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

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

Thanks for the 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,
Copy link
Member

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 🙂

Copy link
Collaborator Author

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.

https://github.com/flutter/flutter/blob/440713c3b2878048be2661a6705b219c51f72c95/packages/flutter/lib/src/material/menu_anchor.dart#L2821-L2858

Copy link
Member

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?

Copy link
Collaborator Author

@sm-sayedi sm-sayedi Jul 19, 2025

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:
Screenshot 2025-07-19 at 8 53 07 PM
And when overflowAxis: Axis.vertical is set, it will expand vertically:
Screenshot 2025-07-19 at 8 56 41 PM
Now Text.overflow will also work:
Screenshot 2025-07-19 at 11 01 41 PM

For more context, here's the upstream PR that added MenuItemButton.overflowAxis: flutter/flutter#143932

Comment on lines +219 to +230
Expanded(child: TextField(
controller: statusTextController,
minLines: 1,
maxLines: 2,
Copy link
Member

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.

Comment on lines 91 to 92
final values = [
(localizations.userStatusBusy, '1f6e0', 'working_on_it'),
Copy link
Member

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))

Comment on lines 106 to 107
emojiName: store.allEmojiCandidates()
.firstWhereOrNull((e) => e.emojiCode == emojiCode)
Copy link
Member

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

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?

Copy link
Collaborator Author

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

Copy link
Member

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.

textCapitalization: TextCapitalization.sentences,
style: TextStyle(fontSize: 19, height: 24 / 19),
decoration: InputDecoration(
counterText: '', // TODO: should we show counter?
Copy link
Member

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 :-)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Member

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.

physics: AlwaysScrollableScrollPhysics(), // TODO: necessary?
padding: EdgeInsets.symmetric(vertical: 6),
child: Column(children: [
for (final status in statusSuggestions(context))
Copy link
Member

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.

Comment on lines +37 to +39
/// https://zulip.com/api/update-status
Future<void> updateStatus(ApiConnection connection, {
Copy link
Member

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

Comment on lines 37 to 41
/// https://zulip.com/api/update-status
Future<void> updateStatus(ApiConnection connection, {
required UserStatus status,
}) {
Copy link
Member

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.

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.
@sm-sayedi sm-sayedi force-pushed the 198-set-user-status branch from ea1d711 to 60c9baf Compare July 19, 2025 19:46
@sm-sayedi
Copy link
Collaborator Author

Thanks @gnprice for the review. New revision pushed.

@sm-sayedi sm-sayedi requested a review from gnprice July 19, 2025 19:49
@sm-sayedi sm-sayedi force-pushed the 198-set-user-status branch from 60c9baf to bef6e76 Compare July 19, 2025 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Set own user status
3 participants