diff --git a/lib/controllers/author_controller.dart b/lib/controllers/author_controller.dart index fc35ed3..8b709d3 100644 --- a/lib/controllers/author_controller.dart +++ b/lib/controllers/author_controller.dart @@ -6,6 +6,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/controllers/members_controller.dart"; import "package:nexus/models/membership.dart"; +import "package:nexus/models/membership_status.dart"; class AuthorController extends AsyncNotifier { final Message message; @@ -35,6 +36,7 @@ class AuthorController extends AsyncNotifier { ); return Membership( + status: member?.status ?? MembershipStatus.leave, avatarUrl: pmp?.avatarUrl ?? member?.avatarUrl, displayName: pmp?.displayName ?? diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index ced57f7..3de4bc0 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -28,6 +28,7 @@ import "package:nexus/models/requests/paginate_request.dart"; import "package:nexus/models/requests/redact_event_request.dart"; import "package:nexus/models/requests/report_request.dart"; import "package:nexus/models/requests/send_message_request.dart"; +import "package:nexus/models/requests/set_membership_request.dart"; import "package:nexus/models/room.dart"; import "package:nexus/models/sync_data.dart"; import "package:nexus/models/sync_status.dart"; @@ -224,8 +225,11 @@ class ClientController extends AsyncNotifier { Future getProfile(String userId) async => Profile.fromJson(await _sendCommand("get_profile", {"user_id": userId})); - Future reportEvent(ReportRequest report) => - _sendCommand("report_event", report.toJson()); + Future reportEvent(ReportRequest request) => + _sendCommand("report_event", request.toJson()); + + Future setMembership(SetMembershipRequest request) => + _sendCommand("set_membership", request.toJson()); Future markRead(Room room) async { final event = room.events.firstWhereOrNull( diff --git a/lib/controllers/members_by_type_controller.dart b/lib/controllers/members_by_type_controller.dart new file mode 100644 index 0000000..cdc8d07 --- /dev/null +++ b/lib/controllers/members_by_type_controller.dart @@ -0,0 +1,25 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/members_controller.dart"; +import "package:nexus/models/membership.dart"; +import "package:nexus/models/membership_status.dart"; + +class MembersByTypeController extends AsyncNotifier> { + final MembershipStatus status; + MembersByTypeController(this.status); + + @override + Future> build() => ref.watch( + MembersController.provider.selectAsync( + (members) => + members.where((membership) => membership.status == status).toIList(), + ), + ); + + static final provider = + AsyncNotifierProvider.family< + MembersByTypeController, + IList, + MembershipStatus + >(MembersByTypeController.new); +} diff --git a/lib/controllers/members_controller.dart b/lib/controllers/members_controller.dart index 2cf7541..39666d4 100644 --- a/lib/controllers/members_controller.dart +++ b/lib/controllers/members_controller.dart @@ -29,7 +29,7 @@ class MembersController extends AsyncNotifier> { ); return state.nonNulls - .where((member) => member.content["membership"] == "join") + .where((state) => state.type == "m.room.member") .map( (membership) => Membership.fromContent( membership.content, diff --git a/lib/controllers/message_controller.dart b/lib/controllers/message_controller.dart index 675a6e5..ed52ef2 100644 --- a/lib/controllers/message_controller.dart +++ b/lib/controllers/message_controller.dart @@ -145,11 +145,11 @@ class MessageController extends AsyncNotifier { "${content["displayname"] ?? event.stateKey} ${switch (content["membership"]) { "invite" => "was invited to", "join" => "joined", - "leave" => event.authorId == event.stateKey ? "left" : "was kicked", + "leave" => event.authorId == event.stateKey ? "left" : (event.unsigned["prev_content"]?["membership"] == "ban" ? "was unbanned from" : "was kicked from"), "ban" => "was banned from", "knock" => "asked to join", _ => "did something relating to", - }} the room.", + }} the room. ${content["reason"] ?? ""}", ), "m.room.server_acl" => toSystemMessage( diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index 6512031..3fe9d74 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -2,7 +2,6 @@ import "dart:async"; import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; -import "package:flutter_chat_core/flutter_chat_core.dart" as chat; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:fluttertagger/fluttertagger.dart"; import "package:nexus/controllers/client_controller.dart"; @@ -258,21 +257,6 @@ class RoomChatController extends AsyncNotifier { if (message != null) insertMessage(message); } - Future resolveUser(String id) async { - final user = await ref - .watch(ClientController.provider.notifier) - .getProfile(id); - return chat.User( - id: id, - name: user.displayName, - // imageSource: user.avatarUrl == null - // ? null - // : (await ref.watch( - // AvatarController.provider(user.avatarUrl!.toString()).future, - // )).toString(), - ); - } - Future scrollToMessage(Message message) async { final controller = await future; Future setFlashing(bool flashing) => controller.updateMessage( diff --git a/lib/models/membership.dart b/lib/models/membership.dart index 4e2bf4c..ce0cc42 100644 --- a/lib/models/membership.dart +++ b/lib/models/membership.dart @@ -1,12 +1,14 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/helpers/extensions/mxc_to_https.dart"; +import "package:nexus/models/membership_status.dart"; part "membership.freezed.dart"; @freezed abstract class Membership with _$Membership { const Membership._(); const factory Membership({ + required MembershipStatus status, required Uri? avatarUrl, required String displayName, required String userId, @@ -17,6 +19,10 @@ abstract class Membership with _$Membership { String userId, String homeserver, ) => Membership( + status: MembershipStatus.values.firstWhere( + (status) => status.name == content["membership"], + orElse: () => MembershipStatus.leave, + ), avatarUrl: Uri.tryParse( content["avatar_url"] ?? "", )?.mxcToHttps(homeserver), diff --git a/lib/models/membership_status.dart b/lib/models/membership_status.dart new file mode 100644 index 0000000..bc85e22 --- /dev/null +++ b/lib/models/membership_status.dart @@ -0,0 +1,4 @@ +import "package:freezed_annotation/freezed_annotation.dart"; + +@JsonEnum() +enum MembershipStatus { leave, invite, ban, join } diff --git a/lib/models/requests/set_membership_request.dart b/lib/models/requests/set_membership_request.dart new file mode 100644 index 0000000..7384f5d --- /dev/null +++ b/lib/models/requests/set_membership_request.dart @@ -0,0 +1,20 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "set_membership_request.freezed.dart"; +part "set_membership_request.g.dart"; + +@freezed +abstract class SetMembershipRequest with _$SetMembershipRequest { + const factory SetMembershipRequest({ + required String userId, + required String roomId, + + @JsonKey(name: "action") required MembershipAction action, + @Default(false) @JsonKey(name: "msc4293_redact_events") bool redact, + }) = _SetMembershipRequest; + + factory SetMembershipRequest.fromJson(Map json) => + _$SetMembershipRequestFromJson(json); +} + +@JsonEnum() +enum MembershipAction { ban, kick, unban, invite } diff --git a/lib/widgets/chat_page/composer/mention_overlay.dart b/lib/widgets/chat_page/composer/mention_overlay.dart index 2c39966..a78bdd1 100644 --- a/lib/widgets/chat_page/composer/mention_overlay.dart +++ b/lib/widgets/chat_page/composer/mention_overlay.dart @@ -1,8 +1,9 @@ import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/members_controller.dart"; +import "package:nexus/controllers/members_by_type_controller.dart"; import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/models/membership_status.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/loading.dart"; @@ -31,7 +32,9 @@ class MentionOverlay extends ConsumerWidget { child: switch (triggerCharacter) { "@" => ref - .watch(MembersController.provider) + .watch( + MembersByTypeController.provider(MembershipStatus.join), + ) .betterWhen( data: (members) => ListView( children: diff --git a/lib/widgets/chat_page/member_list.dart b/lib/widgets/chat_page/member_list.dart index ffa572c..72ce744 100644 --- a/lib/widgets/chat_page/member_list.dart +++ b/lib/widgets/chat_page/member_list.dart @@ -1,8 +1,9 @@ import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/members_controller.dart"; +import "package:nexus/controllers/members_by_type_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/show_user_popover.dart"; +import "package:nexus/models/membership_status.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; class MemberList extends ConsumerWidget { @@ -10,7 +11,9 @@ class MemberList extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final membersProvider = ref.watch(MembersController.provider); + final membersProvider = ref.watch( + MembersByTypeController.provider(MembershipStatus.join), + ); return Drawer( shape: Border(), child: Column( diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index a30a145..d85c826 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -369,7 +369,7 @@ class RoomChat extends HookConsumerWidget { ), ), ), - resolveUser: notifier.resolveUser, + resolveUser: (_) async => null, chatController: controller, ), ), diff --git a/lib/widgets/chat_page/user_popover.dart b/lib/widgets/chat_page/user_popover.dart index 4279c59..365b6ef 100644 --- a/lib/widgets/chat_page/user_popover.dart +++ b/lib/widgets/chat_page/user_popover.dart @@ -1,10 +1,15 @@ import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/controllers/profile_controller.dart"; +import "package:nexus/controllers/selected_room_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/models/membership.dart"; +import "package:nexus/models/membership_status.dart"; +import "package:nexus/models/requests/set_membership_request.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/main.dart"; class UserPopover extends ConsumerWidget { final Membership member; @@ -14,6 +19,11 @@ class UserPopover extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final textTheme = theme.textTheme; + final client = ref.watch(ClientController.provider.notifier); + final roomId = ref.watch( + SelectedRoomController.provider.select((room) => room?.metadata?.id), + ); + return Column( spacing: 16, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -53,14 +63,24 @@ class UserPopover extends ConsumerWidget { ), ], ), - if (member.userId != ref.watch(ClientStateController.provider)?.userId) + if (member.userId != + ref.watch(ClientStateController.provider)?.userId && + roomId != null) Wrap( spacing: 8, runSpacing: 8, children: [ FilledButton.icon(onPressed: null, label: Text("Message")), FilledButton.icon( - onPressed: null, + onPressed: () => client + .setMembership( + SetMembershipRequest( + userId: member.userId, + roomId: roomId, + action: MembershipAction.kick, + ), + ) + .onError(showError), label: Text("Kick"), style: ButtonStyle( backgroundColor: WidgetStatePropertyAll( @@ -72,8 +92,20 @@ class UserPopover extends ConsumerWidget { ), ), ElevatedButton.icon( - onPressed: null, - label: Text("Ban"), + onPressed: () => client + .setMembership( + SetMembershipRequest( + userId: member.userId, + roomId: roomId, + action: member.status == MembershipStatus.ban + ? MembershipAction.unban + : MembershipAction.ban, + ), + ) + .onError(showError), + label: Text( + member.status == MembershipStatus.ban ? "Unban" : "Ban", + ), style: ButtonStyle( backgroundColor: WidgetStatePropertyAll( theme.colorScheme.errorContainer,