diff --git a/.gitmodules b/.gitmodules index 145276a..d7c8fca 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "gomuks"] path = gomuks url = https://github.com/gomuks/gomuks - branch = main + branch = timedoutuk/versions-rpc diff --git a/gomuks b/gomuks index 0f8fb26..d382c66 160000 --- a/gomuks +++ b/gomuks @@ -1 +1 @@ -Subproject commit 0f8fb261ab57977fa24a4f63b7aafbb9a4ff1b7b +Subproject commit d382c664a04f3f202b86f859d1868fd4654a39e8 diff --git a/lib/controllers/account_data.dart b/lib/controllers/account_data.dart index 0926e08..c595565 100644 --- a/lib/controllers/account_data.dart +++ b/lib/controllers/account_data.dart @@ -1,16 +1,22 @@ +import "dart:convert"; + import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/models/account_data.dart"; -class AccountDataController extends Notifier> { +class AccountDataController extends Notifier { @override - IMap build() => .new(); + AccountData build() => .new(); - void update(IMap newData) => - state = .new({...state.unlock, ...newData.unlock}); + void update(IMap> newAccountData) => + state = .fromJson({ + ...json.decode(json.encode(state.toJson())), + ...newAccountData + .map((key, value) => MapEntry(key, value["content"])) + .unlock, + }); - static final provider = - NotifierProvider>( - AccountDataController.new, - ); + static final provider = NotifierProvider( + AccountDataController.new, + ); } diff --git a/lib/controllers/client.dart b/lib/controllers/client.dart index 97c3ed6..d0295ae 100644 --- a/lib/controllers/client.dart +++ b/lib/controllers/client.dart @@ -33,10 +33,12 @@ import "package:nexus/models/requests/redact_event.dart"; import "package:nexus/models/requests/report.dart"; import "package:nexus/models/requests/send_event.dart"; import "package:nexus/models/requests/send_message.dart"; +import "package:nexus/models/requests/set_account_data.dart"; import "package:nexus/models/requests/set_membership.dart"; import "package:nexus/models/requests/set_state.dart"; import "package:nexus/models/requests/upload_media.dart"; import "package:nexus/models/room.dart"; +import "package:nexus/models/spec_versions_response.dart"; import "package:nexus/models/sync_data.dart"; import "package:nexus/src/third_party/gomuks.g.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; @@ -268,6 +270,9 @@ class ClientController extends AsyncNotifier { Future setMembership(SetMembershipRequest request) => _sendCommand("set_membership", request.toJson()); + Future setAccountData(SetAccountDataRequest request) => + _sendCommand("set_account_data", request.toJson()); + Future uploadMedia(UploadMediaRequest request) async => .fromJson(await _sendCommand("upload_media", request.toJson())); @@ -302,6 +307,9 @@ class ClientController extends AsyncNotifier { Future exchangeToken(OAuthExchangeTokenRequest request) async => await _sendCommand("oauth_exchange_token", request.toJson()); + Future getSpecVersions() async => + .fromJson(await _sendCommand("get_versions")); + Future discoverHomeserver(Uri homeserver) async { try { final response = await _sendCommand("discover_homeserver", { diff --git a/lib/controllers/settings_sections.dart b/lib/controllers/settings_sections.dart index 7c9796b..3c9a665 100644 --- a/lib/controllers/settings_sections.dart +++ b/lib/controllers/settings_sections.dart @@ -4,9 +4,11 @@ import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:intl/intl.dart"; import "package:m3e_buttons/m3e_buttons.dart"; +import "package:nexus/controllers/account_data.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client_state.dart"; import "package:nexus/controllers/settings.dart"; +import "package:nexus/models/account_data.dart"; import "package:nexus/models/settings_category.dart"; import "package:nexus/main.dart"; import "package:nexus/widgets/settings/dialog_list_tile.dart"; @@ -16,6 +18,9 @@ class SettingsSectionsController @override Future>> build() async { final settings = await ref.watch(SettingsController.provider.future); + final specVersionsResponse = await ref + .watch(ClientController.provider.notifier) + .getSpecVersions(); return .new({ "General": .new([ @@ -93,6 +98,51 @@ class SettingsSectionsController if (ref.watch(ClientStateController.provider)?.isLoggedIn == true) "Account": .new([ .new(title: "Profile", icon: Icons.person, settings: .new([])), + .new( + title: "Safety", + icon: Icons.gpp_good, + settings: .new([ + .new( + title: "Invite Blocking", + description: + "Block invites, either completely, or block only invites from users without shared private rooms (depends on server support).", + builder: (title, description, icon) => Consumer( + builder: (context, ref, _) => + DialogListTile( + icon: Icon(icon), + title: title, + subtitle: Text(description), + initialValue: ref + .watch(AccountDataController.provider) + .invitePermissionConfig + .defaultAction, + options: specVersionsResponse.unstableFeatures.msc4494 + ? DefaultInviteAction.values + : IList( + DefaultInviteAction.values, + ).remove(.denyPublic).toList(), + getName: (option) => switch (option) { + .allow => "Allow", + .deny => "Deny", + .denyPublic => "Deny public", + }, + onChanged: (value) => ref + .watch(ClientController.provider.notifier) + .setAccountData( + .new( + type: InvitePermissionConfig.key, + content: InvitePermissionConfig( + defaultAction: value, + ), + ), + ) + .onError(showError), + ), + ), + icon: Icons.person_off, + ), + ]), + ), .new( title: "Other", icon: Icons.key, diff --git a/lib/controllers/spaces.dart b/lib/controllers/spaces.dart index 24113ad..60696da 100644 --- a/lib/controllers/spaces.dart +++ b/lib/controllers/spaces.dart @@ -84,9 +84,7 @@ class SpacesController extends Notifier> { ], }.nonNulls.toISet(); - final directMessages = IMap( - accountData["m.direct"]?.content ?? {}, - ).values.expand((e) => e).toISet(); + final directMessages = accountData.directMessages.values.flattened; final otherRooms = rooms.entries .where( diff --git a/lib/models/account_data.dart b/lib/models/account_data.dart index a325ffe..96ba6a7 100644 --- a/lib/models/account_data.dart +++ b/lib/models/account_data.dart @@ -1,16 +1,69 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; part "account_data.freezed.dart"; part "account_data.g.dart"; @freezed abstract class AccountData with _$AccountData { + const AccountData._(); + static List? readRecentEmojiValue( + Map json, + String key, + ) => json[key]?["recent_emoji"]; + + static Map>? recentEmojiToJson( + IList recentEmoji, + ) => {"recent_emoji": recentEmoji.map((emoji) => emoji.toJson()).toList()}; + const factory AccountData({ - required String userId, - required String? roomId, - required String type, - required dynamic content, + @JsonKey(name: InvitePermissionConfig.key) + @Default(InvitePermissionConfig()) + InvitePermissionConfig invitePermissionConfig, + + @JsonKey(name: "m.direct") + @Default(IMap.empty()) + IMap> directMessages, + + @JsonKey( + name: "m.recent_emoji", + readValue: AccountData.readRecentEmojiValue, + toJson: AccountData.recentEmojiToJson, + ) + @Default(IList.empty()) + IList recentEmoji, }) = _AccountData; factory AccountData.fromJson(Map json) => _$AccountDataFromJson(json); } + +@freezed +abstract class InvitePermissionConfig with _$InvitePermissionConfig { + static const key = "m.invite_permission_config"; + + const factory InvitePermissionConfig({ + @JsonKey(unknownEnumValue: DefaultInviteAction.allow) + @Default(DefaultInviteAction.allow) + DefaultInviteAction defaultAction, + }) = _InvitePermissionConfig; + + factory InvitePermissionConfig.fromJson(Map json) => + _$InvitePermissionConfigFromJson(json); +} + +@freezed +abstract class RecentEmoji with _$RecentEmoji { + const factory RecentEmoji({required String emoji, required int total}) = + _RecentEmoji; + + factory RecentEmoji.fromJson(Map json) => + _$RecentEmojiFromJson(json); +} + +@JsonEnum(fieldRename: .snake) +enum DefaultInviteAction { + allow, + deny, + @JsonValue("uk.timedout.msc4494.deny_public") + denyPublic, +} diff --git a/lib/models/requests/set_account_data.dart b/lib/models/requests/set_account_data.dart new file mode 100644 index 0000000..ffccdb4 --- /dev/null +++ b/lib/models/requests/set_account_data.dart @@ -0,0 +1,15 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "set_account_data.freezed.dart"; +part "set_account_data.g.dart"; + +@freezed +abstract class SetAccountDataRequest with _$SetAccountDataRequest { + const factory SetAccountDataRequest({ + required String type, + required dynamic content, + String? roomId, + }) = _SetAccountDataRequest; + + factory SetAccountDataRequest.fromJson(Map json) => + _$SetAccountDataRequestFromJson(json); +} diff --git a/lib/models/spec_versions_response.dart b/lib/models/spec_versions_response.dart new file mode 100644 index 0000000..07a3ada --- /dev/null +++ b/lib/models/spec_versions_response.dart @@ -0,0 +1,25 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +part "spec_versions_response.freezed.dart"; +part "spec_versions_response.g.dart"; + +@freezed +abstract class SpecVersionsResponse with _$SpecVersionsResponse { + const factory SpecVersionsResponse({ + required IList versions, + required UnstableFeatures unstableFeatures, + }) = _SpecVersionsResponse; + + factory SpecVersionsResponse.fromJson(Map json) => + _$SpecVersionsResponseFromJson(json); +} + +@freezed +abstract class UnstableFeatures with _$UnstableFeatures { + const factory UnstableFeatures({ + @JsonKey(name: "uk.timedout.msc4494") @Default(false) bool msc4494, + }) = _UnstableFeatures; + + factory UnstableFeatures.fromJson(Map json) => + _$UnstableFeaturesFromJson(json); +} diff --git a/lib/models/sync_data.dart b/lib/models/sync_data.dart index 0f98bb2..b2699d6 100644 --- a/lib/models/sync_data.dart +++ b/lib/models/sync_data.dart @@ -1,6 +1,5 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/account_data.dart"; import "package:nexus/models/room.dart"; import "package:nexus/models/space_edge.dart"; part "sync_data.freezed.dart"; @@ -10,7 +9,7 @@ part "sync_data.g.dart"; abstract class SyncData with _$SyncData { const factory SyncData({ @Default(false) bool clearState, - @Default(IMap.empty()) IMap accountData, + @Default(IMap.empty()) IMap> accountData, @Default(IMap.empty()) IMap rooms, @Default(ISet.empty()) ISet leftRooms, // required IList invitedRooms, diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart index b1f29c4..cb1e695 100644 --- a/lib/widgets/room_chat.dart +++ b/lib/widgets/room_chat.dart @@ -214,11 +214,9 @@ class RoomChat extends HookConsumerWidget { ...{ ...ref.watch( AccountDataController.provider.select( - (value) => IList( - value["m.recent_emoji"] - ?.content["recent_emoji"] ?? - [], - ).map((entry) => entry["emoji"]).toIList(), + (value) => value.recentEmoji + .map((entry) => entry.emoji) + .toIList(), ), ), "👍",