Skip to content

Conversation

@chrisbobbe
Copy link
Collaborator

@chrisbobbe chrisbobbe commented Jun 26, 2025

Still TODO: tests.

Some known differences from the Figma, which won't be hard to fix but require upstream changes, so not totally trivial:

  • When the switch is on:

    • The switch should be 28px by 48px, not 36px by 56px.
    • The "thumb" (white circle with checkmark) should have radius 10px, not 12px.
  • When the switch is off:

    • The switch should be 26px by 46px, not 34px by 54px.
    • The "thumb" should have radius 7px, not 8px.

(edit: screenshots of what it would look like with these differences resolved: #1631 (comment) , and I filed #1636 for these fixes)

Fixes: #1578

@chrisbobbe chrisbobbe added maintainer review PR ready for review by Zulip maintainers launch feedback Things users specifically asked for upon launch labels Jun 26, 2025
@chrisbobbe
Copy link
Collaborator Author

chrisbobbe commented Jun 26, 2025

Self-profile

Light Dark
image image
image image

Non-self profile (no change):

Light Dark
image image
@chrisbobbe
Copy link
Collaborator Author

chrisbobbe commented Jun 26, 2025

Here's how it could look after some upstream work (flutter/flutter#131478). This removes the differences from the Figma that I mentioned in the PR description:

Screenshots
Before After
image image
image image
@chrisbobbe
Copy link
Collaborator Author

And I did go ahead and download GIPHY CAPTURE to record the animation and drag behavior 🙂:

(This is the PR as-is, not with the upstream work from my previous comment)

The switch responds to taps The button responds to taps The "thumb" responds to dragging
Jun-26-2025 12-28-36 Jun-26-2025 12-29-31 Jun-26-2025 12-30-36
@chrisbobbe
Copy link
Collaborator Author

Error feedback when failing to turn mode on / off, and tapping the "Details" button in the snackbar.

The "Details" dialog isn't user-friendly. We could omit the "Details" button, but it might be helpful for debugging unexplained failures.

On Off
Jun-26-2025 12-33-56 Jun-26-2025 12-36-06
@alya
Copy link
Collaborator

alya commented Jun 26, 2025

Yeah, I guess depends on how much we expect users to actually send us those details. If not, probably not super helpful to show them, unless we can translate them to something more readable (e.g., just say "Network error" or "Couldn't connect to server." or something).

@timabbott
Copy link
Member

I think Sentry is for debugging, and we want some generic network error as the thing to present in the UI.

@chrisbobbe
Copy link
Collaborator Author

Makes sense; here's with the "Details" button removed:

Light/On Dark/Off
image image
@chrisbobbe
Copy link
Collaborator Author

(And I'll add a TODO(#741) for interpreting the error)

Copy link
Member

@rajveermalviya rajveermalviya 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 working on this @chrisbobbe! Works great in local testing.

Couple small comments, and I see that the some tests are TODO (e.g. PerAccountSettingBuilder), otherwise LGTM.

/// so until the spam-taps are finished, the switch responds only to the taps,
/// not to the event stream.
/// Then when the taps stop, it settles to the value from the latest event.)
static final Duration localEchoMininum = Duration(seconds: 1);
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
static final Duration localEchoMininum = Duration(seconds: 1);
static final Duration localEchoMinimum = Duration(seconds: 1);
Comment on lines 178 to 189
});

}
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
});
}
});
}
@chrisbobbe chrisbobbe force-pushed the pr-invisible-mode branch from b67ac62 to a71d75d Compare July 7, 2025 05:57
@chrisbobbe
Copy link
Collaborator Author

Thanks for the review! Revision pushed, with some tests added.

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! Generally this looks great; comments below.

I haven't yet read all the logic in PerAccountSettingBuilder (pending a couple of questions / requests for dartdoc below), or the tests in the last commit. I've read everything else, so including everything before the last commit:
57da943 api: Require InitialSnapshot.userSettings, present since Zulip Server 5
ebdb035 api: Add InitialSnapshot.userSettings.presenceEnabled
f373c94 api: Add updateSettings
4f114af button [nfc]: Extract MenuButton{,sShape} out to here, from action sheet
847b326 button: Fix an inconsistency with Figma in MenuButton
3e496ec button: Give MenuButton a beforeIcon param, to use for a switch
f5598f5 button [nfc]: Factor MenuButton's vertical padding as around just the text
428e97e button: Implement "toggle" component from Figma, for "Invisible mode"
ed544a7 profile test [nfc]: Rename group about custom fields; pull out some tests

and part of that last commit:
a71d75d profile: Add toggle switch for "Invisible mode", with local echo

