From b594f5a1d14eb3bf205d2224f4b98839d3681fbc Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sun, 1 Mar 2026 14:40:14 -0500 Subject: [PATCH] accessiblity fixes --- lib/controllers/members_controller.dart | 22 ++--- lib/controllers/message_controller.dart | 4 +- lib/controllers/room_chat_controller.dart | 9 +- lib/pages/login_page.dart | 2 + lib/widgets/appbar.dart | 7 +- lib/widgets/chat_page/chat_box.dart | 23 ++++- lib/widgets/chat_page/member_list.dart | 85 ++++++++-------- lib/widgets/chat_page/mention_overlay.dart | 102 +++++++++----------- lib/widgets/chat_page/relation_preview.dart | 2 + lib/widgets/chat_page/room_appbar.dart | 7 +- lib/widgets/chat_page/sidebar.dart | 2 + 11 files changed, 147 insertions(+), 118 deletions(-) diff --git a/lib/controllers/members_controller.dart b/lib/controllers/members_controller.dart index 2a250a2..268a30d 100644 --- a/lib/controllers/members_controller.dart +++ b/lib/controllers/members_controller.dart @@ -4,23 +4,21 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/room.dart"; -class MembersController extends AsyncNotifier> { +class MembersController extends Notifier> { final Room room; MembersController(this.room); @override - Future> build() async => - (room.state["m.room.member"]?.values ?? []) - .map( - (eventRowId) => room.events.firstWhereOrNull( - (event) => event.rowId == eventRowId, - ), - ) - .nonNulls - .where((member) => member.content["membership"] == "join") - .toIList(); + IList build() => (room.state["m.room.member"]?.values ?? []) + .map( + (eventRowId) => + room.events.firstWhereOrNull((event) => event.rowId == eventRowId), + ) + .nonNulls + .where((member) => member.content["membership"] == "join") + .toIList(); - static final provider = AsyncNotifierProvider.family + static final provider = NotifierProvider.family .autoDispose, Room>( MembersController.new, ); diff --git a/lib/controllers/message_controller.dart b/lib/controllers/message_controller.dart index 5a2e6e7..dc92ffb 100644 --- a/lib/controllers/message_controller.dart +++ b/lib/controllers/message_controller.dart @@ -37,9 +37,7 @@ class MessageController extends AsyncNotifier { if (!ref.mounted) return null; - final members = await ref.watch( - MembersController.provider(config.room).future, - ); + final members = ref.watch(MembersController.provider(config.room)); final author = members.firstWhereOrNull( (member) => member.stateKey == event.authorId, ); diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index ab04b44..02ad366 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -25,7 +25,7 @@ class RoomChatController extends AsyncNotifier { @override Future build() async { final client = ref.watch(ClientController.provider.notifier); - final room = ref.read(RoomsController.provider)[roomId]; + var room = ref.read(RoomsController.provider)[roomId]; if (room == null) return InMemoryChatController(); final state = await client.getRoomState( @@ -59,13 +59,16 @@ class RoomChatController extends AsyncNotifier { const ISet.empty(), ); + room = ref.read(RoomsController.provider)[roomId]; + if (room == null) return InMemoryChatController(); + final messages = await ref.watch( MessagesController.provider( MessagesConfig( room: room, events: room.timeline .map( - (timelineRowTuple) => room.events.firstWhereOrNull( + (timelineRowTuple) => room!.events.firstWhereOrNull( (event) => event.rowId == timelineRowTuple.eventRowId, ), ) @@ -91,7 +94,7 @@ class RoomChatController extends AsyncNotifier { } else { final message = await ref.watch( MessageController.provider( - MessageConfig(event: event, room: room, includeEdits: true), + MessageConfig(event: event, room: room!, includeEdits: true), ).future, ); if (event.relationType == "m.replace") { diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index b15f6d4..bd41d51 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -97,6 +97,7 @@ class LoginPage extends HookConsumerWidget { ), ), IconButton.filled( + tooltip: "Confirm homeserver choice", onPressed: isLoading.value ? null : () => setHomeserver(Uri.tryParse(homeserverUrl.text)), @@ -143,6 +144,7 @@ class LoginPage extends HookConsumerWidget { ? null : () => setHomeserver(homeserver.url), trailing: IconButton( + tooltip: "Launch homeserver info page", onPressed: () => launch(homeserver.url), icon: Icon(Icons.info_outline), ), diff --git a/lib/widgets/appbar.dart b/lib/widgets/appbar.dart index 00b0e4c..5b14244 100644 --- a/lib/widgets/appbar.dart +++ b/lib/widgets/appbar.dart @@ -49,10 +49,15 @@ class Appbar extends StatelessWidget implements PreferredSizeWidget { if (!(Platform.isAndroid || Platform.isIOS)) ...[ if (!Platform.isLinux) IconButton( + tooltip: "Maximize window", onPressed: maximize, icon: const Icon(Icons.fullscreen), ), - IconButton(onPressed: () => exit(0), icon: const Icon(Icons.close)), + IconButton( + tooltip: "Close window", + onPressed: () => exit(0), + icon: const Icon(Icons.close), + ), ], ], ), diff --git a/lib/widgets/chat_page/chat_box.dart b/lib/widgets/chat_page/chat_box.dart index 20a8116..ec6c4d6 100644 --- a/lib/widgets/chat_page/chat_box.dart +++ b/lib/widgets/chat_page/chat_box.dart @@ -95,7 +95,27 @@ class ChatBox extends HookConsumerWidget { spacing: 8, children: [ PopupMenuButton( - itemBuilder: (context) => [], + tooltip: "Add media", + itemBuilder: (context) => [ + PopupMenuItem( + child: ListTile( + title: Text("Camera"), + leading: Icon(Icons.add_a_photo), + ), + ), + PopupMenuItem( + child: ListTile( + title: Text("Gallery"), + leading: Icon(Icons.add_photo_alternate), + ), + ), + PopupMenuItem( + child: ListTile( + title: Text("Files"), + leading: Icon(Icons.add_photo_alternate), + ), + ), + ], icon: Icon(Icons.add), // enabled: room.canSendDefaultMessages, TODO: Permissions check ), @@ -138,6 +158,7 @@ class ChatBox extends HookConsumerWidget { onPressed: send, // onPressed: room.canSendDefaultMessages ? send : null, icon: Icon(Icons.send), + tooltip: "Send message", ), ], ), diff --git a/lib/widgets/chat_page/member_list.dart b/lib/widgets/chat_page/member_list.dart index 93b5a65..24d22e4 100644 --- a/lib/widgets/chat_page/member_list.dart +++ b/lib/widgets/chat_page/member_list.dart @@ -1,7 +1,6 @@ import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/members_controller.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/models/room.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; @@ -10,49 +9,49 @@ class MemberList extends ConsumerWidget { const MemberList(this.room, {super.key}); @override - Widget build(BuildContext context, WidgetRef ref) => Drawer( - shape: Border(), - child: ref - .watch(MembersController.provider(room)) - .betterWhen( - data: (members) => ListView( - children: [ - AppBar( - scrolledUnderElevation: 0, - leading: Icon(Icons.people), - title: Text("Members (${members.length})"), - actionsPadding: EdgeInsets.only(right: 4), - actions: [ - if (Scaffold.of(context).hasEndDrawer) - IconButton( - onPressed: Scaffold.of(context).closeEndDrawer, - icon: Icon(Icons.close), - ), - ], - ), - ...members.map( - (member) => ListTile( - onTap: () => showDialog( - context: context, - builder: (context) => - Dialog(child: Text("TODO: Open member popover")), - ), - leading: AvatarOrHash( - Uri.tryParse(member.content["avatar_url"] ?? ""), - member.content["displayname"].toString(), - ), - title: Text( - member.content["displayname"].toString(), - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - member.stateKey ?? "Unknown User", - overflow: TextOverflow.ellipsis, - ), + Widget build(BuildContext context, WidgetRef ref) { + final members = ref.watch(MembersController.provider(room)); + return Drawer( + shape: Border(), + child: ListView( + children: [ + AppBar( + scrolledUnderElevation: 0, + leading: Icon(Icons.people), + title: Text("Members (${members.length})"), + actionsPadding: EdgeInsets.only(right: 4), + actions: [ + if (Scaffold.of(context).hasEndDrawer) + IconButton( + onPressed: Scaffold.of(context).closeEndDrawer, + icon: Icon(Icons.close), + tooltip: "Close member list", ), - ), ], ), - ), - ); + ...members.map( + (member) => ListTile( + onTap: () => showDialog( + context: context, + builder: (context) => + Dialog(child: Text("TODO: Open member popover")), + ), + leading: AvatarOrHash( + Uri.tryParse(member.content["avatar_url"] ?? ""), + member.content["displayname"].toString(), + ), + title: Text( + member.content["displayname"].toString(), + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + member.stateKey ?? "Unknown User", + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ); + } } diff --git a/lib/widgets/chat_page/mention_overlay.dart b/lib/widgets/chat_page/mention_overlay.dart index 7151615..9858574 100644 --- a/lib/widgets/chat_page/mention_overlay.dart +++ b/lib/widgets/chat_page/mention_overlay.dart @@ -2,7 +2,6 @@ import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/members_controller.dart"; import "package:nexus/controllers/rooms_controller.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/models/room.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/loading.dart"; @@ -32,60 +31,55 @@ class MentionOverlay extends ConsumerWidget { color: Theme.of(context).colorScheme.surfaceContainerHigh, padding: EdgeInsets.all(8), child: switch (triggerCharacter) { - "@" => - ref - .watch(MembersController.provider(room)) - .betterWhen( - data: (members) => ListView( - children: - (query.isEmpty - ? members - : members.where( - (member) => - member.stateKey - ?.toLowerCase() - .contains( - query.toLowerCase(), - ) == - true || - (member.content["displayname"] - as String?) - ?.toLowerCase() - .contains( - query.toLowerCase(), - ) == - true, - )) - .map( - (member) => ListTile( - leading: AvatarOrHash( - Uri.tryParse( - member.content["avatar_url"] ?? "", - ), - member.content["displayname"] ?? "", - ), - title: Text( - member.content["displayname"] as String? ?? - member.stateKey ?? - "Unknown User", - ), - subtitle: member.stateKey != null - ? Text(member.stateKey!) - : null, - onTap: () => addTag( - id: "[@${member.content["displayname"]}](https://matrix.to/#/${member.stateKey})", - name: - member.stateKey - ?.substring(1) - .split(":") - .first ?? - "Unknown User", - ), + "@" => Consumer( + builder: (_, ref, _) { + final members = ref.watch(MembersController.provider(room)); + return ListView( + children: + (query.isEmpty + ? members + : members.where( + (member) => + member.stateKey?.toLowerCase().contains( + query.toLowerCase(), + ) == + true || + (member.content["displayname"] as String?) + ?.toLowerCase() + .contains(query.toLowerCase()) == + true, + )) + .map( + (member) => ListTile( + leading: AvatarOrHash( + Uri.tryParse( + member.content["avatar_url"] ?? "", ), - ) - .toList(), - ), - ), + member.content["displayname"] ?? "", + ), + title: Text( + member.content["displayname"] as String? ?? + member.stateKey ?? + "Unknown User", + ), + subtitle: member.stateKey != null + ? Text(member.stateKey!) + : null, + onTap: () => addTag( + id: "[@${member.content["displayname"]}](https://matrix.to/#/${member.stateKey})", + name: + member.stateKey + ?.substring(1) + .split(":") + .first ?? + "Unknown User", + ), + ), + ) + .toList(), + ); + }, + ), "#" => ListView( children: (query.isEmpty diff --git a/lib/widgets/chat_page/relation_preview.dart b/lib/widgets/chat_page/relation_preview.dart index a879495..7815096 100644 --- a/lib/widgets/chat_page/relation_preview.dart +++ b/lib/widgets/chat_page/relation_preview.dart @@ -56,6 +56,8 @@ class RelationPreview extends ConsumerWidget { ), ), IconButton( + tooltip: + "Cancel ${relationType == RelationType.edit ? "edit" : "reply"}", onPressed: onDismiss, icon: Icon(Icons.close), iconSize: 20, diff --git a/lib/widgets/chat_page/room_appbar.dart b/lib/widgets/chat_page/room_appbar.dart index 21aa4ae..436bcb9 100644 --- a/lib/widgets/chat_page/room_appbar.dart +++ b/lib/widgets/chat_page/room_appbar.dart @@ -52,9 +52,14 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget { ], ), actions: [ - IconButton(onPressed: () {}, icon: Icon(Icons.push_pin)), + IconButton( + onPressed: null, + icon: Icon(Icons.push_pin), + tooltip: "Open pinned messages", + ), IconButton( onPressed: () => onOpenMemberList(context), + tooltip: "Open member list", icon: Icon(Icons.people), ), RoomMenu(room), diff --git a/lib/widgets/chat_page/sidebar.dart b/lib/widgets/chat_page/sidebar.dart index 341dd60..4642a58 100644 --- a/lib/widgets/chat_page/sidebar.dart +++ b/lib/widgets/chat_page/sidebar.dart @@ -155,6 +155,7 @@ class Sidebar extends HookConsumerWidget { icon: Icon(Icons.add), ), IconButton( + tooltip: "Explore other rooms", onPressed: () => showDialog( context: context, builder: (context) => AlertDialog(title: Text("To-do")), @@ -162,6 +163,7 @@ class Sidebar extends HookConsumerWidget { icon: Icon(Icons.explore), ), IconButton( + tooltip: "Open settings", onPressed: () => Navigator.of( context, ).push(MaterialPageRoute(builder: (_) => SettingsPage())),