diff --git a/.gitignore b/.gitignore index 2bec583..912ced3 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,6 @@ key.properties password.txt # Nix -/result \ No newline at end of file +/result +# Widget Preview related +.widget_preview/ diff --git a/lib/helpers/extensions/focus_room.dart b/lib/helpers/extensions/focus_room.dart new file mode 100644 index 0000000..ac1e6f2 --- /dev/null +++ b/lib/helpers/extensions/focus_room.dart @@ -0,0 +1,36 @@ +import "package:collection/collection.dart"; +import "package:flutter_riverpod/experimental/mutation.dart"; +import "package:nexus/controllers/key.dart"; +import "package:nexus/controllers/spaces.dart"; +import "package:nexus/models/space.dart"; + +extension FocusRoom on MutationTarget { + Future focusRoom(String id) async { + final spaces = container.read(SpacesController.provider); + + if (spaces.firstWhereOrNull((space) => space.id == id) case Space _?) { + return await container + .read(KeyController.provider(KeyController.spaceKey).notifier) + .set(id); + } + + final parent = spaces.firstWhereOrNull( + (space) => + space.children.any((room) => room.metadata?.id == id) || + space.subSpaces.any( + (sub) => + sub.room.metadata?.id == id || + sub.children.any((room) => room.metadata?.id == id), + ), + ); + if (parent == null) return; + + await container + .read(KeyController.provider(KeyController.spaceKey).notifier) + .set(parent.id); + + await container + .read(KeyController.provider(KeyController.roomKey).notifier) + .set(id); + } +} diff --git a/lib/models/lazy_load_summary.dart b/lib/models/lazy_load_summary.dart index f9a814e..fba1c98 100644 --- a/lib/models/lazy_load_summary.dart +++ b/lib/models/lazy_load_summary.dart @@ -7,8 +7,9 @@ part "lazy_load_summary.g.dart"; @Freezed(toJson: false, fromJson: false) @JsonSerializable() class const LazyLoadSummary({ - required final IList? heroes, - required final int? joinedMemberCount, + @JsonKey(name: "m.heroes") required final IList? heroes, + @JsonKey(name: "m.joined_member_count") required final int? joinedMemberCount, + @JsonKey(name: "m.invited_member_count") required final int? invitedMemberCount, }) with _$LazyLoadSummary { Map toJson() => _$LazyLoadSummaryToJson(this); diff --git a/lib/models/room_metadata.dart b/lib/models/room_metadata.dart index 3aafd0e..822fcda 100644 --- a/lib/models/room_metadata.dart +++ b/lib/models/room_metadata.dart @@ -9,9 +9,6 @@ part "room_metadata.g.dart"; @JsonSerializable() class const RoomMetadata({ @JsonKey(name: "room_id") required final String id, - - // CreateEventContent creationContent, - // TombstoneEventContent tombstoneEventContent, final String? name, final Uri? avatar, final String? dmUserId, diff --git a/lib/widgets/avatar_or_hash.dart b/lib/widgets/avatar_or_hash.dart index 982d56e..791dd67 100644 --- a/lib/widgets/avatar_or_hash.dart +++ b/lib/widgets/avatar_or_hash.dart @@ -23,9 +23,8 @@ final class const AvatarOrHash( child: Center( child: ClipRRect( borderRadius: .all(.circular((height - 8) / 2.5)), - child: SizedBox( - width: height, - height: height, + child: SizedBox.square( + dimension: height, child: avatar == null ? fallback ?? box : Image( diff --git a/lib/widgets/join_dialog.dart b/lib/widgets/join_dialog.dart index e818c7d..071ddef 100644 --- a/lib/widgets/join_dialog.dart +++ b/lib/widgets/join_dialog.dart @@ -1,16 +1,111 @@ -import "package:collection/collection.dart"; +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/key.dart"; -import "package:nexus/controllers/spaces.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 Widget build(BuildContext context) { final roomAlias = useTextEditingController(); + + Future onJoin() async { + Navigator.of(context).pop(); + + final roomIdOrAlias = roomAlias.text.mention ?? roomAlias.text; + final IList via = .new( + Uri.tryParse(roomAlias.text.replaceAll("/#", "")) + ?.queryParametersAll["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(), + ), + ], + ), + ), + ), + ); + } + return AlertDialog( title: Text("Join a Room"), content: Column( @@ -21,111 +116,14 @@ class const JoinDialog(final WidgetRef ref, {super.key}) extends HookWidget { SizedBox(height: 12), TextField( controller: roomAlias, - decoration: .new(hintText: "#room:server"), + decoration: .new(hintText: "#room:server.name"), + onEditingComplete: onJoin, ), ], ), actions: [ TextButton(onPressed: Navigator.of(context).pop, child: Text("Cancel")), - TextButton( - onPressed: () async { - Navigator.of(context).pop(); - - if (context.mounted) { - final roomIdOrAlias = roomAlias.text.mention ?? roomAlias.text; - - final scaffoldMessenger = ScaffoldMessenger.of(context); - - final snackbar = scaffoldMessenger.showSnackBar( - .new( - content: Text("Joining room $roomIdOrAlias."), - duration: Duration(days: 999), - ), - ); - - try { - final id = await ref - .watch(ClientController.provider.notifier) - .joinRoom( - .new( - roomIdOrAlias: roomIdOrAlias, - via: .new( - Uri.tryParse(roomAlias.text.replaceAll("/#", "")) - ?.queryParametersAll["via"] ?? - [], - ), - ), - ); - - snackbar.close(); - - scaffoldMessenger.showSnackBar( - .new( - content: Text("Room $roomIdOrAlias successfully joined."), - action: .new( - label: "Open", - onPressed: () async { - final spaces = ref.watch(SpacesController.provider); - final space = spaces.firstWhereOrNull( - (space) => space.id == id, - ); - - await ref - .watch( - KeyController.provider(KeyController.spaceKey) - .notifier, - ) - .set( - space?.id ?? - spaces - .firstWhere( - (space) => - space.children.any( - (child) => - child.metadata?.id == id, - ) || - space.subSpaces.any( - (child) => - child.room.metadata?.id == id, - ), - ) - .id, - ); - - if (space == null) { - await ref - .watch( - KeyController.provider(KeyController.roomKey) - .notifier, - ) - .set(id); - } - }, - ), - ), - ); - } catch (error) { - snackbar.close(); - if (context.mounted) { - scaffoldMessenger.showSnackBar( - .new( - backgroundColor: Theme.of(context) - .colorScheme - .errorContainer, - content: Text( - error.toString(), - style: .new( - color: Theme.of(context).colorScheme.onErrorContainer, - ), - ), - ), - ); - } - } - } - }, - child: Text("Join"), - ), + TextButton(onPressed: onJoin, child: Text("Join")), ], ); } diff --git a/lib/widgets/pages/notifications.dart b/lib/widgets/pages/notifications.dart index d1886d0..269f13f 100644 --- a/lib/widgets/pages/notifications.dart +++ b/lib/widgets/pages/notifications.dart @@ -1,14 +1,12 @@ import "dart:async"; -import "package:collection/collection.dart"; import "package:m3e_buttons/m3e_buttons.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/contextual_event.dart"; -import "package:nexus/controllers/key.dart"; import "package:nexus/controllers/notifications.dart"; -import "package:nexus/controllers/spaces.dart"; +import "package:nexus/helpers/extensions/focus_room.dart"; import "package:nexus/models/event.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/error_dialog.dart"; @@ -131,41 +129,7 @@ class const NotificationsPage({ child: HighlightWrapper( InkWell( onTap: () async { - final spaces = ref.read( - SpacesController.provider, - ); - final space = spaces.firstWhereOrNull( - (space) => - space.children.any( - (room) => - room.metadata?.id == - event.roomId, - ) || - space.subSpaces.any( - (subSpace) => - subSpace.children.any( - (room) => - room.metadata?.id == - event.roomId, - ), - ), - ); - if (space == null) return; - - await ref - .read( - KeyController.provider( - KeyController.spaceKey, - ).notifier, - ) - .set(space.id); - await ref - .read( - KeyController.provider( - KeyController.roomKey, - ).notifier, - ) - .set(event.roomId); + await ref.focusRoom(event.roomId); ref .watch( ContextualEventController.provider( diff --git a/lib/widgets/room_appbar.dart b/lib/widgets/room_appbar.dart index 305c490..631e2f9 100644 --- a/lib/widgets/room_appbar.dart +++ b/lib/widgets/room_appbar.dart @@ -3,9 +3,8 @@ import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/rooms.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; -import "package:nexus/widgets/expandable_image.dart"; -import "package:nexus/widgets/linkified_text.dart"; import "package:nexus/widgets/room_menu.dart"; +import "package:nexus/widgets/room_summary_dialog.dart"; final class const RoomAppbar({ required final String? roomId, @@ -25,61 +24,19 @@ final class const RoomAppbar({ : ref.watch(RoomsController.provider.select((value) => value[roomId!])); return Appbar( - onTap: room == null + onTap: room?.metadata == null ? null : () => showDialog( context: context, - builder: (context) => Dialog( - constraints: .loose(.fromWidth(400)), - child: Padding( - padding: .all(24), - child: SingleChildScrollView( - child: Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - spacing: 8, - children: [ - Row( - spacing: 12, - mainAxisSize: .min, - children: [ - if (room.metadata?.avatar != null) - ExpandableImage( - room.metadata?.avatar == null - ? null - : .new(mxc: room.metadata!.avatar!), - child: AvatarOrHash( - room.metadata?.avatar, - room.metadata?.name ?? "Unnamed Room", - height: 64, - fallback: Icon(Icons.numbers), - ), - ), - Expanded( - child: Text( - room.metadata?.name ?? "Unnamed Room", - overflow: .ellipsis, - maxLines: 3, - style: Theme.of(context) - .textTheme - .headlineSmall, - ), - ), - ], - ), - if (room.metadata?.topic?.isNotEmpty == true) - LinkifiedText( - room.metadata!.topic!, - style: Theme.of(context).textTheme.bodyLarge - ?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - ), - ), - ], - ), - ), + builder: (context) => RoomSummaryDialog( + .new( + roomId: room!.metadata!.id, + joinedMembers: + room.metadata!.lazyLoadSummary?.joinedMemberCount ?? -1, + avatarUrl: room.metadata?.avatar, + canonicalAlias: room.metadata?.canonicalAlias, + name: room.metadata?.name, + topic: room.metadata?.topic, ), ), ), diff --git a/lib/widgets/room_summary_dialog.dart b/lib/widgets/room_summary_dialog.dart new file mode 100644 index 0000000..67927e3 --- /dev/null +++ b/lib/widgets/room_summary_dialog.dart @@ -0,0 +1,72 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:material_ui/material_ui.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/expandable_image.dart"; +import "package:nexus/widgets/linkified_text.dart"; +import "package:nexus/models/room_summary.dart"; + +class const RoomSummaryDialog( + final RoomSummary summary, { + final IList extraActions = const IList.empty(), + super.key, +}) extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) => AlertDialog( + constraints: .loose(.fromWidth(400)), + scrollable: true, + contentPadding: EdgeInsets.all(24).copyWith(top: 8), + title: Row( + spacing: 12, + mainAxisSize: .min, + children: [ + if (summary.avatarUrl != null) + ExpandableImage( + 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, + overflow: .ellipsis, + maxLines: 3, + ), + ), + ], + ), + content: Column( + children: [ + ListTile( + title: Text("${summary.joinedMembers} members"), + leading: Icon(Icons.people), + ), + ListTile( + title: SelectableText(summary.canonicalAlias ?? summary.roomId), + leading: Icon(Icons.numbers), + ), + + if (summary.topic != null) + ListTile( + isThreeLine: true, + subtitle: LinkifiedText( + summary.topic!, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + leading: Icon(Icons.info), + ), + ], + ), + + actions: [ + TextButton(onPressed: Navigator.of(context).pop, child: Text("Cancel")), + ...extraActions, + ], + ); +}