Repository: zulip/zulip-flutter (language: dart)

Issue:
Make `role:administrators` the default for `realm_can_delete_any_message_group`
Server 10 added the `realm_can_delete_any_message_group` permission. For servers before that, we're meant to use `role:administrators` when deciding whether to show the "delete message" button, but (with #1842, not yet merged) we'll actually use `role:nobody`. We should fix that and refactor a bit; see discussion: https://github.com/zulip/zulip-flutter/pull/1842#discussion_r2331362461


Discussion hints:
nan

Pull request: initial_snapshot: Centralize group-permission defaults; fix admin permission for legacy servers
Fixes #1850.

Also does the refactor Greg suggested here: https://github.com/zulip/zulip-flutter/pull/1842#discussion_r2331337006

Test command: flutter test test/model/message_test.dart test/model/realm_test.dart test/model/user_test.dart --reporter expanded 2>&1

Fix patch:
diff --git a/lib/api/model/initial_snapshot.dart b/lib/api/model/initial_snapshot.dart
index a8e99dde28..9250f75fa2 100644
--- a/lib/api/model/initial_snapshot.dart
+++ b/lib/api/model/initial_snapshot.dart
@@ -424,248 +424,3 @@ class UnreadHuddleSnapshot {
 
   Map<String, dynamic> toJson() => _$UnreadHuddleSnapshotToJson(this);
 }
-
-/// Metadata about how to interpret the various group-based permission settings.
-///
-/// This is the type that [InitialSnapshot.serverSupportedPermissionSettings]
-/// would have, according to the API as it exists as of 2025-08;
-/// but that API is documented as unstable and subject to change.
-///
-/// For a useful value of this type, see [SupportedPermissionSettings.fixture].
-///
-/// For docs, search for "d_perm" in: https://zulip.com/api/register-queue
-@JsonSerializable(fieldRename: FieldRename.snake)
-class SupportedPermissionSettings {
-  final Map<String, PermissionSettingsItem> realm;
-  final Map<String, PermissionSettingsItem> stream;
-  final Map<String, PermissionSettingsItem> group;
-
-  /// Metadata about how to interpret certain group-based permission settings,
-  /// including all those that this client uses, based on "current" servers.
-  ///
-  /// "Current" here means as of when this code was written, or last updated;
-  /// details in comments below.  Naturally it'd be better to have an API to
-  /// get this information from the actual server.
-  ///
-  /// Effectively we're counting on it being uncommon for the metadata for a
-  /// given permission to ever change from one server version to the next,
-  /// so that the values we take from one server version usually remain valid
-  /// for all past and future server versions that have the corresponding
-  /// permission at all.
-  ///
-  /// TODO(server): Stabilize [InitialSnapshot.serverSupportedPermissionSettings]
-  ///   or a similar API, and switch to using that.  See thread:
-  ///     https://chat.zulip.org/#narrow/channel/378-api-design/topic/server_supported_permission_settings/near/2247549
-  static SupportedPermissionSettings fixture = SupportedPermissionSettings(
-    realm: {
-      // From the server's Realm.REALM_PERMISSION_GROUP_SETTINGS,
-      // in zerver/models/realms.py.  Current as of 6ab30fcce, 2025-08.
-      'create_multiuse_invite_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.ADMINISTRATORS,
-      ),
-      'can_access_all_users_group': PermissionSettingsItem(
-          // require_system_group=True,
-          // allow_nobody_group=False,
-          allowEveryoneGroup: true,
-          // default_group_name=SystemGroups.EVERYONE,
-          // # Note that user_can_access_all_other_users in the web
-          // # app is relying on members always have access.
-          // allowed_system_groups=[SystemGroups.EVERYONE, SystemGroups.MEMBERS],
-      ),
-      'can_add_subscribers_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.MEMBERS,
-      ),
-      'can_add_custom_emoji_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.MEMBERS,
-      ),
-      'can_create_bots_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.MEMBERS,
-      ),
-      'can_create_groups': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.MEMBERS,
-      ),
-      'can_create_public_channel_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.MEMBERS,
-      ),
-      'can_create_private_channel_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.MEMBERS,
-      ),
-      'can_create_web_public_channel_group': PermissionSettingsItem(
-          // require_system_group=True,
-          // allow_nobody_group=True,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.OWNERS,
-          // allowed_system_groups=[
-          //     SystemGroups.MODERATORS,
-          //     SystemGroups.ADMINISTRATORS,
-          //     SystemGroups.OWNERS,
-          //     SystemGroups.NOBODY,
-          // ],
-      ),
-      'can_create_write_only_bots_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.MEMBERS,
-      ),
-      'can_delete_any_message_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.ADMINISTRATORS,
-      ),
-      'can_delete_own_message_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: true,
-          // default_group_name=SystemGroups.EVERYONE,
-      ),
-      'can_invite_users_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.MEMBERS,
-      ),
-      'can_manage_all_groups': PermissionSettingsItem(
-          // allow_nobody_group=False,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.OWNERS,
-      ),
-      'can_manage_billing_group': PermissionSettingsItem(
-          // allow_nobody_group=False,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.ADMINISTRATORS,
-      ),
-      'can_mention_many_users_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: true,
-          // default_group_name=SystemGroups.ADMINISTRATORS,
-      ),
-      'can_move_messages_between_channels_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.MEMBERS,
-      ),
-      'can_move_messages_between_topics_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: true,
-          // default_group_name=SystemGroups.EVERYONE,
-      ),
-      'can_resolve_topics_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: true,
-          // default_group_name=SystemGroups.EVERYONE,
-      ),
-      'can_set_delete_message_policy_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.MODERATORS,
-      ),
-      'can_set_topics_policy_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: true,
-          // default_group_name=SystemGroups.MEMBERS,
-      ),
-      'can_summarize_topics_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: true,
-          // default_group_name=SystemGroups.EVERYONE,
-      ),
-      'direct_message_initiator_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: true,
-          // default_group_name=SystemGroups.EVERYONE,
-      ),
-      'direct_message_permission_group': PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: true,
-          // default_group_name=SystemGroups.EVERYONE,
-      ),
-    },
-    group: {}, // Please go ahead and fill this in when we come to need it.
-    stream: {
-      // From the server's Stream.stream_permission_group_settings,
-      // in zerver/models/streams.py.  Current as of f9dc13014, 2025-08.
-      "can_add_subscribers_group": PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.NOBODY,
-      ),
-      "can_administer_channel_group": PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: false,
-          // default_group_name="stream_creator_or_nobody",
-      ),
-      "can_delete_any_message_group": PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: true,
-          // default_group_name=SystemGroups.NOBODY,
-      ),
-      "can_delete_own_message_group": PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: true,
-          // default_group_name=SystemGroups.NOBODY,
-      ),
-      "can_move_messages_out_of_channel_group": PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: true,
-          // default_group_name=SystemGroups.NOBODY,
-      ),
-      "can_move_messages_within_channel_group": PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: true,
-          // default_group_name=SystemGroups.NOBODY,
-      ),
-      "can_remove_subscribers_group": PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: true,
-          // default_group_name=SystemGroups.ADMINISTRATORS,
-      ),
-      "can_send_message_group": PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: true,
-          // default_group_name=SystemGroups.EVERYONE,
-      ),
-      "can_subscribe_group": PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: false,
-          // default_group_name=SystemGroups.NOBODY,
-      ),
-      "can_resolve_topics_group": PermissionSettingsItem(
-          // allow_nobody_group=True,
-          allowEveryoneGroup: true,
-          // default_group_name=SystemGroups.NOBODY,
-      ),
-    },
-  );
-
-  SupportedPermissionSettings({required this.realm, required this.stream, required this.group});
-
-  factory SupportedPermissionSettings.fromJson(Map<String, dynamic> json) =>
-    _$SupportedPermissionSettingsFromJson(json);
-
-  Map<String, dynamic> toJson() => _$SupportedPermissionSettingsToJson(this);
-}
-
-@JsonSerializable(fieldRename: FieldRename.snake)
-class PermissionSettingsItem {
-  final bool allowEveryoneGroup;
-  // also other fields not yet used
-
-  PermissionSettingsItem({required this.allowEveryoneGroup});
-
-  factory PermissionSettingsItem.fromJson(Map<String, dynamic> json) =>
-    _$PermissionSettingsItemFromJson(json);
-
-  Map<String, dynamic> toJson() => _$PermissionSettingsItemToJson(this);
-}
diff --git a/lib/api/model/initial_snapshot.g.dart b/lib/api/model/initial_snapshot.g.dart
index 4a4d153e2c..ffcbfba827 100644
--- a/lib/api/model/initial_snapshot.g.dart
+++ b/lib/api/model/initial_snapshot.g.dart
@@ -382,38 +382,3 @@ Map<String, dynamic> _$UnreadHuddleSnapshotToJson(
   'user_ids_string': instance.userIdsString,
   'unread_message_ids': instance.unreadMessageIds,
 };
