diff --git a/README.md b/README.md index 0c66f00..4948ecc 100644 --- a/README.md +++ b/README.md @@ -81,10 +81,10 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [x] Users - [x] Clickable - [x] Rooms - - [ ] Clickable + - [x] Clickable - [x] Matrix URIs - [x] Matrix.to links - - [x] Events + - [ ] Events - [ ] Render more nicely - [ ] Clickable - [x] Custom emojis/stickers diff --git a/lib/helpers/extensions/focus_room.dart b/lib/helpers/extensions/focus_room.dart index ac1e6f2..c4c86c2 100644 --- a/lib/helpers/extensions/focus_room.dart +++ b/lib/helpers/extensions/focus_room.dart @@ -5,13 +5,14 @@ import "package:nexus/controllers/spaces.dart"; import "package:nexus/models/space.dart"; extension FocusRoom on MutationTarget { - Future focusRoom(String id) async { + Future focusRoom(String id) async { final spaces = container.read(SpacesController.provider); if (spaces.firstWhereOrNull((space) => space.id == id) case Space _?) { - return await container + await container .read(KeyController.provider(KeyController.spaceKey).notifier) .set(id); + return true; } final parent = spaces.firstWhereOrNull( @@ -23,7 +24,7 @@ extension FocusRoom on MutationTarget { sub.children.any((room) => room.metadata?.id == id), ), ); - if (parent == null) return; + if (parent == null) return false; await container .read(KeyController.provider(KeyController.spaceKey).notifier) @@ -32,5 +33,7 @@ extension FocusRoom on MutationTarget { await container .read(KeyController.provider(KeyController.roomKey).notifier) .set(id); + + return true; } } diff --git a/lib/helpers/extensions/link_to_mention.dart b/lib/helpers/extensions/link_to_mention.dart index 8595669..41c9bd6 100644 --- a/lib/helpers/extensions/link_to_mention.dart +++ b/lib/helpers/extensions/link_to_mention.dart @@ -1,3 +1,5 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; + extension LinkToMention on String { /// Extracts a Matrix identifier from this string. /// @@ -43,4 +45,7 @@ extension LinkToMention on String { return null; } + + IList get via => + .new(Uri.tryParse(replaceAll("/#", ""))?.queryParametersAll["via"] ?? []); } diff --git a/lib/models/room_summary.dart b/lib/models/room_summary.dart index 7c2d6d2..d0a6bee 100644 --- a/lib/models/room_summary.dart +++ b/lib/models/room_summary.dart @@ -9,7 +9,7 @@ part "room_summary.g.dart"; @JsonSerializable() class const RoomSummary({ required final String roomId, - @JsonKey(name: "num_joined_members") required final int joinedMembers, + @JsonKey(name: "num_joined_members") final int? joinedMembers, final JoinRule? joinRule, final String? name, final Uri? avatarUrl, diff --git a/lib/widgets/html/mention_chip.dart b/lib/widgets/html/mention_chip.dart index 2387a49..e36d2e5 100644 --- a/lib/widgets/html/mention_chip.dart +++ b/lib/widgets/html/mention_chip.dart @@ -2,10 +2,12 @@ import "package:material_ui/material_ui.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/room_summary.dart"; import "package:nexus/controllers/user.dart"; +import "package:nexus/helpers/extensions/focus_room.dart"; import "package:nexus/helpers/extensions/link_to_mention.dart"; import "package:nexus/helpers/extensions/show_user_popover.dart"; import "package:nexus/models/content/membership.dart"; import "package:nexus/models/room_summary.dart"; +import "package:nexus/widgets/room_summary_dialog.dart"; class const MentionChip(final String content, final String? roomId, {super.key}) extends ConsumerWidget { @@ -22,10 +24,11 @@ class const MentionChip(final String content, final String? roomId, {super.key}) "#" || "!" => ref - .watch( - RoomSummaryController.provider(.new(roomIdOrAlias: mention!)), - ) - .whenOrNull(data: (data) => data), + .watch( + RoomSummaryController.provider(.new(roomIdOrAlias: mention!)), + ) + .whenOrNull(data: (data) => data) ?? + RoomSummary(roomId: mention), _ => null, }; @@ -33,11 +36,28 @@ class const MentionChip(final String content, final String? roomId, {super.key}) return mention == null ? SizedBox.shrink() : InkWell( - onTap: () { + onTap: () async { if (data case MembershipContent membership) { context.showUserPopover(membership, mention, roomId: roomId); } else if (data case RoomSummary summary) { - // TODO: Handle summary + if (!await ref.focusRoom(summary.roomId) && context.mounted) { + showDialog( + context: context, + builder: (context) => Consumer( + builder: (context, ref, _) => RoomSummaryDialog( + ref + .watch( + RoomSummaryController.provider( + .new(roomIdOrAlias: mention), + ), + ) + .whenOrNull(data: (data) => data) ?? + summary, + via: content.via, + ), + ), + ); + } } }, child: Text( diff --git a/lib/widgets/join_dialog.dart b/lib/widgets/join_dialog.dart index 071ddef..f6c63e0 100644 --- a/lib/widgets/join_dialog.dart +++ b/lib/widgets/join_dialog.dart @@ -1,14 +1,9 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:material_ui/material_ui.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/room_summary.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/helpers/extensions/focus_room.dart"; import "package:nexus/helpers/extensions/link_to_mention.dart"; import "package:nexus/widgets/room_summary_dialog.dart"; -import "package:nexus/main.dart"; class const JoinDialog(final WidgetRef ref, {super.key}) extends HookWidget { @override @@ -19,90 +14,21 @@ class const JoinDialog(final WidgetRef ref, {super.key}) extends HookWidget { Navigator.of(context).pop(); final roomIdOrAlias = roomAlias.text.mention ?? roomAlias.text; - final IList via = .new( - Uri.tryParse(roomAlias.text.replaceAll("/#", "")) - ?.queryParametersAll["via"] ?? - [], - ); + final via = roomAlias.text.via; showDialog( context: context, - builder: (context) => ref - .watch( - RoomSummaryController.provider( - .new(roomIdOrAlias: roomIdOrAlias, via: via), - ), - ) - .betterWhen( - data: (summary) => RoomSummaryDialog( - summary, - extraActions: .new([ - TextButton( - onPressed: () async { - // Capture everything before the dialog closes. - final container = ProviderScope.containerOf(context); - final scaffoldMessenger = ScaffoldMessenger.of(context); - final colors = Theme.of(context).colorScheme; - Navigator.of(context).pop(); - - final snackbar = scaffoldMessenger.showSnackBar( - .new( - content: Text("Joining room $roomIdOrAlias."), - duration: Duration(days: 999), - ), - ); - - try { - final id = await container - .read(ClientController.provider.notifier) - .joinRoom( - .new(roomIdOrAlias: roomIdOrAlias, via: via), - ); - - snackbar.close(); - - scaffoldMessenger.showSnackBar( - .new( - content: Text( - "Room $roomIdOrAlias successfully joined.", - ), - action: .new( - label: "Open", - onPressed: () => - container.focusRoom(id).onError(showError), - ), - ), - ); - } catch (error) { - snackbar.close(); - scaffoldMessenger.showSnackBar( - .new( - backgroundColor: colors.errorContainer, - content: Text( - error.toString(), - style: .new(color: colors.onErrorContainer), - ), - ), - ); - } - }, - child: Text("Join"), - ), - ]), - ), - loading: () => Dialog( - child: Row( - mainAxisAlignment: .center, - mainAxisSize: .min, - children: [ - Padding( - padding: .all(24), - child: CircularProgressIndicator(), - ), - ], - ), - ), - ), + builder: (context) => switch (ref.watch( + RoomSummaryController.provider( + .new(roomIdOrAlias: roomIdOrAlias, via: via), + ), + )) { + AsyncData(:final value) => RoomSummaryDialog(value, via: via), + AsyncError _ || AsyncLoading _ => RoomSummaryDialog( + .new(roomId: roomAlias.text), + via: via, + ), + }, ); } diff --git a/lib/widgets/room_appbar.dart b/lib/widgets/room_appbar.dart index 631e2f9..f9d9a06 100644 --- a/lib/widgets/room_appbar.dart +++ b/lib/widgets/room_appbar.dart @@ -32,7 +32,7 @@ final class const RoomAppbar({ .new( roomId: room!.metadata!.id, joinedMembers: - room.metadata!.lazyLoadSummary?.joinedMemberCount ?? -1, + room.metadata!.lazyLoadSummary?.joinedMemberCount, avatarUrl: room.metadata?.avatar, canonicalAlias: room.metadata?.canonicalAlias, name: room.metadata?.name, diff --git a/lib/widgets/room_summary_dialog.dart b/lib/widgets/room_summary_dialog.dart index 67927e3..5c7d302 100644 --- a/lib/widgets/room_summary_dialog.dart +++ b/lib/widgets/room_summary_dialog.dart @@ -1,6 +1,8 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:material_ui/material_ui.dart"; +import "package:nexus/controllers/client.dart"; +import "package:nexus/helpers/extensions/focus_room.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/expandable_image.dart"; import "package:nexus/widgets/linkified_text.dart"; @@ -8,11 +10,11 @@ import "package:nexus/models/room_summary.dart"; class const RoomSummaryDialog( final RoomSummary summary, { - final IList extraActions = const IList.empty(), + final IList? via, super.key, -}) extends ConsumerWidget { +}) extends StatelessWidget { @override - Widget build(BuildContext context, WidgetRef ref) => AlertDialog( + Widget build(BuildContext context) => AlertDialog( constraints: .loose(.fromWidth(400)), scrollable: true, contentPadding: EdgeInsets.all(24).copyWith(top: 8), @@ -25,14 +27,14 @@ class const RoomSummaryDialog( summary.avatarUrl == null ? null : .new(mxc: summary.avatarUrl!), child: AvatarOrHash( summary.avatarUrl, - summary.name ?? "Unnamed Room", + "", height: 64, fallback: Icon(Icons.numbers), ), ), Expanded( child: Text( - summary.name ?? summary.canonicalAlias ?? summary.roomId, + summary.name ?? summary.canonicalAlias ?? "Unnamed Room", overflow: .ellipsis, maxLines: 3, ), @@ -42,7 +44,9 @@ class const RoomSummaryDialog( content: Column( children: [ ListTile( - title: Text("${summary.joinedMembers} members"), + title: Text( + "${summary.joinedMembers ?? "Unknown number of"} members", + ), leading: Icon(Icons.people), ), ListTile( @@ -66,7 +70,55 @@ class const RoomSummaryDialog( actions: [ TextButton(onPressed: Navigator.of(context).pop, child: Text("Cancel")), - ...extraActions, + if (via != null) + TextButton( + onPressed: () async { + // Capture everything before the dialog closes. + final container = ProviderScope.containerOf(context); + final scaffoldMessenger = ScaffoldMessenger.of(context); + final colors = Theme.of(context).colorScheme; + Navigator.of(context).pop(); + + final roomIdOrAlias = summary.canonicalAlias ?? summary.roomId; + + final snackbar = scaffoldMessenger.showSnackBar( + .new( + content: Text("Joining room $roomIdOrAlias."), + duration: Duration(days: 999), + ), + ); + + try { + final id = await container + .read(ClientController.provider.notifier) + .joinRoom(.new(roomIdOrAlias: summary.roomId, via: via!)); + + snackbar.close(); + + scaffoldMessenger.showSnackBar( + .new( + content: Text("Room $roomIdOrAlias successfully joined."), + action: .new( + label: "Open", + onPressed: () => container.focusRoom(id), + ), + ), + ); + } catch (error) { + snackbar.close(); + scaffoldMessenger.showSnackBar( + .new( + backgroundColor: colors.errorContainer, + content: Text( + error.toString(), + style: .new(color: colors.onErrorContainer), + ), + ), + ); + } + }, + child: Text("Join"), + ), ], ); }