From 0ee2e992882ee13c1205df1117c524cf87ba1074 Mon Sep 17 00:00:00 2001 From: seres Date: Tue, 20 Oct 2026 23:18:31 +0200 Subject: [PATCH 1/4] add a global toggle to hide membership events --- lib/controllers/settings_sections.dart | 15 +++++++++++++++ lib/models/settings.dart | 1 + lib/widgets/renderers/event.dart | 12 ++++++++++++ 3 files changed, 28 insertions(+) diff --git a/lib/controllers/settings_sections.dart b/lib/controllers/settings_sections.dart index 1ca07a8..c0bd193 100644 --- a/lib/controllers/settings_sections.dart +++ b/lib/controllers/settings_sections.dart @@ -166,6 +166,21 @@ class SettingsSectionsController : null, ), ), + .new( + title: "Hide Membership Events", + description: "Hides membership events (invites, joins, leaves, bans & knocks) from appearing in conversations.", + icon: Icons.person_off, + builder: (title, description, icon) => SwitchListTile( + title: Text(title), + subtitle: Text(description), + secondary: Icon(icon), + value: settings.hideMembershipEvents, + onChanged: (value) => ref + .read(SettingsController.provider.notifier) + .set(settings.copyWith(hideMembershipEvents: value)) + .onError(showError), + ), + ), ]), ), ]), diff --git a/lib/models/settings.dart b/lib/models/settings.dart index f9198ed..f6cc1c5 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -14,6 +14,7 @@ class const Settings({ final bool showAllInHome = false, final bool showMinimizeButton = false, final bool swipeToReply = false, + final bool hideMembershipEvents = false, final RoomOrder roomOrder = .unreadFirst, }) with _$Settings { Map toJson() => _$SettingsToJson(this); diff --git a/lib/widgets/renderers/event.dart b/lib/widgets/renderers/event.dart index 3ec3d4a..f34448f 100644 --- a/lib/widgets/renderers/event.dart +++ b/lib/widgets/renderers/event.dart @@ -6,6 +6,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/event.dart"; import "package:nexus/controllers/room_chat.dart"; import "package:nexus/controllers/rooms.dart"; +import "package:nexus/controllers/settings.dart"; import "package:nexus/helpers/extensions/show_adaptive_menu.dart"; import "package:nexus/helpers/hooks/chat_scroll.dart"; import "package:nexus/models/content/avatar.dart"; @@ -78,6 +79,17 @@ class const EventRenderer( if (event == null) return SizedBox.shrink(); + final hideMembershipEvents = (ref + .watch(SettingsController.provider) + .maybeWhen( + orElse: () => false, + data: (settings) => settings.hideMembershipEvents ? true : false, + )); + + if (event.type == "m.room.member" && hideMembershipEvents) { + return SizedBox.shrink(); + } + final isGrouped = previousEvent?.content is MessageContent && previousEvent?.redactedBy == null && From 7e85c1078bf0fc02901c8545caaa97917b2c75fd Mon Sep 17 00:00:00 2001 From: seres Date: Tue, 20 Oct 2026 23:44:02 +0200 Subject: [PATCH 2/4] use typed check instead of comparing a string for event type --- lib/widgets/renderers/event.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/renderers/event.dart b/lib/widgets/renderers/event.dart index f34448f..d70f731 100644 --- a/lib/widgets/renderers/event.dart +++ b/lib/widgets/renderers/event.dart @@ -83,10 +83,10 @@ class const EventRenderer( .watch(SettingsController.provider) .maybeWhen( orElse: () => false, - data: (settings) => settings.hideMembershipEvents ? true : false, + data: (settings) => settings.hideMembershipEvents, )); - if (event.type == "m.room.member" && hideMembershipEvents) { + if (event.content is MembershipContent && hideMembershipEvents) { return SizedBox.shrink(); } From 800fb5566d6d8aeb2860a7c7adf691e9bc3e72ab Mon Sep 17 00:00:00 2001 From: seres Date: Tue, 20 Oct 2026 18:15:01 -0400 Subject: [PATCH 3/4] add a global toggle to hide membership events (#142) fixes #141 Co-authored-by: seres Reviewed-on: https://git.federated.nexus/Nexus/nexus/pulls/142 --- lib/controllers/settings_sections.dart | 15 +++++++++++++++ lib/models/settings.dart | 1 + lib/widgets/renderers/event.dart | 12 ++++++++++++ 3 files changed, 28 insertions(+) diff --git a/lib/controllers/settings_sections.dart b/lib/controllers/settings_sections.dart index 1ca07a8..c0bd193 100644 --- a/lib/controllers/settings_sections.dart +++ b/lib/controllers/settings_sections.dart @@ -166,6 +166,21 @@ class SettingsSectionsController : null, ), ), + .new( + title: "Hide Membership Events", + description: "Hides membership events (invites, joins, leaves, bans & knocks) from appearing in conversations.", + icon: Icons.person_off, + builder: (title, description, icon) => SwitchListTile( + title: Text(title), + subtitle: Text(description), + secondary: Icon(icon), + value: settings.hideMembershipEvents, + onChanged: (value) => ref + .read(SettingsController.provider.notifier) + .set(settings.copyWith(hideMembershipEvents: value)) + .onError(showError), + ), + ), ]), ), ]), diff --git a/lib/models/settings.dart b/lib/models/settings.dart index f9198ed..f6cc1c5 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -14,6 +14,7 @@ class const Settings({ final bool showAllInHome = false, final bool showMinimizeButton = false, final bool swipeToReply = false, + final bool hideMembershipEvents = false, final RoomOrder roomOrder = .unreadFirst, }) with _$Settings { Map toJson() => _$SettingsToJson(this); diff --git a/lib/widgets/renderers/event.dart b/lib/widgets/renderers/event.dart index 3ec3d4a..d70f731 100644 --- a/lib/widgets/renderers/event.dart +++ b/lib/widgets/renderers/event.dart @@ -6,6 +6,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/event.dart"; import "package:nexus/controllers/room_chat.dart"; import "package:nexus/controllers/rooms.dart"; +import "package:nexus/controllers/settings.dart"; import "package:nexus/helpers/extensions/show_adaptive_menu.dart"; import "package:nexus/helpers/hooks/chat_scroll.dart"; import "package:nexus/models/content/avatar.dart"; @@ -78,6 +79,17 @@ class const EventRenderer( if (event == null) return SizedBox.shrink(); + final hideMembershipEvents = (ref + .watch(SettingsController.provider) + .maybeWhen( + orElse: () => false, + data: (settings) => settings.hideMembershipEvents, + )); + + if (event.content is MembershipContent && hideMembershipEvents) { + return SizedBox.shrink(); + } + final isGrouped = previousEvent?.content is MessageContent && previousEvent?.redactedBy == null && From a1a15af30d9b1a5a00a5d702b5f92befee873213 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 20 Oct 2026 18:21:05 -0400 Subject: [PATCH 4/4] use dot shorthands for default immutable collections where possible --- lib/controllers/rooms.dart | 2 +- lib/models/account_data.dart | 4 ++-- lib/models/content/power_levels.dart | 2 +- lib/models/event.dart | 4 ++-- lib/models/event_context.dart | 2 +- lib/models/paginate_manual.dart | 2 +- lib/models/profile_response.dart | 2 +- lib/models/requests/create_room.dart | 4 ++-- lib/models/requests/join_room.dart | 2 +- lib/models/requests/send_message.dart | 2 +- lib/models/room.dart | 10 +++++----- lib/models/sync_data.dart | 8 ++++---- lib/widgets/room_menu_button.dart | 2 +- 13 files changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/controllers/rooms.dart b/lib/controllers/rooms.dart index 7a5c181..908e415 100644 --- a/lib/controllers/rooms.dart +++ b/lib/controllers/rooms.dart @@ -35,7 +35,7 @@ class RoomsController extends Notifier> { void update( IMap rooms, [ - ISet leftRooms = const ISet.empty(), + ISet leftRooms = const .empty(), ]) { final merged = rooms.entries.fold(state, (acc, entry) { final roomId = entry.key; diff --git a/lib/models/account_data.dart b/lib/models/account_data.dart index 955d8d1..0ab5990 100644 --- a/lib/models/account_data.dart +++ b/lib/models/account_data.dart @@ -12,7 +12,7 @@ class const AccountData({ const InvitePermissionConfig(), @JsonKey(name: AccountData.directKey) - final IMap> directMessages = const IMap.empty(), + final IMap> directMessages = const .empty(), @JsonKey(name: AccountData.recentEmojiKey) final RecentEmojiData recentEmoji = const RecentEmojiData(), @@ -30,7 +30,7 @@ class const AccountData({ @Freezed(toJson: false, fromJson: false) @JsonSerializable() class const RecentEmojiData({ - final IList recentEmoji = const IList.empty(), + final IList recentEmoji = const .empty(), }) with _$RecentEmojiData { Map toJson() => _$RecentEmojiDataToJson(this); diff --git a/lib/models/content/power_levels.dart b/lib/models/content/power_levels.dart index 5ae6561..0df21c9 100644 --- a/lib/models/content/power_levels.dart +++ b/lib/models/content/power_levels.dart @@ -28,7 +28,7 @@ sealed class PowerLevelsContent extends Content with _$PowerLevelsContent { @JsonSerializable() class const Notifications({ final int room = 50, - final IMap other = const IMap.empty(), + final IMap other = const .empty(), }) with _$Notifications { Map toJson() => _$NotificationsToJson(this); diff --git a/lib/models/event.dart b/lib/models/event.dart index 18ad11b..93352ca 100644 --- a/lib/models/event.dart +++ b/lib/models/event.dart @@ -18,7 +18,7 @@ class const Event({ required final String sender, @JsonKey(readValue: Event.typeJsonFromJson) required final String type, @EpochDateTimeConverter() required final DateTime timestamp, - final IMap unsigned = const IMap.empty(), + final IMap unsigned = const .empty(), final LocalContent? localContent, final String? transactionId, final String? redactedBy, @@ -27,7 +27,7 @@ class const Event({ final String? replyTo, final String? decryptionError, final String? sendError, - final IMap reactions = const IMap.empty(), + final IMap reactions = const .empty(), @JsonKey(name: "last_edit_rowid") final int lastEditRowId = 0, final UnreadType? unreadType, final Profile? pmp, diff --git a/lib/models/event_context.dart b/lib/models/event_context.dart index 6103c7d..e4ae517 100644 --- a/lib/models/event_context.dart +++ b/lib/models/event_context.dart @@ -13,7 +13,7 @@ class const EventContext({ required final IList after, required final String start, required final String end, - final IList relatedEvents = const IList.empty(), + final IList relatedEvents = const .empty(), }) with _$EventContext { Map toJson() => _$EventContextToJson(this); diff --git a/lib/models/paginate_manual.dart b/lib/models/paginate_manual.dart index 9f45c1f..752e09c 100644 --- a/lib/models/paginate_manual.dart +++ b/lib/models/paginate_manual.dart @@ -9,7 +9,7 @@ part "paginate_manual.g.dart"; @JsonSerializable() class const PaginateManual({ required final IList events, - final IList relatedEvents = const IList.empty(), + final IList relatedEvents = const .empty(), required final String? nextBatch, }) with _$PaginateManual { Map toJson() => _$PaginateManualToJson(this); diff --git a/lib/models/profile_response.dart b/lib/models/profile_response.dart index 57f76c5..3896e32 100644 --- a/lib/models/profile_response.dart +++ b/lib/models/profile_response.dart @@ -40,7 +40,7 @@ class const Profile({ final String? timezone, @JsonKey(readValue: Profile.readPronouns, name: "m.pronouns") - final IList pronouns = const IList.empty(), + final IList pronouns = const .empty(), }) with _$Profile { Map toJson() => _$ProfileToJson(this); diff --git a/lib/models/requests/create_room.dart b/lib/models/requests/create_room.dart index d2fbcc9..262ee73 100644 --- a/lib/models/requests/create_room.dart +++ b/lib/models/requests/create_room.dart @@ -13,10 +13,10 @@ class const CreateRoomRequest({ final String? roomAliasName, final String? name, final String? topic, - final IList invite = const IList.empty(), + final IList invite = const .empty(), @JsonKey(name: "uk.timedout.msc4491.invite_reason") final String? inviteReason, - final IList initialState = const IList.empty(), + final IList initialState = const .empty(), final RoomPreset? preset, final bool isDirect = false, final IMap? creationContent, diff --git a/lib/models/requests/join_room.dart b/lib/models/requests/join_room.dart index 3cdbc88..22c4501 100644 --- a/lib/models/requests/join_room.dart +++ b/lib/models/requests/join_room.dart @@ -8,7 +8,7 @@ part "join_room.g.dart"; @JsonSerializable() class const JoinRoomRequest({ required final String roomIdOrAlias, - final IList via = const IList.empty(), + final IList via = const .empty(), /// Set when accepting an invite, so gomuks can mark DMs as such. final bool fromInvite = false, diff --git a/lib/models/requests/send_message.dart b/lib/models/requests/send_message.dart index 9907439..10a9330 100644 --- a/lib/models/requests/send_message.dart +++ b/lib/models/requests/send_message.dart @@ -25,7 +25,7 @@ class const SendMessageRequest({ @JsonSerializable() class const Mentions({ final bool room = false, - final IList userIds = const IList.empty(), + final IList userIds = const .empty(), }) with _$Mentions { Map toJson() => _$MentionsToJson(this); diff --git a/lib/models/room.dart b/lib/models/room.dart index 24b0c5e..f02c6ac 100644 --- a/lib/models/room.dart +++ b/lib/models/room.dart @@ -14,15 +14,15 @@ class const Room({ /// [timeline] is an IMap of timelineRowId to eventRowId @JsonKey(fromJson: Room.timelineTupleJsonToIMap) - final IMap timeline = const IMap.empty(), + final IMap timeline = const .empty(), /// [clientSticky] is an ISet of eventRowId @JsonKey(includeFromJson: false, includeToJson: false) - final ISet clientSticky = const ISet.empty(), + final ISet clientSticky = const .empty(), /// [events] is an IMap of eventRowId to event @JsonKey(fromJson: Room.eventsJsonToIMap) - final IMap events = const IMap.empty(), + final IMap events = const .empty(), final bool reset = false, @JsonKey(includeFromJson: false, includeToJson: false) @@ -34,9 +34,9 @@ class const Room({ @JsonKey(includeFromJson: false, includeToJson: false) final bool hasFetchedMembers = false, - final IMap> state = const IMap.empty(), + final IMap> state = const .empty(), - final IMap> receipts = const IMap.empty(), + final IMap> receipts = const .empty(), final bool dismissNotifications = false, final bool hasMore = true, diff --git a/lib/models/sync_data.dart b/lib/models/sync_data.dart index a26043b..4639956 100644 --- a/lib/models/sync_data.dart +++ b/lib/models/sync_data.dart @@ -11,10 +11,10 @@ part "sync_data.g.dart"; @JsonSerializable(createToJson: false) class const SyncData({ final bool clearState = false, - final IMap> accountData = const IMap.empty(), - final IMap rooms = const IMap.empty(), - final ISet leftRooms = const ISet.empty(), - final IList invitedRooms = const IList.empty(), + final IMap> accountData = const .empty(), + final IMap rooms = const .empty(), + final ISet leftRooms = const .empty(), + final IList invitedRooms = const .empty(), final IMap>? spaceEdges, final IList? topLevelSpaces, }) with _$SyncData { diff --git a/lib/widgets/room_menu_button.dart b/lib/widgets/room_menu_button.dart index bc8e9ee..38ce25d 100644 --- a/lib/widgets/room_menu_button.dart +++ b/lib/widgets/room_menu_button.dart @@ -15,7 +15,7 @@ import "package:nexus/main.dart"; final class const RoomMenuButton( final Room? room, { - final IList children = const IList.empty(), + final IList children = const .empty(), super.key, }) extends ConsumerWidget { @override