-
-SupportedPermissionSettings _$SupportedPermissionSettingsFromJson(
-  Map<String, dynamic> json,
-) => SupportedPermissionSettings(
-  realm: (json['realm'] as Map<String, dynamic>).map(
-    (k, e) =>
-        MapEntry(k, PermissionSettingsItem.fromJson(e as Map<String, dynamic>)),
-  ),
-  stream: (json['stream'] as Map<String, dynamic>).map(
-    (k, e) =>
-        MapEntry(k, PermissionSettingsItem.fromJson(e as Map<String, dynamic>)),
-  ),
-  group: (json['group'] as Map<String, dynamic>).map(
-    (k, e) =>
-        MapEntry(k, PermissionSettingsItem.fromJson(e as Map<String, dynamic>)),
-  ),
-);
-
-Map<String, dynamic> _$SupportedPermissionSettingsToJson(
-  SupportedPermissionSettings instance,
-) => <String, dynamic>{
-  'realm': instance.realm,
-  'stream': instance.stream,
-  'group': instance.group,
-};
-
-PermissionSettingsItem _$PermissionSettingsItemFromJson(
-  Map<String, dynamic> json,
-) => PermissionSettingsItem(
-  allowEveryoneGroup: json['allow_everyone_group'] as bool,
-);
-
-Map<String, dynamic> _$PermissionSettingsItemToJson(
-  PermissionSettingsItem instance,
-) => <String, dynamic>{'allow_everyone_group': instance.allowEveryoneGroup};
diff --git a/lib/api/model/permission.dart b/lib/api/model/permission.dart
new file mode 100644
index 0000000000..315f781ea8
--- /dev/null
+++ b/lib/api/model/permission.dart
@@ -0,0 +1,354 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'permission.g.dart';
+
+/// Metadata about how to interpret the various group-based permission settings.
+///
+/// This is the type that [InitialSnapshot.serverSupportedPermissionSettings]
+/// would have, according to the API as it exists as of 2025-08;
+/// but that API is documented as unstable and subject to change.
+///
+/// For a useful value of this type, see [SupportedPermissionSettings.fixture].
+///
+/// For docs, search for "d_perm" in: https://zulip.com/api/register-queue
+@JsonSerializable(fieldRename: FieldRename.snake)
+class SupportedPermissionSettings {
+  final Map<String, PermissionSettingsItem> realm;
+  final Map<String, PermissionSettingsItem> stream;
+  final Map<String, PermissionSettingsItem> group;
+
+  /// Metadata about how to interpret certain group-based permission settings,
+  /// including all those that this client uses, based on "current" servers.
+  ///
+  /// "Current" here means as of when this code was written, or last updated;
+  /// details in comments below.  Naturally it'd be better to have an API to
+  /// get this information from the actual server.
+  ///
+  /// Effectively we're counting on it being uncommon for the metadata for a
+  /// given permission to ever change from one server version to the next,
+  /// so that the values we take from one server version usually remain valid
+  /// for all past and future server versions that have the corresponding
+  /// permission at all.
+  ///
+  /// TODO(server): Stabilize [InitialSnapshot.serverSupportedPermissionSettings]
+  ///   or a similar API, and switch to using that.  See thread:
+  ///     https://chat.zulip.org/#narrow/channel/378-api-design/topic/server_supported_permission_settings/near/2247549
+  // TODO: When we get this data from the server, it will sometimes be missing
+  //   items that appear here, because they're for newer permissions that the
+  //   server doesn't know about. We'll want reasonable fallback behavior for
+  //   those missing items, and as a source for that, we can still record
+  //   current-server data somewhere in our codebase. Discussion:
+  //     https://github.com/zulip/zulip-flutter/pull/1842#discussion_r2331337006
+  static SupportedPermissionSettings fixture = SupportedPermissionSettings(
+    realm: {
+      // From the server's Realm.REALM_PERMISSION_GROUP_SETTINGS,
+      // in zerver/models/realms.py.  Current as of 6ab30fcce, 2025-08.
+      'create_multiuse_invite_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.administrators,
+      ),
+      'can_access_all_users_group': PermissionSettingsItem(
+          // require_system_group=True,
+          // allow_nobody_group=False,
+          allowEveryoneGroup: true,
+          defaultGroupName: SystemGroupName.everyone,
+          // # Note that user_can_access_all_other_users in the web
+          // # app is relying on members always have access.
+          // allowed_system_groups=[SystemGroups.EVERYONE, SystemGroups.MEMBERS],
+      ),
+      'can_add_subscribers_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.members,
+      ),
+      'can_add_custom_emoji_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.members,
+      ),
+      'can_create_bots_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.members,
+      ),
+      'can_create_groups': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.members,
+      ),
+      'can_create_public_channel_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.members,
+      ),
+      'can_create_private_channel_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.members,
+      ),
+      'can_create_web_public_channel_group': PermissionSettingsItem(
+          // require_system_group=True,
+          // allow_nobody_group=True,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.owners,
+          // allowed_system_groups=[
+          //     SystemGroups.MODERATORS,
+          //     SystemGroups.ADMINISTRATORS,
+          //     SystemGroups.OWNERS,
+          //     SystemGroups.NOBODY,
+          // ],
+      ),
+      'can_create_write_only_bots_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.members,
+      ),
+      'can_delete_any_message_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.administrators,
+      ),
+      'can_delete_own_message_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: true,
+          defaultGroupName: SystemGroupName.everyone,
+      ),
+      'can_invite_users_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.members,
+      ),
+      'can_manage_all_groups': PermissionSettingsItem(
+          // allow_nobody_group=False,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.owners,
+      ),
+      'can_manage_billing_group': PermissionSettingsItem(
+          // allow_nobody_group=False,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.administrators,
+      ),
+      'can_mention_many_users_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: true,
+          defaultGroupName: SystemGroupName.administrators,
+      ),
+      'can_move_messages_between_channels_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.members,
+      ),
+      'can_move_messages_between_topics_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: true,
+          defaultGroupName: SystemGroupName.everyone,
+      ),
+      'can_resolve_topics_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: true,
+          defaultGroupName: SystemGroupName.everyone,
+      ),
+      'can_set_delete_message_policy_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.moderators,
+      ),
+      'can_set_topics_policy_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: true,
+          defaultGroupName: SystemGroupName.members,
+      ),
+      'can_summarize_topics_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: true,
+          defaultGroupName: SystemGroupName.everyone,
+      ),
+      'direct_message_initiator_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: true,
+          defaultGroupName: SystemGroupName.everyone,
+      ),
+      'direct_message_permission_group': PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: true,
+          defaultGroupName: SystemGroupName.everyone,
+      ),
+    },
+    group: {}, // Please go ahead and fill this in when we come to need it.
+    stream: {
+      // From the server's Stream.stream_permission_group_settings,
+      // in zerver/models/streams.py.  Current as of f9dc13014, 2025-08.
+      "can_add_subscribers_group": PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.nobody,
+      ),
+      "can_administer_channel_group": PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: false,
+          defaultGroupName: PseudoSystemGroupName.streamCreatorOrNobody,
+      ),
+      "can_delete_any_message_group": PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: true,
+          defaultGroupName: SystemGroupName.nobody,
+      ),
+      "can_delete_own_message_group": PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: true,
+          defaultGroupName: SystemGroupName.nobody,
+      ),
+      "can_move_messages_out_of_channel_group": PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: true,
+          defaultGroupName: SystemGroupName.nobody,
+      ),
+      "can_move_messages_within_channel_group": PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: true,
+          defaultGroupName: SystemGroupName.nobody,
+      ),
+      "can_remove_subscribers_group": PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: true,
+          defaultGroupName: SystemGroupName.administrators,
+      ),
+      "can_send_message_group": PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: true,
+          defaultGroupName: SystemGroupName.everyone,
+      ),
+      "can_subscribe_group": PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: false,
+          defaultGroupName: SystemGroupName.nobody,
+      ),
+      "can_resolve_topics_group": PermissionSettingsItem(
+          // allow_nobody_group=True,
+          allowEveryoneGroup: true,
+          defaultGroupName: SystemGroupName.nobody,
+      ),
+    },
+  );
+
+  SupportedPermissionSettings({required this.realm, required this.stream, required this.group});
+
+  factory SupportedPermissionSettings.fromJson(Map<String, dynamic> json) =>
+    _$SupportedPermissionSettingsFromJson(json);
+
+  Map<String, dynamic> toJson() => _$SupportedPermissionSettingsToJson(this);
+}
+
+@JsonSerializable(fieldRename: FieldRename.snake)
+class PermissionSettingsItem {
+  final bool allowEveryoneGroup;
+
+  final DefaultGroupName defaultGroupName;
+
+  // also other fields not yet used
+
+  PermissionSettingsItem({
+    required this.allowEveryoneGroup,
+    required this.defaultGroupName,
+  });
+
+  factory PermissionSettingsItem.fromJson(Map<String, dynamic> json) =>
+    _$PermissionSettingsItemFromJson(json);
+
+  Map<String, dynamic> toJson() => _$PermissionSettingsItemToJson(this);
+}
+
+/// A value of [PermissionSettingsItem.defaultGroupName].
+///
+/// Can be any of these:
+/// - a known system group [SystemGroupName]
+/// - a known special string [PseudoSystemGroupName]
+/// - an unknown system group or special string [DefaultGroupNameUnknown]
+sealed class DefaultGroupName {
+  DefaultGroupName();
+
+  factory DefaultGroupName.fromJson(String json) {
+    final DefaultGroupName? maybeResult = json.startsWith('role:')
+      ? SystemGroupName.fromJson(json)
+      : PseudoSystemGroupName.fromJson(json);
+    return maybeResult ?? DefaultGroupNameUnknown(json);
+  }
+
+  String toJson();
+}
+
+class DefaultGroupNameUnknown extends DefaultGroupName {
+  DefaultGroupNameUnknown(this.apiValue);
+  final String apiValue;
+
+  @override
+  String toJson() => apiValue;
+}
+
+/// A known special string
+/// that [PermissionSettingsItem.defaultGroupName] might be.
+///
+/// See server implementation, e.g.
+/// `can_administer_channel_group` in zerver/models/streams.py.
+@JsonEnum(valueField: 'apiValue', alwaysCreate: true)
+enum PseudoSystemGroupName implements DefaultGroupName {
+  // Discussion on this; it looks like it might get renamed:
+  //   https://chat.zulip.org/#narrow/channel/378-api-design/topic/stream_creator_or_nobody/near/2258637
+  streamCreatorOrNobody(apiValue: 'stream_creator_or_nobody'),
+  ;
+
+  const PseudoSystemGroupName({required this.apiValue});
+
+  final String apiValue;
+
+  /// Get a [PseudoSystemGroupName] from an [apiValue],
+  /// or null if it's not recognized.
+  ///
+  /// Example:
+  ///   'stream_creator_or_nobody' -> PseudoSystemGroupName.streamCreatorOrNobody
+  static PseudoSystemGroupName? fromJson(String json) => _byApiValue[json];
+
+  // _$…EnumMap is thanks to `alwaysCreate: true`
+  static final _byApiValue = _$PseudoSystemGroupNameEnumMap
+    .map((key, value) => MapEntry(value, key));
+
+  @override
+  String toJson() => _$PseudoSystemGroupNameEnumMap[this]!;
+}
+
+/// A known canonical name for a system group.
+///
+/// Doc: https://zulip.com/api/group-setting-values#system-groups
+@JsonEnum(valueField: 'apiValue', alwaysCreate: true)
+enum SystemGroupName implements DefaultGroupName {
+  // TODO(#1096) audit all references when implementing public-access option
+  everyoneOnInternet(apiValue: 'role:internet'),
+
+  everyone(apiValue: 'role:everyone'),
+  members(apiValue: 'role:members'),
+  fullMembers(apiValue: 'role:fullmembers'),
+  moderators(apiValue: 'role:moderators'),
+  administrators(apiValue: 'role:administrators'),
+  owners(apiValue: 'role:owners'),
+  nobody(apiValue: 'role:nobody'),
+  ;
+
+  const SystemGroupName({required this.apiValue});
+
+  final String apiValue;
+
+  /// Get a [SystemGroupName] from an [apiValue],
+  /// or null if it's not recognized.
+  ///
+  /// Example:
+  ///   'role:administrators' -> SystemGroupName.administrators
+  static SystemGroupName? fromJson(String json) => _byApiValue[json];
+
+  // _$…EnumMap is thanks to `alwaysCreate: true`
+  static final _byApiValue = _$SystemGroupNameEnumMap
+    .map((key, value) => MapEntry(value, key));
+
+  @override
+  String toJson() => _$SystemGroupNameEnumMap[this]!;
+}
diff --git a/lib/api/model/permission.g.dart b/lib/api/model/permission.g.dart
new file mode 100644
index 0000000000..280822f58f
--- /dev/null
+++ b/lib/api/model/permission.g.dart
@@ -0,0 +1,65 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+// ignore_for_file: constant_identifier_names, unnecessary_cast
+
+part of 'permission.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+SupportedPermissionSettings _$SupportedPermissionSettingsFromJson(
+  Map<String, dynamic> json,
+) => SupportedPermissionSettings(
+  realm: (json['realm'] as Map<String, dynamic>).map(
+    (k, e) =>
+        MapEntry(k, PermissionSettingsItem.fromJson(e as Map<String, dynamic>)),
+  ),
+  stream: (json['stream'] as Map<String, dynamic>).map(
+    (k, e) =>
+        MapEntry(k, PermissionSettingsItem.fromJson(e as Map<String, dynamic>)),
+  ),
+  group: (json['group'] as Map<String, dynamic>).map(
+    (k, e) =>
+        MapEntry(k, PermissionSettingsItem.fromJson(e as Map<String, dynamic>)),
+  ),
+);
+
+Map<String, dynamic> _$SupportedPermissionSettingsToJson(
+  SupportedPermissionSettings instance,
+) => <String, dynamic>{
+  'realm': instance.realm,
+  'stream': instance.stream,
+  'group': instance.group,
+};
+
+PermissionSettingsItem _$PermissionSettingsItemFromJson(
+  Map<String, dynamic> json,
+) => PermissionSettingsItem(
+  allowEveryoneGroup: json['allow_everyone_group'] as bool,
+  defaultGroupName: DefaultGroupName.fromJson(
+    json['default_group_name'] as String,
+  ),
+);
+
+Map<String, dynamic> _$PermissionSettingsItemToJson(
+  PermissionSettingsItem instance,
+) => <String, dynamic>{
+  'allow_everyone_group': instance.allowEveryoneGroup,
+  'default_group_name': instance.defaultGroupName,
+};
+
+const _$PseudoSystemGroupNameEnumMap = {
+  PseudoSystemGroupName.streamCreatorOrNobody: 'stream_creator_or_nobody',
+};
+
+const _$SystemGroupNameEnumMap = {
+  SystemGroupName.everyoneOnInternet: 'role:internet',
+  SystemGroupName.everyone: 'role:everyone',
+  SystemGroupName.members: 'role:members',
+  SystemGroupName.fullMembers: 'role:fullmembers',
+  SystemGroupName.moderators: 'role:moderators',
+  SystemGroupName.administrators: 'role:administrators',
+  SystemGroupName.owners: 'role:owners',
+  SystemGroupName.nobody: 'role:nobody',
+};
diff --git a/lib/model/channel.dart b/lib/model/channel.dart
index e4a40790d4..0403ba0bdf 100644
--- a/lib/model/channel.dart
+++ b/lib/model/channel.dart
@@ -174,25 +174,14 @@ mixin ChannelStore on UserStore {
 
   bool _selfHasContentAccessViaGroupPermissions(ZulipStream channel) {
     // Compare web's stream_data.has_content_access_via_group_permissions.
-    // TODO(#814) try to clean up this logic; perhaps record more explicitly
-    //   what default/fallback value to use for a given group-based permission
-    //   on older servers.
-
-    if (channel.canAddSubscribersGroup != null
-        && selfHasPermissionForGroupSetting(channel.canAddSubscribersGroup!,
-             GroupSettingType.stream, 'can_add_subscribers_group')) {
-      // The behavior before this permission was introduced was equivalent to
-      // the "nobody" group.
-      // TODO(server-10): simplify
+
+    if (selfHasPermissionForGroupSetting(channel.canAddSubscribersGroup,
+          GroupSettingType.stream, 'can_add_subscribers_group')) {
       return true;
     }
 
-    if (channel.canSubscribeGroup != null
-        && selfHasPermissionForGroupSetting(channel.canSubscribeGroup!,
-             GroupSettingType.stream, 'can_subscribe_group')) {
-      // The behavior before this permission was introduced was equivalent to
-      // the "nobody" group.
-      // TODO(server-10): simplify
+    if (selfHasPermissionForGroupSetting(channel.canSubscribeGroup,
+          GroupSettingType.stream, 'can_subscribe_group')) {
       return true;
     }
 
@@ -213,7 +202,7 @@ mixin ChannelStore on UserStore {
       case ChannelPostPolicy.fullMembers:     {
         if (!role.isAtLeast(UserRole.member)) return false;
         if (role == UserRole.member) {
-          return hasPassedWaitingPeriod(selfUser, byDate: byDate);
+          return selfHasPassedWaitingPeriod(byDate: byDate);
         }
         return true;
       }
diff --git a/lib/model/message.dart b/lib/model/message.dart
index 9cc4d76f48..68c20c0d65 100644
--- a/lib/model/message.dart
+++ b/lib/model/message.dart
@@ -107,18 +107,14 @@ mixin MessageStore on ChannelStore {
       return false;
     }
 
-    // TODO(#1850) really the default should be `role:administrators`:
-    //   https://github.com/zulip/zulip-flutter/pull/1842#discussion_r2331362461
-    if (realmCanDeleteAnyMessageGroup != null
-        && selfHasPermissionForGroupSetting(realmCanDeleteAnyMessageGroup!,
-             GroupSettingType.realm, 'can_delete_any_message_group')) {
+    if (selfHasPermissionForGroupSetting(realmCanDeleteAnyMessageGroup,
+          GroupSettingType.realm, 'can_delete_any_message_group')) {
       return true;
     }
 
     if (channel != null) {
-      if (channel.canDeleteAnyMessageGroup != null
-          && selfHasPermissionForGroupSetting(channel.canDeleteAnyMessageGroup!,
-               GroupSettingType.stream, 'can_delete_any_message_group')) {
+      if (selfHasPermissionForGroupSetting(channel.canDeleteAnyMessageGroup,
+            GroupSettingType.stream, 'can_delete_any_message_group')) {
         return true;
       }
     }
@@ -137,6 +133,9 @@ mixin MessageStore on ChannelStore {
     // that's impossible here because `message` can't be an [OutboxMessage]
     // (it's a [Message] from [MessageStore.messages]).
 
+    // (selfHasPermissionForGroupSetting isn't equipped to handle the old-server
+    // fallback logic for this specific permission; it's dynamic and depends on
+    // realmDeleteOwnMessagePolicy, so we do our own null check here.)
     if (realmCanDeleteOwnMessageGroup != null) {
       if (!selfHasPermissionForGroupSetting(realmCanDeleteOwnMessageGroup!,
             GroupSettingType.realm, 'can_delete_own_message_group')) {
@@ -145,11 +144,8 @@ mixin MessageStore on ChannelStore {
           return false;
         }
 
-        if (
-          channel.canDeleteOwnMessageGroup == null
-          || !selfHasPermissionForGroupSetting(channel.canDeleteOwnMessageGroup!,
-               GroupSettingType.stream, 'can_delete_own_message_group')
-        ) {
+        if (!selfHasPermissionForGroupSetting(channel.canDeleteOwnMessageGroup,
+              GroupSettingType.stream, 'can_delete_own_message_group')) {
           return false;
         }
       }
@@ -185,7 +181,7 @@ mixin MessageStore on ChannelStore {
       case RealmDeleteOwnMessagePolicy.fullMembers: {
         if (!role.isAtLeast(UserRole.member)) return false;
         if (role == UserRole.member) {
-          return hasPassedWaitingPeriod(selfUser, byDate: atDate);
+          return selfHasPassedWaitingPeriod(byDate: atDate);
         }
         return true;
       }
diff --git a/lib/model/realm.dart b/lib/model/realm.dart
index 782740405d..48036a343a 100644
--- a/lib/model/realm.dart
+++ b/lib/model/realm.dart
@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
 import '../api/model/events.dart';
 import '../api/model/initial_snapshot.dart';
 import '../api/model/model.dart';
+import '../api/model/permission.dart';
 import 'store.dart';
 import 'user_group.dart';
 
@@ -128,9 +129,22 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore {
     return topic;
   }
 
+  /// Whether the self-user has passed the realm's waiting period
+  /// to be a full member.
+  ///
+  /// See:
+  ///   https://zulip.com/api/roles-and-permissions#determining-if-a-user-is-a-full-member
+  ///
+  /// To determine if the self-user is a full member,
+  /// callers must also check that the user's role is at least [UserRole.member].
+  bool selfHasPassedWaitingPeriod({required DateTime byDate});
+
   /// Whether the self-user has the given (group-based) permission.
-  bool selfHasPermissionForGroupSetting(GroupSettingValue value,
-      GroupSettingType type, String name);
+  ///
+  /// If the server doesn't know about the permission,
+  /// pass null for [value] and a reasonable default will be chosen.
+  bool selfHasPermissionForGroupSetting(GroupSettingValue? value,
+    GroupSettingType type, String name);
 }
 
 enum GroupSettingType { realm, stream, group }
@@ -180,7 +194,10 @@ mixin ProxyRealmStore on RealmStore {
   @override
   List<CustomProfileField> get customProfileFields => realmStore.customProfileFields;
   @override
-  bool selfHasPermissionForGroupSetting(GroupSettingValue value, GroupSettingType type, String name) =>
+  bool selfHasPassedWaitingPeriod({required DateTime byDate}) =>
+    realmStore.selfHasPassedWaitingPeriod(byDate: byDate);
+  @override
+  bool selfHasPermissionForGroupSetting(GroupSettingValue? value, GroupSettingType type, String name) =>
     realmStore.selfHasPermissionForGroupSetting(value, type, name);
 }
 
@@ -203,6 +220,7 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
     required User selfUser,
   }) :
     _selfUserRole = selfUser.role,
+    _selfUserDateJoined = selfUser.dateJoined,
     serverPresencePingIntervalSeconds = initialSnapshot.serverPresencePingIntervalSeconds,
     serverPresenceOfflineThresholdSeconds = initialSnapshot.serverPresenceOfflineThresholdSeconds,
     serverTypingStartedExpiryPeriodMilliseconds = initialSnapshot.serverTypingStartedExpiryPeriodMilliseconds,
@@ -225,7 +243,23 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
     customProfileFields = _sortCustomProfileFields(initialSnapshot.customProfileFields);
 
   @override
-  bool selfHasPermissionForGroupSetting(GroupSettingValue value,
+  bool selfHasPassedWaitingPeriod({required DateTime byDate}) {
+    // [User.dateJoined] is in UTC. For logged-in users, the format is:
+    // YYYY-MM-DDTHH:mm+00:00, which includes the timezone offset for UTC.
+    // For logged-out spectators, the format is: YYYY-MM-DD, which doesn't
+    // include the timezone offset. In the later case, [DateTime.parse] will
+    // interpret it as the client's local timezone, which could lead to
+    // incorrect results; but that's acceptable for now because the app
+    // doesn't support viewing as a spectator.
+    //
+    // See the related discussion:
+    //   https://chat.zulip.org/#narrow/channel/412-api-documentation/topic/provide.20an.20explicit.20format.20for.20.60realm_user.2Edate_joined.60/near/1980194
+    final dateJoined = DateTime.parse(_selfUserDateJoined);
+    return byDate.difference(dateJoined).inDays >= realmWaitingPeriodThreshold;
+  }
+
+  @override
+  bool selfHasPermissionForGroupSetting(GroupSettingValue? value,
       GroupSettingType type, String name) {
     // Compare web's settings_data.user_has_permission_for_group_setting.
     //
@@ -236,13 +270,60 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
     // That exists for deciding whether to offer the "Generate email address"
     // button, and if so then which users to offer in the dropdown;
     // it's predicting whether /api/get-stream-email-address would succeed.
-    if (_selfUserRole == UserRole.guest) {
-      final config = _groupSettingConfig(type, name);
-      if (!config.allowEveryoneGroup) return false;
+
+    final config = _groupSettingConfig(type, name);
+
+    if (_selfUserRole == UserRole.guest && !config.allowEveryoneGroup) {
+      return false;
     }
+
+    if (value == null) {
+      // The server doesn't know about the permission. *We* know about it
+      // (or presumably we wouldn't have called this method),
+      // and we know a reasonable default; use that.
+      return _hasPermissionByDefault(config);
+    }
+
     return selfInGroupSetting(value);
   }
 
+  bool _hasPermissionByDefault(PermissionSettingsItem config) {
+    switch (config.defaultGroupName) {
+      case DefaultGroupNameUnknown():
+        // When we know about a permission, we should also know about the group
+        // we've said is the default value for it.
+        assert(false);
+        return true;
+      case PseudoSystemGroupName.streamCreatorOrNobody:
+        // TODO(#1102) implement
+        assert(() {
+          throw UnimplementedError();
+        }());
+        return true;
+      case SystemGroupName.everyoneOnInternet:
+      case SystemGroupName.everyone:
+        return true;
+      case SystemGroupName.members:
+        return _selfUserRole.isAtLeast(UserRole.member);
+      case SystemGroupName.fullMembers:
+        // There aren't any permissions where this is the default, and we
+        // probably won't add any. So for now we skip the complication of
+        // doing the waiting-period check.
+        assert(() {
+          throw UnimplementedError();
+        }());
+        return _selfUserRole.isAtLeast(UserRole.member);
+      case SystemGroupName.moderators:
+        return _selfUserRole.isAtLeast(UserRole.moderator);
+      case SystemGroupName.administrators:
+        return _selfUserRole.isAtLeast(UserRole.administrator);
+      case SystemGroupName.owners:
+        return _selfUserRole.isAtLeast(UserRole.owner);
+      case SystemGroupName.nobody:
+        return false;
+    }
+  }
+
   /// The metadata for how to interpret the given group-based permission setting.
   PermissionSettingsItem _groupSettingConfig(GroupSettingType type, String name) {
     final supportedSettings = SupportedPermissionSettings.fixture;
@@ -262,8 +343,20 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
   /// The main home of this information is [UserStore]: `store.selfUser.role`.
   /// We need it here for interpreting some permission settings;
   /// so we denormalize it here to avoid a cycle between substores.
+  ///
+  /// See also [_selfUserDateJoined].
   UserRole _selfUserRole;
 
+  /// The [User.dateJoined] of the self-user.
+  ///
+  /// The main home of this information is [UserStore]:
+  /// `store.selfUser.dateJoined`.
+  /// We need it here for interpreting some permission settings;
+  /// so we denormalize it here to avoid a cycle between substores.
+  ///
+  /// See also [_selfUserRole].
+  final String _selfUserDateJoined;
+
   @override
   final int serverPresencePingIntervalSeconds;
   @override
diff --git a/lib/model/user.dart b/lib/model/user.dart
index 4983cf48ca..ef26b9e589 100644
--- a/lib/model/user.dart
+++ b/lib/model/user.dart
@@ -91,28 +91,6 @@ mixin UserStore on PerAccountStoreBase, RealmStore {
     return getUser(senderId)?.fullName ?? message.senderFullName;
   }
 
-  /// Whether [user] has passed the realm's waiting period to be a full member.
-  ///
-  /// See:
-  ///   https://zulip.com/api/roles-and-permissions#determining-if-a-user-is-a-full-member
-  ///
-  /// To determine if a user is a full member, callers must also check that the
-  /// user's role is at least [UserRole.member].
-  bool hasPassedWaitingPeriod(User user, {required DateTime byDate}) {
-    // [User.dateJoined] is in UTC. For logged-in users, the format is:
-    // YYYY-MM-DDTHH:mm+00:00, which includes the timezone offset for UTC.
-    // For logged-out spectators, the format is: YYYY-MM-DD, which doesn't
-    // include the timezone offset. In the later case, [DateTime.parse] will
-    // interpret it as the client's local timezone, which could lead to
-    // incorrect results; but that's acceptable for now because the app
-    // doesn't support viewing as a spectator.
-    //
-    // See the related discussion:
-    //   https://chat.zulip.org/#narrow/channel/412-api-documentation/topic/provide.20an.20explicit.20format.20for.20.60realm_user.2Edate_joined.60/near/1980194
-    final dateJoined = DateTime.parse(user.dateJoined);
-    return byDate.difference(dateJoined).inDays >= realmWaitingPeriodThreshold;
-  }
-
   /// Whether the user with [userId] is muted by the self-user.
   ///
   /// Looks for [userId] in a private [Set],


Test patch:
diff --git a/test/model/message_test.dart b/test/model/message_test.dart
index 7dbf28a47d..4eccf75112 100644
--- a/test/model/message_test.dart
+++ b/test/model/message_test.dart
@@ -868,7 +868,7 @@ void main() {
   group('selfCanDeleteMessage', () {
     /// Call the method, with setup from [params].
     Future<bool> evaluate(CanDeleteMessageParams params) async {
-      final selfUser = eg.selfUser;
+      final selfUser = eg.user(role: params.selfUserRole);
       final botUserOwnedBySelf = eg.user(isBot: true, botOwnerId: selfUser.userId);
       final botUserNotOwnedBySelf = eg.user(isBot: true, botOwnerId: eg.otherUser.userId);
 
@@ -1078,8 +1078,8 @@ void main() {
     group('legacy behavior', () {
       group('pre-407', () {
         // The channel-level group permissions don't exist,
-        // so we act as though they were present and denied,
-        // notably by not throwing.
+        // so we act as though they were present with role:nobody,
+        // and we don't throw.
 
         test('denial is not forced just because one of the permissions is absent (the any-message one)', () async {
           check(await evaluate(
@@ -1127,7 +1127,7 @@ void main() {
         // doesn't exist, so we follow realmDeleteOwnMessagePolicy instead,
         // and we don't error.
 
-        test('allowed', () async {
+        test('allowed (permissive policy, low role)', () async {
           check(await evaluate(
             CanDeleteMessageParams.pre291(
               senderConfig: CanDeleteMessageSenderConfig.self,
@@ -1135,6 +1135,27 @@ void main() {
               inRealmCanDeleteAnyMessageGroup: false,
               isChannelArchived: false,
               realmDeleteOwnMessagePolicy: RealmDeleteOwnMessagePolicy.everyone,
+              selfUserRole: UserRole.member,
+          )))
+            ..equals(await evaluate(
+             CanDeleteMessageParams.pre407(
+               senderConfig: CanDeleteMessageSenderConfig.self,
+               timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited,
+               inRealmCanDeleteAnyMessageGroup: false,
+               inRealmCanDeleteOwnMessageGroup: true,
+               isChannelArchived: false)))
+            ..isTrue();
+        });
+
+        test('allowed (strict policy, high role)', () async {
+          check(await evaluate(
+            CanDeleteMessageParams.pre291(
+              senderConfig: CanDeleteMessageSenderConfig.self,
+              timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited,
+              inRealmCanDeleteAnyMessageGroup: false,
+              isChannelArchived: false,
+              realmDeleteOwnMessagePolicy: RealmDeleteOwnMessagePolicy.admins,
+              selfUserRole: UserRole.administrator,
           )))
             ..equals(await evaluate(
              CanDeleteMessageParams.pre407(
@@ -1154,6 +1175,7 @@ void main() {
               inRealmCanDeleteAnyMessageGroup: false,
               isChannelArchived: false,
               realmDeleteOwnMessagePolicy: RealmDeleteOwnMessagePolicy.admins,
+              selfUserRole: UserRole.moderator,
           )))..equals(await evaluate(
               CanDeleteMessageParams.pre407(
                 senderConfig: CanDeleteMessageSenderConfig.self,
@@ -1166,26 +1188,47 @@ void main() {
       });
 
       group('pre-281', () {
-        // The realm-level can-delete-any-message permission
-        // doesn't exist, so we act as though that's present and denied,
-        // notably by not throwing.
+        // The realm-level can-delete-any-message permission doesn't exist,
+        // so we act as though that's present with role:administrators,
+        // and we don't throw.
 
-        test('denied', () async {
+        test('self-user is not admin', () async {
           check(await evaluate(
             CanDeleteMessageParams.pre281(
               senderConfig: CanDeleteMessageSenderConfig.otherHuman,
               timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited,
               isChannelArchived: false,
               realmDeleteOwnMessagePolicy: RealmDeleteOwnMessagePolicy.everyone,
+              selfUserRole: UserRole.member,
           )))..equals(await evaluate(
               CanDeleteMessageParams.pre291(
                 senderConfig: CanDeleteMessageSenderConfig.otherHuman,
                 timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited,
                 inRealmCanDeleteAnyMessageGroup: false,
                 isChannelArchived: false,
-                realmDeleteOwnMessagePolicy: RealmDeleteOwnMessagePolicy.everyone)))
+                realmDeleteOwnMessagePolicy: RealmDeleteOwnMessagePolicy.everyone,
+                selfUserRole: UserRole.member)))
             ..isFalse();
         });
+
+        test('self-user is admin', () async {
+          check(await evaluate(
+            CanDeleteMessageParams.pre281(
+              senderConfig: CanDeleteMessageSenderConfig.otherHuman,
+              timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited,
+              isChannelArchived: false,
+              realmDeleteOwnMessagePolicy: RealmDeleteOwnMessagePolicy.everyone,
+              selfUserRole: UserRole.administrator,
+          )))..equals(await evaluate(
+              CanDeleteMessageParams.pre291(
+                senderConfig: CanDeleteMessageSenderConfig.otherHuman,
+                timeLimitConfig: CanDeleteMessageTimeLimitConfig.notLimited,
+                inRealmCanDeleteAnyMessageGroup: true,
+                isChannelArchived: false,
+                realmDeleteOwnMessagePolicy: RealmDeleteOwnMessagePolicy.everyone,
+                selfUserRole: UserRole.administrator)))
+            ..isTrue();
+        });
       });
     });
   });
@@ -2006,6 +2049,7 @@ class CanDeleteMessageParams {
   final bool? inChannelCanDeleteAnyMessageGroup;
   final bool? inChannelCanDeleteOwnMessageGroup;
   final RealmDeleteOwnMessagePolicy? realmDeleteOwnMessagePolicy;
+  final UserRole? selfUserRole;
 
   CanDeleteMessageParams._({
     required this.senderConfig,
@@ -2016,6 +2060,7 @@ class CanDeleteMessageParams {
     required this.inChannelCanDeleteAnyMessageGroup,
     required this.inChannelCanDeleteOwnMessageGroup,
     required this.realmDeleteOwnMessagePolicy,
+    required this.selfUserRole,
   });
 
   CanDeleteMessageParams.modern({
@@ -2026,7 +2071,9 @@ class CanDeleteMessageParams {
     required this.isChannelArchived,
     required this.inChannelCanDeleteAnyMessageGroup,
     required this.inChannelCanDeleteOwnMessageGroup,
-  }) : realmDeleteOwnMessagePolicy = null;
+  }) :
+    realmDeleteOwnMessagePolicy = null,
+    selfUserRole = null;
 
   factory CanDeleteMessageParams.restrictiveForChannelMessageExcept({
     CanDeleteMessageSenderConfig? senderConfig,
@@ -2110,6 +2157,7 @@ class CanDeleteMessageParams {
     inChannelCanDeleteAnyMessageGroup: null,
     inChannelCanDeleteOwnMessageGroup: null,
     realmDeleteOwnMessagePolicy: null,
+    selfUserRole: null,
   );
 
   // TODO(server-10) delete
@@ -2119,6 +2167,7 @@ class CanDeleteMessageParams {
     required bool inRealmCanDeleteAnyMessageGroup,
     required bool? isChannelArchived,
     required RealmDeleteOwnMessagePolicy realmDeleteOwnMessagePolicy,
+    required UserRole selfUserRole,
   }) => CanDeleteMessageParams._(
     senderConfig: senderConfig,
     timeLimitConfig: timeLimitConfig,
@@ -2128,6 +2177,7 @@ class CanDeleteMessageParams {
     inChannelCanDeleteAnyMessageGroup: null,
     inChannelCanDeleteOwnMessageGroup: null,
     realmDeleteOwnMessagePolicy: realmDeleteOwnMessagePolicy,
+    selfUserRole: selfUserRole,
   );
 
   // TODO(server-10) delete
@@ -2136,6 +2186,7 @@ class CanDeleteMessageParams {
     required CanDeleteMessageTimeLimitConfig timeLimitConfig,
     required bool? isChannelArchived,
     required RealmDeleteOwnMessagePolicy realmDeleteOwnMessagePolicy,
+    required UserRole selfUserRole,
   }) => CanDeleteMessageParams._(
     senderConfig: senderConfig,
     timeLimitConfig: timeLimitConfig,
@@ -2145,6 +2196,7 @@ class CanDeleteMessageParams {
     inChannelCanDeleteAnyMessageGroup: null,
     inChannelCanDeleteOwnMessageGroup: null,
     realmDeleteOwnMessagePolicy: realmDeleteOwnMessagePolicy,
+    selfUserRole: selfUserRole,
   );
 
   String describe() {
diff --git a/test/model/realm_test.dart b/test/model/realm_test.dart
index 04ceb28855..222a2d1d87 100644
--- a/test/model/realm_test.dart
+++ b/test/model/realm_test.dart
@@ -2,7 +2,9 @@ import 'package:checks/checks.dart';
 import 'package:test/scaffolding.dart';
 import 'package:zulip/api/model/events.dart';
 import 'package:zulip/api/model/model.dart';
+import 'package:zulip/api/model/permission.dart';
 import 'package:zulip/model/realm.dart';
+import 'package:zulip/model/store.dart';
 
 import '../example_data.dart' as eg;
 
@@ -35,6 +37,32 @@ void main() {
     doCheck(eg.t('(no topic)'),          eg.t(''),                    370);
   });
 
+  group('selfHasPassedWaitingPeriod', () {
+    final testCases = [
+      ('2024-11-25T10:00+00:00', DateTime.utc(2024, 11, 25 + 0, 10, 00), false),
+      ('2024-11-25T10:00+00:00', DateTime.utc(2024, 11, 25 + 1, 10, 00), false),
+      ('2024-11-25T10:00+00:00', DateTime.utc(2024, 11, 25 + 2, 09, 59), false),
+      ('2024-11-25T10:00+00:00', DateTime.utc(2024, 11, 25 + 2, 10, 00), true),
+      ('2024-11-25T10:00+00:00', DateTime.utc(2024, 11, 25 + 1000, 07, 00), true),
+    ];
+
+    for (final (String dateJoined, DateTime currentDate, bool expected) in testCases) {
+      test('self-user joined at $dateJoined ${expected ? 'has' : "hasn't"} '
+          'passed waiting period by $currentDate', () {
+        final selfUser = eg.user(dateJoined: dateJoined);
+        final store = eg.store(
+          selfUser: selfUser,
+          initialSnapshot: eg.initialSnapshot(
+            realmWaitingPeriodThreshold: 2,
+            realmUsers: [selfUser],
+          ),
+        );
+        check(store.selfHasPassedWaitingPeriod(byDate: currentDate))
+          .equals(expected);
+      });
+    }
+  });
+
   group('selfHasPermissionForGroupSetting', () {
     // Most of the implementation of this is in [UserGroupStore.selfInGroupSetting],
     // and is tested in more detail in user_group_test.dart .
@@ -82,6 +110,93 @@ void main() {
       check(hasPermission(selfUser, group, 'can_send_message_group'))
         .isFalse();
     });
+
+    group('fallbacks for permissions not known to the server', () {
+      late PerAccountStore store;
+
+      void prepare({UserRole? selfUserRole}) {
+        final selfUser = eg.user(role: selfUserRole);
+        store = eg.store(selfUser: selfUser,
+          initialSnapshot: eg.initialSnapshot(realmUsers: [selfUser]));
+      }
+
+      void doCheck(GroupSettingType type, String name, bool expected) {
+        check(store.selfHasPermissionForGroupSetting(null, type, name)).equals(expected);
+      }
+
+      for (final pseudoSystemGroupName in PseudoSystemGroupName.values) {
+        switch (pseudoSystemGroupName) {
+          case PseudoSystemGroupName.streamCreatorOrNobody:
+            // TODO implement and test
+        }
+      }
+
+      for (final systemGroupName in SystemGroupName.values) {
+        switch (systemGroupName) {
+          case SystemGroupName.everyoneOnInternet:
+            // (No permissions where we use this default value; continue.)
+            break;
+          case SystemGroupName.everyone:
+            test('everyone', () {
+              prepare(selfUserRole: UserRole.guest);
+              doCheck(GroupSettingType.realm, 'can_access_all_users_group', true);
+            });
+          case SystemGroupName.members:
+            test('members, is guest', () {
+              prepare(selfUserRole: UserRole.guest);
+              doCheck(GroupSettingType.realm, 'can_add_custom_emoji_group', false);
+            });
+            test('members, is member', () {
+              prepare(selfUserRole: UserRole.member);
+              doCheck(GroupSettingType.realm, 'can_add_custom_emoji_group', true);
+            });
+          case SystemGroupName.fullMembers:
+            // (No permissions where we use this default value; continue.)
+            break;
+          case SystemGroupName.moderators:
+            test('moderators, is member', () {
+              prepare(selfUserRole: UserRole.member);
+              doCheck(GroupSettingType.realm, 'can_set_delete_message_policy_group', false);
+            });
+            test('moderators, is moderator', () {
+              prepare(selfUserRole: UserRole.moderator);
+              doCheck(GroupSettingType.realm, 'can_set_delete_message_policy_group', true);
+            });
+          case SystemGroupName.administrators:
+            test('administrators, is moderator', () {
+              prepare(selfUserRole: UserRole.moderator);
+              doCheck(GroupSettingType.stream, 'can_remove_subscribers_group', false);
+            });
+            test('administrators, is administrator', () {
+              prepare(selfUserRole: UserRole.administrator);
+              doCheck(GroupSettingType.stream, 'can_remove_subscribers_group', true);
+            });
+          case SystemGroupName.owners:
+            test('owners, is administrator', () {
+              prepare(selfUserRole: UserRole.administrator);
+              doCheck(GroupSettingType.realm, 'can_create_web_public_channel_group', false);
+            });
+            test('owners, is owner', () {
+              prepare(selfUserRole: UserRole.owner);
+              doCheck(GroupSettingType.realm, 'can_create_web_public_channel_group', true);
+            });
+          case SystemGroupName.nobody:
+            test('nobody', () {
+              prepare(selfUserRole: UserRole.owner);
+              doCheck(GroupSettingType.stream, 'can_delete_own_message_group', false);
+            });
+        }
+      }
+
+      test('throw on unknown name', () {
+        // We should know about all the permissions we're trying to implement,
+        // even the ones old servers don't know about.
+        prepare(selfUserRole: UserRole.member);
+        check(() => store.selfHasPermissionForGroupSetting(null,
+          GroupSettingType.realm, 'example_future_permission_name'),
+        ).throws<Error>();
+      });
+    });
   });
 
   group('customProfileFields', () {
diff --git a/test/model/user_test.dart b/test/model/user_test.dart
index 6d5e5c2519..d306d12f0a 100644
--- a/test/model/user_test.dart
+++ b/test/model/user_test.dart
@@ -58,28 +58,6 @@ void main() {
     });
   });
 
-  group('hasPassedWaitingPeriod', () {
-    final store = eg.store(initialSnapshot:
-      eg.initialSnapshot(realmWaitingPeriodThreshold: 2));
-
-    final testCases = [
-      ('2024-11-25T10:00+00:00', DateTime.utc(2024, 11, 25 + 0, 10, 00), false),
-      ('2024-11-25T10:00+00:00', DateTime.utc(2024, 11, 25 + 1, 10, 00), false),
-      ('2024-11-25T10:00+00:00', DateTime.utc(2024, 11, 25 + 2, 09, 59), false),
-      ('2024-11-25T10:00+00:00', DateTime.utc(2024, 11, 25 + 2, 10, 00), true),
-      ('2024-11-25T10:00+00:00', DateTime.utc(2024, 11, 25 + 1000, 07, 00), true),
-    ];
-
-    for (final (String dateJoined, DateTime currentDate, bool hasPassedWaitingPeriod) in testCases) {
-      test('user joined at $dateJoined ${hasPassedWaitingPeriod ? 'has' : "hasn't"} '
-          'passed waiting period by $currentDate', () {
-        final user = eg.user(dateJoined: dateJoined);
-        check(store.hasPassedWaitingPeriod(user, byDate: currentDate))
-          .equals(hasPassedWaitingPeriod);
-      });
-    }
-  });
-
   group('RealmUserUpdateEvent', () {
     // TODO write more tests for handling RealmUserUpdateEvent