-
Notifications
You must be signed in to change notification settings - Fork 193
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
Support topic muting/unmuting/following #1041
base: main
Are you sure you want to change the base?
Conversation
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! Quick initial comments from skimming this draft.
@override void onPressed(BuildContext context) async { | ||
Navigator.of(context).pop(); | ||
try { | ||
await updateUserTopic( |
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 seems more specific than the name TopicActionSheetButton
reflects. We'll have other options in the topic action sheet that aren't about visibility policy, like resolve/unresolve.
It makes sense to group all these options under a common base class; it should just get a more specific name.
lib/widgets/action_sheet.dart
Outdated
|
||
final int streamId; | ||
final String topic; | ||
final BuildContext topicParentContext; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This name doesn't feel right conceptually. I'll see about sending a quick PR refactoring the existing message-action-sheet options that might help clarify what this field is about.
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.
Sent #1044.
c21f055
to
be64a22
Compare
0b6cdac
to
11209cc
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! Comments below, and I agree with your comment at #348 (comment) 🙂
I think a design requirement of this feature is to also display mute/following states for each topic. At this point we can borrow that from the legacy mobile app.
lib/widgets/action_sheet.dart
Outdated
|
||
void _showActionSheet( | ||
BuildContext context, { | ||
required List<Widget> optionButtons, |
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.
required List<Widget> optionButtons, | |
required List<ActionSheetMenuItemButton> optionButtons, |
lib/widgets/action_sheet.dart
Outdated
void showMessageActionSheet({required BuildContext context, required Message message}) { | ||
void showMessageActionSheet(BuildContext context, {required Message message}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
action_sheet [nfc]: Make context an unamed parameter
nit: "unnamed" (spelling); also, is there room in the summary line to name the function whose params we're talking about? I guess there's just one other function in the action_sheet subsystem with a named context param, fetchRawContentWithFeedback
. How about:
action_sheet [nfc]: Make showMessageActionSheet's context an unnamed param
lib/api/route/channels.dart
Outdated
// There can be an error when muting a topic that is already muted | ||
// or unmuting one that is already unmuted. Let it throw because we can't | ||
// reliably filter out the error, which doesn't have a specific "code". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear from the comment that this would be the right layer to "filter out" such errors even if we could; do we need the 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.
I agree that this isn't the right layer to handle the error. Since we are not planning to catch that anyway before we drop this fallback, calling this out doesn't really do anything helpful. I will just move this to the commit message.
lib/api/route/channels.dart
Outdated
// There can be an error when muting a topic that is already muted | ||
// or unmuting one that is already unmuted. Let it throw because we can't | ||
// reliably filter out the error, which doesn't have a specific "code". | ||
return connection.post('muteTopic', (_) {}, 'users/me/subscriptions/muted_topics', { |
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.
Should be connection.patch
: https://zulip.com/api/mute-topic
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.
Oh good catch! Looks like we don't even have the connection.patch
method; will add it in a prep commit.
lib/widgets/message_list.dart
Outdated
@@ -305,7 +305,7 @@ class MessageListAppBarTitle extends StatelessWidget { | |||
}) { | |||
// A null [Icon.icon] makes a blank space. | |||
final icon = (stream != null) ? iconDataForStream(stream) : null; | |||
return Row( | |||
final appBar = 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.
appBar
doesn't sound like the right name for something that can get returned from a function named _buildStreamRow
. How about just result
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, the name _buildStreamRow
seems like a hint that it's not really the right place for something that opens a topic action sheet. I think we want a comment in the if (narrow case TopicNarrow
that the added GestureDetector
will go somewhere else or be removed as part of #1039.
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.
While updating this PR to support message list app bar with two rows, I found a better place where this GestureDetector
belongs to:
case TopicNarrow(:var streamId, :var topic):
final store = PerAccountStoreWidget.of(context);
final stream = store.streams[streamId];
return SizedBox(
width: double.infinity,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onLongPress: () => showTopicActionSheet(context,
channelId: streamId, topic: topic),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildStreamRow(context, stream: stream),
_buildTopicRow(context, stream: stream, topic: topic),
])));
to: UserTopicVisibilityPolicy.none); | ||
|
||
Future<void> setupToTopicActionSheet(WidgetTester tester, { | ||
required bool? isChannelMuted, |
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 be more explicit about what null
means for isChannelMuted
. I think it's supposed to mean that the self-user isn't subscribed to the channel (right?), but that isn't clear from test-failure output—
example
$ flutter test test/widgets/action_sheet_test.dart
00:02 +17: UserTopicUpdateButton check expected buttons null UserTopicVisibilityPolicy.none
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown running a test:
Expected: a Finder that:
Actual: means none were found but one was expected
Which: exactly one matching candidate
When the exception was thrown, this was the stack:
#0 check.<anonymous closure> (package:checks/src/checks.dart:85:9)
#1 _TestContext.expect (package:checks/src/checks.dart:708:12)
#2 LegacyMatcher.legacyMatcher (package:legacy_checks/src/matcher_compat.dart:27:13)
#3 FinderBaseChecks.findsOne (package:flutter_checks/src/flutter_checks.dart:187:5)
#4 main.<anonymous closure>.checkButtons (file:///Users/chrisbobbe/dev/zulip-flutter/test/widgets/action_sheet_test.dart:225:39)
#5 main.<anonymous closure>.<anonymous closure>.<anonymous closure> (file:///Users/chrisbobbe/dev/zulip-flutter/test/widgets/action_sheet_test.dart:322:11)
<asynchronous suspension>
#6 testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:189:15)
<asynchronous suspension>
#7 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1027:5)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
The test description was:
null UserTopicVisibilityPolicy.none
════════════════════════════════════════════════════════════════════════════════════════════════════
00:02 +17 -1: UserTopicUpdateButton check expected buttons null UserTopicVisibilityPolicy.none [E]
Test failed. See exception logs above.
The test description was: null UserTopicVisibilityPolicy.none
or from reading the tests, except if you dig into the implementation of setupToTopicActionSheet
. A minimal fix would be to give this function a dartdoc and update the test descriptions where they currently just say $isChannelMuted
.
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 be more explicit about what null means for isChannelMuted. I think it's supposed to mean that the self-user isn't subscribed to the channel (right?), but that isn't clear from test-failure output—
Yeah. Added dartdoc, updated the test description and left the topic names as-is, because the description is what shows up when a test fails.
lib/widgets/action_sheet.dart
Outdated
case UserTopicVisibilityPolicy.unknown: | ||
assert(false); | ||
return; |
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 this case be reached? It seems like we should either:
- Reject unrecognized policy values at the edge as malformed server data, or
- Handle them gracefully in the app, e.g., not handle them by giving up on showing the topic action sheet, which might still have useful buttons like "move topic" or "resolve topic" etc. once we implement those
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. By having this unknown
value in the enum we're already paying most of the cost of gracefully handling the situation where the server introduces a new policy value, so we should handle it here too.
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 do not expect it here because we already semi-explicitly keep it out of our data structure, so it would be a bug to get unknown
here. However, I think it is not obvious that we are doing so because the enum always includes unknown
.
zulip-flutter/lib/model/channel.dart
Lines 216 to 222 in 9e42f26
static bool _warnInvalidVisibilityPolicy(UserTopicVisibilityPolicy visibilityPolicy) { | |
if (visibilityPolicy == UserTopicVisibilityPolicy.unknown) { | |
// Not a value we expect. Keep it out of our data structures. // TODO(log) | |
return true; | |
} | |
return false; | |
} |
zulip-flutter/lib/model/channel.dart
Lines 348 to 350 in 9e42f26
if (_warnInvalidVisibilityPolicy(visibilityPolicy)) { | |
visibilityPolicy = UserTopicVisibilityPolicy.none; | |
} |
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, indeed. In that case it doesn't matter what this line does — though it's still probably cleaner to have it skip this type of button rather than abort the whole function. And it should have a comment saying why it's impossible.
I've noticed in a few other places that it'd be good to have a type for "visibility policy, but only the known/valid ones". Probably that points to removing unknown
from the enum, and using a nullable type where we actually want that value. Out of scope for this PR, though.
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, this will allow us to remove the cases here (where this new assert(false)
originally comes from). It is good to have the type confirm that we process the unknown values at the edge.
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.
Opened #1074 and included TODO comments linked in this PR. (Also removed assert(false)
's under UserTopicVisibilityPolicy.none
because it was incorrectly grouped with UserTopicVisibilityPolicy.unknown
)
lib/widgets/action_sheet.dart
Outdated
// Not subscribed to the channel; there is no user topic change to be made. | ||
return; |
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 comment helps understand what this case is about, but I think I would remove the return
.
Then, we can add chunks of code for more buttons in the future, like copy-link-to-topic, without having to go back and think about this other chunk of code that mostly looks like it's about different buttons.
lib/widgets/action_sheet.dart
Outdated
} | ||
|
||
if (optionButtons.isEmpty) { | ||
assert(!supportsUnmutingTopics); |
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 don't need this assert
; it should be redundant with tests, and it doesn't unlock any opportunities to simplify code that comes after it.
lib/widgets/action_sheet.dart
Outdated
if (optionButtons.isEmpty) { | ||
assert(!supportsUnmutingTopics); | ||
return; | ||
} |
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 we return before showing the action sheet, this long-press handler is a no-op. Screen reader software can't tell it's a no-op, so I think it still presents the element as though it offers a long-press interaction, which isn't really accurate.
So for accessibility, what we'd normally want is to pass null
to the GestureDetector
instead of a no-op function. Alternatively we could design an "empty" appearance for the action sheet.
Those solutions could be finicky or take some time—probably our solution is to eventually just remove the case where the action sheet has no buttons, by implementing a button that's present unconditionally. So for now let's just leave a TODO(a11y)
for that. (I think "Copy link to topic" would be such a button.)
Thanks for the review! Updated the PR. |
21cccab
to
8bd44c2
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! Comments below, including some things I missed last time (oops).
|| visibilityPolicy == UserTopicVisibilityPolicy.muted); | ||
final op = visibilityPolicy == UserTopicVisibilityPolicy.none ? 'remove' | ||
: 'add'; | ||
return connection.patch('muteTopic', (_) {}, 'users/me/subscriptions/muted_topics', { |
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.
api: Add route updateUserTopic
For the legacy case, there can be an error when muting a topic that
is already muted or unmuting one that is already unmuted. Let it
throw because we can't reliably filter out the error, which doesn't
have a specific "code".
Signed-off-by: Zixuan James Li <[email protected]>
Moving this from a code comment to the commit message doesn't make it more convincing 🙂—as I understand it, the reason we don't catch the error is the same reason we don't generally catch API errors in the binding layer: we want the bindings to correspond as closely as possible to the documented API, as a thin wrapper, so they don't hide or mess with things that callers might be interested in.
I see just one catch
in lib/api/route—
/// Convenience function to get a single message from any server.
///
/// This encapsulates a server-feature check.
///
/// Gives null if the server reports that the message doesn't exist.
// TODO(server-5) Simplify this away; just use getMessage.
Future<Message?> getMessageCompat(ApiConnection connection, {
required int messageId,
bool? applyMarkdown,
}) async {
final useLegacyApi = connection.zulipFeatureLevel! < 120;
if (useLegacyApi) {
final response = await getMessages(connection,
narrow: [ApiNarrowMessageId(messageId)],
anchor: NumericAnchor(messageId),
numBefore: 0,
numAfter: 0,
applyMarkdown: applyMarkdown,
// Hard-code this param to `true`, as the new single-message API
// effectively does:
// https://chat.zulip.org/#narrow/stream/378-api-design/topic/.60client_gravatar.60.20in.20.60messages.2F.7Bmessage_id.7D.60/near/1418337
clientGravatar: true,
);
return response.messages.firstOrNull;
} else {
try {
final response = await getMessage(connection,
messageId: messageId,
applyMarkdown: applyMarkdown,
);
return response.message;
} on ZulipApiException catch (e) {
if (e.code == 'BAD_REQUEST') {
// Servers use this code when the message doesn't exist, according to
// the example in the doc.
return null;
}
rethrow;
}
}
}
In that case we have an explicitly thicker wrapper (marked "compat") that encapsulates a difference between legacy and current behavior, so callers don't have to think about the two behaviors separately. (Because of that catch
, callers don't need both a null
check and their own catch
.)
That kind of encapsulation might actually be a fine reason to want to "filter out" these errors in this binding layer, I'm not sure. But if that's the reason, it's not clear from your paragraph about it. Are the errors useful on those legacy servers, or are they basically just an API wart? Let's write an opinion on that first, if we're going to talk about dropping things from the server, instead of just saying we don't drop things because we can't drop them.
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. The story is that the caller of this is not expected to handle the error, mostly because it isn't helpful to the user. If the topic is already muted/unmuted, and that the user wants it to be muted/unmuted, respectively, there really isn't anything that requires actions from the user:
api: Add route updateUserTopic
For the legacy case, there can be an error when muting a topic that
- is already muted or unmuting one that is already unmuted. Let it
- throw because we can't reliably filter out the error, which doesn't
- have a specific "code".
+ is already muted or unmuting one that is already unmuted.
+
+ The callers are not expected to handle such errors because they aren't
+ really actionable.
case UserTopicVisibilityPolicy.unknown: | ||
// TODO(#1074): This should be unreachable as we keep `unknown` out of | ||
// our data structures. | ||
assert(false); |
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.
case UserTopicVisibilityPolicy.unknown: | |
// TODO(#1074): This should be unreachable as we keep `unknown` out of | |
// our data structures. | |
assert(false); | |
case UserTopicVisibilityPolicy.unknown: | |
// TODO(#1074): This should be unreachable as we keep `unknown` out of | |
// our data structures. | |
assert(false); |
At first I read "TODO" and "This should be unreachable" as saying that the case is currently reachable and that we plan to make it unreachable in #1074. In reality, it's currently unreachable (or we expect so, anyway), and we want the case to disappear in #1074. Is that correct?
So I think we'll want
// TODO(#1074) remove this
unknown(apiValue: null);
in the UserTopicVisibilityPolicy
definition, right? Then when we do that TODO, these unknown
cases will fall away naturally as part of that (the analyzer will help us there), so they don't need their own separate TODOs. How about:
case UserTopicVisibilityPolicy.unknown:
// This case is unreachable (or should be) because we keep `unknown` out
// of our data structures. We plan to remove the `unknown` case in #1074.
assert(false);
lib/widgets/action_sheet.dart
Outdated
if (optionButtons.isEmpty) { | ||
// TODO(a11y): While long press has no effect when we return early without | ||
// bringing up the action sheet, the screen readers do not have a way to | ||
// know that. However, we may return this early return after we add a | ||
// always-present button. | ||
return; | ||
} |
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 be clearer about how the bug affects users? How about:
if (optionButtons.isEmpty) {
// TODO(a11y): This case makes a no-op gesture handler; as a consequence,
// we're presenting some UI (to people who use screen-reader software) as
// though it offers a gesture interaction that it doesn't meaningfully
// offer, which is confusing. The solution here is probably to remove this
// is-empty case by having at least one button that's always present,
// such as "copy link to topic".
return;
}
lib/widgets/action_sheet.dart
Outdated
case (_, UserTopicVisibilityPolicy.none): | ||
return ''; |
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.
A label
method returning the empty string looks like a code smell to me; that can't be a helpful label for anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It turned out that this would be better as a separate case and keep the assert(false)
. Added an explanation below:
case (_, UserTopicVisibilityPolicy.none):
// This is unexpected because `UserTopicVisibilityPolicy.muted` and
// `UserTopicVisibilityPolicy.followed` (handled in separate `case`'s)
// are the only expected `currentVisibilityPolicy`
// when `newVisibilityPolicy` is `UserTopicVisibilityPolicy.none`.
assert(false);
return '';
final mute = button(to: UserTopicVisibilityPolicy.muted); | ||
final unmute = button(from: UserTopicVisibilityPolicy.muted, | ||
to: UserTopicVisibilityPolicy.none); | ||
final unmuteInMutedChannel = button(to: UserTopicVisibilityPolicy.unmuted); | ||
final follow = button(to: UserTopicVisibilityPolicy.followed); | ||
final unfollow = button(from: UserTopicVisibilityPolicy.followed, | ||
to: UserTopicVisibilityPolicy.none); |
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 approach in this PR—which I understand follows zulip-mobile—doesn't support all the state transitions that web supports. Maybe we can open a followup issue for the unsupported ones?
Here's what web does:
In a muted channel, the menu for a topic lets you choose one of four options: "Mute", "Default", "Unmute", "Follow":
In an unmuted channel, the menu for a topic lets you choose one of three options: "Mute", "Default", "Follow", unless "Unmute" is currently selected; if so, that option is shown too until you select something different. (You can choose "Unmute" for a topic in a muted channel, and that choice persists when you unmute the channel.)
So that should fully describe which state transitions web supports. Among those, here are the transitions that this zulip-flutter PR doesn't yet support:
In a muted channel:
- You can't go from "Mute" to "Default" (unless you go through "Follow").
- You can't go from "Default" to "Mute" (unless you go through "Unmute" or "Follow").
- You can't go from "Unmute" to "Default" (unless you go through "Follow").
(In a muted channel, the distinction between "Mute" and "Default" matters because it controls whether we show the topic in the whole-stream message list.)
- You can't go from "Follow" to "Unmute" (unless you go through "Mute" or "Default"). ("Follow" and "Unmute" cause different behavior with notifications.)
In an unmuted channel:
- You can't go from "Unmute" to "Default" (unless you go through "Mute" or "Follow"). (I'm not sure if "Unmute" and "Default" cause different behavior in an unmuted channel, I think maybe they don't.)
None of those "unless you go through" workarounds are intuitive. Even if you know about them, you might get a surprise when you try going through "Mute" and the conversation disappears from the UI and you can't find it to choose the state you wanted in the first place.
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.
Web's radio-buttons approach seems like a fine solution to me, but again we might postpone that; we should make an issue if it's planned though. @gnprice, what do you think?
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, this PR follows the state transitions in the mobile app very closely, including the UX. If we are going with a newer radio-button design, it will probably save some time to get this right with a Figma redesign.
See discussion here.
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.
Oh I see, that discussion is helpful context! I missed it in my review because I didn't see it linked from this PR or the issue. 🙂 I agree with the outcome there:
Yeah, some version of [web's radio-buttons design] would probably be good to put in mobile's action sheet too.
Definitely post-launch, though.
Would you add the link to that discussion on #348, and file an issue for that post-launch task? It can link to my comment here to memoize the work of figuring out what state transitions we're missing. (I used a lot of post-it notes, haha.)
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.
Sure! Opened #1078 and added the link.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
assets/icons/ZulipIcons.ttf
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
icons: Take mute/unmute/following icons from the web app
Hmm yeah, looks like the Figma doesn't have icons for these yet. Following web seems fine, but let's note in the commit message that we checked https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=544-22131&node-type=canvas&m=dev and didn't find corresponding icons there.
@@ -59,6 +59,22 @@ | |||
"@permissionsDeniedReadExternalStorage": { | |||
"description": "Message for dialog asking the user to grant permissions for external storage read access." | |||
}, | |||
"actionSheetOptionMuteTopic": "Mute topic", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
action_sheet: Support muting/unmuting/following topics
Bump on #1041 (review) :
Thanks! Comments below, and I agree with your comment at #348 (comment) 🙂
I think a design requirement of this feature is to also display mute/following states for each topic. At this point we can borrow that from the legacy mobile app.
The Fixes:
line doesn't belong on this commit until we do that 🙂 or make it not part of #348.
Signed-off-by: Zixuan James Li <[email protected]>
Signed-off-by: Zixuan James Li <[email protected]>
Signed-off-by: Zixuan James Li <[email protected]>
Signed-off-by: Zixuan James Li <[email protected]>
…param Signed-off-by: Zixuan James Li <[email protected]>
The tests, ApiConnection.{post,patch,delete}, are mostly similar to each other, because the majority of them are testing that the params are parsed into the body with the same content type. If we find the need to update these test cases with new examples, it will be ripe to refactor them with a helper. Until then, just duplicate them for simplicity. Signed-off-by: Zixuan James Li <[email protected]>
For the legacy case, there can be an error when muting a topic that is already muted or unmuting one that is already unmuted. The callers are not expected to handle such errors because they aren't really actionable. Signed-off-by: Zixuan James Li <[email protected]>
Renamed "mute-new.svg", "unmute-new.svg" to "mute.svg" and "unmute.svg", respectively. These are taken from the web app because we checked https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=544-22131&node-type=canvas&m=dev and the Figma doesn't have the corresponding icons at the time this is implemented. See: https://github.com/zulip/zulip/tree/da4443f392cc8aa9e6879d905cb1ccd50b66127b/web/shared/icons Signed-off-by: Zixuan James Li <[email protected]>
Currently, we don't have buttons, like "resolve topic", other than the ones added here. The switch statements follow the layout of the legacy app implementation. See also: https://github.com/zulip/zulip-mobile/blob/715d60a5e87fe37032bce58bd72edb99208e15be/src/action-sheets/index.js#L656-L753 Signed-off-by: Zixuan James Li <[email protected]>
I have updated the PR to implement (sorry about the debug banner on the screenshots!) |
25f5f64
to
d38bc57
Compare
This mostly follows the legacy mobile app. Notably, this changes the title text to use normal font weight (wght=400), and breaks the text into two rows for topic narrow. This will eventually be superseded by zulip#1039, so we should keep the implementation as simple as possible for now. There are some differences from the old design: The legacy mobile uses different colors for the title text, depending on the color of the channel, to make the text more visible. We currently don't have that, so the text just uses the ambient color. The original design also displays the 'mute' icon when the channel is muted: https://github.com/zulip/zulip-mobile/blob/a115df1f71c9dc31e9b41060a8d57b51c017d786/src/streams/StreamIcon.js#L20-L29 In the Flutter app, however, only the privacy level related icons are displayed (e.g.: web public, invite only). We continue to leave out the 'mute' icon in this implementation. This can change after we have a concrete redesign plan. This implementation also shows the corresponding icons for 'muted' and 'unmuted' topics; previously, only the icon for 'follow' was displayed. And we continue using the existing icons in the Flutter app, without trying to match with the exact ones in the old design. References: https://github.com/zulip/zulip-mobile/blob/a115df1f71c9dc31e9b41060a8d57b51c017d786/src/title/TitleStream.js#L113-L141 https://github.com/zulip/zulip-mobile/blob/a115df1f71c9dc31e9b41060a8d57b51c017d786/src/styles/navStyles.js#L5-L18 Fixes: zulip#348 Signed-off-by: Zixuan James Li <[email protected]>
Screenshots
Reference zulip-mobile implementation: https://github.com/zulip/zulip-mobile/blob/715d60a5e87fe37032bce58bd72edb99208e15be/src/action-sheets/index.js#L656-L753
Fixes: #348