add inviting from room menu

This commit is contained in:
Henry Hiles 2026-10-06 19:23:00 -04:00
commit 7be1dfe0bc
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
5 changed files with 85 additions and 69 deletions

2
gomuks

@ -1 +1 @@
Subproject commit 15ac73be09155a7c5319a144f8af0e75a409fc51 Subproject commit 38e5033c3a91db0cbea6cbd9f0feef9ff6441038

View file

@ -10,7 +10,7 @@ class const SetMembershipRequest({
required final String userId, required final String userId,
required final String roomId, required final String roomId,
final String? reason, final String? reason,
@JsonKey(name: "action") required final MembershipAction action, required final MembershipAction action,
@JsonKey(name: "msc4293_redact_events") final bool redact = false, @JsonKey(name: "msc4293_redact_events") final bool redact = false,
}) with _$SetMembershipRequest { }) with _$SetMembershipRequest {
Map<String, Object?> toJson() => _$SetMembershipRequestToJson(this); Map<String, Object?> toJson() => _$SetMembershipRequestToJson(this);

View file

@ -3,7 +3,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/rooms.dart"; import "package:nexus/controllers/rooms.dart";
import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/room_menu.dart"; import "package:nexus/widgets/room_menu_button.dart";
import "package:nexus/widgets/room_summary_dialog.dart"; import "package:nexus/widgets/room_summary_dialog.dart";
final class const RoomAppbar({ final class const RoomAppbar({
@ -72,9 +72,10 @@ final class const RoomAppbar({
), ),
], ],
), ),
actions: room == null actions: .new(
? .new() room == null
: .new([ ? []
: [
IconButton( IconButton(
onPressed: onOpenPinnedMessagesList?.call, onPressed: onOpenPinnedMessagesList?.call,
icon: Icon(Icons.push_pin), icon: Icon(Icons.push_pin),
@ -85,8 +86,9 @@ final class const RoomAppbar({
tooltip: "Open member list", tooltip: "Open member list",
icon: Icon(Icons.people), icon: Icon(Icons.people),
), ),
RoomMenu(room), RoomMenuButton(room),
]), ],
),
); );
} }
} }

View file

@ -10,7 +10,7 @@ import "package:nexus/widgets/divider_widget.dart";
import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/error_dialog.dart";
import "package:nexus/widgets/pages/settings.dart"; import "package:nexus/widgets/pages/settings.dart";
import "package:nexus/widgets/join_dialog.dart"; import "package:nexus/widgets/join_dialog.dart";
import "package:nexus/widgets/room_menu.dart"; import "package:nexus/widgets/room_menu_button.dart";
import "package:nexus/controllers/key.dart"; import "package:nexus/controllers/key.dart";
import "package:nexus/controllers/spaces.dart"; import "package:nexus/controllers/spaces.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
@ -232,7 +232,10 @@ class const RoomsPane({super.key}) extends ConsumerWidget {
title: Text(selectedSpace.title, overflow: .ellipsis), title: Text(selectedSpace.title, overflow: .ellipsis),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
actions: [ actions: [
RoomMenu(selectedSpace.room, children: selectedSpace.allChildRooms), RoomMenuButton(
selectedSpace.room,
children: selectedSpace.allChildRooms,
),
], ],
), ),
body: MaterialUiCompatibilityBridge( body: MaterialUiCompatibilityBridge(

View file

@ -0,0 +1,146 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:material_ui/material_ui.dart";
import "package:flutter/services.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/via.dart";
import "package:nexus/models/room.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:nexus/main.dart";
final class const RoomMenuButton(
final Room? room, {
final IList<Room> children = const IList.empty(),
super.key,
}) extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final danger = Theme.of(context).colorScheme.error;
final client = ref.read(ClientController.provider.notifier);
return PopupMenuButton(
itemBuilder: (_) => [
PopupMenuItem(
onTap: () async {
if (room != null) await client.markRead(room!);
await Future.wait(children.map((child) => client.markRead(child)));
},
child: ListTile(
leading: Icon(Icons.check),
title: Text("Mark as Read"),
),
),
if (room != null) ...[
PopupMenuItem(
onTap: () async {
final vias = ref.watch(ViaController.provider(room!));
await Clipboard.setData(
.new(
text: "matrix:roomid/${room!.metadata?.id.substring(1)}$vias",
),
);
},
child: ListTile(
leading: Icon(Icons.link),
title: Text("Copy Link"),
),
),
PopupMenuItem(
onTap: () => showDialog(
context: context,
builder: (context) => HookBuilder(
builder: (context) {
final userIdController = useTextEditingController();
final text = useValueListenable(userIdController).text.trim();
final valid = RegExp(r"^@[^:\s]+:[^\s]+$").hasMatch(text);
void submit() {
if (!valid) return;
Navigator.of(context).pop();
client
.setMembership(
.new(
userId: text,
roomId: room!.metadata!.id,
action: .invite,
),
)
.onError(showError);
}
return AlertDialog(
title: Text("Invite User"),
content: TextField(
controller: userIdController,
autofocus: true,
keyboardType: .emailAddress,
onSubmitted: (_) => submit(),
decoration: .new(
labelText: "User ID",
hintText: "@user:example.com",
errorText: text.isEmpty || valid
? null
: "Enter a valid user ID",
),
),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: Text("Cancel"),
),
TextButton(
onPressed: valid ? submit : null,
child: Text("Invite"),
),
],
);
},
),
),
child: ListTile(
leading: Icon(Icons.person_add),
title: Text("Invite"),
),
),
PopupMenuItem(
onTap: () => showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("Leave Room"),
content: Text(
"Are you sure you want to leave \"${room!.metadata?.name ?? "Unnamed Room"}\"?",
),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: Text("Cancel"),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
final snackbar = ScaffoldMessenger.of(context)
.showSnackBar(
.new(
content: Text("Leaving room..."),
duration: Duration(days: 1),
),
);
await client.leaveRoom(room!);
snackbar.close();
},
child: Text("Leave"),
),
],
),
),
child: ListTile(
leading: Icon(Icons.logout, color: danger),
title: Text("Leave", style: TextStyle(color: danger)),
),
),
],
],
);
}
}