diff --git a/lib/api/model/model.dart b/lib/api/model/model.dart index a2cff09b54..b733a1e490 100644 --- a/lib/api/model/model.dart +++ b/lib/api/model/model.dart @@ -659,8 +659,20 @@ enum MessageFlag { // TODO(#1250): Migrate all implicit uses as String; remove "implements String". extension type const TopicName(String _value) implements String { /// 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]. String get apiName => _value; + /// The string this topic is displayed as to the user in our UI. + /// + /// At the moment this always equals [apiName]. + /// In the future this will become null for the "general chat" topic (#1250), + /// so that UI code can identify when it needs to represent the topic + /// specially in the way prescribed for "general chat". + // TODO(#1250) carry out that plan + String get displayName => _value; + /// The key to use for "same topic as" comparisons. String canonicalize() => apiName.toLowerCase(); diff --git a/lib/model/autocomplete.dart b/lib/model/autocomplete.dart index c0093b3357..cd651bdc65 100644 --- a/lib/model/autocomplete.dart +++ b/lib/model/autocomplete.dart @@ -864,7 +864,8 @@ class TopicAutocompleteQuery extends AutocompleteQuery { bool testTopic(TopicName topic) { // TODO(#881): Sort by match relevance, like web does. - return topic != raw && topic.toLowerCase().contains(raw.toLowerCase()); + return topic.displayName != raw + && topic.displayName.toLowerCase().contains(raw.toLowerCase()); } @override diff --git a/lib/model/narrow.dart b/lib/model/narrow.dart index a6e7192772..9e29808ceb 100644 --- a/lib/model/narrow.dart +++ b/lib/model/narrow.dart @@ -114,7 +114,7 @@ class TopicNarrow extends Narrow implements SendableNarrow { StreamDestination get destination => StreamDestination(streamId, topic); @override - String toString() => 'TopicNarrow($streamId, $topic)'; + String toString() => 'TopicNarrow($streamId, ${topic.displayName})'; @override bool operator ==(Object other) { diff --git a/lib/notifications/display.dart b/lib/notifications/display.dart index 7f17e53708..30d30e9021 100644 --- a/lib/notifications/display.dart +++ b/lib/notifications/display.dart @@ -264,9 +264,9 @@ class NotificationDisplayManager { // the first. messagingStyle.conversationTitle = switch (data.recipient) { FcmMessageChannelRecipient(:var streamName?, :var topic) => - '#$streamName > $topic', + '#$streamName > ${topic.displayName}', FcmMessageChannelRecipient(:var topic) => - '#(unknown channel) > $topic', // TODO get stream name from data + '#(unknown channel) > ${topic.displayName}', // TODO get stream name from data FcmMessageDmRecipient(:var allRecipientIds) when allRecipientIds.length > 2 => zulipLocalizations.notifGroupDmConversationLabel( data.senderFullName, allRecipientIds.length - 2), // TODO use others' names, from data diff --git a/lib/widgets/autocomplete.dart b/lib/widgets/autocomplete.dart index 79f0162298..ba921e7f08 100644 --- a/lib/widgets/autocomplete.dart +++ b/lib/widgets/autocomplete.dart @@ -334,6 +334,6 @@ class TopicAutocomplete extends AutocompleteField { } void setTopic(TopicName newTopic) { - value = TextEditingValue(text: newTopic); + value = TextEditingValue(text: newTopic.displayName); } } @@ -550,7 +550,8 @@ class _FixedDestinationContentInput extends StatelessWidget { final store = PerAccountStoreWidget.of(context); final streamName = store.streams[streamId]?.name ?? zulipLocalizations.composeBoxUnknownChannelName; - return zulipLocalizations.composeBoxChannelContentHint(streamName, topic); + return zulipLocalizations.composeBoxChannelContentHint( + streamName, topic.displayName); case DmNarrow(otherRecipientIds: []): // The self-1:1 thread. return zulipLocalizations.composeBoxSelfDmContentHint; diff --git a/lib/widgets/inbox.dart b/lib/widgets/inbox.dart index 75aab7c16a..04d5246195 100644 --- a/lib/widgets/inbox.dart +++ b/lib/widgets/inbox.dart @@ -524,7 +524,7 @@ class _TopicItem extends StatelessWidget { ), maxLines: 2, overflow: TextOverflow.ellipsis, - topic))), + topic.displayName))), const SizedBox(width: 12), if (hasMention) const _IconMarker(icon: ZulipIcons.at_sign), // TODO(design) copies the "@" marker color; is there a better color? diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 014e589936..f5416e3ccf 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -344,7 +344,7 @@ class MessageListAppBarTitle extends StatelessWidget { return Row( mainAxisSize: MainAxisSize.min, children: [ - Flexible(child: Text(topic, style: const TextStyle( + Flexible(child: Text(topic.displayName, style: const TextStyle( fontSize: 13, ).merge(weightVariableTextStyle(context)))), if (icon != null) @@ -1091,7 +1091,7 @@ class StreamMessageRecipientHeader extends StatelessWidget { child: Row( children: [ Flexible( - child: Text(topic, + child: Text(topic.displayName, // TODO: Give a way to see the whole topic (maybe a // long-press interaction?) overflow: TextOverflow.ellipsis, diff --git a/test/api/model/model_checks.dart b/test/api/model/model_checks.dart index eda34ca5d6..8b39b1ad57 100644 --- a/test/api/model/model_checks.dart +++ b/test/api/model/model_checks.dart @@ -48,6 +48,7 @@ extension MessageChecks on Subject { extension TopicNameChecks on Subject { Subject get apiName => has((x) => x.apiName, 'apiName'); + Subject get displayName => has((x) => x.displayName, 'displayName'); } extension StreamMessageChecks on Subject { diff --git a/test/widgets/autocomplete_test.dart b/test/widgets/autocomplete_test.dart index cd22d452f4..d6aab7e1cb 100644 --- a/test/widgets/autocomplete_test.dart +++ b/test/widgets/autocomplete_test.dart @@ -277,7 +277,7 @@ void main() { group('TopicAutocomplete', () { void checkTopicShown(GetStreamTopicsEntry topic, PerAccountStore store, {required bool expected}) { - check(find.text(topic.name).evaluate().length).equals(expected ? 1 : 0); + check(find.text(topic.name.displayName).evaluate().length).equals(expected ? 1 : 0); } testWidgets('options appear, disappear, and change correctly', (WidgetTester tester) async { @@ -302,7 +302,7 @@ void main() { await tester.tap(find.text('Topic three')); await tester.pumpAndSettle(); check(tester.widget(topicInputFinder).controller!.text) - .equals(topic3.name); + .equals(topic3.name.displayName); checkTopicShown(topic1, store, expected: false); checkTopicShown(topic2, store, expected: false); checkTopicShown(topic3, store, expected: true); // shown in `_TopicInput` once