accessiblity fixes

This commit is contained in:
Henry Hiles 2026-03-01 14:40:14 -05:00
commit b594f5a1d1
No known key found for this signature in database
11 changed files with 147 additions and 118 deletions

View file

@ -4,23 +4,21 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/models/event.dart"; import "package:nexus/models/event.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
class MembersController extends AsyncNotifier<IList<Event>> { class MembersController extends Notifier<IList<Event>> {
final Room room; final Room room;
MembersController(this.room); MembersController(this.room);
@override @override
Future<IList<Event>> build() async => IList<Event> build() => (room.state["m.room.member"]?.values ?? [])
(room.state["m.room.member"]?.values ?? []) .map(
.map( (eventRowId) =>
(eventRowId) => room.events.firstWhereOrNull( room.events.firstWhereOrNull((event) => event.rowId == eventRowId),
(event) => event.rowId == eventRowId, )
), .nonNulls
) .where((member) => member.content["membership"] == "join")
.nonNulls .toIList();
.where((member) => member.content["membership"] == "join")
.toIList();
static final provider = AsyncNotifierProvider.family static final provider = NotifierProvider.family
.autoDispose<MembersController, IList<Event>, Room>( .autoDispose<MembersController, IList<Event>, Room>(
MembersController.new, MembersController.new,
); );

View file

@ -37,9 +37,7 @@ class MessageController extends AsyncNotifier<Message?> {
if (!ref.mounted) return null; if (!ref.mounted) return null;
final members = await ref.watch( final members = ref.watch(MembersController.provider(config.room));
MembersController.provider(config.room).future,
);
final author = members.firstWhereOrNull( final author = members.firstWhereOrNull(
(member) => member.stateKey == event.authorId, (member) => member.stateKey == event.authorId,
); );

View file

@ -25,7 +25,7 @@ class RoomChatController extends AsyncNotifier<ChatController> {
@override @override
Future<ChatController> build() async { Future<ChatController> build() async {
final client = ref.watch(ClientController.provider.notifier); 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(); if (room == null) return InMemoryChatController();
final state = await client.getRoomState( final state = await client.getRoomState(
@ -59,13 +59,16 @@ class RoomChatController extends AsyncNotifier<ChatController> {
const ISet.empty(), const ISet.empty(),
); );
room = ref.read(RoomsController.provider)[roomId];
if (room == null) return InMemoryChatController();
final messages = await ref.watch( final messages = await ref.watch(
MessagesController.provider( MessagesController.provider(
MessagesConfig( MessagesConfig(
room: room, room: room,
events: room.timeline events: room.timeline
.map( .map(
(timelineRowTuple) => room.events.firstWhereOrNull( (timelineRowTuple) => room!.events.firstWhereOrNull(
(event) => event.rowId == timelineRowTuple.eventRowId, (event) => event.rowId == timelineRowTuple.eventRowId,
), ),
) )
@ -91,7 +94,7 @@ class RoomChatController extends AsyncNotifier<ChatController> {
} else { } else {
final message = await ref.watch( final message = await ref.watch(
MessageController.provider( MessageController.provider(
MessageConfig(event: event, room: room, includeEdits: true), MessageConfig(event: event, room: room!, includeEdits: true),
).future, ).future,
); );
if (event.relationType == "m.replace") { if (event.relationType == "m.replace") {

View file

@ -97,6 +97,7 @@ class LoginPage extends HookConsumerWidget {
), ),
), ),
IconButton.filled( IconButton.filled(
tooltip: "Confirm homeserver choice",
onPressed: isLoading.value onPressed: isLoading.value
? null ? null
: () => setHomeserver(Uri.tryParse(homeserverUrl.text)), : () => setHomeserver(Uri.tryParse(homeserverUrl.text)),
@ -143,6 +144,7 @@ class LoginPage extends HookConsumerWidget {
? null ? null
: () => setHomeserver(homeserver.url), : () => setHomeserver(homeserver.url),
trailing: IconButton( trailing: IconButton(
tooltip: "Launch homeserver info page",
onPressed: () => launch(homeserver.url), onPressed: () => launch(homeserver.url),
icon: Icon(Icons.info_outline), icon: Icon(Icons.info_outline),
), ),

View file

@ -49,10 +49,15 @@ class Appbar extends StatelessWidget implements PreferredSizeWidget {
if (!(Platform.isAndroid || Platform.isIOS)) ...[ if (!(Platform.isAndroid || Platform.isIOS)) ...[
if (!Platform.isLinux) if (!Platform.isLinux)
IconButton( IconButton(
tooltip: "Maximize window",
onPressed: maximize, onPressed: maximize,
icon: const Icon(Icons.fullscreen), 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),
),
], ],
], ],
), ),

View file

@ -95,7 +95,27 @@ class ChatBox extends HookConsumerWidget {
spacing: 8, spacing: 8,
children: [ children: [
PopupMenuButton( 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), icon: Icon(Icons.add),
// enabled: room.canSendDefaultMessages, TODO: Permissions check // enabled: room.canSendDefaultMessages, TODO: Permissions check
), ),
@ -138,6 +158,7 @@ class ChatBox extends HookConsumerWidget {
onPressed: send, onPressed: send,
// onPressed: room.canSendDefaultMessages ? send : null, // onPressed: room.canSendDefaultMessages ? send : null,
icon: Icon(Icons.send), icon: Icon(Icons.send),
tooltip: "Send message",
), ),
], ],
), ),

View file

@ -1,7 +1,6 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/members_controller.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/models/room.dart";
import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/avatar_or_hash.dart";
@ -10,49 +9,49 @@ class MemberList extends ConsumerWidget {
const MemberList(this.room, {super.key}); const MemberList(this.room, {super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) => Drawer( Widget build(BuildContext context, WidgetRef ref) {
shape: Border(), final members = ref.watch(MembersController.provider(room));
child: ref return Drawer(
.watch(MembersController.provider(room)) shape: Border(),
.betterWhen( child: ListView(
data: (members) => ListView( children: [
children: [ AppBar(
AppBar( scrolledUnderElevation: 0,
scrolledUnderElevation: 0, leading: Icon(Icons.people),
leading: Icon(Icons.people), title: Text("Members (${members.length})"),
title: Text("Members (${members.length})"), actionsPadding: EdgeInsets.only(right: 4),
actionsPadding: EdgeInsets.only(right: 4), actions: [
actions: [ if (Scaffold.of(context).hasEndDrawer)
if (Scaffold.of(context).hasEndDrawer) IconButton(
IconButton( onPressed: Scaffold.of(context).closeEndDrawer,
onPressed: Scaffold.of(context).closeEndDrawer, icon: Icon(Icons.close),
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,
),
), ),
),
], ],
), ),
), ...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,
),
),
),
],
),
);
}
} }

View file

@ -2,7 +2,6 @@ import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/members_controller.dart"; import "package:nexus/controllers/members_controller.dart";
import "package:nexus/controllers/rooms_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/models/room.dart";
import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/loading.dart";
@ -32,60 +31,55 @@ class MentionOverlay extends ConsumerWidget {
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
padding: EdgeInsets.all(8), padding: EdgeInsets.all(8),
child: switch (triggerCharacter) { child: switch (triggerCharacter) {
"@" => "@" => Consumer(
ref builder: (_, ref, _) {
.watch(MembersController.provider(room)) final members = ref.watch(MembersController.provider(room));
.betterWhen( return ListView(
data: (members) => ListView( children:
children: (query.isEmpty
(query.isEmpty ? members
? members : members.where(
: members.where( (member) =>
(member) => member.stateKey?.toLowerCase().contains(
member.stateKey query.toLowerCase(),
?.toLowerCase() ) ==
.contains( true ||
query.toLowerCase(), (member.content["displayname"] as String?)
) == ?.toLowerCase()
true || .contains(query.toLowerCase()) ==
(member.content["displayname"] true,
as String?) ))
?.toLowerCase() .map(
.contains( (member) => ListTile(
query.toLowerCase(), leading: AvatarOrHash(
) == Uri.tryParse(
true, member.content["avatar_url"] ?? "",
))
.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",
),
), ),
) member.content["displayname"] ?? "",
.toList(), ),
), 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( "#" => ListView(
children: children:
(query.isEmpty (query.isEmpty

View file

@ -56,6 +56,8 @@ class RelationPreview extends ConsumerWidget {
), ),
), ),
IconButton( IconButton(
tooltip:
"Cancel ${relationType == RelationType.edit ? "edit" : "reply"}",
onPressed: onDismiss, onPressed: onDismiss,
icon: Icon(Icons.close), icon: Icon(Icons.close),
iconSize: 20, iconSize: 20,

View file

@ -52,9 +52,14 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
], ],
), ),
actions: [ actions: [
IconButton(onPressed: () {}, icon: Icon(Icons.push_pin)), IconButton(
onPressed: null,
icon: Icon(Icons.push_pin),
tooltip: "Open pinned messages",
),
IconButton( IconButton(
onPressed: () => onOpenMemberList(context), onPressed: () => onOpenMemberList(context),
tooltip: "Open member list",
icon: Icon(Icons.people), icon: Icon(Icons.people),
), ),
RoomMenu(room), RoomMenu(room),

View file

@ -155,6 +155,7 @@ class Sidebar extends HookConsumerWidget {
icon: Icon(Icons.add), icon: Icon(Icons.add),
), ),
IconButton( IconButton(
tooltip: "Explore other rooms",
onPressed: () => showDialog( onPressed: () => showDialog(
context: context, context: context,
builder: (context) => AlertDialog(title: Text("To-do")), builder: (context) => AlertDialog(title: Text("To-do")),
@ -162,6 +163,7 @@ class Sidebar extends HookConsumerWidget {
icon: Icon(Icons.explore), icon: Icon(Icons.explore),
), ),
IconButton( IconButton(
tooltip: "Open settings",
onPressed: () => Navigator.of( onPressed: () => Navigator.of(
context, context,
).push(MaterialPageRoute(builder: (_) => SettingsPage())), ).push(MaterialPageRoute(builder: (_) => SettingsPage())),