Add (WIP) ban/klck ability

This commit is contained in:
Henry Hiles 2026-04-02 17:37:27 -04:00
commit 3a1bcb5b8f
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
13 changed files with 113 additions and 30 deletions

View file

@ -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<Membership> {
final Message message;
@ -35,6 +36,7 @@ class AuthorController extends AsyncNotifier<Membership> {
);
return Membership(
status: member?.status ?? MembershipStatus.leave,
avatarUrl: pmp?.avatarUrl ?? member?.avatarUrl,
displayName:
pmp?.displayName ??

View file

@ -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<int> {
Future<Profile> getProfile(String userId) async =>
Profile.fromJson(await _sendCommand("get_profile", {"user_id": userId}));
Future<void> reportEvent(ReportRequest report) =>
_sendCommand("report_event", report.toJson());
Future<void> reportEvent(ReportRequest request) =>
_sendCommand("report_event", request.toJson());
Future<void> setMembership(SetMembershipRequest request) =>
_sendCommand("set_membership", request.toJson());
Future<void> markRead(Room room) async {
final event = room.events.firstWhereOrNull(

View file

@ -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<IList<Membership>> {
final MembershipStatus status;
MembersByTypeController(this.status);
@override
Future<IList<Membership>> build() => ref.watch(
MembersController.provider.selectAsync(
(members) =>
members.where((membership) => membership.status == status).toIList(),
),
);
static final provider =
AsyncNotifierProvider.family<
MembersByTypeController,
IList<Membership>,
MembershipStatus
>(MembersByTypeController.new);
}

View file

@ -29,7 +29,7 @@ class MembersController extends AsyncNotifier<IList<Membership>> {
);
return state.nonNulls
.where((member) => member.content["membership"] == "join")
.where((state) => state.type == "m.room.member")
.map(
(membership) => Membership.fromContent(
membership.content,

View file

@ -145,11 +145,11 @@ class MessageController extends AsyncNotifier<Message?> {
"${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(

View file

@ -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<InMemoryChatController> {
if (message != null) insertMessage(message);
}
Future<chat.User> 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<void> scrollToMessage(Message message) async {
final controller = await future;
Future<void> setFlashing(bool flashing) => controller.updateMessage(

View file

@ -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),

View file

@ -0,0 +1,4 @@
import "package:freezed_annotation/freezed_annotation.dart";
@JsonEnum()
enum MembershipStatus { leave, invite, ban, join }

View file

@ -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<String, Object?> json) =>
_$SetMembershipRequestFromJson(json);
}
@JsonEnum()
enum MembershipAction { ban, kick, unban, invite }

View file

@ -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:

View file

@ -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(

View file

@ -369,7 +369,7 @@ class RoomChat extends HookConsumerWidget {
),
),
),
resolveUser: notifier.resolveUser,
resolveUser: (_) async => null,
chatController: controller,
),
),

View file

@ -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,