diff --git a/lib/api/core.dart b/lib/api/core.dart index b4d05b5b34..7ac3f1f852 100644 --- a/lib/api/core.dart +++ b/lib/api/core.dart @@ -14,7 +14,7 @@ import 'exception.dart'; /// /// When updating this, also update [kMinSupportedZulipFeatureLevel] /// and the README. -// TODO(#992) address all TODO(server-6) and TODO(server-7) +// TODO(#1838) address all TODO(server-7) const kMinSupportedZulipVersion = '7.0'; /// The Zulip feature level reserved for the [kMinSupportedZulipVersion] release. diff --git a/lib/api/model/events.dart b/lib/api/model/events.dart index 5d86935186..9fd83c88f4 100644 --- a/lib/api/model/events.dart +++ b/lib/api/model/events.dart @@ -71,7 +71,6 @@ sealed class Event { case 'peer_remove': return SubscriptionPeerRemoveEvent.fromJson(json); default: return UnexpectedEvent.fromJson(json); } - // case 'muted_topics': … // TODO(#422) we ignore this feature on older servers case 'user_status': return UserStatusEvent.fromJson(json); case 'user_topic': return UserTopicEvent.fromJson(json); case 'muted_users': return MutedUsersEvent.fromJson(json); @@ -763,6 +762,7 @@ class SubscriptionUpdateEvent extends SubscriptionEvent { final int streamId; + @JsonKey(unknownEnumValue: SubscriptionProperty.unknown) final SubscriptionProperty property; /// The new value, or null if we don't recognize the setting. @@ -783,7 +783,6 @@ class SubscriptionUpdateEvent extends SubscriptionEvent { assert(RegExp(r'^#[0-9a-f]{6}$').hasMatch(str)); return 0xff000000 | int.parse(str.substring(1), radix: 16); case SubscriptionProperty.isMuted: - case SubscriptionProperty.inHomeView: case SubscriptionProperty.pinToTop: case SubscriptionProperty.desktopNotifications: case SubscriptionProperty.audibleNotifications: @@ -820,13 +819,18 @@ enum SubscriptionProperty { color, isMuted, - inHomeView, pinToTop, desktopNotifications, audibleNotifications, pushNotifications, emailNotifications, wildcardMentionsNotify, + + /// A new, unrecognized property, or a deprecated one we don't use. + /// + /// Could be `in_home_view`, deprecated in FL 139 (Server 6) but still sent + /// as of CZO on 2025-10-03. + // TODO(server-future) Remove `in_home_view` comment once it stops being sent. unknown; static SubscriptionProperty fromRawString(String raw) => _byRawString[raw] ?? unknown; diff --git a/lib/api/model/events.g.dart b/lib/api/model/events.g.dart index 0313976018..7bf9ef0fcd 100644 --- a/lib/api/model/events.g.dart +++ b/lib/api/model/events.g.dart @@ -513,7 +513,11 @@ SubscriptionUpdateEvent _$SubscriptionUpdateEventFromJson( ) => SubscriptionUpdateEvent( id: (json['id'] as num).toInt(), streamId: (json['stream_id'] as num).toInt(), - property: $enumDecode(_$SubscriptionPropertyEnumMap, json['property']), + property: $enumDecode( + _$SubscriptionPropertyEnumMap, + json['property'], + unknownValue: SubscriptionProperty.unknown, + ), value: SubscriptionUpdateEvent._readValue(json, 'value'), ); @@ -531,7 +535,6 @@ Map _$SubscriptionUpdateEventToJson( const _$SubscriptionPropertyEnumMap = { SubscriptionProperty.color: 'color', SubscriptionProperty.isMuted: 'is_muted', - SubscriptionProperty.inHomeView: 'in_home_view', SubscriptionProperty.pinToTop: 'pin_to_top', SubscriptionProperty.desktopNotifications: 'desktop_notifications', SubscriptionProperty.audibleNotifications: 'audible_notifications', diff --git a/lib/api/model/initial_snapshot.dart b/lib/api/model/initial_snapshot.dart index 9250f75fa2..83e53fa1ca 100644 --- a/lib/api/model/initial_snapshot.dart +++ b/lib/api/model/initial_snapshot.dart @@ -35,8 +35,6 @@ class InitialSnapshot { @JsonKey(defaultValue: 10000) final int serverTypingStartedWaitPeriodMilliseconds; - // final List<…> mutedTopics; // TODO(#422) we ignore this feature on older servers - final List mutedUsers; // In the modern format because we pass `slim_presence`. @@ -70,7 +68,7 @@ class InitialSnapshot { final UserSettings userSettings; - final List? userTopics; // TODO(server-6) + final List userTopics; final GroupSettingValue? realmCanDeleteAnyMessageGroup; // TODO(server-10) @@ -111,7 +109,7 @@ class InitialSnapshot { final int maxFileUploadSizeMib; - final Uri? serverEmojiDataUrl; // TODO(server-6) + final Uri serverEmojiDataUrl; final String? realmEmptyTopicDisplayName; // TODO(server-10) @@ -284,7 +282,7 @@ class UserSettings { ) TwentyFourHourTimeMode twentyFourHourTime; - bool? displayEmojiReactionUsers; // TODO(server-6) + bool displayEmojiReactionUsers; Emojiset emojiset; bool presenceEnabled; diff --git a/lib/api/model/initial_snapshot.g.dart b/lib/api/model/initial_snapshot.g.dart index ffcbfba827..c2fd97dde5 100644 --- a/lib/api/model/initial_snapshot.g.dart +++ b/lib/api/model/initial_snapshot.g.dart @@ -78,8 +78,8 @@ InitialSnapshot _$InitialSnapshotFromJson( userSettings: UserSettings.fromJson( json['user_settings'] as Map, ), - userTopics: (json['user_topics'] as List?) - ?.map((e) => UserTopicItem.fromJson(e as Map)) + userTopics: (json['user_topics'] as List) + .map((e) => UserTopicItem.fromJson(e as Map)) .toList(), realmCanDeleteAnyMessageGroup: json['realm_can_delete_any_message_group'] == null @@ -115,9 +115,7 @@ InitialSnapshot _$InitialSnapshotFromJson( ), ), maxFileUploadSizeMib: (json['max_file_upload_size_mib'] as num).toInt(), - serverEmojiDataUrl: json['server_emoji_data_url'] == null - ? null - : Uri.parse(json['server_emoji_data_url'] as String), + serverEmojiDataUrl: Uri.parse(json['server_emoji_data_url'] as String), realmEmptyTopicDisplayName: json['realm_empty_topic_display_name'] as String?, realmUsers: (InitialSnapshot._readUsersIsActiveFallbackTrue(json, 'realm_users') @@ -186,7 +184,7 @@ Map _$InitialSnapshotToJson( 'realm_presence_disabled': instance.realmPresenceDisabled, 'realm_default_external_accounts': instance.realmDefaultExternalAccounts, 'max_file_upload_size_mib': instance.maxFileUploadSizeMib, - 'server_emoji_data_url': instance.serverEmojiDataUrl?.toString(), + 'server_emoji_data_url': instance.serverEmojiDataUrl.toString(), 'realm_empty_topic_display_name': instance.realmEmptyTopicDisplayName, 'realm_users': instance.realmUsers, 'realm_non_active_users': instance.realmNonActiveUsers, @@ -248,7 +246,7 @@ UserSettings _$UserSettingsFromJson(Map json) => UserSettings( twentyFourHourTime: TwentyFourHourTimeMode.fromApiValue( json['twenty_four_hour_time'] as bool?, ), - displayEmojiReactionUsers: json['display_emoji_reaction_users'] as bool?, + displayEmojiReactionUsers: json['display_emoji_reaction_users'] as bool, emojiset: $enumDecode(_$EmojisetEnumMap, json['emojiset']), presenceEnabled: json['presence_enabled'] as bool, ); diff --git a/lib/api/model/model.dart b/lib/api/model/model.dart index 2bc6d7d487..fdfd0c856d 100644 --- a/lib/api/model/model.dart +++ b/lib/api/model/model.dart @@ -69,7 +69,7 @@ class CustomProfileField { final String name; final String hint; final String fieldData; - final bool? displayInProfileSummary; // TODO(server-6) + final bool? displayInProfileSummary; CustomProfileField({ required this.id, @@ -97,7 +97,7 @@ enum CustomProfileFieldType { link(apiValue: 5), user(apiValue: 6), externalAccount(apiValue: 7), - pronouns(apiValue: 8), // TODO(server-6) newly added + pronouns(apiValue: 8), unknown(apiValue: null); const CustomProfileFieldType({ @@ -257,7 +257,6 @@ class StatusEmoji { /// /// The absence of one of these means there is no change. class UserStatusChange { - // final Option away; // deprecated in server-6 (FL-148); ignore final Option text; final Option emoji; @@ -793,7 +792,6 @@ class Subscription extends ZulipStream { bool pinToTop; bool isMuted; - // final bool? inHomeView; // deprecated; ignore /// As an int that dart:ui's Color constructor will take: /// diff --git a/lib/model/channel.dart b/lib/model/channel.dart index 098f146df1..ea05dc01d3 100644 --- a/lib/model/channel.dart +++ b/lib/model/channel.dart @@ -307,7 +307,7 @@ class ChannelStoreImpl extends HasUserStore with ChannelStore { } final topicVisibility = >{}; - for (final item in initialSnapshot.userTopics ?? const []) { + for (final item in initialSnapshot.userTopics) { if (_warnInvalidVisibilityPolicy(item.visibilityPolicy)) { // Not a value we expect. Keep it out of our data structures. // TODO(log) continue; @@ -473,8 +473,6 @@ class ChannelStoreImpl extends HasUserStore with ChannelStore { case SubscriptionProperty.isMuted: // TODO(#1255) update [MessageListView] if affected subscription.isMuted = event.value as bool; - case SubscriptionProperty.inHomeView: - subscription.isMuted = !(event.value as bool); case SubscriptionProperty.pinToTop: subscription.pinToTop = event.value as bool; case SubscriptionProperty.desktopNotifications: diff --git a/lib/model/message_list.dart b/lib/model/message_list.dart index fcb6943905..91868df2b2 100644 --- a/lib/model/message_list.dart +++ b/lib/model/message_list.dart @@ -873,12 +873,6 @@ class MessageListView with ChangeNotifier, _MessageSequence { numBefore: kMessageListFetchBatchSize, numAfter: 0, processResult: (result) { - if (result.messages.isNotEmpty - && result.messages.last.id == messages[0].id) { - // TODO(server-6): includeAnchor should make this impossible - result.messages.removeLast(); - } - store.reconcileMessages(result.messages); store.recentSenders.handleMessages(result.messages); // TODO(#824) @@ -909,12 +903,6 @@ class MessageListView with ChangeNotifier, _MessageSequence { numBefore: 0, numAfter: kMessageListFetchBatchSize, processResult: (result) { - if (result.messages.isNotEmpty - && result.messages.first.id == messages.last.id) { - // TODO(server-6): includeAnchor should make this impossible - result.messages.removeAt(0); - } - store.reconcileMessages(result.messages); store.recentSenders.handleMessages(result.messages); // TODO(#824) diff --git a/lib/model/store.dart b/lib/model/store.dart index fe503da36e..f39757343d 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -1117,12 +1117,7 @@ class UpdateMachine { final updateMachine = UpdateMachine.fromInitialSnapshot( store: store, initialSnapshot: initialSnapshot); updateMachine.poll(); - if (initialSnapshot.serverEmojiDataUrl != null) { - // TODO(server-6): If the server is ancient, just skip trying to have - // a list of its emoji. (The old servers that don't provide - // serverEmojiDataUrl are already unsupported at time of writing.) - unawaited(updateMachine.fetchEmojiData(initialSnapshot.serverEmojiDataUrl!)); - } + unawaited(updateMachine.fetchEmojiData(initialSnapshot.serverEmojiDataUrl)); store.presence.start(); return updateMachine; } diff --git a/lib/model/unreads.dart b/lib/model/unreads.dart index 616feb18ef..543cde28b6 100644 --- a/lib/model/unreads.dart +++ b/lib/model/unreads.dart @@ -453,12 +453,8 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier { final newlyUnreadInDms = >{}; for (final messageId in event.messages) { final detail = event.messageDetails![messageId]; - if (detail == null) { // TODO(log) if on Zulip 6.0+ - // Happens as a bug in some cases before fixed in Zulip 6.0: - // https://chat.zulip.org/#narrow/stream/378-api-design/topic/unreads.20in.20unsubscribed.20streams/near/1458467 - // TODO(server-6) remove Zulip 6.0 comment - continue; - } + if (detail == null) continue; // TODO(log) + if (detail.mentioned == true) { mentions.add(messageId); } @@ -537,11 +533,7 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier { final topics = streams[streamId] ??= makeTopicKeyedMap(); topics.update(topic, ifAbsent: () => messageIds, - // setUnion dedupes existing and incoming unread IDs, - // so we tolerate zulip/zulip#22164, fixed in 6.0 - // TODO(server-6) remove 6.0 comment - (existing) => setUnion(existing, messageIds), - ); + (existing) => setUnion(existing, messageIds)); } /// Remove [idsToRemove] from [streams] and [dms]. @@ -640,10 +632,6 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier { void _addAllInDm(QueueList messageIds, DmNarrow dmNarrow) { dms.update(dmNarrow, ifAbsent: () => messageIds, - // setUnion dedupes existing and incoming unread IDs, - // so we tolerate zulip/zulip#22164, fixed in 6.0 - // TODO(server-6) remove 6.0 comment - (existing) => setUnion(existing, messageIds), - ); + (existing) => setUnion(existing, messageIds)); } } diff --git a/lib/widgets/action_sheet.dart b/lib/widgets/action_sheet.dart index c01b8b9937..1d41a574d5 100644 --- a/lib/widgets/action_sheet.dart +++ b/lib/widgets/action_sheet.dart @@ -1013,8 +1013,6 @@ void showMessageActionSheet({required BuildContext context, required Message mes final isComposeBoxOffered = messageListPage.composeBoxState != null; final isMessageRead = message.flags.contains(MessageFlag.read); - final markAsUnreadSupported = store.zulipFeatureLevel >= 155; // TODO(server-6) - final showMarkAsUnreadButton = markAsUnreadSupported && isMessageRead; final isSenderMuted = store.isUserMuted(message.senderId); @@ -1029,7 +1027,7 @@ void showMessageActionSheet({required BuildContext context, required Message mes StarButton(message: message, pageContext: pageContext), if (isComposeBoxOffered) QuoteAndReplyButton(message: message, pageContext: pageContext), - if (showMarkAsUnreadButton) + if (isMessageRead) MarkAsUnreadButton(message: message, pageContext: pageContext), if (isSenderMuted) // The message must have been revealed in order to open this action sheet. @@ -1109,10 +1107,7 @@ bool _getShouldShowEditButton(BuildContext pageContext, Message message) { final now = ZulipBinding.instance.utcNow().millisecondsSinceEpoch ~/ 1000; final editLimit = store.realmMessageContentEditLimitSeconds; - final outsideEditLimit = - editLimit != null - && editLimit != 0 // TODO(server-6) remove (pre-FL 138, 0 represents no limit) - && now - message.timestamp > editLimit; + final outsideEditLimit = editLimit != null && now - message.timestamp > editLimit; return message.senderId == store.selfUserId && isComposeBoxOffered diff --git a/lib/widgets/actions.dart b/lib/widgets/actions.dart index dd34564768..b75a85ecef 100644 --- a/lib/widgets/actions.dart +++ b/lib/widgets/actions.dart @@ -68,7 +68,6 @@ abstract final class ZulipAction { Message message, Narrow narrow, ) async { - assert(PerAccountStoreWidget.of(context).zulipFeatureLevel >= 155); // TODO(server-6) final zulipLocalizations = ZulipLocalizations.of(context); await updateMessageFlagsStartingFromAnchor( context: context, diff --git a/lib/widgets/emoji_reaction.dart b/lib/widgets/emoji_reaction.dart index b3117e465e..2c7c5d61ab 100644 --- a/lib/widgets/emoji_reaction.dart +++ b/lib/widgets/emoji_reaction.dart @@ -130,7 +130,7 @@ class ReactionChipsList extends StatelessWidget { Widget build(BuildContext context) { final zulipLocalizations = ZulipLocalizations.of(context); final store = PerAccountStoreWidget.of(context); - final displayEmojiReactionUsers = store.userSettings.displayEmojiReactionUsers ?? false; + final displayEmojiReactionUsers = store.userSettings.displayEmojiReactionUsers; final showNames = displayEmojiReactionUsers && reactions.total <= 3; Widget result = Wrap(spacing: 4, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center, diff --git a/test/example_data.dart b/test/example_data.dart index 9bc82b9d6d..a26b94ff13 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -821,14 +821,13 @@ GetMessagesResult nearGetMessagesResult({ /// A GetMessagesResult the server might return when we request older messages. GetMessagesResult olderGetMessagesResult({ required int anchor, - bool foundAnchor = false, // the value if the server understood includeAnchor false required bool foundOldest, bool historyLimited = false, required List messages, }) { return GetMessagesResult( anchor: anchor, - foundAnchor: foundAnchor, + foundAnchor: false, foundNewest: false, // empirically always this, even when anchor happens to be latest foundOldest: foundOldest, historyLimited: historyLimited, @@ -839,14 +838,13 @@ GetMessagesResult olderGetMessagesResult({ /// A GetMessagesResult the server might return when we request newer messages. GetMessagesResult newerGetMessagesResult({ required int anchor, - bool foundAnchor = false, // the value if the server understood includeAnchor false required bool foundNewest, bool historyLimited = false, required List messages, }) { return GetMessagesResult( anchor: anchor, - foundAnchor: foundAnchor, + foundAnchor: false, foundOldest: false, foundNewest: foundNewest, historyLimited: historyLimited, @@ -1334,7 +1332,7 @@ InitialSnapshot initialSnapshot({ emojiset: Emojiset.google, presenceEnabled: true, ), - userTopics: userTopics, + userTopics: userTopics ?? [], // no default; allow `null` to simulate servers without this realmCanDeleteAnyMessageGroup: realmCanDeleteAnyMessageGroup, realmCanDeleteOwnMessageGroup: realmCanDeleteOwnMessageGroup, diff --git a/test/model/channel_test.dart b/test/model/channel_test.dart index e7e57357d6..e73965dac2 100644 --- a/test/model/channel_test.dart +++ b/test/model/channel_test.dart @@ -121,20 +121,6 @@ void main() { value: true)); check(store.subscriptions[stream.streamId]!.isMuted).isTrue(); }); - - test('SubscriptionProperty.inHomeView updates isMuted instead', () async { - final store = eg.store(initialSnapshot: eg.initialSnapshot( - streams: [stream], - subscriptions: [eg.subscription(stream, isMuted: false)], - )); - check(store.subscriptions[stream.streamId]!.isMuted).isFalse(); - - await store.handleEvent(SubscriptionUpdateEvent(id: 1, - streamId: stream.streamId, - property: SubscriptionProperty.inHomeView, - value: false)); - check(store.subscriptions[stream.streamId]!.isMuted).isTrue(); - }); }); group('topic visibility', () { diff --git a/test/model/message_list_test.dart b/test/model/message_list_test.dart index 9cb4977a32..39f4305ff3 100644 --- a/test/model/message_list_test.dart +++ b/test/model/message_list_test.dart @@ -625,40 +625,6 @@ void main() { check(connection.takeRequests()).single; })); - test('fetchOlder handles servers not understanding includeAnchor', () async { - await prepare(); - await prepareMessages(foundOldest: false, - messages: List.generate(100, (i) => eg.streamMessage(id: 1000 + i))); - - // The old behavior is to include the anchor message regardless of includeAnchor. - connection.prepare(json: olderResult( - anchor: 1000, foundOldest: false, foundAnchor: true, - messages: List.generate(101, (i) => eg.streamMessage(id: 900 + i)), - ).toJson()); - await model.fetchOlder(); - checkNotified(count: 2); - check(model) - ..busyFetchingMore.isFalse() - ..messages.length.equals(200); - }); - - test('fetchNewer handles servers not understanding includeAnchor', () async { - await prepare(anchor: NumericAnchor(1000)); - await prepareMessages(foundOldest: true, foundNewest: false, - messages: List.generate(101, (i) => eg.streamMessage(id: 1000 + i))); - - // The old behavior is to include the anchor message regardless of includeAnchor. - connection.prepare(json: newerResult( - anchor: 1100, foundNewest: false, foundAnchor: true, - messages: List.generate(101, (i) => eg.streamMessage(id: 1100 + i)), - ).toJson()); - await model.fetchNewer(); - checkNotified(count: 2); - check(model) - ..busyFetchingMore.isFalse() - ..messages.length.equals(201); - }); - // TODO(#824): move this test test('fetchOlder recent senders track all the messages', () async { await prepare(); diff --git a/test/model/unreads_test.dart b/test/model/unreads_test.dart index 4952354044..089ef315cb 100644 --- a/test/model/unreads_test.dart +++ b/test/model/unreads_test.dart @@ -1250,22 +1250,6 @@ void main() { checkMatchesMessages([message1, message2]); }); - // TODO(server-6) remove mention of zulip/zulip#22164, fixed in 6.0 - test('tolerates event pointing to DM/stream messages that are already unread (zulip/zulip#22164)', () { - final message1 = eg.streamMessage(id: 1, flags: []); - final message2 = eg.dmMessage(id: 2, from: eg.otherUser, to: [eg.selfUser], flags: []); - - prepare(); - fillWithMessages([message1, message2]); - - model.handleUpdateMessageFlagsEvent(mkEvent([message1, message2])); - checkNotifiedOnce(); - - message1.flags.remove(MessageFlag.read); - message2.flags.remove(MessageFlag.read); - checkMatchesMessages([message1, message2]); - }); - test('tolerates "message details" missing', () { final stream = eg.stream(); const topic = 'a'; diff --git a/test/widgets/action_sheet_test.dart b/test/widgets/action_sheet_test.dart index 22a4b06d8e..06e0eab5ca 100644 --- a/test/widgets/action_sheet_test.dart +++ b/test/widgets/action_sheet_test.dart @@ -2202,8 +2202,6 @@ void main() { } testVisibility(true); - // TODO(server-6) limit 0 not expected on 6.0+ - testVisibility(true, limit: 0); testVisibility(true, limit: 600); testVisibility(true, narrow: ChannelNarrow(1));