part 'settings.g.dart';

/// https://zulip.com/api/update-settings
Future<UpdateSettingsResult> updateSettings(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:

api: Add updateSettings 

add the word "route" at the end — it wasn't clear to me before reading the code what sort of a thing "updateSettings" was

// (1) add it to the [UserSettingName] enum
// (2) then re-run the command to refresh the .g.dart files
// (3) handle the event that signals an update to the setting
// (4) add the setting to the updateSettings binding
Copy link
Member

Choose a reason for hiding this comment

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

nit: similarly,

Suggested change
// (4) add the setting to the updateSettings binding
// (4) add the setting to the [updateSettings] route binding
Comment on lines 30 to 33
class UpdateSettingsResult {
UpdateSettingsResult();

factory UpdateSettingsResult.fromJson(Map<String, dynamic> json) =>
Copy link
Member

Choose a reason for hiding this comment

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

Since there's no content in this result, we can skip it — just have the endpoint function return Future<void>. Grep for "Future" in this directory to see other examples.

Comment on lines 16 to 17
final newSettings = <UserSettingName, Object?>{};
final expectedBodyFields = <String, String>{};

for (final name in UserSettingName.values) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: join declarations with their initializers

Suggested change
final newSettings = <UserSettingName, Object?>{};
final expectedBodyFields = <String, String>{};
for (final name in UserSettingName.values) {
final newSettings = <UserSettingName, Object?>{};
final expectedBodyFields = <String, String>{};
for (final name in UserSettingName.values) {
Comment on lines +19 to +22
for (final name in UserSettingName.values) {
switch (name) {
case UserSettingName.twentyFourHourTime:
newSettings[name] = true;
expectedBodyFields['twenty_four_hour_time'] = 'true';
case UserSettingName.displayEmojiReactionUsers:
Copy link
Member

Choose a reason for hiding this comment

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

Interesting, so this ensures that future additions to the enum get tested on this endpoint.

}
}

class _LocalEchoNotifier<T> extends ValueNotifier<_LocalEchoValue<T>?> {
Copy link
Member

Choose a reason for hiding this comment

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

I think this would benefit from a short dartdoc: particularly on what it means to have a value here, or for [value] to be null.

});
}

Future<void> stop() async {
Copy link
Member

Choose a reason for hiding this comment

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

Also good to document would be what this Future represents — what does it mean to await it?

Comment on lines 90 to 92
if (!store.realmPresenceDisabled && userId == store.selfUserId) ...[
const SizedBox(height: 16),
MenuButtonsShape(buttons: [
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 whole MenuButtonsShape subtree out to its own widget — e.g. call it _InvisibleModeToggle — so that this just has the condition, the spacers, and a reference to that widget. I think that'll help keep this more organized to read.

(The same might also be good for some of these other items; but this is more complex than any of the existing items here. And conversely it doesn't share interactions like oh they're all centered, they're all using _TextStyles.primaryFieldText, which are advantages to having the others inline next to each other.)

Comment on lines 96 to 98
findValueInStore: (store) => !store.userSettings.presenceEnabled,
sendValueToServer: (value) => updateSettings(store.connection,
newSettings: {UserSettingName.presenceEnabled: !value}),
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 that findValueInStore gets passed the store to use, but then sendValueToServer ends up using the store it got from the parent widget.

(This is fine to ship, doesn't need to be refactored, but feels like it suggests the API could ideally be adjusted)

Comment on lines 339 to 341
children: [
if (beforeIcon != null) beforeIcon!,
if (icon != null) Icon(icon, color: designVariables.contextMenuItemText),
Copy link
Member

Choose a reason for hiding this comment

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

Interesting that this provides for both an icon and some other widget before it. Are there examples of that in the Figma designs?

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, indeed 😄

Copy link
Member

Choose a reason for hiding this comment

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

The name "before icon" feels like it doesn't quite get to the point — I feel like I have to think a bit to work through what it's supposed to end up meaning. And in the current example, as the dartdoc says:

 /// An element to go before [icon], or in its place if it's null.  ///  /// E.g. a [Toggle].  final Widget? beforeIcon;

this "before icon" widget appears even though there is no icon.

How about a name like "control"? It's the visible control in the button, when the button has such a thing.

Or perhaps with less generalization: "toggle"? That's the name it has in that Figma subtree: the children are "label", "toggle", "icon". If we later have a menu button somewhere that has some other kind of widget in here other than a toggle, I'm not sure we'll necessarily want to treat it the same way.

@chrisbobbe chrisbobbe force-pushed the pr-invisible-mode branch from a71d75d to f7ebd5a Compare July 7, 2025 23:16
@chrisbobbe
Copy link
Collaborator Author

Thanks for the review! Revision pushed.

@chrisbobbe chrisbobbe force-pushed the pr-invisible-mode branch from f7ebd5a to 62029e3 Compare July 7, 2025 23:24
@chrisbobbe
Copy link
Collaborator Author

And pushed again, with the "before icon" / "toggle" change suggestion #1631 (comment)

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 revisions! Just read the changes and re-scanned my comments above; all looks good except a few small comments below.

Next I'll read the remaining parts of the last/main commit, as mentioned at #1631 (review).

check(because: 'find error icon', find.byIcon(Icons.error).evaluate()).isNotEmpty();
});

testWidgets('page builds; dm links to correct narrow', (tester) async {
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Most of the tests in this ProfilePage group are about custom profile fields. Pull out the two that aren't, and rename the group. 

s/two/handful/ or some other update 🙂 (after #1631 (comment))

/// But the local echo minimizes flickering (see [localEchoMinimum])
/// while ensuring that the real in-store value is shown soon after the
/// user finishes interacting, whether the request(s) succeeded or failed.
typedef PerAccountSettingBuilderFn<T> =
Copy link
Member

Choose a reason for hiding this comment

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

nit: rename this to match the class; rename the file, too

///
/// If extending a local-echo period, replaces the old local-echo value.
///
/// This may be called any time. API requests are not debounced,
Copy link
Member

Choose a reason for hiding this comment

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

"this" should be [handleRequestNewValue], right? Not the function whose type this is documenting.

Comment on lines 193 to 198
/// The session will be stopped either immediately or
/// [RemoteSettingBuilder.localEchoMinimum] after the last [startOrExtend] call,
/// whichever is later.
///
/// The returned [Future] resolves when the session is stopped.
Future<void> stop() async {
Copy link
Member

Choose a reason for hiding this comment

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

Very helpful, thanks.

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.

OK, and finished reading the rest. Generally this all looks good — comments below, most notably some checks missing in the tests.

Comment on lines 147 to 150
final value = switch (_notifier.value) {
OptionSome(:final value) => value,
OptionNone() => widget.findValueInStore(store),
};
Copy link
Member

Choose a reason for hiding this comment

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

I guess this would be a use case for adding an orElse:

Suggested change
final value = switch (_notifier.value) {
OptionSome(:final value) => value,
OptionNone() => widget.findValueInStore(store),
};
final value = _notifier.value.orElse(() => widget.findValueInStore(store));

(taking API inspiration from Rust's Option, per the Option dartdoc)

This version is fine, though.

Comment on lines 144 to 145
// (Create the PerAccountStoreWidget dependency unconditionally.)
final store = PerAccountStoreWidget.of(context);
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason we actively want to do this? That's what I take the comment to mean, but then I'm curious what the reason is 🙂

(I think it's harmless to do so — in fact we'll always have the dependency anyway before even reaching the build method, because we use it from didChangeDependencies)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think my thinking was:

  • didChangeDependencies needs to be called when local-echo isn't active, which means the dependency needs to exist when local-echo isn't active
  • The (only) way to create a dependency is by calling PerAccountStoreWidget.of in the build method; it doesn't count if we call it in didChangeDependencies
  • The dependency might disappear if a build call doesn't call PerAccountStoreWidget.of (I remember being pretty sure about this but can't right away find where I read about it)

I think I was indeed wrong on the second point though 🙂—

 /// This method should not be called from widget constructors or from  /// [State.initState] methods, because those methods would not get called  /// again if the inherited value were to change. To ensure that the widget  /// correctly updates itself when the inherited value changes, only call this  /// (directly or indirectly) from build methods, layout and paint callbacks,  /// or from [State.didChangeDependencies] (which is called immediately after  /// [State.initState]).

so yeah, the comment isn't guarding against a potential bug, and I'll remove it.

Copy link
Member

Choose a reason for hiding this comment

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

  • The dependency might disappear if a build call doesn't call PerAccountStoreWidget.of (I remember being pretty sure about this but can't right away find where I read about it)

There've been occasional requests upstream for this behavior. But the current behavior is that once an element has a dependency, it keeps it throughout its lifetime; there's no attempt to clear out dependencies.

See background here:
flutter/flutter#12992 (comment)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good to know, thanks!

if (_disposed) return;
// Don't call _notifier.stop(). We do that when the event arrives,
// causing the in-store value to change (see didChangeDependencies).
} catch (e) {
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 potentially a real error, so let's leave a TODO(log) comment:

Suggested change
} catch (e) {
} catch (e) { // TODO(log)
Comment on lines 148 to 151
}


class _ProfileErrorPage extends StatelessWidget {
Copy link
Member

Choose a reason for hiding this comment

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

nit: double blank line

Comment on lines 164 to 168
factory _LocalEchoNotifier() {
return _LocalEchoNotifier._(OptionNone());
}

_LocalEchoNotifier._(super.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: can simplify:

Suggested change
factory _LocalEchoNotifier() {
return _LocalEchoNotifier._(OptionNone());
}
_LocalEchoNotifier._(super.value);
_LocalEchoNotifier() : super(OptionNone());
Comment on lines 209 to 213
void dispose() {
_lowerBoundTimer?.cancel();
_upperBoundTimer?.cancel();
_disposed = true;
super.dispose();
Copy link
Member

Choose a reason for hiding this comment

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

Also _lowerBoundCompleter?.complete()?

Otherwise, I think if there's an outstanding stop call awaiting the completer's future, then that will hang around forever because the future never completes. Which might then keep a _handleRequestNewValue call hanging around, and therefore the state object and widget object, and so on.

Comment on lines 398 to 399
final toggle = tester.widget<Toggle>(findToggle);
check(toggle.value).equals(expected);
Copy link
Member

Choose a reason for hiding this comment

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

nit: this is equivalent, right?

Suggested change
final toggle = tester.widget<Toggle>(findToggle);
check(toggle.value).equals(expected);
check(getValue(tester)).equals(expected);
Comment on lines 603 to 608
// Events will be coming in, but those don't control the switch;
// only the user interaction does, until there have been no interactions
// for [PerAccountSettingBuilder.localEchoMinimum].
await doSpamTap();
await tester.pump(Duration(milliseconds: 90));
await doSpamTap();
Copy link
Member

Choose a reason for hiding this comment

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

It doesn't seem like these actually check that events don't control the switch.

In fact I tried deleting the key part of stop:

 Future<void> stop() async { - if (_lowerBoundCompleter != null) { - await _lowerBoundCompleter!.future; - if (_disposed) return; - } value = OptionNone(); }

and it seems tests still pass.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Eep, indeed! Will fix.

Comment on lines 570 to 574
checkAppearsActive(tester, true);
checkRequest(true);

// After [PerAccountSettingBuilder.localEchoMinimum], changes back
await tester.pump(RemoteSettingBuilder.localEchoMinimum);
Copy link
Member

Choose a reason for hiding this comment

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

Similarly, this should gain a check that the appearance doesn't change before that minimum (when the request failure comes back).

@chrisbobbe chrisbobbe force-pushed the pr-invisible-mode branch from 62029e3 to d34e419 Compare July 8, 2025 01:20
@chrisbobbe
Copy link
Collaborator Author

Thanks! Revision pushed.

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! Just small comments now.

Comment on lines 23 to 27
/// The value contained in this option, if any;
/// else the value returned by [fn].
T orElse(T Function() fn);
Copy link
Member

Choose a reason for hiding this comment

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

nit: specify the other key piece of this method's semantics:

Suggested change
/// The value contained in this option, if any;
/// else the value returned by [fn].
T orElse(T Function() fn);
/// The value contained in this option, if any;
/// else the value returned by [fn].
///
/// [fn] is called only if its return value is needed.
T orElse(T Function() fn);
Copy link
Member

Choose a reason for hiding this comment

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

Oh and I guess test that too, e.g. by having the callback throw.

Comment on lines +573 to +576
// The appearance doesn't change as soon as the request errors,
// if it errored quickly…
await tester.pump(requestDuration);
checkAppearsActive(tester, true);
Copy link
Member

Choose a reason for hiding this comment

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

We've seen elsewhere (those route transitions) that sometimes one needs to call tester.pump for a duration that's epsilon longer than nominally should be necessary in order to get something to happen.

So when a test is, like here, checking that something doesn't happen, it's not entirely convincing as a reader unless it waits for a bit longer than the nominal time, for good measure.

Could say requestDuration + Duration(milliseconds: 1). Or could say like Duration(milliseconds: 101), or Duration(milliseconds: 200), and let the reader compare to the 100ms constant above.

Comment on lines 633 to 644
await doSpamTap(expectedCurrentValue: false);

await tester.pump(RemoteSettingBuilder.localEchoMinimum - Duration(milliseconds: 1));
check(getValue(tester)).equals(true);
Copy link
Member

Choose a reason for hiding this comment

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

nit: I think a bit more explicitness would help understanding here:

Suggested change
await doSpamTap(expectedCurrentValue: false);
await tester.pump(RemoteSettingBuilder.localEchoMinimum - Duration(milliseconds: 1));
check(getValue(tester)).equals(true);
await doSpamTap(expectedCurrentValue: false);
check(getValue(tester)).equals(true);
await tester.pump(RemoteSettingBuilder.localEchoMinimum - Duration(milliseconds: 1));
check(getValue(tester)).equals(true);

(That is the right expectation, 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.

Yep!

@gnprice gnprice added the integration review Added by maintainers when PR may be ready for integration label Jul 8, 2025
@alya
Copy link
Collaborator

alya commented Jul 8, 2025

I left a design comment on CZO: #mobile-design > presence: invisible mode @ 💬

@gnprice
Copy link
Member

gnprice commented Jul 9, 2025

I'm planning to include this PR in a v30.0.261 release tomorrow. It'd be great to have it merged first, and to have that design tweak made.

@chrisbobbe chrisbobbe force-pushed the pr-invisible-mode branch from d34e419 to ada007e Compare July 9, 2025 04:56
@chrisbobbe
Copy link
Collaborator Author

chrisbobbe commented Jul 9, 2025

Thanks for the reviews and design guidance! Revision pushed, with three new commits:

60a3e2b button: Fix MenuButton's icon color
84181e8 button [nfc]: Add "list button" variant, factored into MenuButton widget
36d9735 profile: Change "Invisible mode" toggle to more neutral "list button" style

The first of those fixes a bug in main where we had the wrong icon color in action sheets, only in light mode:

Before After
image image

New screenshots:

Switch on Switch off
image image
image image
@chrisbobbe chrisbobbe force-pushed the pr-invisible-mode branch 2 times, most recently from 36d9735 to 5cece3a Compare July 9, 2025 18:15
@chrisbobbe
Copy link
Collaborator Author

Thanks both! Revision pushed.

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! Looks good; one nit and a small question.

if (ancestor != null) return true;
throw FlutterError.fromParts([
ErrorSummary('No MenuButtonsShape ancestor found.'),
ErrorDescription('MenuButton widgets require a MenuButtonsShape ancestor.'),
Copy link
Member

Choose a reason for hiding this comment

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

nit: one stray reference here to the MenuButton name

Comment on lines +157 to 160
contextMenuItemIcon: const Color(0xff4f42c9),
contextMenuItemLabel: const Color(0xff242631),
contextMenuItemMeta: const Color(0xff626573),
contextMenuItemText: const Color(0xff381da7),
Copy link
Member

Choose a reason for hiding this comment

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

button: Fix MenuButton's icon color Oops, this wasn't following the Figma. Fixed. 

Hmm good catch.

I guess this affects the icons in the action sheets? Is that the existing place this affects?

Looks like the difference is pretty subtle, thankfully. Here's a quick preview, from my IDE showing color swatches in the left gutter next to the source code:
image

Bringing those right next to each other (new above, old below):
image

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep yep, and I posted a screenshot of the change in the action sheet: #1631 (comment)

Copy link
Member

Choose a reason for hiding this comment

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

Ah indeed, thanks :-)

chrisbobbe and others added 13 commits July 9, 2025 13:04
Most of the tests in this ProfilePage group are about custom profile fields. Pull out some that aren't, and rename the group.
The PerAccountSettingBuilder widget should be helpful for plenty of other things too, like channel settings. I think we'll get an updated design for the profile page soon, but pending that, this is our current thinking for how "Invisible mode" should look: https://chat.zulip.org/#narrow/channel/530-mobile-design/topic/presence.3A.20invisible.20mode/near/2204336 Fixes: zulip#1578
Oops, this wasn't following the Figma. Fixed.
And rename MenuButton to ZulipMenuItemButton, since it's no longer only about the Figma's "menu button" component.
@chrisbobbe chrisbobbe force-pushed the pr-invisible-mode branch from 5cece3a to 9b5a637 Compare July 9, 2025 20:06
@chrisbobbe
Copy link
Collaborator Author

Thanks! Revision pushed.

@gnprice gnprice merged commit 9b5a637 into zulip:main Jul 9, 2025
1 check passed
@gnprice
Copy link
Member

gnprice commented Jul 9, 2025

Thanks! Looks good; merging.

@gnprice gnprice mentioned this pull request Jul 9, 2025
laurynmm added a commit to laurynmm/zulip that referenced this pull request Jul 11, 2025
timabbott pushed a commit to zulip/zulip that referenced this pull request Jul 11, 2025
timabbott pushed a commit to zulip/zulip that referenced this pull request Jul 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration review Added by maintainers when PR may be ready for integration launch feedback Things users specifically asked for upon launch maintainer review PR ready for review by Zulip maintainers

5 participants