Skip to content
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

general chat #2: support sending to empty topic from channel narrow, hide resolve/unresolve conditionally #1364

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/api/model/initial_snapshot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ class InitialSnapshot {

final Uri? serverEmojiDataUrl; // TODO(server-6)

final String? realmEmptyTopicDisplayName; // TODO(server-10)

@JsonKey(readValue: _readUsersIsActiveFallbackTrue)
final List<User> realmUsers;
@JsonKey(readValue: _readUsersIsActiveFallbackFalse)
Expand Down Expand Up @@ -138,6 +140,7 @@ class InitialSnapshot {
required this.realmDefaultExternalAccounts,
required this.maxFileUploadSizeMib,
required this.serverEmojiDataUrl,
required this.realmEmptyTopicDisplayName,
required this.realmUsers,
required this.realmNonActiveUsers,
required this.crossRealmBots,
Expand Down
3 changes: 3 additions & 0 deletions lib/api/model/initial_snapshot.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/api/model/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,7 @@ extension type const TopicName(String _value) {
/// 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;
String? get displayName => _value.isEmpty ? null : _value;

/// The key to use for "same topic as" comparisons.
String canonicalize() => apiName.toLowerCase();
Expand Down
6 changes: 5 additions & 1 deletion lib/api/route/channels.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ part 'channels.g.dart';
/// https://zulip.com/api/get-stream-topics
Future<GetStreamTopicsResult> getStreamTopics(ApiConnection connection, {
required int streamId,
bool? allowEmptyTopicName,
}) {
return connection.get('getStreamTopics', GetStreamTopicsResult.fromJson, 'users/me/$streamId/topics', {});
assert(allowEmptyTopicName != false, '`allowEmptyTopicName` should only be true or null');
return connection.get('getStreamTopics', GetStreamTopicsResult.fromJson, 'users/me/$streamId/topics', {
if (allowEmptyTopicName != null) 'allow_empty_topic_name': allowEmptyTopicName,
});
}

@JsonSerializable(fieldRename: FieldRename.snake)
Expand Down
1 change: 1 addition & 0 deletions lib/api/route/events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Future<InitialSnapshot> registerQueue(ApiConnection connection) {
'user_avatar_url_field_optional': false, // TODO(#254): turn on
'stream_typing_notifications': true,
'user_settings_object': true,
'empty_topic_name': true,
},
});
}
Expand Down
9 changes: 9 additions & 0 deletions lib/api/route/messages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ part 'messages.g.dart';
Future<Message?> getMessageCompat(ApiConnection connection, {
required int messageId,
bool? applyMarkdown,
bool? allowEmptyTopicName,
}) async {
final useLegacyApi = connection.zulipFeatureLevel! < 120;
if (useLegacyApi) {
assert(allowEmptyTopicName == null);
final response = await getMessages(connection,
narrow: [ApiNarrowMessageId(messageId)],
anchor: NumericAnchor(messageId),
Expand All @@ -37,6 +39,7 @@ Future<Message?> getMessageCompat(ApiConnection connection, {
final response = await getMessage(connection,
messageId: messageId,
applyMarkdown: applyMarkdown,
allowEmptyTopicName: allowEmptyTopicName,
);
return response.message;
} on ZulipApiException catch (e) {
Expand All @@ -57,10 +60,13 @@ Future<Message?> getMessageCompat(ApiConnection connection, {
Future<GetMessageResult> getMessage(ApiConnection connection, {
required int messageId,
bool? applyMarkdown,
bool? allowEmptyTopicName,
}) {
assert(allowEmptyTopicName != false, '`allowEmptyTopicName` should only be true or null');
assert(connection.zulipFeatureLevel! >= 120);
return connection.get('getMessage', GetMessageResult.fromJson, 'messages/$messageId', {
if (applyMarkdown != null) 'apply_markdown': applyMarkdown,
if (allowEmptyTopicName != null) 'allow_empty_topic_name': allowEmptyTopicName,
});
}

Expand Down Expand Up @@ -88,8 +94,10 @@ Future<GetMessagesResult> getMessages(ApiConnection connection, {
required int numAfter,
bool? clientGravatar,
bool? applyMarkdown,
bool? allowEmptyTopicName,
// bool? useFirstUnreadAnchor // omitted because deprecated
}) {
assert(allowEmptyTopicName != false, '`allowEmptyTopicName` should only be true or null');
return connection.get('getMessages', GetMessagesResult.fromJson, 'messages', {
'narrow': resolveApiNarrowForServer(narrow, connection.zulipFeatureLevel!),
'anchor': RawParameter(anchor.toJson()),
Expand All @@ -98,6 +106,7 @@ Future<GetMessagesResult> getMessages(ApiConnection connection, {
'num_after': numAfter,
if (clientGravatar != null) 'client_gravatar': clientGravatar,
if (applyMarkdown != null) 'apply_markdown': applyMarkdown,
if (allowEmptyTopicName != null) 'allow_empty_topic_name': allowEmptyTopicName,
});
}

Expand Down
18 changes: 14 additions & 4 deletions lib/model/autocomplete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,11 @@ class TopicAutocompleteView extends AutocompleteView<TopicAutocompleteQuery, Top
Future<void> _fetch() async {
assert(!_isFetching);
_isFetching = true;
final result = await getStreamTopics(store.connection, streamId: streamId);
final result = await getStreamTopics(store.connection, streamId: streamId,
allowEmptyTopicName:
// TODO(server-10): simplify this condition away
store.zulipFeatureLevel >= 334 ? true : null,
);
_topics = result.topics.map((e) => e.name);
_isFetching = false;
return _startSearch();
Expand All @@ -921,7 +925,7 @@ class TopicAutocompleteView extends AutocompleteView<TopicAutocompleteQuery, Top
}

TopicAutocompleteResult? _testTopic(TopicAutocompleteQuery query, TopicName topic) {
if (query.testTopic(topic)) {
if (query.testTopic(store, topic)) {
return TopicAutocompleteResult(topic: topic);
}
return null;
Expand All @@ -939,10 +943,16 @@ class TopicAutocompleteView extends AutocompleteView<TopicAutocompleteQuery, Top
class TopicAutocompleteQuery extends AutocompleteQuery {
TopicAutocompleteQuery(super.raw);

bool testTopic(TopicName topic) {
bool testTopic(PerAccountStore store, TopicName topic) {
// TODO(#881): Sort by match relevance, like web does.

if (topic.displayName == null) {
return store.realmEmptyTopicDisplayName.toLowerCase()
.contains(raw.toLowerCase());
}
// Skip this option if it matches the query exactly.
return topic.displayName != raw
&& topic.displayName.toLowerCase().contains(raw.toLowerCase());
&& topic.displayName!.toLowerCase().contains(raw.toLowerCase());
}

@override
Expand Down
6 changes: 6 additions & 0 deletions lib/model/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
anchor: AnchorCode.newest,
numBefore: kMessageListFetchBatchSize,
numAfter: 0,
allowEmptyTopicName:
// TODO(server-10): simplify this condition away
store.zulipFeatureLevel >= 334 ? true : null,
);
if (this.generation > generation) return;
_adjustNarrowForTopicPermalink(result.messages.firstOrNull);
Expand Down Expand Up @@ -581,6 +584,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
includeAnchor: false,
numBefore: kMessageListFetchBatchSize,
numAfter: 0,
allowEmptyTopicName:
// TODO(server-10): simplify this condition away
store.zulipFeatureLevel >= 334 ? true : null,
);
} catch (e) {
hasFetchError = true;
Expand Down
8 changes: 8 additions & 0 deletions lib/model/store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
realmMandatoryTopics: initialSnapshot.realmMandatoryTopics,
realmWaitingPeriodThreshold: initialSnapshot.realmWaitingPeriodThreshold,
maxFileUploadSizeMib: initialSnapshot.maxFileUploadSizeMib,
realmEmptyTopicDisplayName: initialSnapshot.realmEmptyTopicDisplayName,
realmDefaultExternalAccounts: initialSnapshot.realmDefaultExternalAccounts,
customProfileFields: _sortCustomProfileFields(initialSnapshot.customProfileFields),
emailAddressVisibility: initialSnapshot.emailAddressVisibility,
Expand Down Expand Up @@ -344,6 +345,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
required this.realmMandatoryTopics,
required this.realmWaitingPeriodThreshold,
required this.maxFileUploadSizeMib,
required String? realmEmptyTopicDisplayName,
required this.realmDefaultExternalAccounts,
required this.customProfileFields,
required this.emailAddressVisibility,
Expand All @@ -362,6 +364,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
assert(realmUrl == connection.realmUrl),
assert(emoji.realmUrl == realmUrl),
_globalStore = globalStore,
_realmEmptyTopicDisplayName = realmEmptyTopicDisplayName,
_emoji = emoji,
_users = users,
_channels = channels,
Expand Down Expand Up @@ -414,6 +417,11 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
/// For docs, please see [InitialSnapshot.realmWaitingPeriodThreshold].
final int realmWaitingPeriodThreshold; // TODO(#668): update this realm setting
final int maxFileUploadSizeMib; // No event for this.
String get realmEmptyTopicDisplayName {
assert(_realmEmptyTopicDisplayName != null); // TODO(log)
return _realmEmptyTopicDisplayName ?? 'general chat';
}
final String? _realmEmptyTopicDisplayName; // TODO(#668): update this realm setting
final Map<String, RealmDefaultExternalAccount> realmDefaultExternalAccounts;
List<CustomProfileField> customProfileFields;
/// For docs, please see [InitialSnapshot.emailAddressVisibility].
Expand Down
12 changes: 10 additions & 2 deletions lib/widgets/action_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,12 @@ void showTopicActionSheet(BuildContext context, {
pageContext: pageContext);
}));

if (someMessageIdInTopic != null) {
final message = store.messages[someMessageIdInTopic] as StreamMessage?;
// TODO: check for other cases that may disallow this action (e.g.: time
// limit for editing topics).
final allowResolveUnresolve =
message == null || message.topic.displayName != null;
if (someMessageIdInTopic != null && allowResolveUnresolve) {
optionButtons.add(ResolveUnresolveButton(pageContext: pageContext,
topic: topic,
someMessageIdInTopic: someMessageIdInTopic));
Expand Down Expand Up @@ -693,9 +698,12 @@ Future<String?> fetchRawContentWithFeedback({
// On final failure or success, auto-dismiss the snackbar.
final zulipLocalizations = ZulipLocalizations.of(context);
try {
fetchedMessage = await getMessageCompat(PerAccountStoreWidget.of(context).connection,
final store = PerAccountStoreWidget.of(context);
fetchedMessage = await getMessageCompat(store.connection,
messageId: messageId,
applyMarkdown: false,
// TODO(server-10): simplify this condition away
allowEmptyTopicName: store.zulipFeatureLevel >= 334 ? true : null,
);
if (fetchedMessage == null) {
errorMessage = zulipLocalizations.errorMessageDoesNotSeemToExist;
Expand Down
11 changes: 10 additions & 1 deletion lib/widgets/autocomplete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -367,12 +367,21 @@ class TopicAutocomplete extends AutocompleteField<TopicAutocompleteQuery, TopicA

@override
Widget buildItem(BuildContext context, int index, TopicAutocompleteResult option) {
final Widget child;
if (option.topic.displayName == null) {
final store = PerAccountStoreWidget.of(context);
child = Text(store.realmEmptyTopicDisplayName,
style: const TextStyle(fontStyle: FontStyle.italic));
} else {
child = Text(option.topic.displayName!);
}

return InkWell(
onTap: () {
_onTapOption(context, option);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Text(option.topic.displayName)));
child: child));
}
}
Loading