-
Notifications
You must be signed in to change notification settings - Fork 306
general chat #3: support alternative styling for topic input on edits/focus/blur #1365
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?
Conversation
fb97e82
to
635b377
Compare
8029ffb
to
2efb7f0
Compare
2efb7f0
to
6307134
Compare
lib/widgets/compose_box.dart
Outdated
String hintText = zulipLocalizations.composeBoxTopicHintText; | ||
TextStyle hintStyle = topicTextStyle.copyWith( | ||
color: designVariables.textInput.withFadedAlpha(0.5)); | ||
final defaultTopicDisplayName = _defaultTopicDisplayName(); | ||
if (defaultTopicDisplayName != null) { | ||
if (widget.controller.topicFocusNode.hasFocus) { | ||
// The user is actively interacting with the input. | ||
// Show a long and engaging hint text. | ||
hintText = zulipLocalizations.composeBoxEnterTopicOrSkipHintText( | ||
defaultTopicDisplayName); | ||
} else if (widget.controller.hasChosenTopic.value) { | ||
// The topic has been chosen. Show the default topic display name in | ||
// the input as if the user has entered that when they left it empty. | ||
hintText = defaultTopicDisplayName; | ||
hintStyle = topicTextStyle.copyWith( | ||
// TODO(server-10) simplify | ||
fontStyle: store.zulipFeatureLevel >= 334 ? FontStyle.italic : null); | ||
} | ||
} |
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.
Alternatively, with switch
:
final defaultTopicDisplayName = store.zulipFeatureLevel >= 334
? store.realmEmptyTopicDisplayName : kNoTopicTopic;
final decoration = switch ((
store.realmMandatoryTopics,
widget.controller.hasChosenTopic.value,
widget.controller.topicFocusNode.hasFocus,
)) {
(false, true, _) => InputDecoration(
hintText: defaultTopicDisplayName,
hintStyle: topicTextStyle.copyWith(
fontStyle: store.zulipFeatureLevel >= 334 ? FontStyle.italic : null)),
(false, false, true) => InputDecoration(
hintText: zulipLocalizations.composeBoxEnterTopicOrSkipHintText(
defaultTopicDisplayName),
hintStyle: hintStyle),
(_, _, _) => InputDecoration(
hintText: zulipLocalizations.composeBoxTopicHintText,
hintStyle: hintStyle),
};
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 that switch
would be easier to understand as an if/else chain — I feel like as the reader I'd be basically transforming it into that in my head in order to understand what it's saying.
6307134
to
0697ccb
Compare
0697ccb
to
a50d8ae
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Small comments below.
lib/widgets/compose_box.dart
Outdated
/// Whether the user has made up their mind choosing a topic. | ||
/// | ||
/// Empirically, this should be set to `false` whenever the user focuses on | ||
/// the topic input, and set to `true` whenever the user focuses on the | ||
/// content input. | ||
ValueNotifier<bool> hasChosenTopic = ValueNotifier(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.
How about "Whether the user seems to have made up their mind choosing a topic." or similar? It's impossible to know for sure, right :) they could come back and change it again before sending, or they could send accidentally before making up their mind.
Also I don't understand what "empirically" means here. The sentence sounds like we're an observer making notes from what we've seen of the app's behavior, rather than the ones deciding what the behavior is and causing it. 🙂
lib/widgets/compose_box.dart
Outdated
void _contentFocusChanged() { | ||
setState(() { | ||
// The relevant state lives on widget.controller.contentFocusNode itself. | ||
}); | ||
if (widget.controller.contentFocusNode.hasFocus){ |
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: spacing
lib/widgets/compose_box.dart
Outdated
@@ -576,16 +576,26 @@ class _StreamContentInputState extends State<_StreamContentInput> { | |||
}); | |||
} | |||
|
|||
void _hasChosenTopicChanged() { |
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.
compose: Track user interaction with inputs statefully
What's the behavior change in this commit?
lib/widgets/compose_box.dart
Outdated
(_, _, _) => InputDecoration( | ||
// In all other cases, show a short hint text for less distraction. | ||
hintText: zulipLocalizations.composeBoxTopicHintText, | ||
hintStyle: hintStyle), |
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 assert that cases (_, false, false)
and (_, true, true)
never happen? If those states can happen, they probably won't persist for more than a frame—but that would be a frame where the text would be flickering to something different, and it would look janky.
Also would it be helpful to mention realmMandatoryTopics
in some of these comments?
I wonder if an if/else might be a better fit for this logic than a switch
.
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 (_ false, false)
can represent the initial state when the user open a page without interacting with any of the inputs, so it is definitely possible.
(_, true, true)
seems possible as a really intermediate state, but it is hard to tell without staring at the conditions for some time.
These three cases might be better represented with an enum:
none
when the user hasn't interacted with the inputs;isEditing
when the topic input has focus;hasEdited
when the content input has focus/topic input lost focus previously.
We can tweak the behavior a bit so none
-> isEditing
, none
-> hasEdited
, and isEditing
<-> hasEdited
are the only possible state transitions. This eliminates the possible flickering when more than one transition can happen between frames (namely the case when topic input is unfocused and content input gains focus, which can be represented as the isEditing
-> hasEdited
transition).
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.
Actually, if we implement the states but keeping the current behavior, it still seems to be conceptually subject to the flickering issue when the topic input loses focus and content input gains focus, if the changes to the focus node do not happen in the same frame.
However, it doesn't seem to be the case when testing this:
void _topicFocusChanged() {
print('topic changed: $buildCount');
setState(() {
if (widget.controller.topicFocusNode.hasFocus) {
widget.controller.topicEditStatus.value = ComposeTopicEditStatus.isEditing;
} else if (!widget.controller.contentFocusNode.hasFocus) {
widget.controller.topicEditStatus.value = ComposeTopicEditStatus.notInteracted;
}
});
}
void _contentFocusChanged() {
print('content changed: $buildCount');
setState(() {
if (widget.controller.contentFocusNode.hasFocus) {
widget.controller.topicEditStatus.value = ComposeTopicEditStatus.hasChosen;
}
});
}
Log message when the focus is shifted from topic to content input:
I/flutter (13922): topic changed: 2
I/flutter (13922): content changed: 2
The else if (!widget.controller.contentFocusNode.hasFocus)
check should then be sufficient to prevent the flickering issue.
a50d8ae
to
3043984
Compare
Thanks for the review! I have updated the PR. This revision rewrites the feature using |
9692e2a
to
808d8d7
Compare
Thanks! This seems to have gathered a rebase conflict; could you resolve that please? |
808d8d7
to
dd4ebf0
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.
lib/widgets/compose_box.dart
Outdated
// ignore: dead_null_aware_expression // null topic names soon to be enabled | ||
: '#$streamName > ${hintTopic.displayName ?? store.realmEmptyTopicDisplayName}'; | ||
// null topic names soon to be enabled | ||
: '#$streamName > ' | ||
'${hintTopic.displayName.isEmpty ? store.realmEmptyTopicDisplayName | ||
: hintTopic.displayName}'; |
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.
compose: Unskip test; handle TopicName with empty displayName for now
This logic was introduced in 769cc7df, which relied on the fact that
TopicName.displayName is `null` when the topic is empty. Normally it is
fine to assume that the displayName is non-empty with the server flags,
but in this case the TopicName is constructed by our code.
I'm having trouble understanding the commit message. I'm having trouble with this part:
the fact that TopicName.displayName is
null
when the topic is empty
because that wasn't a fact at 769cc7d, and it's still not a fact in main
. The TopicName.displayName
field's type is String
, not String?
, so the value can't be null.
I'm also having trouble with the second sentence ("Normally…"). Is the empty string a valid value for TopicName.displayName
or not? I suspect it isn't, and our code shouldn't be constructing a TopicName
that way.
It looks like the only thing our code does with that TopicName
is to get its displayName
. How about skipping the trip through TopicName
and just working only with strings? (This would mean the commit-message summary line needs updating.)
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, I notice the code uses the store.realmEmptyTopicDisplayName
getter. That getter has this in its dartdoc:
/// This should only be accessed when FL >= 334 […]
but it takes some nonlocal reasoning to confirm that this access indeed only happens when FL >= 334. How about at least adding an assert
on the zulipFeatureLevel
in that getter, so we can be more confident that we won't regress on that part of its contract?
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 value of TopicName
usually comes from the server, and before we start passing the "empty_topic_names" client capability, displayName
being empty is unexpected. I think it should be fine to work with strings here.
Not sure if we want to switch back to TopicName
once TopicName.displayName
becomes nullable. In UI code, we usually handle empty topic names by checking if TopicName.displayName
is null
, which should also make grepping easier.
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.
TopicName is for requests we make (or might make) to the server, as much as it is for values we get from the server. That's the reason for its apiName
getter:
/// The string this topic is identified by in the Zulip API.
///
/// This should be used in constructing HTTP requests to the server,
/// but rarely for other purposes. See [displayName] and [canonicalize].
and is why it appears in a number of places in the compose-box code.
As of the tip of the branch, I think it's helpful that this hintTopic
local is a TopicName, because that allows the logic for falling back to "general chat" to look the same as the corresponding logic across the rest of the codebase:
: '#$streamName > ${hintTopic.displayName ?? store.realmEmptyTopicDisplayName}';
Looking at references to realmEmptyTopicDisplayName
, one sees that almost all of them are gated behind a condition of some TopicName.displayName
being null, and the majority are in exactly this form with a ??
.
I haven't looked closely at the individual commits in this branch to have a suggestion on the clearest way to break the changes down into steps, though.
@@ -671,12 +671,17 @@ class _StreamContentInputState extends State<_StreamContentInput> { | |||
} | |||
} | |||
|
|||
class _TopicInput extends StatelessWidget { | |||
class _TopicInput extends StatefulWidget { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
compose [nfc]: Convert _TopicInput to a StatefulWidget
We will shortly start adding listeners for this.
What is "this" in the commit message? I think this commit message doesn't need a body, but as-is, it sounds like it's saying we want something to listen to the newly stateful widget itself, and I don't know what that means.
@@ -379,6 +379,13 @@ | |||
"@composeBoxTopicHintText": { | |||
"description": "Hint text for topic input widget in compose box." | |||
}, | |||
"composeBoxEnterTopicOrSkipHintText": "Enter a topic (skip for “{defaultTopicName}”)", |
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.
compose: Change topic input hint text
This is similar to web's behavior. When topics are not mandatory:
- an alternative hint text "Enter a topic (skip for general chat)"
is shown when the topic input has focus;
- an opaque placeholder text (e.g.: "general chat") is shown if
the user skipped to content input;
Because the topic input is always shown in a message list page channel
narrow (assuming permission to send messages), this also adds an intial
state:
- a short hint text, "Topic", is shown if the user hasn't
interacted with topic or content inputs at all.
This only changes the topic input's hint text.
See CZO discussion for design details:
https://chat.zulip.org/#narrow/channel/530-mobile-design/topic/general.20chat.20design.20.23F1297/near/2106736
Commit-message nits:
- 'Enter a topic (skip for “general chat”)'
- spelling of 'initial'
|
||
@override | ||
void didUpdateWidget(covariant _TopicInput oldWidget) { | ||
super.didUpdateWidget(oldWidget); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since widget.controller
keeps a reference to the old store (widget.controller.topic.store
), we should remove that reference when there's a new store; _TopicInputState
should use PerAccountStoreAwareStateMixin
.
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'm not sure what to include in the onNewStore
method. _TopicInputState
doesn't have listeners on widget.controller.topic
. Its only reference to widget.controller.topic
is in the build method, where it passes the widget.controller.topic
instance as-is.
widget.controller
comes from and is owned by _ComposeBoxState
. It does implement a onNewStore
method that swaps out widget.controller.topic.store
with the new store. Maybe that should be sufficient?
lib/widgets/compose_box.dart
Outdated
/// Represent how a user has edited the topic, by tracking their previous | ||
/// interactions with topic and content inputs. | ||
/// | ||
/// State-transition diagram: | ||
/// | ||
/// ``` | ||
/// content input | ||
/// gains focus | ||
/// none ─────────────► hasChosen | ||
/// ▲ │ │ ▲ | ||
/// │ └────────────────┤ │ | ||
/// │ topic input │ │ content input | ||
/// │ gains focus │ │ gains focus | ||
/// │ ▼ │ | ||
/// └────────────────── isEditing | ||
/// topic input loses focus | ||
/// and content input has no focus | ||
/// ``` | ||
/// | ||
/// This state machine offers the following invariants: | ||
/// - When topic input has focus, the edit status must be [isEditing]. | ||
/// - When content input has focus, the edit status must be [hasChosen]. | ||
/// - Otherwise, the edit status can be either of [none] or [hasChosen], | ||
/// but never [isEditing]. | ||
enum ComposeTopicEditStatus { |
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 "how a user has edited the topic" isn't the best description for what this represents. If I use this to try to straightforwardly describe each state, the results are misleading:
- "The user has edited the topic in the 'none' way"—or perhaps "The user has not edited the topic"…but looking at the diagram, that's definitely not what this state means.
Or awkward/confusing:
- "The user has edited the topic in the 'has chosen' way."
- "The user has edited the topic in the 'is editing' way."
Or from another angle: it's equally possible to be in any of the states after any given sequence of edits (if an edit means changing the text in the input).
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 the "topic input gains focus" label meant to apply equally to the hasChosen
-> isEditing
transition? It's positioned so it looks like it's just meant to apply to none
-> isEditing
.
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.
/// - Otherwise, the edit status can be either of [none] or [hasChosen],
/// but never [isEditing].
"Otherwise" means neither the topic input nor the content input has focus, right? Looking at the diagram, I don't see how the state could be hasChosen
in that "otherwise" case.
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. How about maybe ComposeTopicInteractionStatus
? I'm also not happy with the none
name. We can be a bit more literal about what that means. Updated the diagram:
/// ```
/// (default)
/// Topic input │ Content input
/// lost focus. ▼ gained focus.
/// ┌────────────► notEditingNotChosen ────────────┐
/// │ │ │
/// │ Topic input │ │
/// │ gained focus. │ │
/// │ ◄─────────────────────────┘ ▼
/// isEditing ◄───────────────────────────── hasChosen
/// │ Focus moved from ▲
/// │ content to topic. │
/// │ │
/// └──────────────────────────────────────────────┘
/// Focus moved from
/// topic to content.
/// ```
The state can be hasChosen
even after content input has lost focus. Will revise that to make it a bit clearer.
1f01088
to
987e199
Compare
Thanks for the review! Pushed an update. |
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! I've now read through all the commits. Small comments below.
@@ -379,6 +379,13 @@ | |||
"@composeBoxTopicHintText": { | |||
"description": "Hint text for topic input widget in compose box." | |||
}, | |||
"composeBoxEnterTopicOrSkipHintText": "Enter a topic (skip for “{defaultTopicName}”)", |
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.
compose: Change topic input hint text
When you rebase, this commit will need a flutter run
to update the new Ukrainian translation file that exists in main
.
test/widgets/autocomplete_test.dart
Outdated
}) async { | ||
addTearDown(testBinding.reset); | ||
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot( | ||
final account = eg.selfAccount.copyWith(zulipFeatureLevel: zulipFeatureLevel); |
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: Since this is meant to be the self-account, I'd name it selfAccount
instead of just account
.
test/widgets/action_sheet_test.dart
Outdated
testWidgets('show from app bar: resolve/unresolve not offered when topic is empty', (tester) async { | ||
await prepare(); | ||
await prepare(zulipFeatureLevel: 334); |
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.
Would eg.recentZulipFeatureLevel
(bumped so it's actually recent and >=334) be just as effective here and in the other places?
lib/widgets/compose_box.dart
Outdated
/// This state machine offers the following invariants: | ||
/// - When topic input has focus, the status must be [isEditing]. | ||
/// - When content input has focus, the status must be [hasChosen]. | ||
/// - When neither inputs has focus, and content input was the last | ||
/// input among the two to be focused, the status must be [hasChosen]. | ||
/// - Otherwise, the status must be [notEditingNotChosen]. |
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 possible for the content input to lose focus without the focus moving to the topic input, but that transition doesn't have an arrow in the diagram. If we add one (which I think would be an arrow from hasChosen
to itself, in a loop shape), that should help clarify this invariant:
/// - When neither inputs has focus, and content input was the last
/// input among the two to be focused, the status must be [hasChosen].
(also nit: "neither input has focus")
lib/widgets/compose_box.dart
Outdated
final hintStyle = topicTextStyle.copyWith( | ||
color: designVariables.textInput.withFadedAlpha(0.5)); | ||
final defaultTopicDisplayName = store.zulipFeatureLevel >= 334 | ||
? store.realmEmptyTopicDisplayName : kNoTopicTopic; | ||
|
||
final decoration = switch (( | ||
store.realmMandatoryTopics, | ||
widget.controller.topicInteractionStatus.value, | ||
)) { | ||
(false, ComposeTopicInteractionStatus.hasChosen) => InputDecoration( | ||
// The topic has likely been chosen. Since topics are not mandaotry, | ||
// show the default topic display name as if the user has entered that | ||
// when they left the input empty. | ||
hintText: defaultTopicDisplayName, | ||
hintStyle: topicTextStyle.copyWith( | ||
fontStyle: store.zulipFeatureLevel >= 334 ? FontStyle.italic : null)), | ||
|
||
(false, ComposeTopicInteractionStatus.isEditing) => InputDecoration( | ||
// The user is actively interacting with the input. Since topics are | ||
// not mandatory, show a long hint text mentioning that they can be | ||
// left empty. | ||
hintText: zulipLocalizations.composeBoxEnterTopicOrSkipHintText( | ||
defaultTopicDisplayName), | ||
hintStyle: hintStyle), | ||
|
||
(false, ComposeTopicInteractionStatus.notEditingNotChosen) || | ||
(true, _) => InputDecoration( | ||
// Otherwise, show a short hint text for less distraction. | ||
hintText: zulipLocalizations.composeBoxTopicHintText, | ||
hintStyle: hintStyle), | ||
}; |
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.
Here's a draft that takes some complexity out of the switch
and removes some repetition (like hintStyle: hintStyle
). It's more lines of code, but I think easier to read from top to bottom:
final topicTextStyle = TextStyle(
fontSize: 20,
height: 22 / 20,
color: designVariables.textInput.withFadedAlpha(0.9),
).merge(weightVariableTextStyle(context, wght: 600));
// TODO(server-10) simplify away
final emptyTopicsSupported = store.zulipFeatureLevel >= 334;
late final String hintText;
TextStyle hintStyle = topicTextStyle.copyWith(
color: designVariables.textInput.withFadedAlpha(0.5));
if (store.realmMandatoryTopics) {
// Something short and not distracting.
hintText = zulipLocalizations.composeBoxTopicHintText;
} else {
switch (widget.controller.topicInteractionStatus.value) {
case ComposeTopicInteractionStatus.notEditingNotChosen:
// Something short and not distracting.
hintText = zulipLocalizations.composeBoxTopicHintText;
case ComposeTopicInteractionStatus.isEditing:
// The user is actively interacting with the input. Since topics are
// not mandatory, show a long hint text mentioning that they can be
// left empty.
hintText = zulipLocalizations.composeBoxEnterTopicOrSkipHintText(
emptyTopicsSupported
? store.realmEmptyTopicDisplayName
: kNoTopicTopic);
case ComposeTopicInteractionStatus.hasChosen:
// The topic has likely been chosen. Since topics are not mandatory,
// show the default topic display name as if the user has entered that
// when they left the input empty.
if (emptyTopicsSupported) {
hintText = store.realmEmptyTopicDisplayName;
hintStyle = topicTextStyle.copyWith(fontStyle: FontStyle.italic);
} else {
hintText = kNoTopicTopic;
hintStyle = topicTextStyle;
}
}
}
final decoration = InputDecoration(hintText: hintText, hintStyle: hintStyle);
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! This looks good to me (removed late
from late final String hintText
).
test/model/autocomplete_test.dart
Outdated
test('TopicAutocompleteView allow empty topic name on modern servers', () async { | ||
final account = eg.account(user: eg.selfUser, zulipFeatureLevel: 334); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the test is about "modern servers", let's use eg.recentZulipFeatureLevel
instead of hard-coded 334, bumping (in a prep commit) eg.recentZulipFeatureLevel
so that it is actually recent and >=334.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Relatedly, when I write tests, I think I generally omit "on modern servers" in a test description, leaving that as implied, and I write the description (and the test) so that it won't need changes when we eventually clear out the legacy code. Then the legacy-servers test is the thing that gets a marker in its description, like "on legacy servers", and a TODO(server
comment to remove the test.
So here, maybe "TopicAutocompleteView getStreamTopics request"? (And add ..method.equals('GET')
, for completeness in matching that description.)
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. We should be able to skip a lot of changes in the first commit if we bump eg.recentZulipFeatureLevel
.
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 description changes make sense to be, but I thought that we usually don't have TODO(server-x)
comments on tests (since they ought to fail when we drop legacy server support)?
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.
Mm, yeah, reasonable to leave them out on the tests I think.
test/widgets/action_sheet_test.dart
Outdated
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message), | ||
zulipFeatureLevel: 334); |
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.
(same comment about using eg.recentZulipFeatureLevel
)
In particular, this assumes support for empty topics in our app code. To make sure we are aware of possible behavior changes in the app code we test against. Here's a helpful `git grep` command and its result (thanks to "zulipFeatureLevel" being quite a greppable name): ``` $ git grep 'zulipFeatureLevel [<|>]' lib lib/api/model/narrow.dart: final supportsOperatorDm = zulipFeatureLevel >= 177; // TODO(server-7) lib/api/model/narrow.dart: final supportsOperatorWith = zulipFeatureLevel >= 271; // TODO(server-9) lib/model/autocomplete.dart: final isChannelWildcardAvailable = store.zulipFeatureLevel >= 247; // TODO(server-9) lib/model/autocomplete.dart: final isTopicWildcardAvailable = store.zulipFeatureLevel >= 224; // TODO(server-8) lib/model/compose.dart: final isChannelWildcardAvailable = store.zulipFeatureLevel >= 247; // TODO(server-9) lib/model/compose.dart: final isTopicWildcardAvailable = store.zulipFeatureLevel >= 224; // TODO(server-8) lib/model/store.dart: assert(zulipFeatureLevel >= 334); lib/model/store.dart: if (zulipFeatureLevel >= 163) { // TODO(server-7) lib/model/store.dart: bool get isUnsupported => zulipFeatureLevel < kMinSupportedZulipFeatureLevel; lib/widgets/action_sheet.dart: final supportsUnmutingTopics = store.zulipFeatureLevel >= 170; lib/widgets/action_sheet.dart: final supportsFollowingTopics = store.zulipFeatureLevel >= 219; lib/widgets/action_sheet.dart: final markAsUnreadSupported = store.zulipFeatureLevel >= 155; // TODO(server-6) lib/widgets/actions.dart: final useLegacy = store.zulipFeatureLevel < 155; // TODO(server-6) lib/widgets/actions.dart: assert(PerAccountStoreWidget.of(context).zulipFeatureLevel >= 155); // TODO(server-6) lib/widgets/autocomplete.dart: final isChannelWildcardAvailable = store.zulipFeatureLevel >= 247; // TODO(server-9) lib/widgets/compose_box.dart: if (store.zulipFeatureLevel < 334) { lib/widgets/compose_box.dart: if (store.zulipFeatureLevel >= 334) { ``` We can tell that this bump only affects 3 entries from above: ``` lib/model/store.dart: assert(zulipFeatureLevel >= 334); lib/widgets/compose_box.dart: if (store.zulipFeatureLevel < 334) { lib/widgets/compose_box.dart: if (store.zulipFeatureLevel >= 334) { ``` All are related to the FL 334 (general chat) changes. We could have bumped it further to FL 334+, but that's beyond the needs of changes that follow.
This logic was introduced in 769cc7d, which assumed that TopicName.displayName is `null` when the topic is empty. TopicName that came from the server are guaranteed to be non-empty, but here our code can construct an empty TopicName, breaking this assumption. Switch to using plain strings, and go back to constructing TopicName with empty topics once TopicName.displayName becomes nullable.
This is similar to web's behavior. When topics are not mandatory: - an alternative hint text "Enter a topic (skip for “general chat”)" is shown when the topic input has focus; - an opaque placeholder text (e.g.: "general chat") is shown if the user skipped to content input; Because the topic input is always shown in a message list page channel narrow (assuming permission to send messages), this also adds an initial state: - a short hint text, "Topic", is shown if the user hasn't interacted with topic or content inputs at all, or when the user unfocused topic input without moving focus to content input. This only changes the topic input's hint text. See CZO discussion for design details: https://chat.zulip.org/#narrow/channel/530-mobile-design/topic/general.20chat.20design.20.23F1297/near/2106736
Before, the content input shows the "#stream > topic" hint text as long as it has focus, and set the hint text to "#stream" when it loses focus. Now, the content input still shows "#stream > topic" when it gains focus, except that it will keep showing it even after losing focus, until the user moves focus to the topic input.
Signed-off-by: Zixuan James Li <[email protected]>
Look for `allow_empty_topic_name` and `empty_topic_name` under "Feature level 334" in the API Changelog to verify the affected routes: https://zulip.com/api/changelog To keep the API bindings thin, instead of setting `allow_empty_topic_name` for the callers, we require the callers to pass the appropriate values instead. Instead of making this parameter a `bool` that defaults to `false` (and have the bindings remove the field when it's `false`), we type it as `bool?` and only drop it when it is `null`. This is also for making the API binding thin. Fixes: zulip#1250 Signed-off-by: Zixuan James Li <[email protected]>
987e199
to
f9a4f56
Compare
Thanks for the review! Pushed a new update. |
The last PR of the sequence.
Previous ones:
Fixes: #1250
Screenshots: