diff --git a/lib/controllers/emoji_shortcodes.dart b/lib/controllers/emoji_shortcodes.dart deleted file mode 100644 index a24ebb1..0000000 --- a/lib/controllers/emoji_shortcodes.dart +++ /dev/null @@ -1,31 +0,0 @@ -import "package:collection/collection.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:material_emoji_picker/material_emoji_picker.dart"; - -/// Maps each unicode emoji, without variation selectors, to its shortcode. -class EmojiShortcodesController extends Notifier> { - static final _variationSelectors = RegExp("[\uFE0E\uFE0F]"); - - static String normalize(String emoji) => - emoji.replaceAll(_variationSelectors, ""); - - @override - IMap build() => .fromEntries( - ref - .watch(EmojiController.provider) - .requireValue - .map((category) => category.emojis) - .flattened - .where((emoji) => emoji.aliases.isNotEmpty) - .map( - (emoji) => - MapEntry(normalize(emoji.value), ":${emoji.aliases.first}:"), - ), - ); - - static final provider = - NotifierProvider>( - EmojiShortcodesController.new, - ); -} diff --git a/lib/controllers/reactions.dart b/lib/controllers/reactions.dart index 662661c..0c30128 100644 --- a/lib/controllers/reactions.dart +++ b/lib/controllers/reactions.dart @@ -1,18 +1,14 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/emoji_shortcodes.dart"; import "package:nexus/controllers/rooms.dart"; import "package:nexus/models/configs/event.dart"; import "package:nexus/models/content/reaction.dart"; -import "package:nexus/models/reaction_group.dart"; class ReactionsController(final EventConfig config) - extends AsyncNotifier> { + extends AsyncNotifier>> { @override - Future> build() async { - final unicodeShortcodes = ref.watch(EmojiShortcodesController.provider); - + Future>> build() async { final eventInfo = ref.watch( RoomsController.provider.select((value) { final event = value[config.roomId]?.events[config.eventRowId]; @@ -34,25 +30,12 @@ class ReactionsController(final EventConfig config) return reactionEvents ?.where((event) => event.redactedBy == null) - .fold>(.new(), (acc, event) { - if (event.content case ReactionContent( - :final key?, - :final shortcode, - )) { + .fold>>(.new(), (acc, event) { + if (event.content case ReactionContent(:final key?)) { return acc.update( key, - (group) => group.copyWith( - senders: group.senders.add(event.sender), - shortcode: group.shortcode ?? shortcode, - ), - ifAbsent: () => .new( - senders: .new([event.sender]), - shortcode: - shortcode ?? - unicodeShortcodes[EmojiShortcodesController.normalize( - key, - )], - ), + (list) => list.add(event.sender), + ifAbsent: () => .new([event.sender]), ); } @@ -64,7 +47,7 @@ class ReactionsController(final EventConfig config) static final provider = AsyncNotifierProvider.family< ReactionsController, - IMap, + IMap>, EventConfig >(ReactionsController.new); } diff --git a/lib/controllers/room_settings_sections.dart b/lib/controllers/room_settings_sections.dart index bababee..bf7159a 100644 --- a/lib/controllers/room_settings_sections.dart +++ b/lib/controllers/room_settings_sections.dart @@ -36,9 +36,9 @@ import "package:nexus/models/join_rule.dart"; import "package:nexus/models/power_level_permission.dart"; import "package:nexus/models/setting.dart"; import "package:nexus/models/settings_category.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; -import "package:nexus/widgets/common/divider_text.dart"; -import "package:nexus/widgets/avatars/image_pack_avatar.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/divider_text.dart"; +import "package:nexus/widgets/image_pack_avatar.dart"; import "package:nexus/widgets/settings/add_users_power_level_sheet.dart"; import "package:nexus/widgets/settings/aliases_sheet.dart"; import "package:nexus/widgets/settings/allowed_rooms_sheet.dart"; diff --git a/lib/controllers/settings_sections.dart b/lib/controllers/settings_sections.dart index 999fea6..35c7f94 100644 --- a/lib/controllers/settings_sections.dart +++ b/lib/controllers/settings_sections.dart @@ -35,15 +35,15 @@ import "package:nexus/models/settings.dart"; import "package:nexus/models/settings_category.dart"; import "package:nexus/models/show_and_send.dart"; import "package:nexus/main.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; -import "package:nexus/widgets/common/divider_text.dart"; -import "package:nexus/widgets/avatars/image_pack_avatar.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/divider_text.dart"; +import "package:nexus/widgets/image_pack_avatar.dart"; import "package:nexus/widgets/settings/block_user_field.dart"; import "package:nexus/widgets/settings/dialog_list_tile.dart"; import "package:nexus/widgets/settings/enable_image_packs_sheet.dart"; import "package:nexus/widgets/settings/keyword_field.dart"; -import "package:nexus/widgets/users/user_bottom_sheet.dart"; -import "package:nexus/widgets/encryption/reset_encryption_sheet.dart"; +import "package:nexus/widgets/user_bottom_sheet.dart"; +import "package:nexus/widgets/reset_encryption_sheet.dart"; class SettingsSectionsController extends AsyncNotifier>> { diff --git a/lib/helpers/extensions/better_when.dart b/lib/helpers/extensions/better_when.dart index 459d139..e054d02 100644 --- a/lib/helpers/extensions/better_when.dart +++ b/lib/helpers/extensions/better_when.dart @@ -1,7 +1,7 @@ import "package:flutter/widgets.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/widgets/common/error_dialog.dart"; -import "package:nexus/widgets/common/loading.dart"; +import "package:nexus/widgets/error_dialog.dart"; +import "package:nexus/widgets/loading.dart"; extension BetterWhen on AsyncValue { Widget betterWhen({ diff --git a/lib/helpers/extensions/build_event_options.dart b/lib/helpers/extensions/build_event_options.dart index a6ecc06..aff4ba0 100644 --- a/lib/helpers/extensions/build_event_options.dart +++ b/lib/helpers/extensions/build_event_options.dart @@ -15,8 +15,8 @@ import "package:nexus/helpers/extensions/event_id_is_fake.dart"; import "package:nexus/models/content/message.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/relation_type.dart"; -import "package:nexus/widgets/emoji/emoji_picker.dart"; -import "package:nexus/widgets/emoji/mxc_emoji.dart"; +import "package:nexus/widgets/emoji_picker.dart"; +import "package:nexus/widgets/mxc_emoji.dart"; import "package:nexus/main.dart"; extension BuildEventOptions on Event { diff --git a/lib/helpers/extensions/image_pack_emoji.dart b/lib/helpers/extensions/image_pack_emoji.dart index fe480d5..2b04cf1 100644 --- a/lib/helpers/extensions/image_pack_emoji.dart +++ b/lib/helpers/extensions/image_pack_emoji.dart @@ -2,7 +2,7 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:material_emoji_picker/material_emoji_picker.dart"; import "package:nexus/models/content/image_pack.dart"; import "package:nexus/models/image_pack.dart"; -import "package:nexus/widgets/emoji/mxc_emoji.dart"; +import "package:nexus/widgets/mxc_emoji.dart"; extension ImagePackEmoji on ImagePack { EmojiCategory? get emojiCategory => _category(emoticons); diff --git a/lib/helpers/extensions/open_link_info.dart b/lib/helpers/extensions/open_link_info.dart index 60947bb..e15e9b0 100644 --- a/lib/helpers/extensions/open_link_info.dart +++ b/lib/helpers/extensions/open_link_info.dart @@ -7,7 +7,7 @@ import "package:nexus/helpers/extensions/focus_room.dart"; import "package:nexus/helpers/extensions/get_link_info.dart"; import "package:nexus/helpers/extensions/show_user_popover.dart"; import "package:nexus/models/room_summary.dart"; -import "package:nexus/widgets/rooms/room_summary_sheet.dart"; +import "package:nexus/widgets/room_summary_sheet.dart"; extension OpenLinkInfo on WidgetRef { Future openLinkInfo( diff --git a/lib/helpers/extensions/room_destinations.dart b/lib/helpers/extensions/room_destinations.dart index 3d0323b..3628d05 100644 --- a/lib/helpers/extensions/room_destinations.dart +++ b/lib/helpers/extensions/room_destinations.dart @@ -2,7 +2,7 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:material_ui/material_ui.dart"; import "package:navigation_rail_m3e/navigation_rail_m3e.dart"; import "package:nexus/models/room.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; extension RoomDestination on Room { NavigationRailM3EDestination toDestination({ diff --git a/lib/helpers/extensions/scheme_to_theme.dart b/lib/helpers/extensions/scheme_to_theme.dart index 73ce176..97d239d 100644 --- a/lib/helpers/extensions/scheme_to_theme.dart +++ b/lib/helpers/extensions/scheme_to_theme.dart @@ -20,8 +20,6 @@ extension SchemeToTheme on ColorScheme { shape: RoundedRectangleBorder(borderRadius: .circular(16)), color: surfaceContainerHigh, ), - // So ink at the edges of a sheet doesn't spill past its rounded corners - bottomSheetTheme: .new(clipBehavior: .antiAlias), appBarTheme: AppBarTheme( titleSpacing: 0, backgroundColor: surfaceContainerLow, diff --git a/lib/helpers/extensions/show_image_viewer.dart b/lib/helpers/extensions/show_image_viewer.dart deleted file mode 100644 index 9be95b8..0000000 --- a/lib/helpers/extensions/show_image_viewer.dart +++ /dev/null @@ -1,83 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:m3e_buttons/m3e_buttons.dart"; -import "package:material_ui/material_ui.dart"; -import "package:nexus/helpers/extensions/save_attachment.dart"; -import "package:nexus/helpers/extensions/size_to_string.dart"; -import "package:nexus/helpers/mxc_image.dart"; -import "package:nexus/models/requests/download_media.dart"; -import "package:nexus/widgets/common/error_dialog.dart"; - -extension ShowImageViewer on BuildContext { - /// Shows the image from [request] fullscreen, zoomable. - /// - /// If [downloadFilename] is set, a download button is shown, suggesting it. - void showImageViewer( - WidgetRef ref, - DownloadMediaRequest request, { - String? downloadFilename, - int? size, - }) => showDialog( - context: this, - builder: (context) => SafeArea( - child: Stack( - children: [ - Positioned.fill( - child: GestureDetector( - onTap: Navigator.of(context).pop, - child: InteractiveViewer( - maxScale: 10, - child: Image( - errorBuilder: (_, error, stackTrace) => ErrorDialog( - "Loading failed for ${request.mxc}\nError: $error", - stackTrace, - ), - image: MxcImage(ref, request), - ), - ), - ), - ), - if (size != null) - Align( - alignment: .topLeft, - child: Padding( - padding: .all(32), - child: Card( - margin: .zero, - child: Padding( - padding: .symmetric(horizontal: 12, vertical: 8), - child: Text(size.sizeAsString), - ), - ), - ), - ), - Align( - alignment: .topRight, - child: Padding( - padding: .all(32), - child: Row( - mainAxisSize: .min, - spacing: 8, - children: [ - if (downloadFilename != null) - M3EButton( - onPressed: () => context.saveAttachment( - ref, - request.mxc, - encrypted: request.encrypted, - filename: downloadFilename, - ), - child: Icon(Icons.download), - ), - M3EButton( - onPressed: Navigator.of(context).pop, - child: Icon(Icons.close), - ), - ], - ), - ), - ), - ], - ), - ), - ); -} diff --git a/lib/helpers/extensions/show_user_popover.dart b/lib/helpers/extensions/show_user_popover.dart index e7018c0..af3e00e 100644 --- a/lib/helpers/extensions/show_user_popover.dart +++ b/lib/helpers/extensions/show_user_popover.dart @@ -1,6 +1,6 @@ import "package:material_ui/material_ui.dart"; import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; -import "package:nexus/widgets/users/user_bottom_sheet.dart"; +import "package:nexus/widgets/user_bottom_sheet.dart"; extension ShowUserPopover on BuildContext { void showUserPopover(String userId, {String? roomId, Rect? anchor}) => diff --git a/lib/main.dart b/lib/main.dart index 590c74d..6234b0c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,9 +22,9 @@ import "package:nexus/widgets/pages/select_server.dart"; import "package:nexus/widgets/pages/settings.dart"; import "package:nexus/widgets/pages/verify.dart"; import "package:nexus/widgets/account_scope.dart"; -import "package:nexus/widgets/common/appbar.dart"; -import "package:nexus/widgets/common/error_dialog.dart"; -import "package:nexus/widgets/common/loading.dart"; +import "package:nexus/widgets/appbar.dart"; +import "package:nexus/widgets/error_dialog.dart"; +import "package:nexus/widgets/loading.dart"; import "package:window_manager/window_manager.dart"; import "package:material_ui/material_ui.dart"; diff --git a/lib/models/reaction_group.dart b/lib/models/reaction_group.dart deleted file mode 100644 index b3ea32c..0000000 --- a/lib/models/reaction_group.dart +++ /dev/null @@ -1,10 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; - -part "reaction_group.freezed.dart"; - -@Freezed(toJson: false, fromJson: false) -class const ReactionGroup({ - required final IList senders, - final String? shortcode, -}) with _$ReactionGroup; diff --git a/lib/widgets/common/account_switcher.dart b/lib/widgets/account_switcher.dart similarity index 98% rename from lib/widgets/common/account_switcher.dart rename to lib/widgets/account_switcher.dart index 3d0f996..6c9c93b 100644 --- a/lib/widgets/common/account_switcher.dart +++ b/lib/widgets/account_switcher.dart @@ -7,7 +7,7 @@ import "package:nexus/controllers/active_account.dart"; import "package:nexus/helpers/extensions/log_out.dart"; import "package:nexus/main.dart"; import "package:nexus/models/account.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; /// Switches between, adds and logs out of accounts. With [onlyWithOthers], it's /// hidden when there's only one account. diff --git a/lib/widgets/rooms/add_directory_server_sheet.dart b/lib/widgets/add_directory_server_sheet.dart similarity index 100% rename from lib/widgets/rooms/add_directory_server_sheet.dart rename to lib/widgets/add_directory_server_sheet.dart diff --git a/lib/widgets/rooms/add_space_rooms_sheet.dart b/lib/widgets/add_space_rooms_sheet.dart similarity index 99% rename from lib/widgets/rooms/add_space_rooms_sheet.dart rename to lib/widgets/add_space_rooms_sheet.dart index e78ba7a..b6d8767 100644 --- a/lib/widgets/rooms/add_space_rooms_sheet.dart +++ b/lib/widgets/add_space_rooms_sheet.dart @@ -14,7 +14,7 @@ import "package:nexus/helpers/extensions/with_progress_snackbar.dart"; import "package:nexus/main.dart"; import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/space_child.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/settings/search_chooser.dart"; /// Chooses rooms to add to the space [spaceId], then adds them. diff --git a/lib/widgets/common/appbar.dart b/lib/widgets/appbar.dart similarity index 100% rename from lib/widgets/common/appbar.dart rename to lib/widgets/appbar.dart diff --git a/lib/widgets/avatars/avatar_or_hash.dart b/lib/widgets/avatar_or_hash.dart similarity index 100% rename from lib/widgets/avatars/avatar_or_hash.dart rename to lib/widgets/avatar_or_hash.dart diff --git a/lib/widgets/avatars/avatar_ring.dart b/lib/widgets/avatar_ring.dart similarity index 100% rename from lib/widgets/avatars/avatar_ring.dart rename to lib/widgets/avatar_ring.dart diff --git a/lib/widgets/common/bouncing_dots.dart b/lib/widgets/bouncing_dots.dart similarity index 100% rename from lib/widgets/common/bouncing_dots.dart rename to lib/widgets/bouncing_dots.dart diff --git a/lib/widgets/common/expandable_image.dart b/lib/widgets/common/expandable_image.dart deleted file mode 100644 index 34a1f76..0000000 --- a/lib/widgets/common/expandable_image.dart +++ /dev/null @@ -1,28 +0,0 @@ -import "package:material_ui/material_ui.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/helpers/extensions/show_image_viewer.dart"; -import "package:nexus/models/requests/download_media.dart"; - -final class const ExpandableImage( - final DownloadMediaRequest? request, { - required final Widget child, - - /// If set, a download button is shown, suggesting this filename - final String? downloadFilename, - final int? size, - super.key, -}) extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) => InkWell( - onTap: switch (request) { - final request? => () => context.showImageViewer( - ref, - request, - downloadFilename: downloadFilename, - size: size, - ), - null => null, - }, - child: child, - ); -} diff --git a/lib/widgets/composer/bot_command_panel.dart b/lib/widgets/composer/bot_command_panel.dart index bc6c05a..8a06754 100644 --- a/lib/widgets/composer/bot_command_panel.dart +++ b/lib/widgets/composer/bot_command_panel.dart @@ -9,7 +9,7 @@ import "package:nexus/controllers/bot_command_draft.dart"; import "package:nexus/models/bot_argument_suggestion.dart"; import "package:nexus/models/bot_command.dart"; import "package:nexus/models/text_command.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; class const BotCommandPanel( final ComposerParams composer, { diff --git a/lib/widgets/composer/composer.dart b/lib/widgets/composer/composer.dart index 4c3caf6..1b70ec3 100644 --- a/lib/widgets/composer/composer.dart +++ b/lib/widgets/composer/composer.dart @@ -17,7 +17,7 @@ import "package:nexus/models/composer_params.dart"; import "package:nexus/models/text_command.dart"; import "package:nexus/widgets/composer/bot_command_form.dart"; import "package:nexus/widgets/composer/bot_command_panel.dart"; -import "package:nexus/widgets/emoji/emoji_picker.dart"; +import "package:nexus/widgets/emoji_picker.dart"; import "package:material_ui/material_ui.dart"; import "package:flutter/services.dart"; import "package:flutter_hooks/flutter_hooks.dart"; @@ -35,10 +35,10 @@ import "package:nexus/models/content/image_pack.dart"; import "package:nexus/models/content/message.dart"; import "package:nexus/models/content/poll_start.dart"; import "package:nexus/models/image_pack.dart"; -import "package:nexus/widgets/emoji/mxc_emoji.dart"; -import "package:nexus/widgets/emoji/sticker_picker.dart"; -import "package:nexus/widgets/composer/create_poll_sheet.dart"; -import "package:nexus/widgets/location/location_picker_sheet.dart"; +import "package:nexus/widgets/mxc_emoji.dart"; +import "package:nexus/widgets/sticker_picker.dart"; +import "package:nexus/widgets/create_poll_sheet.dart"; +import "package:nexus/widgets/location_picker_sheet.dart"; import "package:nexus/models/location.dart"; import "package:nexus/widgets/composer/overlays/tagger_overlay.dart"; import "package:nexus/widgets/composer/paste_files_action.dart"; diff --git a/lib/widgets/composer/overlays/room_overlay.dart b/lib/widgets/composer/overlays/room_overlay.dart index c95db16..f4778a8 100644 --- a/lib/widgets/composer/overlays/room_overlay.dart +++ b/lib/widgets/composer/overlays/room_overlay.dart @@ -3,7 +3,7 @@ import "package:material_ui/material_ui.dart"; import "package:nexus/controllers/rooms.dart"; import "package:nexus/controllers/via.dart"; import "package:nexus/helpers/extensions/weighted_search.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; class const RoomOverlay( final String query, { diff --git a/lib/widgets/composer/overlays/tagger_overlay.dart b/lib/widgets/composer/overlays/tagger_overlay.dart index f760e01..7d973dc 100644 --- a/lib/widgets/composer/overlays/tagger_overlay.dart +++ b/lib/widgets/composer/overlays/tagger_overlay.dart @@ -3,7 +3,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/widgets/composer/overlays/room_overlay.dart"; import "package:nexus/widgets/composer/overlays/user_overlay.dart"; import "package:nexus/widgets/composer/overlays/emoji_overlay.dart"; -import "package:nexus/widgets/common/loading.dart"; +import "package:nexus/widgets/loading.dart"; class const TaggerOverlay( final String query, { diff --git a/lib/widgets/composer/overlays/user_overlay.dart b/lib/widgets/composer/overlays/user_overlay.dart index eec15f5..e6f122b 100644 --- a/lib/widgets/composer/overlays/user_overlay.dart +++ b/lib/widgets/composer/overlays/user_overlay.dart @@ -5,7 +5,7 @@ import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/extensions/weighted_search.dart"; import "package:nexus/models/content/membership.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; class const UserOverlay( final String query, { diff --git a/lib/widgets/composer/relation_preview.dart b/lib/widgets/composer/relation_preview.dart index f319d74..5af01ee 100644 --- a/lib/widgets/composer/relation_preview.dart +++ b/lib/widgets/composer/relation_preview.dart @@ -2,7 +2,7 @@ import "package:material_ui/material_ui.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/relation_type.dart"; -import "package:nexus/widgets/renderers/event_preview.dart"; +import "package:nexus/widgets/event_preview.dart"; class const RelationPreview( final Event? relatedEvent, { diff --git a/lib/widgets/composer/typing_indicator.dart b/lib/widgets/composer/typing_indicator.dart index 9cacb9c..9624937 100644 --- a/lib/widgets/composer/typing_indicator.dart +++ b/lib/widgets/composer/typing_indicator.dart @@ -5,8 +5,8 @@ import "package:nexus/controllers/room_typing.dart"; import "package:nexus/controllers/user.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/hooks/tap_recognizer.dart"; -import "package:nexus/widgets/common/bouncing_dots.dart"; -import "package:nexus/widgets/users/user_span.dart"; +import "package:nexus/widgets/bouncing_dots.dart"; +import "package:nexus/widgets/user_span.dart"; class const TypingIndicator(final String roomId, {super.key}) extends HookConsumerWidget { diff --git a/lib/widgets/common/count_badge.dart b/lib/widgets/count_badge.dart similarity index 100% rename from lib/widgets/common/count_badge.dart rename to lib/widgets/count_badge.dart diff --git a/lib/widgets/composer/create_poll_sheet.dart b/lib/widgets/create_poll_sheet.dart similarity index 100% rename from lib/widgets/composer/create_poll_sheet.dart rename to lib/widgets/create_poll_sheet.dart diff --git a/lib/widgets/rooms/create_room_sheet.dart b/lib/widgets/create_room_sheet.dart similarity index 100% rename from lib/widgets/rooms/create_room_sheet.dart rename to lib/widgets/create_room_sheet.dart diff --git a/lib/widgets/common/divider_text.dart b/lib/widgets/divider_text.dart similarity index 82% rename from lib/widgets/common/divider_text.dart rename to lib/widgets/divider_text.dart index 2c623ba..c44ae11 100644 --- a/lib/widgets/common/divider_text.dart +++ b/lib/widgets/divider_text.dart @@ -1,5 +1,5 @@ import "package:material_ui/material_ui.dart"; -import "package:nexus/widgets/common/divider_widget.dart"; +import "package:nexus/widgets/divider_widget.dart"; final class const DividerText(final String text, {super.key}) extends StatelessWidget { diff --git a/lib/widgets/common/divider_widget.dart b/lib/widgets/divider_widget.dart similarity index 100% rename from lib/widgets/common/divider_widget.dart rename to lib/widgets/divider_widget.dart diff --git a/lib/widgets/avatars/editable_avatar.dart b/lib/widgets/editable_avatar.dart similarity index 100% rename from lib/widgets/avatars/editable_avatar.dart rename to lib/widgets/editable_avatar.dart diff --git a/lib/widgets/emoji/emoji_picker.dart b/lib/widgets/emoji_picker.dart similarity index 100% rename from lib/widgets/emoji/emoji_picker.dart rename to lib/widgets/emoji_picker.dart diff --git a/lib/widgets/common/error_dialog.dart b/lib/widgets/error_dialog.dart similarity index 100% rename from lib/widgets/common/error_dialog.dart rename to lib/widgets/error_dialog.dart diff --git a/lib/widgets/renderers/event_preview.dart b/lib/widgets/event_preview.dart similarity index 100% rename from lib/widgets/renderers/event_preview.dart rename to lib/widgets/event_preview.dart diff --git a/lib/widgets/expandable_image.dart b/lib/widgets/expandable_image.dart new file mode 100644 index 0000000..848596f --- /dev/null +++ b/lib/widgets/expandable_image.dart @@ -0,0 +1,89 @@ +import "package:material_ui/material_ui.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:m3e_buttons/m3e_buttons.dart"; +import "package:nexus/helpers/extensions/save_attachment.dart"; +import "package:nexus/helpers/extensions/size_to_string.dart"; +import "package:nexus/helpers/mxc_image.dart"; +import "package:nexus/models/requests/download_media.dart"; +import "package:nexus/widgets/error_dialog.dart"; + +final class const ExpandableImage( + final DownloadMediaRequest? request, { + required final Widget child, + + /// If set, a download button is shown, suggesting this filename + final String? downloadFilename, + final int? size, + super.key, +}) extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) => InkWell( + onTap: request == null + ? null + : () => showDialog( + context: context, + builder: (_) => SafeArea( + child: Stack( + children: [ + Positioned.fill( + child: GestureDetector( + onTap: Navigator.of(context).pop, + child: InteractiveViewer( + maxScale: 10, + child: Image( + errorBuilder: (_, error, stackTrace) => ErrorDialog( + "Loading failed for ${request?.mxc}\nError: $error", + stackTrace, + ), + image: MxcImage(ref, request!), + ), + ), + ), + ), + if (size != null) + Align( + alignment: .topLeft, + child: Padding( + padding: .all(32), + child: Card( + margin: .zero, + child: Padding( + padding: .symmetric(horizontal: 12, vertical: 8), + child: Text(size!.sizeAsString), + ), + ), + ), + ), + Align( + alignment: .topRight, + child: Padding( + padding: .all(32), + child: Row( + mainAxisSize: .min, + spacing: 8, + children: [ + if (downloadFilename != null) + M3EButton( + onPressed: () => context.saveAttachment( + ref, + request!.mxc, + encrypted: request!.encrypted, + filename: downloadFilename, + ), + child: Icon(Icons.download), + ), + M3EButton( + onPressed: Navigator.of(context).pop, + child: Icon(Icons.close), + ), + ], + ), + ), + ), + ], + ), + ), + ), + child: child, + ); +} diff --git a/lib/widgets/renderers/file_card.dart b/lib/widgets/file_card.dart similarity index 100% rename from lib/widgets/renderers/file_card.dart rename to lib/widgets/file_card.dart diff --git a/lib/widgets/common/highlight_wrapper.dart b/lib/widgets/highlight_wrapper.dart similarity index 100% rename from lib/widgets/common/highlight_wrapper.dart rename to lib/widgets/highlight_wrapper.dart diff --git a/lib/widgets/html/html.dart b/lib/widgets/html/html.dart index 9236e9a..d717a8a 100644 --- a/lib/widgets/html/html.dart +++ b/lib/widgets/html/html.dart @@ -6,7 +6,7 @@ import "package:html/dom.dart" as dom; import "package:nexus/helpers/extensions/get_link_info.dart"; import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/helpers/mxc_image.dart"; -import "package:nexus/widgets/common/expandable_image.dart"; +import "package:nexus/widgets/expandable_image.dart"; import "package:nexus/widgets/html/mention_chip.dart"; import "package:nexus/widgets/html/spoiler_text.dart"; import "package:nexus/widgets/html/code_block.dart"; diff --git a/lib/widgets/avatars/image_pack_avatar.dart b/lib/widgets/image_pack_avatar.dart similarity index 86% rename from lib/widgets/avatars/image_pack_avatar.dart rename to lib/widgets/image_pack_avatar.dart index 42504ae..aaebca2 100644 --- a/lib/widgets/avatars/image_pack_avatar.dart +++ b/lib/widgets/image_pack_avatar.dart @@ -1,5 +1,5 @@ import "package:material_ui/material_ui.dart"; -import "package:nexus/widgets/emoji/mxc_emoji.dart"; +import "package:nexus/widgets/mxc_emoji.dart"; class const ImagePackAvatar(final Uri? icon, {super.key}) extends StatelessWidget { diff --git a/lib/widgets/sidebar/invites_list.dart b/lib/widgets/invites_list.dart similarity index 63% rename from lib/widgets/sidebar/invites_list.dart rename to lib/widgets/invites_list.dart index 18fb5f0..68d8b3b 100644 --- a/lib/widgets/sidebar/invites_list.dart +++ b/lib/widgets/invites_list.dart @@ -1,10 +1,14 @@ -import "package:nexus/widgets/sidebar/invite_card.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:material_ui/material_ui.dart"; import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; import "package:nexus/controllers/invites.dart"; +import "package:nexus/helpers/extensions/get_localpart.dart"; +import "package:nexus/helpers/extensions/room_summary_display.dart"; import "package:nexus/main.dart"; import "package:nexus/models/nav_page.dart"; +import "package:nexus/models/room_summary.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/room_summary_sheet.dart"; class const InvitesList({super.key}) extends ConsumerWidget implements NavPage { @override @@ -88,3 +92,46 @@ class const InvitesList({super.key}) extends ConsumerWidget implements NavPage { ); } } + +class const InviteCard(final RoomSummary invite, {super.key}) + extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final responding = ref + .watch(InvitesController.respondMutation(invite.roomId)) + .isPending; + final name = invite.name ?? invite.canonicalAlias ?? invite.roomId; + final inviterName = + invite.inviterProfile?.displayName ?? invite.invitedBy?.localpart; + + return Card( + clipBehavior: .antiAlias, + child: ListTile( + onTap: responding + ? null + : () => context.showAdaptiveSheet((_) => RoomSummarySheet(invite)), + trailing: responding + ? const SizedBox.square( + dimension: 24, + child: CircularProgressIndicator(), + ) + : null, + leading: AvatarOrHash( + invite.avatarUrl, + name, + dimension: 40, + fallback: invite.isDirect ? null : Icon(invite.icon), + ), + title: Text(name, maxLines: 1, overflow: .ellipsis), + subtitle: Text( + [ + if (inviterName != null) + "${invite.isDirect ? "Direct message from" : "Invited by"} $inviterName", + ].join("\n"), + maxLines: 2, + overflow: .ellipsis, + ), + ), + ); + } +} diff --git a/lib/widgets/rooms/join_sheet.dart b/lib/widgets/join_sheet.dart similarity index 96% rename from lib/widgets/rooms/join_sheet.dart rename to lib/widgets/join_sheet.dart index e9ee719..8977814 100644 --- a/lib/widgets/rooms/join_sheet.dart +++ b/lib/widgets/join_sheet.dart @@ -4,7 +4,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; import "package:nexus/controllers/room_summary.dart"; import "package:nexus/helpers/extensions/get_link_info.dart"; -import "package:nexus/widgets/rooms/room_summary_sheet.dart"; +import "package:nexus/widgets/room_summary_sheet.dart"; class const JoinSheet(final WidgetRef ref, {super.key}) extends HookWidget { @override diff --git a/lib/widgets/rooms/knock_sheet.dart b/lib/widgets/knock_sheet.dart similarity index 100% rename from lib/widgets/rooms/knock_sheet.dart rename to lib/widgets/knock_sheet.dart diff --git a/lib/widgets/lazy_loading/message_avatar.dart b/lib/widgets/lazy_loading/message_avatar.dart index 95f987f..0279eca 100644 --- a/lib/widgets/lazy_loading/message_avatar.dart +++ b/lib/widgets/lazy_loading/message_avatar.dart @@ -4,7 +4,7 @@ import "package:nexus/controllers/author.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/extensions/show_user_popover.dart"; import "package:nexus/models/event.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; class const MessageAvatar( final Event event, { diff --git a/lib/widgets/common/linkified_text.dart b/lib/widgets/linkified_text.dart similarity index 100% rename from lib/widgets/common/linkified_text.dart rename to lib/widgets/linkified_text.dart diff --git a/lib/widgets/common/loading.dart b/lib/widgets/loading.dart similarity index 100% rename from lib/widgets/common/loading.dart rename to lib/widgets/loading.dart diff --git a/lib/widgets/location/location_map.dart b/lib/widgets/location_map.dart similarity index 100% rename from lib/widgets/location/location_map.dart rename to lib/widgets/location_map.dart diff --git a/lib/widgets/location/location_message.dart b/lib/widgets/location_message.dart similarity index 94% rename from lib/widgets/location/location_message.dart rename to lib/widgets/location_message.dart index 661b7dd..2f22b9d 100644 --- a/lib/widgets/location/location_message.dart +++ b/lib/widgets/location_message.dart @@ -2,8 +2,8 @@ import "package:material_ui/material_ui.dart"; import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; import "package:nexus/models/content/message.dart"; import "package:nexus/models/location.dart"; -import "package:nexus/widgets/location/location_map.dart"; -import "package:nexus/widgets/location/location_viewer_sheet.dart"; +import "package:nexus/widgets/location_map.dart"; +import "package:nexus/widgets/location_viewer_sheet.dart"; class const LocationMessage(final LocationMessageContent content, {super.key}) extends StatelessWidget { diff --git a/lib/widgets/location/location_picker_sheet.dart b/lib/widgets/location_picker_sheet.dart similarity index 98% rename from lib/widgets/location/location_picker_sheet.dart rename to lib/widgets/location_picker_sheet.dart index 4fb61bf..10b77ae 100644 --- a/lib/widgets/location/location_picker_sheet.dart +++ b/lib/widgets/location_picker_sheet.dart @@ -7,7 +7,7 @@ import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; import "package:nexus/helpers/current_location_helper.dart"; import "package:nexus/models/content/message.dart"; import "package:nexus/models/location.dart"; -import "package:nexus/widgets/location/location_map.dart"; +import "package:nexus/widgets/location_map.dart"; /// Chooses a location to share, either the user's own or a pin dropped on the /// map. diff --git a/lib/widgets/location/location_viewer_sheet.dart b/lib/widgets/location_viewer_sheet.dart similarity index 95% rename from lib/widgets/location/location_viewer_sheet.dart rename to lib/widgets/location_viewer_sheet.dart index ca74ab0..48965bc 100644 --- a/lib/widgets/location/location_viewer_sheet.dart +++ b/lib/widgets/location_viewer_sheet.dart @@ -4,7 +4,7 @@ import "package:material_ui/material_ui.dart"; import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/models/location.dart"; -import "package:nexus/widgets/location/location_map.dart"; +import "package:nexus/widgets/location_map.dart"; class const LocationViewerSheet( final LatLng point, { diff --git a/lib/widgets/common/m3e_rail_theme.dart b/lib/widgets/m3e_rail_theme.dart similarity index 100% rename from lib/widgets/common/m3e_rail_theme.dart rename to lib/widgets/m3e_rail_theme.dart diff --git a/lib/widgets/room_chat/member_list.dart b/lib/widgets/member_list.dart similarity index 97% rename from lib/widgets/room_chat/member_list.dart rename to lib/widgets/member_list.dart index bf86b9c..02cc0c6 100644 --- a/lib/widgets/room_chat/member_list.dart +++ b/lib/widgets/member_list.dart @@ -11,10 +11,10 @@ import "package:nexus/helpers/extensions/show_user_popover.dart"; import "package:nexus/helpers/extensions/displayname_color.dart"; import "package:nexus/models/content/membership.dart"; import "package:nexus/models/membership_status.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; -import "package:nexus/widgets/common/divider_text.dart"; -import "package:nexus/widgets/common/error_dialog.dart"; -import "package:nexus/widgets/common/loading.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/divider_text.dart"; +import "package:nexus/widgets/error_dialog.dart"; +import "package:nexus/widgets/loading.dart"; class const MemberList(final String roomId, {super.key}) extends HookConsumerWidget { diff --git a/lib/widgets/renderers/message_image.dart b/lib/widgets/message_image.dart similarity index 94% rename from lib/widgets/renderers/message_image.dart rename to lib/widgets/message_image.dart index a8c44e3..c0dfa87 100644 --- a/lib/widgets/renderers/message_image.dart +++ b/lib/widgets/message_image.dart @@ -4,8 +4,8 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/helpers/mxc_image.dart"; import "package:nexus/models/info/image.dart" as i; import "package:nexus/models/requests/download_media.dart"; -import "package:nexus/widgets/common/expandable_image.dart"; -import "package:nexus/widgets/common/loading.dart"; +import "package:nexus/widgets/expandable_image.dart"; +import "package:nexus/widgets/loading.dart"; final class const MessageImage( final Uri url, { diff --git a/lib/widgets/users/mutual_rooms_sheet.dart b/lib/widgets/mutual_rooms_sheet.dart similarity index 97% rename from lib/widgets/users/mutual_rooms_sheet.dart rename to lib/widgets/mutual_rooms_sheet.dart index 16066b0..4c47577 100644 --- a/lib/widgets/users/mutual_rooms_sheet.dart +++ b/lib/widgets/mutual_rooms_sheet.dart @@ -7,7 +7,7 @@ import "package:nexus/controllers/rooms.dart"; import "package:nexus/controllers/sidebar_page.dart"; import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/focus_room.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; /// The rooms the user shares with [userId]. class const MutualRoomsSheet(final String userId, {super.key}) diff --git a/lib/widgets/emoji/mxc_emoji.dart b/lib/widgets/mxc_emoji.dart similarity index 100% rename from lib/widgets/emoji/mxc_emoji.dart rename to lib/widgets/mxc_emoji.dart diff --git a/lib/widgets/sidebar/notifications_list.dart b/lib/widgets/notifications_list.dart similarity index 97% rename from lib/widgets/sidebar/notifications_list.dart rename to lib/widgets/notifications_list.dart index f52f944..23ff676 100644 --- a/lib/widgets/sidebar/notifications_list.dart +++ b/lib/widgets/notifications_list.dart @@ -9,9 +9,9 @@ import "package:nexus/controllers/notifications.dart"; import "package:nexus/helpers/extensions/focus_room.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/nav_page.dart"; -import "package:nexus/widgets/common/error_dialog.dart"; -import "package:nexus/widgets/common/highlight_wrapper.dart"; -import "package:nexus/widgets/common/loading.dart"; +import "package:nexus/widgets/error_dialog.dart"; +import "package:nexus/widgets/highlight_wrapper.dart"; +import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/renderers/event.dart"; import "package:super_sliver_list/super_sliver_list.dart"; diff --git a/lib/widgets/pages/chat.dart b/lib/widgets/pages/chat.dart index c5d704f..53d3571 100644 --- a/lib/widgets/pages/chat.dart +++ b/lib/widgets/pages/chat.dart @@ -8,11 +8,11 @@ import "package:nexus/helpers/extensions/open_link_info.dart"; import "package:nexus/main.dart"; import "package:nexus/controllers/selected_room.dart"; import "package:nexus/controllers/notification_request.dart"; -import "package:nexus/widgets/common/appbar.dart"; -import "package:nexus/widgets/sidebar/sidebar.dart"; -import "package:nexus/widgets/common/sync_status_banner.dart"; +import "package:nexus/widgets/appbar.dart"; +import "package:nexus/widgets/sidebar.dart"; +import "package:nexus/widgets/sync_status_banner.dart"; import "package:nexus/widgets/room_chat/room_chat.dart"; -import "package:nexus/widgets/common/loading.dart"; +import "package:nexus/widgets/loading.dart"; class const ChatPage({super.key}) extends HookConsumerWidget { @override diff --git a/lib/widgets/pages/explore.dart b/lib/widgets/pages/explore.dart index 3cd77ea..bc8ab2d 100644 --- a/lib/widgets/pages/explore.dart +++ b/lib/widgets/pages/explore.dart @@ -14,14 +14,14 @@ import "package:nexus/controllers/settings.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/main.dart"; import "package:nexus/helpers/extensions/room_summary_display.dart"; -import "package:nexus/widgets/rooms/add_directory_server_sheet.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; -import "package:nexus/widgets/rooms/room_summary_sheet.dart"; -import "package:nexus/widgets/common/divider_text.dart"; -import "package:nexus/widgets/common/error_dialog.dart"; -import "package:nexus/widgets/common/loading.dart"; -import "package:nexus/widgets/common/m3e_rail_theme.dart"; -import "package:nexus/widgets/rooms/room_directory_card.dart"; +import "package:nexus/widgets/add_directory_server_sheet.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/room_summary_sheet.dart"; +import "package:nexus/widgets/divider_text.dart"; +import "package:nexus/widgets/error_dialog.dart"; +import "package:nexus/widgets/loading.dart"; +import "package:nexus/widgets/m3e_rail_theme.dart"; +import "package:nexus/widgets/room_directory_card.dart"; class const ExplorePage({super.key}) extends HookConsumerWidget { @override diff --git a/lib/widgets/pages/select_server.dart b/lib/widgets/pages/select_server.dart index a44e4b8..1e20f32 100644 --- a/lib/widgets/pages/select_server.dart +++ b/lib/widgets/pages/select_server.dart @@ -13,9 +13,9 @@ import "package:nexus/models/gomuks_exception.dart"; import "package:nexus/models/homeserver.dart"; import "package:nexus/models/oauth_auth_code_response.dart"; import "package:nexus/widgets/pages/settings.dart"; -import "package:nexus/widgets/common/account_switcher.dart"; -import "package:nexus/widgets/common/appbar.dart"; -import "package:nexus/widgets/common/divider_text.dart"; +import "package:nexus/widgets/account_switcher.dart"; +import "package:nexus/widgets/appbar.dart"; +import "package:nexus/widgets/divider_text.dart"; class const SelectServerPage({super.key}) extends HookConsumerWidget { @override diff --git a/lib/widgets/pages/settings_category.dart b/lib/widgets/pages/settings_category.dart index 0efa68e..69513b8 100644 --- a/lib/widgets/pages/settings_category.dart +++ b/lib/widgets/pages/settings_category.dart @@ -8,7 +8,7 @@ import "package:hooks_riverpod/misc.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:nexus/models/settings_category.dart"; import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/widgets/common/highlight_wrapper.dart"; +import "package:nexus/widgets/highlight_wrapper.dart"; import "package:super_sliver_list/super_sliver_list.dart"; class const SettingsCategoryPage( diff --git a/lib/widgets/pages/verify.dart b/lib/widgets/pages/verify.dart index aa342cd..6cf935d 100644 --- a/lib/widgets/pages/verify.dart +++ b/lib/widgets/pages/verify.dart @@ -5,12 +5,12 @@ import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client_state.dart"; import "package:nexus/models/client_state.dart"; -import "package:nexus/widgets/common/loading.dart"; +import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/pages/settings.dart"; -import "package:nexus/widgets/common/account_switcher.dart"; -import "package:nexus/widgets/common/appbar.dart"; +import "package:nexus/widgets/account_switcher.dart"; +import "package:nexus/widgets/appbar.dart"; import "package:nexus/helpers/required_validator_helper.dart"; -import "package:nexus/widgets/encryption/reset_encryption_sheet.dart"; +import "package:nexus/widgets/reset_encryption_sheet.dart"; class const VerifyPage({super.key}) extends HookConsumerWidget { @override diff --git a/lib/widgets/room_chat/pinned_events_drawer.dart b/lib/widgets/pinned_events_drawer.dart similarity index 96% rename from lib/widgets/room_chat/pinned_events_drawer.dart rename to lib/widgets/pinned_events_drawer.dart index 1e0ab6a..cff6ad6 100644 --- a/lib/widgets/room_chat/pinned_events_drawer.dart +++ b/lib/widgets/pinned_events_drawer.dart @@ -4,8 +4,8 @@ import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/pinned_events.dart"; import "package:nexus/helpers/hooks/chat_scroll.dart"; import "package:nexus/models/event.dart"; -import "package:nexus/widgets/common/error_dialog.dart"; -import "package:nexus/widgets/common/loading.dart"; +import "package:nexus/widgets/error_dialog.dart"; +import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/renderers/event.dart"; final class const PinnedEventsDrawer( diff --git a/lib/widgets/renderers/poll_option.dart b/lib/widgets/poll_option.dart similarity index 100% rename from lib/widgets/renderers/poll_option.dart rename to lib/widgets/poll_option.dart diff --git a/lib/widgets/reaction_row.dart b/lib/widgets/reaction_row.dart new file mode 100644 index 0000000..288ebd6 --- /dev/null +++ b/lib/widgets/reaction_row.dart @@ -0,0 +1,113 @@ +import "package:material_ui/material_ui.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/client_state.dart"; +import "package:nexus/controllers/reactions.dart"; +import "package:nexus/controllers/room_chat.dart"; +import "package:nexus/helpers/mxc_image.dart"; +import "package:nexus/models/event.dart"; +import "package:nexus/widgets/error_dialog.dart"; +import "package:nexus/main.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; + +class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final clientState = ref.watch(ClientStateController.provider); + + return Padding( + padding: .only(top: 4), + child: switch (ref.watch( + ReactionsController.provider( + .new(roomId: event.roomId, eventRowId: event.rowId), + ), + )) { + AsyncData(value: final IMap>? reactors) || + AsyncLoading(value: final reactors) => Wrap( + spacing: 4, + runSpacing: 4, + children: event.reactions + .where((_, value) => value != 0) + .mapTo( + (reaction, count) => HookBuilder( + builder: (context) { + final enabled = useState(true); + + final selected = + reactors?[reaction]?.contains(clientState!.userId) ?? + false; + return Tooltip( + message: reactors?[reaction]?.join(", ") ?? "", + child: ChoiceChip( + showCheckmark: false, + selected: selected, + label: Row( + mainAxisSize: .min, + spacing: 8, + children: [ + Flexible( + child: reaction.startsWith("mxc://") + ? Image( + height: 20, + image: MxcImage( + ref, + .new(mxc: Uri.parse(reaction)), + ), + errorBuilder: (context, _, _) => Icon( + Icons.broken_image, + color: Theme.of(context) + .colorScheme + .error, + ), + ) + : Text(reaction, overflow: .ellipsis), + ), + Text(count.toString(), overflow: .ellipsis), + ], + ), + onSelected: enabled.value + ? (value) async { + enabled.value = false; + try { + final controller = ref.watch( + RoomChatController.provider(( + roomId: event.roomId, + contextualEvent: null, + threadRoot: null, + )).notifier, + ); + + if (selected) { + await controller + .removeReaction( + reaction, + event, + clientState!.userId!, + ) + .onError(showError); + } else { + await controller + .sendReaction(reaction, event) + .onError(showError); + } + } finally { + enabled.value = true; + } + } + : null, + ), + ); + }, + ), + ) + .toList(), + ), + + AsyncError(:final error, :final stackTrace) => ErrorDialog( + error, + stackTrace, + ), + }, + ); + } +} diff --git a/lib/widgets/reactions/reaction_key.dart b/lib/widgets/reactions/reaction_key.dart deleted file mode 100644 index ca00602..0000000 --- a/lib/widgets/reactions/reaction_key.dart +++ /dev/null @@ -1,25 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:material_ui/material_ui.dart"; -import "package:nexus/helpers/mxc_image.dart"; - -/// A reaction's key, either an emoji or a custom (mxc) image. -class const ReactionKey( - final String reaction, { - required final double height, - final TextStyle? style, - super.key, -}) extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) => - reaction.startsWith("mxc://") - ? Image( - height: height, - image: MxcImage(ref, .new(mxc: Uri.parse(reaction))), - errorBuilder: (context, _, _) => Icon( - Icons.broken_image, - size: height, - color: Theme.of(context).colorScheme.error, - ), - ) - : Text(reaction, style: style, overflow: .ellipsis); -} diff --git a/lib/widgets/reactions/reaction_row.dart b/lib/widgets/reactions/reaction_row.dart deleted file mode 100644 index f8336c2..0000000 --- a/lib/widgets/reactions/reaction_row.dart +++ /dev/null @@ -1,118 +0,0 @@ -import "package:flutter/gestures.dart"; -import "package:material_ui/material_ui.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/reactions.dart"; -import "package:nexus/controllers/room_chat.dart"; -import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/reaction_group.dart"; -import "package:nexus/widgets/common/error_dialog.dart"; -import "package:nexus/widgets/reactions/reaction_key.dart"; -import "package:nexus/widgets/reactions/reactions_popover.dart"; -import "package:nexus/main.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; - -class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final clientState = ref.watch(ClientStateController.provider); - - return Padding( - padding: .only(top: 4), - child: switch (ref.watch( - ReactionsController.provider( - .new(roomId: event.roomId, eventRowId: event.rowId), - ), - )) { - AsyncData(value: final IMap? reactors) || - AsyncLoading(value: final reactors) => Wrap( - spacing: 4, - runSpacing: 4, - children: event.reactions - .where((_, value) => value != 0) - .mapTo( - (reaction, count) => HookBuilder( - builder: (context) { - final enabled = useState(true); - - final selected = - reactors?[reaction]?.senders.contains( - clientState!.userId, - ) ?? - false; - void showReactions(PositionedGestureDetails details) => - context.showAdaptivePopover( - (_) => ReactionsPopover( - event, - initialReaction: reaction, - ), - anchor: context.globalRectAt(details.globalPosition), - ); - - return GestureDetector( - onSecondaryTapUp: showReactions, - onLongPressStart: showReactions, - child: Tooltip( - message: reactors?[reaction]?.shortcode ?? reaction, - child: ChoiceChip( - showCheckmark: false, - selected: selected, - label: Row( - mainAxisSize: .min, - spacing: 8, - children: [ - Flexible( - child: ReactionKey(reaction, height: 20), - ), - Text(count.toString(), overflow: .ellipsis), - ], - ), - onSelected: enabled.value - ? (value) async { - enabled.value = false; - try { - final controller = ref.watch( - RoomChatController.provider(( - roomId: event.roomId, - contextualEvent: null, - threadRoot: null, - )).notifier, - ); - - if (selected) { - await controller - .removeReaction( - reaction, - event, - clientState!.userId!, - ) - .onError(showError); - } else { - await controller - .sendReaction(reaction, event) - .onError(showError); - } - } finally { - enabled.value = true; - } - } - : null, - ), - ), - ); - }, - ), - ) - .toList(), - ), - - AsyncError(:final error, :final stackTrace) => ErrorDialog( - error, - stackTrace, - ), - }, - ); - } -} diff --git a/lib/widgets/reactions/reactions_popover.dart b/lib/widgets/reactions/reactions_popover.dart deleted file mode 100644 index 44c455b..0000000 --- a/lib/widgets/reactions/reactions_popover.dart +++ /dev/null @@ -1,144 +0,0 @@ -import "dart:math"; - -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:material_ui/material_ui.dart"; -import "package:nexus/controllers/reactions.dart"; -import "package:nexus/controllers/user.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/helpers/extensions/show_user_popover.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; -import "package:nexus/helpers/extensions/show_image_viewer.dart"; -import "package:nexus/widgets/reactions/reaction_key.dart"; - -/// Every reaction to an event in tabs, each listing who reacted with it. -class const ReactionsPopover( - final Event event, { - final String? initialReaction, - super.key, -}) extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final reactions = ref.watch( - ReactionsController.provider( - .new(roomId: event.roomId, eventRowId: event.rowId), - ), - ); - // Ordered like the reaction row on the timeline - final order = event.reactions.keys.toIList(); - final keys = - reactions.value?.keys.toIList().sortOrdered( - (a, b) => order.indexOf(a).compareTo(order.indexOf(b)), - ) ?? - const .empty(); - - final vsync = useSingleTickerProvider(keys: [keys.length]); - final tabController = useMemoized( - () => TabController( - length: keys.length, - initialIndex: keys - .indexOf(initialReaction ?? "") - .clamp(0, max(0, keys.length - 1)), - vsync: vsync, - ), - [keys.length], - ); - useEffect(() => tabController.dispose, [tabController]); - useListenable(tabController); - - return reactions.betterWhen( - skipLoadingOnReload: true, - data: (reactions) { - if (keys.isEmpty) return SizedBox.shrink(); - - final reaction = keys[tabController.index]; - - return Column( - mainAxisSize: .min, - crossAxisAlignment: .stretch, - children: [ - TabBar( - controller: tabController, - isScrollable: true, - tabAlignment: .start, - tabs: keys.map((reaction) { - void expand() => context.showImageViewer( - ref, - .new(mxc: Uri.parse(reaction)), - downloadFilename: reaction.split("/").last, - ); - final isImage = reaction.startsWith("mxc://"); - - return GestureDetector( - onSecondaryTap: isImage ? expand : null, - onLongPress: isImage ? expand : null, - child: Tab( - child: Row( - mainAxisSize: .min, - spacing: 8, - children: [ - ReactionKey(reaction, height: 20), - Text(reactions[reaction]!.senders.length.toString()), - ], - ), - ), - ); - }).toList(), - ), - Flexible( - child: AnimatedSize( - duration: Durations.medium2, - curve: Curves.easeInOutCubicEmphasized, - alignment: .topCenter, - child: SingleChildScrollView( - key: ValueKey(reaction), - padding: .symmetric(vertical: 8), - child: Column( - crossAxisAlignment: .stretch, - children: reactions[reaction]!.senders - .map( - (userId) => Consumer( - builder: (context, ref, _) { - final member = ref - .watch( - UserController.provider( - .new( - roomId: event.roomId, - userId: userId, - ), - ), - ) - .value; - final name = - member?.displayName ?? userId.localpart; - - return ListTile( - leading: AvatarOrHash( - member?.avatarUrl, - name, - dimension: 40, - ), - title: Text(name, overflow: .ellipsis), - subtitle: Text(userId, overflow: .ellipsis), - onTap: () => context.showUserPopover( - userId, - roomId: event.roomId, - ), - ); - }, - ), - ) - .toList(), - ), - ), - ), - ), - ], - ); - }, - ); - } -} diff --git a/lib/widgets/read_receipts/read_receipts_row.dart b/lib/widgets/read_receipts_row.dart similarity index 96% rename from lib/widgets/read_receipts/read_receipts_row.dart rename to lib/widgets/read_receipts_row.dart index 156d3f8..fb20e3d 100644 --- a/lib/widgets/read_receipts/read_receipts_row.dart +++ b/lib/widgets/read_receipts_row.dart @@ -8,9 +8,9 @@ import "package:nexus/controllers/room_chat.dart"; import "package:nexus/controllers/user.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/extensions/show_user_popover.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; -import "package:nexus/widgets/avatars/avatar_ring.dart"; -import "package:nexus/widgets/read_receipts/read_receipts_sheet.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/avatar_ring.dart"; +import "package:nexus/widgets/read_receipts_sheet.dart"; class const ReadReceiptsRow( final String eventId, { diff --git a/lib/widgets/read_receipts/read_receipts_sheet.dart b/lib/widgets/read_receipts_sheet.dart similarity index 96% rename from lib/widgets/read_receipts/read_receipts_sheet.dart rename to lib/widgets/read_receipts_sheet.dart index 3021102..135e3fb 100644 --- a/lib/widgets/read_receipts/read_receipts_sheet.dart +++ b/lib/widgets/read_receipts_sheet.dart @@ -6,7 +6,7 @@ import "package:nexus/controllers/room_chat.dart"; import "package:nexus/controllers/user.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/extensions/show_user_popover.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; /// Everyone who has read up to an event, kept up to date while open. class const ReadReceiptsSheet( diff --git a/lib/widgets/encryption/recovery_key_sheet.dart b/lib/widgets/recovery_key_sheet.dart similarity index 100% rename from lib/widgets/encryption/recovery_key_sheet.dart rename to lib/widgets/recovery_key_sheet.dart diff --git a/lib/widgets/renderers/diffs/change_line.dart b/lib/widgets/renderers/change_line.dart similarity index 100% rename from lib/widgets/renderers/diffs/change_line.dart rename to lib/widgets/renderers/change_line.dart diff --git a/lib/widgets/renderers/event.dart b/lib/widgets/renderers/event.dart index 4e14fad..deff7f7 100644 --- a/lib/widgets/renderers/event.dart +++ b/lib/widgets/renderers/event.dart @@ -32,21 +32,21 @@ import "package:nexus/models/content/server_acl.dart"; import "package:nexus/models/content/tombstone.dart"; import "package:nexus/models/content/topic.dart"; import "package:nexus/models/event.dart"; -import "package:nexus/widgets/common/error_dialog.dart"; +import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/renderers/message.dart"; -import "package:nexus/widgets/reactions/reaction_row.dart"; -import "package:nexus/widgets/read_receipts/read_receipts_row.dart"; +import "package:nexus/widgets/reaction_row.dart"; +import "package:nexus/widgets/read_receipts_row.dart"; import "package:nexus/widgets/renderers/membership.dart"; -import "package:nexus/widgets/renderers/diffs/change_line.dart"; +import "package:nexus/widgets/renderers/change_line.dart"; import "package:nexus/widgets/renderers/generic_event.dart"; -import "package:nexus/widgets/renderers/diffs/join_rules_diff.dart"; -import "package:nexus/widgets/renderers/diffs/power_levels_diff.dart"; -import "package:nexus/widgets/renderers/diffs/pinned_events_diff.dart"; -import "package:nexus/widgets/renderers/diffs/server_acl_diff.dart"; -import "package:nexus/widgets/renderers/swipe_to_reply.dart"; -import "package:nexus/widgets/threads/thread_summary.dart"; -import "package:nexus/widgets/common/timestamp.dart"; -import "package:nexus/widgets/users/user_span.dart"; +import "package:nexus/widgets/renderers/join_rules_diff.dart"; +import "package:nexus/widgets/renderers/power_levels_diff.dart"; +import "package:nexus/widgets/renderers/pinned_events_diff.dart"; +import "package:nexus/widgets/renderers/server_acl_diff.dart"; +import "package:nexus/widgets/swipe_to_reply.dart"; +import "package:nexus/widgets/thread_summary.dart"; +import "package:nexus/widgets/timestamp.dart"; +import "package:nexus/widgets/user_span.dart"; class const EventRenderer( final int eventRowId, { diff --git a/lib/widgets/renderers/diffs/join_rules_diff.dart b/lib/widgets/renderers/join_rules_diff.dart similarity index 97% rename from lib/widgets/renderers/diffs/join_rules_diff.dart rename to lib/widgets/renderers/join_rules_diff.dart index 1698aa0..6d41c0b 100644 --- a/lib/widgets/renderers/diffs/join_rules_diff.dart +++ b/lib/widgets/renderers/join_rules_diff.dart @@ -5,7 +5,7 @@ import "package:nexus/controllers/rooms.dart"; import "package:nexus/helpers/extensions/join_rule_name.dart"; import "package:nexus/helpers/extensions/success_scheme.dart"; import "package:nexus/models/content/join_rules.dart"; -import "package:nexus/widgets/renderers/diffs/change_line.dart"; +import "package:nexus/widgets/renderers/change_line.dart"; /// The join rule a join rules event changed to, and for restricted rooms, the /// rooms whose members it let join, or stopped letting join. diff --git a/lib/widgets/renderers/membership.dart b/lib/widgets/renderers/membership.dart index 125ea64..17f8128 100644 --- a/lib/widgets/renderers/membership.dart +++ b/lib/widgets/renderers/membership.dart @@ -6,7 +6,7 @@ import "package:nexus/helpers/hooks/tap_recognizer.dart"; import "package:nexus/models/content/membership.dart"; import "package:nexus/models/event.dart"; import "package:nexus/widgets/renderers/generic_event.dart"; -import "package:nexus/widgets/users/user_span.dart"; +import "package:nexus/widgets/user_span.dart"; class const MembershipRenderer( final Event event, { diff --git a/lib/widgets/renderers/message.dart b/lib/widgets/renderers/message.dart index 40ee5e7..4e6a45a 100644 --- a/lib/widgets/renderers/message.dart +++ b/lib/widgets/renderers/message.dart @@ -12,22 +12,22 @@ import "package:nexus/models/location.dart"; import "package:nexus/models/content/poll_start.dart"; import "package:nexus/models/content/sticker.dart"; import "package:nexus/models/event.dart"; -import "package:nexus/widgets/renderers/file_card.dart"; +import "package:nexus/widgets/file_card.dart"; import "package:nexus/widgets/html/html.dart"; import "package:nexus/widgets/lazy_loading/message_avatar.dart"; import "package:nexus/widgets/lazy_loading/message_displayname.dart"; -import "package:nexus/widgets/common/linkified_text.dart"; -import "package:nexus/widgets/location/location_message.dart"; -import "package:nexus/widgets/common/loading.dart"; -import "package:nexus/widgets/renderers/message_image.dart"; -import "package:nexus/widgets/reactions/reaction_row.dart"; -import "package:nexus/widgets/common/timestamp.dart"; -import "package:nexus/widgets/renderers/url_preview.dart"; -import "package:nexus/widgets/renderers/event_preview.dart"; +import "package:nexus/widgets/linkified_text.dart"; +import "package:nexus/widgets/location_message.dart"; +import "package:nexus/widgets/loading.dart"; +import "package:nexus/widgets/message_image.dart"; +import "package:nexus/widgets/reaction_row.dart"; +import "package:nexus/widgets/timestamp.dart"; +import "package:nexus/widgets/url_preview.dart"; +import "package:nexus/widgets/event_preview.dart"; import "package:nexus/widgets/players/video.dart"; import "package:nexus/widgets/players/audio.dart"; import "package:nexus/widgets/renderers/poll.dart"; -import "package:nexus/widgets/threads/thread_summary.dart"; +import "package:nexus/widgets/thread_summary.dart"; class const MessageRenderer( final Event event, { diff --git a/lib/widgets/renderers/diffs/pinned_events_diff.dart b/lib/widgets/renderers/pinned_events_diff.dart similarity index 100% rename from lib/widgets/renderers/diffs/pinned_events_diff.dart rename to lib/widgets/renderers/pinned_events_diff.dart diff --git a/lib/widgets/renderers/poll.dart b/lib/widgets/renderers/poll.dart index 1f738b5..d3a8cb0 100644 --- a/lib/widgets/renderers/poll.dart +++ b/lib/widgets/renderers/poll.dart @@ -15,7 +15,7 @@ import "package:nexus/helpers/extensions/event_id_is_fake.dart"; import "package:nexus/main.dart"; import "package:nexus/models/content/poll_start.dart"; import "package:nexus/models/event.dart"; -import "package:nexus/widgets/renderers/poll_option.dart"; +import "package:nexus/widgets/poll_option.dart"; class const PollRenderer(final Event event, final PollStart poll, {super.key}) extends HookConsumerWidget { diff --git a/lib/widgets/renderers/diffs/power_levels_diff.dart b/lib/widgets/renderers/power_levels_diff.dart similarity index 100% rename from lib/widgets/renderers/diffs/power_levels_diff.dart rename to lib/widgets/renderers/power_levels_diff.dart diff --git a/lib/widgets/renderers/diffs/server_acl_diff.dart b/lib/widgets/renderers/server_acl_diff.dart similarity index 97% rename from lib/widgets/renderers/diffs/server_acl_diff.dart rename to lib/widgets/renderers/server_acl_diff.dart index 9364c04..e232614 100644 --- a/lib/widgets/renderers/diffs/server_acl_diff.dart +++ b/lib/widgets/renderers/server_acl_diff.dart @@ -2,7 +2,7 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:material_ui/material_ui.dart"; import "package:nexus/helpers/extensions/success_scheme.dart"; import "package:nexus/models/content/server_acl.dart"; -import "package:nexus/widgets/renderers/diffs/change_line.dart"; +import "package:nexus/widgets/renderers/change_line.dart"; /// The servers newly denied (in red) and allowed (in green) by a server ACL /// event, and those no longer denied or allowed, struck through. diff --git a/lib/widgets/encryption/reset_encryption_sheet.dart b/lib/widgets/reset_encryption_sheet.dart similarity index 99% rename from lib/widgets/encryption/reset_encryption_sheet.dart rename to lib/widgets/reset_encryption_sheet.dart index 40b0074..c223b37 100644 --- a/lib/widgets/encryption/reset_encryption_sheet.dart +++ b/lib/widgets/reset_encryption_sheet.dart @@ -6,7 +6,7 @@ import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client_state.dart"; import "package:nexus/main.dart"; import "package:nexus/models/client_state.dart"; -import "package:nexus/widgets/encryption/recovery_key_sheet.dart"; +import "package:nexus/widgets/recovery_key_sheet.dart"; class const ResetEncryptionSheet({ /// Whether this is shown as a page rather than in a sheet, in which case it diff --git a/lib/widgets/room_chat/room_appbar.dart b/lib/widgets/room_appbar.dart similarity index 87% rename from lib/widgets/room_chat/room_appbar.dart rename to lib/widgets/room_appbar.dart index 14b9df5..83a3278 100644 --- a/lib/widgets/room_chat/room_appbar.dart +++ b/lib/widgets/room_appbar.dart @@ -2,13 +2,13 @@ import "package:material_ui/material_ui.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; import "package:nexus/controllers/rooms.dart"; -import "package:nexus/widgets/common/appbar.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; -import "package:nexus/widgets/common/count_badge.dart"; -import "package:nexus/widgets/rooms/room_menu_button.dart"; -import "package:nexus/widgets/rooms/room_summary_sheet.dart"; -import "package:nexus/widgets/rooms/room_title.dart"; -import "package:nexus/widgets/sidebar/sidebar.dart"; +import "package:nexus/widgets/appbar.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/count_badge.dart"; +import "package:nexus/widgets/room_menu_button.dart"; +import "package:nexus/widgets/room_summary_sheet.dart"; +import "package:nexus/widgets/room_title.dart"; +import "package:nexus/widgets/sidebar.dart"; final class const RoomAppbar({ required final String? roomId, diff --git a/lib/widgets/room_chat/chat_body.dart b/lib/widgets/room_chat/chat_body.dart index 3b5f752..239ae4b 100644 --- a/lib/widgets/room_chat/chat_body.dart +++ b/lib/widgets/room_chat/chat_body.dart @@ -13,10 +13,10 @@ import "package:nexus/main.dart"; import "package:nexus/models/event.dart"; import "package:nexus/widgets/composer/composer.dart"; import "package:nexus/widgets/composer/typing_indicator.dart"; -import "package:nexus/widgets/common/error_dialog.dart"; -import "package:nexus/widgets/common/loading.dart"; +import "package:nexus/widgets/error_dialog.dart"; +import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/room_chat/chat_timeline.dart"; -import "package:nexus/widgets/room_chat/tombstone_banner.dart"; +import "package:nexus/widgets/tombstone_banner.dart"; final class const ChatBody({ required final ChatParams chat, diff --git a/lib/widgets/room_chat/chat_timeline.dart b/lib/widgets/room_chat/chat_timeline.dart index d6aaef7..57388b9 100644 --- a/lib/widgets/room_chat/chat_timeline.dart +++ b/lib/widgets/room_chat/chat_timeline.dart @@ -4,7 +4,7 @@ import "package:nexus/controllers/room_chat.dart"; import "package:nexus/helpers/hooks/chat_scroll.dart"; import "package:nexus/models/event.dart"; import "package:nexus/widgets/renderers/event.dart"; -import "package:nexus/widgets/common/highlight_wrapper.dart"; +import "package:nexus/widgets/highlight_wrapper.dart"; import "package:super_sliver_list/super_sliver_list.dart"; class const ChatTimeline({ diff --git a/lib/widgets/room_chat/room_chat.dart b/lib/widgets/room_chat/room_chat.dart index 929be84..0062450 100644 --- a/lib/widgets/room_chat/room_chat.dart +++ b/lib/widgets/room_chat/room_chat.dart @@ -10,13 +10,13 @@ import "package:nexus/controllers/room_chat.dart"; import "package:nexus/helpers/hooks/chat_composer.dart"; import "package:nexus/helpers/hooks/chat_scroll.dart"; import "package:nexus/models/event.dart"; -import "package:nexus/widgets/room_chat/pinned_events_drawer.dart"; -import "package:nexus/widgets/room_chat/member_list.dart"; -import "package:nexus/widgets/room_chat/room_appbar.dart"; -import "package:nexus/widgets/room_chat/search_drawer.dart"; -import "package:nexus/widgets/common/sync_status_banner.dart"; -import "package:nexus/widgets/threads/thread_drawer.dart"; -import "package:nexus/widgets/threads/threads_drawer.dart"; +import "package:nexus/widgets/pinned_events_drawer.dart"; +import "package:nexus/widgets/member_list.dart"; +import "package:nexus/widgets/room_appbar.dart"; +import "package:nexus/widgets/search_drawer.dart"; +import "package:nexus/widgets/sync_status_banner.dart"; +import "package:nexus/widgets/thread_drawer.dart"; +import "package:nexus/widgets/threads_drawer.dart"; import "package:nexus/widgets/room_chat/chat_body.dart"; import "package:nexus/helpers/extensions/build_event_options.dart"; import "package:nexus/controllers/jump_request.dart"; diff --git a/lib/widgets/rooms/room_directory_card.dart b/lib/widgets/room_directory_card.dart similarity index 96% rename from lib/widgets/rooms/room_directory_card.dart rename to lib/widgets/room_directory_card.dart index 10cc071..e8d8bfc 100644 --- a/lib/widgets/rooms/room_directory_card.dart +++ b/lib/widgets/room_directory_card.dart @@ -8,9 +8,9 @@ import "package:nexus/helpers/extensions/focus_room.dart"; import "package:nexus/helpers/extensions/join_room.dart"; import "package:nexus/helpers/extensions/room_summary_display.dart"; import "package:nexus/models/room_summary.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; -import "package:nexus/widgets/rooms/knock_sheet.dart"; -import "package:nexus/widgets/rooms/room_summary_sheet.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/knock_sheet.dart"; +import "package:nexus/widgets/room_summary_sheet.dart"; class const RoomDirectoryCard( final RoomSummary summary, { diff --git a/lib/widgets/room_list.dart b/lib/widgets/room_list.dart new file mode 100644 index 0000000..bdbf9cd --- /dev/null +++ b/lib/widgets/room_list.dart @@ -0,0 +1,328 @@ +import "dart:math"; + +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:navigation_rail_m3e/navigation_rail_m3e.dart"; +import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; +import "package:nexus/helpers/extensions/get_all_child_rooms.dart"; +import "package:nexus/helpers/extensions/room_destinations.dart"; +import "package:nexus/widgets/account_switcher.dart"; +import "package:nexus/models/nav_page.dart"; +import "package:nexus/widgets/appbar.dart"; +import "package:nexus/widgets/room_title.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/create_room_sheet.dart"; +import "package:nexus/widgets/start_dm_sheet.dart"; +import "package:nexus/widgets/divider_widget.dart"; +import "package:nexus/widgets/error_dialog.dart"; +import "package:nexus/widgets/pages/explore.dart"; +import "package:nexus/widgets/pages/settings.dart"; +import "package:nexus/widgets/join_sheet.dart"; +import "package:nexus/widgets/m3e_rail_theme.dart"; +import "package:nexus/widgets/room_summary_sheet.dart"; +import "package:nexus/widgets/room_menu_button.dart"; +import "package:nexus/widgets/space_hierarchy_sheet.dart"; +import "package:nexus/controllers/key.dart"; +import "package:nexus/controllers/selected_room.dart"; +import "package:nexus/controllers/selected_space.dart"; +import "package:nexus/controllers/rooms.dart"; +import "package:nexus/controllers/spaces.dart"; + +class const RoomList({super.key}) extends ConsumerWidget implements NavPage { + @override + String get title => "Rooms"; + + @override + IconData get icon => Icons.list; + + @override + int? badgeCount(WidgetRef ref) { + final (notifications, hasUnread) = ref.watch( + RoomsController.provider.select( + (rooms) => rooms.values.fold( + (0, false), + (total, room) => ( + total.$1 + (room.metadata?.unreadNotifications ?? 0), + total.$2 || (room.metadata?.unreadMessages ?? 0) > 0, + ), + ), + ), + ); + + return notifications > 0 + ? notifications + : hasUnread + ? 0 + : null; + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final spacesAsync = ref.watch(SpacesController.provider); + if (spacesAsync.value == null) { + return switch (spacesAsync) { + AsyncError(:final error, :final stackTrace) => ErrorDialog( + error, + stackTrace, + ), + _ => const Center(child: CircularProgressIndicator()), + }; + } + + return const Row( + children: [ + SpacesRail(), + Expanded(child: RoomsPane()), + ], + ); + } +} + +class const SpacesRail({super.key}) extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final spaces = ref.watch( + SpacesController.provider.select((async) => async.requireValue), + ); + final selectedSpaceId = ref.watch( + KeyController.provider(KeyController.spaceKey) + .select((async) => async.requireValue), + ); + + return M3ERailTheme( + extensions: [ + NavigationRailM3ETheme(itemCollapsedHeight: 48, itemVerticalGap: 0), + ], + builder: (context) => Container( + color: NavigationRailTokensAdapter(context).containerColor, + padding: .only(top: 16), + child: NavigationRailM3E( + type: .alwaysCollapse, + labelBehavior: .alwaysHide, + scrollable: true, + onDestinationSelected: (value) { + ref + .read(KeyController.provider(KeyController.spaceKey).notifier) + .set(spaces[value].id); + ref + .read(KeyController.provider(KeyController.roomKey).notifier) + .set(spaces[value].children.firstOrNull?.metadata?.id); + }, + sections: [ + .new( + destinations: spaces.map((space) { + final notifications = space.allChildRooms.fold( + 0, + (sum, room) => + sum + (room.metadata?.unreadNotifications ?? 0), + ); + + return NavigationRailM3EDestination( + badgeCount: notifications > 0 + ? notifications + : space.allChildRooms.any( + (room) => room.metadata?.unreadMessages != 0, + ) + ? 0 + : null, + short: true, + icon: AvatarOrHash( + dimension: 28, + space.room?.metadata?.avatar, + fallback: space.icon == null ? null : Icon(space.icon), + space.title, + ), + label: space.title, + ); + }).toList(), + ), + ], + selectedIndex: max( + 0, + spaces.indexWhere((space) => space.id == selectedSpaceId), + ), + trailingAtBottom: true, + trailing: Padding( + padding: .only(top: 8), + child: Column( + children: [ + AdaptiveMenuButton( + tooltip: "Add a room", + itemBuilder: (_) => [ + PopupMenuItem( + onTap: () => + context.showAdaptiveSheet((_) => JoinSheet(ref)), + child: const ListTile( + title: Text("Join an existing room (or space)"), + leading: Icon(Icons.numbers), + ), + ), + PopupMenuItem( + onTap: () => context.showAdaptiveSheet( + (_) => const CreateRoomSheet(), + ), + child: const ListTile( + title: Text("Create a new room (or space)"), + leading: Icon(Icons.add), + ), + ), + PopupMenuItem( + onTap: () => context.showAdaptiveSheet( + (_) => const StartDmSheet(), + ), + child: const ListTile( + title: Text("Start a direct message"), + leading: Icon(Icons.person_add), + ), + ), + ], + icon: const Icon(Icons.add), + ), + const AccountSwitcher(), + IconButton( + tooltip: "Explore other rooms", + onPressed: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const ExplorePage()), + ), + icon: const Icon(Icons.explore), + ), + IconButton( + tooltip: "Open settings", + onPressed: () => showDialog( + context: context, + builder: (_) => SettingsPage(), + ), + icon: const Icon(Icons.settings), + ), + ], + ), + ), + ), + ), + ); + } +} + +class const RoomsPane({super.key}) extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedRoomId = ref.watch(SelectedRoomController.provider); + final selectedSpace = ref.watch(SelectedSpaceController.provider)!; + final isDm = selectedSpace.id == "dms"; + final spaceMetadata = selectedSpace.room?.metadata; + // In the order the rail shows them, with the space's own timeline first + final destinationIds = IList([?spaceMetadata?.id]) + .addAll(selectedSpace.allChildRooms.map((room) => room.metadata?.id)); + + return Scaffold( + backgroundColor: Colors.transparent, + appBar: Appbar( + // Home and DMs aren't real spaces, so they don't have a summary. + onTap: spaceMetadata == null + ? null + : () => context.showAdaptiveSheet( + (_) => RoomSummarySheet(.fromMetadata(spaceMetadata)), + ), + windowControls: false, + leading: AvatarOrHash( + spaceMetadata?.avatar, + fallback: selectedSpace.icon == null + ? null + : Icon(selectedSpace.icon), + selectedSpace.title, + ), + title: RoomTitle(selectedSpace.title, topic: spaceMetadata?.topic), + backgroundColor: Colors.transparent, + actions: .new([ + RoomMenuButton( + selectedSpace.room, + children: selectedSpace.allChildRooms, + ), + ]), + ), + body: M3ERailTheme( + extensions: [ + NavigationRailM3ETheme( + itemExpandedHeight: 48, + iconLabelGap: 16, + sectionHeaderSpacingTop: 0, + ), + ], + builder: (context) => Padding( + padding: const .only(top: 4), + child: NavigationRailM3E( + fab: selectedSpace.room == null + ? null + : .new( + icon: const Icon(Icons.explore), + label: "Browse space", + tooltip: "Browse and join rooms in this space", + onPressed: () => context.showAdaptiveSheet( + (_) => SpaceHierarchySheet(selectedSpace.id), + ), + ), + expandedWidth: double.infinity, + scrollable: true, + background: Colors.transparent, + type: .alwaysExpand, + selectedIndex: max(0, destinationIds.indexOf(selectedRoomId)), + sections: [ + if (selectedSpace.room case final room? + when spaceMetadata != null) + .new( + destinations: [ + room.toDestination( + isDm: isDm, + label: "Space timeline", + icon: const Icon(Icons.forum), + ), + ], + ), + .new( + header: selectedSpace.room == null + ? null + : const DividerWidget(Text("Rooms")), + destinations: selectedSpace.children.toDestinations(isDm: isDm), + ), + ...selectedSpace.subSpaces.map( + (subSpace) => NavigationRailM3ESection( + header: DividerWidget( + Row( + mainAxisSize: .min, + spacing: 8, + children: [ + if (subSpace.room.metadata?.avatar != null) + AvatarOrHash( + subSpace.room.metadata?.avatar, + subSpace.room.metadata?.name ?? "Unnamed Room", + dimension: 16, + ), + Flexible( + child: Text( + subSpace.room.metadata?.name ?? "Unnamed Space", + maxLines: 1, + overflow: .ellipsis, + ), + ), + ], + ), + ), + destinations: subSpace.children.toDestinations(isDm: isDm), + ), + ), + ], + onDestinationSelected: (value) { + ref + .read(KeyController.provider(KeyController.roomKey).notifier) + .set(destinationIds[value]); + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } + }, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/rooms/room_menu_button.dart b/lib/widgets/room_menu_button.dart similarity index 100% rename from lib/widgets/rooms/room_menu_button.dart rename to lib/widgets/room_menu_button.dart diff --git a/lib/widgets/rooms/room_summary_sheet.dart b/lib/widgets/room_summary_sheet.dart similarity index 96% rename from lib/widgets/rooms/room_summary_sheet.dart rename to lib/widgets/room_summary_sheet.dart index 2195101..5579e59 100644 --- a/lib/widgets/rooms/room_summary_sheet.dart +++ b/lib/widgets/room_summary_sheet.dart @@ -9,10 +9,10 @@ import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/extensions/join_room.dart"; import "package:nexus/helpers/extensions/room_summary_display.dart"; import "package:nexus/main.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; -import "package:nexus/widgets/common/expandable_image.dart"; -import "package:nexus/widgets/rooms/knock_sheet.dart"; -import "package:nexus/widgets/common/linkified_text.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/expandable_image.dart"; +import "package:nexus/widgets/knock_sheet.dart"; +import "package:nexus/widgets/linkified_text.dart"; import "package:nexus/models/room_summary.dart"; class const RoomSummarySheet( diff --git a/lib/widgets/rooms/room_title.dart b/lib/widgets/room_title.dart similarity index 100% rename from lib/widgets/rooms/room_title.dart rename to lib/widgets/room_title.dart diff --git a/lib/widgets/room_chat/search_drawer.dart b/lib/widgets/search_drawer.dart similarity index 98% rename from lib/widgets/room_chat/search_drawer.dart rename to lib/widgets/search_drawer.dart index 2905789..03418df 100644 --- a/lib/widgets/room_chat/search_drawer.dart +++ b/lib/widgets/search_drawer.dart @@ -6,8 +6,8 @@ import "package:nexus/controllers/event_search.dart"; import "package:nexus/helpers/hooks/chat_scroll.dart"; import "package:nexus/main.dart"; import "package:nexus/models/event.dart"; -import "package:nexus/widgets/common/error_dialog.dart"; -import "package:nexus/widgets/common/loading.dart"; +import "package:nexus/widgets/error_dialog.dart"; +import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/renderers/event.dart"; final class const SearchDrawer( diff --git a/lib/widgets/settings/allowed_rooms_sheet.dart b/lib/widgets/settings/allowed_rooms_sheet.dart index 63aac8b..8123c51 100644 --- a/lib/widgets/settings/allowed_rooms_sheet.dart +++ b/lib/widgets/settings/allowed_rooms_sheet.dart @@ -7,7 +7,7 @@ import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/rooms.dart"; import "package:nexus/main.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/settings/search_chooser.dart"; /// Chooses the rooms whose members can join [roomId] without an invite. diff --git a/lib/widgets/settings/enable_image_packs_sheet.dart b/lib/widgets/settings/enable_image_packs_sheet.dart index 427f7d8..9bc952f 100644 --- a/lib/widgets/settings/enable_image_packs_sheet.dart +++ b/lib/widgets/settings/enable_image_packs_sheet.dart @@ -8,8 +8,8 @@ import "package:nexus/controllers/room_image_packs.dart"; import "package:nexus/controllers/rooms.dart"; import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/models/account_data.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; -import "package:nexus/widgets/avatars/image_pack_avatar.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/image_pack_avatar.dart"; /// Chooses a room, then which of its image packs to use in every room. class const EnableImagePacksSheet({ diff --git a/lib/widgets/settings/image_pack_sheet.dart b/lib/widgets/settings/image_pack_sheet.dart index d18d3f6..2dd394e 100644 --- a/lib/widgets/settings/image_pack_sheet.dart +++ b/lib/widgets/settings/image_pack_sheet.dart @@ -9,8 +9,8 @@ import "package:nexus/helpers/hooks/saving.dart"; import "package:nexus/main.dart"; import "package:nexus/models/content/image_pack.dart"; import "package:nexus/models/content/message.dart"; -import "package:nexus/widgets/avatars/image_pack_avatar.dart"; -import "package:nexus/widgets/emoji/mxc_emoji.dart"; +import "package:nexus/widgets/image_pack_avatar.dart"; +import "package:nexus/widgets/mxc_emoji.dart"; import "package:nexus/widgets/settings/image_usage_chips.dart"; import "package:nexus/widgets/settings/pack_image_sheet.dart"; diff --git a/lib/widgets/settings/member_chooser.dart b/lib/widgets/settings/member_chooser.dart index fbaaa8e..18ed2e2 100644 --- a/lib/widgets/settings/member_chooser.dart +++ b/lib/widgets/settings/member_chooser.dart @@ -6,7 +6,7 @@ import "package:material_ui/material_ui.dart"; import "package:nexus/controllers/members.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/models/content/membership.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/settings/search_chooser.dart"; /// The [chosen] users, above a field searching [roomId]'s members, or taking a user ID, to choose more. diff --git a/lib/widgets/settings/pack_image_sheet.dart b/lib/widgets/settings/pack_image_sheet.dart index 89ed4f7..da17f38 100644 --- a/lib/widgets/settings/pack_image_sheet.dart +++ b/lib/widgets/settings/pack_image_sheet.dart @@ -3,7 +3,7 @@ import "package:flutter_hooks/flutter_hooks.dart"; import "package:material_ui/material_ui.dart"; import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; import "package:nexus/models/content/image_pack.dart"; -import "package:nexus/widgets/emoji/mxc_emoji.dart"; +import "package:nexus/widgets/mxc_emoji.dart"; import "package:nexus/widgets/settings/image_usage_chips.dart"; /// Edits an image in a pack, or only shows it if [onSave] is null. diff --git a/lib/widgets/settings/settings_layout.dart b/lib/widgets/settings/settings_layout.dart index de20137..9741dcb 100644 --- a/lib/widgets/settings/settings_layout.dart +++ b/lib/widgets/settings/settings_layout.dart @@ -10,9 +10,9 @@ import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/weighted_search.dart"; import "package:nexus/models/settings_category.dart"; import "package:nexus/widgets/pages/settings_category.dart"; -import "package:nexus/widgets/common/divider_text.dart"; -import "package:nexus/widgets/common/highlight_wrapper.dart"; -import "package:nexus/widgets/common/m3e_rail_theme.dart"; +import "package:nexus/widgets/divider_text.dart"; +import "package:nexus/widgets/highlight_wrapper.dart"; +import "package:nexus/widgets/m3e_rail_theme.dart"; import "package:super_sliver_list/super_sliver_list.dart"; /// A settings screen split into categories: a list of them that opens each diff --git a/lib/widgets/sidebar/sidebar.dart b/lib/widgets/sidebar.dart similarity index 88% rename from lib/widgets/sidebar/sidebar.dart rename to lib/widgets/sidebar.dart index d8f98cb..3ed8daa 100644 --- a/lib/widgets/sidebar/sidebar.dart +++ b/lib/widgets/sidebar.dart @@ -4,10 +4,10 @@ import "package:material_ui/material_ui.dart"; import "package:nexus/controllers/sidebar_page.dart"; import "package:nexus/models/nav_page.dart"; import "package:nexus/models/sidebar_page.dart"; -import "package:nexus/widgets/common/count_badge.dart"; -import "package:nexus/widgets/sidebar/invites_list.dart"; -import "package:nexus/widgets/sidebar/notifications_list.dart"; -import "package:nexus/widgets/sidebar/room_list.dart"; +import "package:nexus/widgets/count_badge.dart"; +import "package:nexus/widgets/invites_list.dart"; +import "package:nexus/widgets/notifications_list.dart"; +import "package:nexus/widgets/room_list.dart"; class const Sidebar({super.key}) extends ConsumerWidget { static NavPage pageFor(SidebarPage page) => switch (page) { diff --git a/lib/widgets/sidebar/invite_card.dart b/lib/widgets/sidebar/invite_card.dart deleted file mode 100644 index 239d3c1..0000000 --- a/lib/widgets/sidebar/invite_card.dart +++ /dev/null @@ -1,52 +0,0 @@ -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:material_ui/material_ui.dart"; -import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; -import "package:nexus/controllers/invites.dart"; -import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/helpers/extensions/room_summary_display.dart"; -import "package:nexus/models/room_summary.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; -import "package:nexus/widgets/rooms/room_summary_sheet.dart"; - -class const InviteCard(final RoomSummary invite, {super.key}) - extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final responding = ref - .watch(InvitesController.respondMutation(invite.roomId)) - .isPending; - final name = invite.name ?? invite.canonicalAlias ?? invite.roomId; - final inviterName = - invite.inviterProfile?.displayName ?? invite.invitedBy?.localpart; - - return Card( - clipBehavior: .antiAlias, - child: ListTile( - onTap: responding - ? null - : () => context.showAdaptiveSheet((_) => RoomSummarySheet(invite)), - trailing: responding - ? const SizedBox.square( - dimension: 24, - child: CircularProgressIndicator(), - ) - : null, - leading: AvatarOrHash( - invite.avatarUrl, - name, - dimension: 40, - fallback: invite.isDirect ? null : Icon(invite.icon), - ), - title: Text(name, maxLines: 1, overflow: .ellipsis), - subtitle: Text( - [ - if (inviterName != null) - "${invite.isDirect ? "Direct message from" : "Invited by"} $inviterName", - ].join("\n"), - maxLines: 2, - overflow: .ellipsis, - ), - ), - ); - } -} diff --git a/lib/widgets/sidebar/room_list.dart b/lib/widgets/sidebar/room_list.dart deleted file mode 100644 index 0e94cdb..0000000 --- a/lib/widgets/sidebar/room_list.dart +++ /dev/null @@ -1,59 +0,0 @@ -import "package:nexus/widgets/sidebar/spaces_rail.dart"; -import "package:nexus/widgets/sidebar/rooms_pane.dart"; - -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:material_ui/material_ui.dart"; -import "package:nexus/models/nav_page.dart"; -import "package:nexus/widgets/common/error_dialog.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/controllers/spaces.dart"; - -class const RoomList({super.key}) extends ConsumerWidget implements NavPage { - @override - String get title => "Rooms"; - - @override - IconData get icon => Icons.list; - - @override - int? badgeCount(WidgetRef ref) { - final (notifications, hasUnread) = ref.watch( - RoomsController.provider.select( - (rooms) => rooms.values.fold( - (0, false), - (total, room) => ( - total.$1 + (room.metadata?.unreadNotifications ?? 0), - total.$2 || (room.metadata?.unreadMessages ?? 0) > 0, - ), - ), - ), - ); - - return notifications > 0 - ? notifications - : hasUnread - ? 0 - : null; - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - final spacesAsync = ref.watch(SpacesController.provider); - if (spacesAsync.value == null) { - return switch (spacesAsync) { - AsyncError(:final error, :final stackTrace) => ErrorDialog( - error, - stackTrace, - ), - _ => const Center(child: CircularProgressIndicator()), - }; - } - - return const Row( - children: [ - SpacesRail(), - Expanded(child: RoomsPane()), - ], - ); - } -} diff --git a/lib/widgets/sidebar/rooms_pane.dart b/lib/widgets/sidebar/rooms_pane.dart deleted file mode 100644 index aabebbb..0000000 --- a/lib/widgets/sidebar/rooms_pane.dart +++ /dev/null @@ -1,143 +0,0 @@ -import "dart:math"; - -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:navigation_rail_m3e/navigation_rail_m3e.dart"; -import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; -import "package:nexus/helpers/extensions/get_all_child_rooms.dart"; -import "package:nexus/helpers/extensions/room_destinations.dart"; -import "package:nexus/widgets/common/appbar.dart"; -import "package:nexus/widgets/rooms/room_title.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; -import "package:nexus/widgets/common/divider_widget.dart"; -import "package:nexus/widgets/common/m3e_rail_theme.dart"; -import "package:nexus/widgets/rooms/room_summary_sheet.dart"; -import "package:nexus/widgets/rooms/room_menu_button.dart"; -import "package:nexus/widgets/rooms/space_hierarchy_sheet.dart"; -import "package:nexus/controllers/key.dart"; -import "package:nexus/controllers/selected_room.dart"; -import "package:nexus/controllers/selected_space.dart"; - -class const RoomsPane({super.key}) extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final selectedRoomId = ref.watch(SelectedRoomController.provider); - final selectedSpace = ref.watch(SelectedSpaceController.provider)!; - final isDm = selectedSpace.id == "dms"; - final spaceMetadata = selectedSpace.room?.metadata; - // In the order the rail shows them, with the space's own timeline first - final destinationIds = IList([?spaceMetadata?.id]) - .addAll(selectedSpace.allChildRooms.map((room) => room.metadata?.id)); - - return Scaffold( - backgroundColor: Colors.transparent, - appBar: Appbar( - // Home and DMs aren't real spaces, so they don't have a summary. - onTap: spaceMetadata == null - ? null - : () => context.showAdaptiveSheet( - (_) => RoomSummarySheet(.fromMetadata(spaceMetadata)), - ), - windowControls: false, - leading: AvatarOrHash( - spaceMetadata?.avatar, - fallback: selectedSpace.icon == null - ? null - : Icon(selectedSpace.icon), - selectedSpace.title, - ), - title: RoomTitle(selectedSpace.title, topic: spaceMetadata?.topic), - backgroundColor: Colors.transparent, - actions: .new([ - RoomMenuButton( - selectedSpace.room, - children: selectedSpace.allChildRooms, - ), - ]), - ), - body: M3ERailTheme( - extensions: [ - NavigationRailM3ETheme( - itemExpandedHeight: 48, - iconLabelGap: 16, - sectionHeaderSpacingTop: 0, - ), - ], - builder: (context) => Padding( - padding: const .only(top: 4), - child: NavigationRailM3E( - fab: selectedSpace.room == null - ? null - : .new( - icon: const Icon(Icons.explore), - label: "Browse space", - tooltip: "Browse and join rooms in this space", - onPressed: () => context.showAdaptiveSheet( - (_) => SpaceHierarchySheet(selectedSpace.id), - ), - ), - expandedWidth: double.infinity, - scrollable: true, - background: Colors.transparent, - type: .alwaysExpand, - selectedIndex: max(0, destinationIds.indexOf(selectedRoomId)), - sections: [ - if (selectedSpace.room case final room? - when spaceMetadata != null) - .new( - destinations: [ - room.toDestination( - isDm: isDm, - label: "Space timeline", - icon: const Icon(Icons.forum), - ), - ], - ), - .new( - header: selectedSpace.room == null - ? null - : const DividerWidget(Text("Rooms")), - destinations: selectedSpace.children.toDestinations(isDm: isDm), - ), - ...selectedSpace.subSpaces.map( - (subSpace) => NavigationRailM3ESection( - header: DividerWidget( - Row( - mainAxisSize: .min, - spacing: 8, - children: [ - if (subSpace.room.metadata?.avatar != null) - AvatarOrHash( - subSpace.room.metadata?.avatar, - subSpace.room.metadata?.name ?? "Unnamed Room", - dimension: 16, - ), - Flexible( - child: Text( - subSpace.room.metadata?.name ?? "Unnamed Space", - maxLines: 1, - overflow: .ellipsis, - ), - ), - ], - ), - ), - destinations: subSpace.children.toDestinations(isDm: isDm), - ), - ), - ], - onDestinationSelected: (value) { - ref - .read(KeyController.provider(KeyController.roomKey).notifier) - .set(destinationIds[value]); - if (Navigator.of(context).canPop()) { - Navigator.of(context).pop(); - } - }, - ), - ), - ), - ); - } -} diff --git a/lib/widgets/sidebar/spaces_rail.dart b/lib/widgets/sidebar/spaces_rail.dart deleted file mode 100644 index d84a40a..0000000 --- a/lib/widgets/sidebar/spaces_rail.dart +++ /dev/null @@ -1,142 +0,0 @@ -import "dart:math"; - -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:material_ui/material_ui.dart"; -import "package:navigation_rail_m3e/navigation_rail_m3e.dart"; -import "package:material_adaptive_sheets/material_adaptive_sheets.dart"; -import "package:nexus/helpers/extensions/get_all_child_rooms.dart"; -import "package:nexus/widgets/common/account_switcher.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; -import "package:nexus/widgets/rooms/create_room_sheet.dart"; -import "package:nexus/widgets/users/start_dm_sheet.dart"; -import "package:nexus/widgets/pages/explore.dart"; -import "package:nexus/widgets/pages/settings.dart"; -import "package:nexus/widgets/rooms/join_sheet.dart"; -import "package:nexus/widgets/common/m3e_rail_theme.dart"; -import "package:nexus/controllers/key.dart"; -import "package:nexus/controllers/spaces.dart"; - -class const SpacesRail({super.key}) extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final spaces = ref.watch( - SpacesController.provider.select((async) => async.requireValue), - ); - final selectedSpaceId = ref.watch( - KeyController.provider(KeyController.spaceKey) - .select((async) => async.requireValue), - ); - - return M3ERailTheme( - extensions: [ - NavigationRailM3ETheme(itemCollapsedHeight: 48, itemVerticalGap: 0), - ], - builder: (context) => Container( - color: NavigationRailTokensAdapter(context).containerColor, - padding: .only(top: 16), - child: NavigationRailM3E( - type: .alwaysCollapse, - labelBehavior: .alwaysHide, - scrollable: true, - onDestinationSelected: (value) { - ref - .read(KeyController.provider(KeyController.spaceKey).notifier) - .set(spaces[value].id); - ref - .read(KeyController.provider(KeyController.roomKey).notifier) - .set(spaces[value].children.firstOrNull?.metadata?.id); - }, - sections: [ - .new( - destinations: spaces.map((space) { - final notifications = space.allChildRooms.fold( - 0, - (sum, room) => - sum + (room.metadata?.unreadNotifications ?? 0), - ); - - return NavigationRailM3EDestination( - badgeCount: notifications > 0 - ? notifications - : space.allChildRooms.any( - (room) => room.metadata?.unreadMessages != 0, - ) - ? 0 - : null, - short: true, - icon: AvatarOrHash( - dimension: 28, - space.room?.metadata?.avatar, - fallback: space.icon == null ? null : Icon(space.icon), - space.title, - ), - label: space.title, - ); - }).toList(), - ), - ], - selectedIndex: max( - 0, - spaces.indexWhere((space) => space.id == selectedSpaceId), - ), - trailingAtBottom: true, - trailing: Padding( - padding: .only(top: 8), - child: Column( - children: [ - AdaptiveMenuButton( - tooltip: "Add a room", - itemBuilder: (_) => [ - PopupMenuItem( - onTap: () => - context.showAdaptiveSheet((_) => JoinSheet(ref)), - child: const ListTile( - title: Text("Join an existing room (or space)"), - leading: Icon(Icons.numbers), - ), - ), - PopupMenuItem( - onTap: () => context.showAdaptiveSheet( - (_) => const CreateRoomSheet(), - ), - child: const ListTile( - title: Text("Create a new room (or space)"), - leading: Icon(Icons.add), - ), - ), - PopupMenuItem( - onTap: () => context.showAdaptiveSheet( - (_) => const StartDmSheet(), - ), - child: const ListTile( - title: Text("Start a direct message"), - leading: Icon(Icons.person_add), - ), - ), - ], - icon: const Icon(Icons.add), - ), - const AccountSwitcher(), - IconButton( - tooltip: "Explore other rooms", - onPressed: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const ExplorePage()), - ), - icon: const Icon(Icons.explore), - ), - IconButton( - tooltip: "Open settings", - onPressed: () => showDialog( - context: context, - builder: (_) => SettingsPage(), - ), - icon: const Icon(Icons.settings), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/widgets/rooms/space_hierarchy_sheet.dart b/lib/widgets/space_hierarchy_sheet.dart similarity index 97% rename from lib/widgets/rooms/space_hierarchy_sheet.dart rename to lib/widgets/space_hierarchy_sheet.dart index 65fb5da..613c13c 100644 --- a/lib/widgets/rooms/space_hierarchy_sheet.dart +++ b/lib/widgets/space_hierarchy_sheet.dart @@ -17,12 +17,12 @@ import "package:nexus/main.dart"; import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/create.dart"; import "package:nexus/models/space_hierarchy.dart"; -import "package:nexus/widgets/rooms/add_space_rooms_sheet.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; -import "package:nexus/widgets/common/expandable_image.dart"; -import "package:nexus/widgets/rooms/knock_sheet.dart"; -import "package:nexus/widgets/common/loading.dart"; -import "package:nexus/widgets/rooms/room_summary_sheet.dart"; +import "package:nexus/widgets/add_space_rooms_sheet.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/expandable_image.dart"; +import "package:nexus/widgets/knock_sheet.dart"; +import "package:nexus/widgets/loading.dart"; +import "package:nexus/widgets/room_summary_sheet.dart"; class const SpaceHierarchySheet(final String spaceId, {super.key}) extends ConsumerWidget { diff --git a/lib/widgets/users/start_dm_sheet.dart b/lib/widgets/start_dm_sheet.dart similarity index 100% rename from lib/widgets/users/start_dm_sheet.dart rename to lib/widgets/start_dm_sheet.dart diff --git a/lib/widgets/emoji/sticker_picker.dart b/lib/widgets/sticker_picker.dart similarity index 100% rename from lib/widgets/emoji/sticker_picker.dart rename to lib/widgets/sticker_picker.dart diff --git a/lib/widgets/renderers/swipe_to_reply.dart b/lib/widgets/swipe_to_reply.dart similarity index 100% rename from lib/widgets/renderers/swipe_to_reply.dart rename to lib/widgets/swipe_to_reply.dart diff --git a/lib/widgets/common/sync_status_banner.dart b/lib/widgets/sync_status_banner.dart similarity index 98% rename from lib/widgets/common/sync_status_banner.dart rename to lib/widgets/sync_status_banner.dart index 4199866..10aec7e 100644 --- a/lib/widgets/common/sync_status_banner.dart +++ b/lib/widgets/sync_status_banner.dart @@ -1,7 +1,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:material_ui/material_ui.dart"; import "package:nexus/controllers/sync_status.dart"; -import "package:nexus/widgets/common/error_dialog.dart"; +import "package:nexus/widgets/error_dialog.dart"; import "package:timeago/timeago.dart"; class const SyncStatusBanner({super.key}) extends ConsumerWidget { diff --git a/lib/widgets/threads/thread_drawer.dart b/lib/widgets/thread_drawer.dart similarity index 100% rename from lib/widgets/threads/thread_drawer.dart rename to lib/widgets/thread_drawer.dart diff --git a/lib/widgets/threads/thread_summary.dart b/lib/widgets/thread_summary.dart similarity index 100% rename from lib/widgets/threads/thread_summary.dart rename to lib/widgets/thread_summary.dart diff --git a/lib/widgets/threads/threads_drawer.dart b/lib/widgets/threads_drawer.dart similarity index 96% rename from lib/widgets/threads/threads_drawer.dart rename to lib/widgets/threads_drawer.dart index e7931da..a2ff127 100644 --- a/lib/widgets/threads/threads_drawer.dart +++ b/lib/widgets/threads_drawer.dart @@ -4,10 +4,10 @@ import "package:nexus/controllers/thread_list.dart"; import "package:nexus/controllers/rooms.dart"; import "package:nexus/helpers/extensions/thread_events.dart"; import "package:nexus/main.dart"; -import "package:nexus/widgets/common/error_dialog.dart"; -import "package:nexus/widgets/common/loading.dart"; -import "package:nexus/widgets/renderers/event_preview.dart"; -import "package:nexus/widgets/common/timestamp.dart"; +import "package:nexus/widgets/error_dialog.dart"; +import "package:nexus/widgets/loading.dart"; +import "package:nexus/widgets/event_preview.dart"; +import "package:nexus/widgets/timestamp.dart"; final class const ThreadsDrawer( final String roomId, { diff --git a/lib/widgets/common/timestamp.dart b/lib/widgets/timestamp.dart similarity index 100% rename from lib/widgets/common/timestamp.dart rename to lib/widgets/timestamp.dart diff --git a/lib/widgets/users/timezone_field.dart b/lib/widgets/timezone_field.dart similarity index 100% rename from lib/widgets/users/timezone_field.dart rename to lib/widgets/timezone_field.dart diff --git a/lib/widgets/room_chat/tombstone_banner.dart b/lib/widgets/tombstone_banner.dart similarity index 100% rename from lib/widgets/room_chat/tombstone_banner.dart rename to lib/widgets/tombstone_banner.dart diff --git a/lib/widgets/renderers/url_preview.dart b/lib/widgets/url_preview.dart similarity index 100% rename from lib/widgets/renderers/url_preview.dart rename to lib/widgets/url_preview.dart diff --git a/lib/widgets/users/user_bottom_sheet.dart b/lib/widgets/user_bottom_sheet.dart similarity index 97% rename from lib/widgets/users/user_bottom_sheet.dart rename to lib/widgets/user_bottom_sheet.dart index 12e0e24..373ed52 100644 --- a/lib/widgets/users/user_bottom_sheet.dart +++ b/lib/widgets/user_bottom_sheet.dart @@ -19,19 +19,19 @@ import "package:nexus/helpers/extensions/create_room.dart"; import "package:nexus/helpers/extensions/success_scheme.dart"; import "package:nexus/helpers/extensions/with_progress_snackbar.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/widgets/common/error_dialog.dart"; -import "package:nexus/widgets/common/loading.dart"; -import "package:nexus/widgets/users/mutual_rooms_sheet.dart"; +import "package:nexus/widgets/error_dialog.dart"; +import "package:nexus/widgets/loading.dart"; +import "package:nexus/widgets/mutual_rooms_sheet.dart"; import "package:nexus/models/content/membership.dart"; import "package:nexus/models/membership_action.dart"; import "package:nexus/models/profile_response.dart"; -import "package:nexus/widgets/avatars/avatar_or_hash.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/main.dart"; -import "package:nexus/widgets/avatars/editable_avatar.dart"; -import "package:nexus/widgets/common/expandable_image.dart"; +import "package:nexus/widgets/editable_avatar.dart"; +import "package:nexus/widgets/expandable_image.dart"; import "package:nexus/widgets/settings/inline_text_field.dart"; -import "package:nexus/widgets/users/start_dm_sheet.dart"; -import "package:nexus/widgets/users/timezone_field.dart"; +import "package:nexus/widgets/start_dm_sheet.dart"; +import "package:nexus/widgets/timezone_field.dart"; final class const UserBottomSheet( final String userId, { diff --git a/lib/widgets/users/user_span.dart b/lib/widgets/user_span.dart similarity index 100% rename from lib/widgets/users/user_span.dart rename to lib/widgets/user_span.dart