diff --git a/lib/controllers/attachment.dart b/lib/controllers/attachment.dart index d4a5e09..2cabbae 100644 --- a/lib/controllers/attachment.dart +++ b/lib/controllers/attachment.dart @@ -23,7 +23,7 @@ class AttachmentController(final String roomId) ); final content = await ref - .watch(ClientController.provider.notifier) + .read(ClientController.provider.notifier) .uploadMedia(.new(path: file.path, encrypt: isEncrypted)); state = (filename, content); diff --git a/lib/controllers/auth_url.dart b/lib/controllers/auth_url.dart index 963df86..be0d1b0 100644 --- a/lib/controllers/auth_url.dart +++ b/lib/controllers/auth_url.dart @@ -8,7 +8,7 @@ class AuthUrlController(final Uri homeserver) extends AsyncNotifier { @override Future build() async => ref - .watch(ClientController.provider.notifier) + .read(ClientController.provider.notifier) .getAuthUrl( .new( homeserverUrl: homeserver, diff --git a/lib/controllers/client_id.dart b/lib/controllers/client_id.dart index 7748f97..46c98de 100644 --- a/lib/controllers/client_id.dart +++ b/lib/controllers/client_id.dart @@ -4,7 +4,7 @@ import "package:nexus/controllers/client.dart"; class ClientIdController(final Uri homeserver) extends AsyncNotifier { @override Future build() => ref - .watch(ClientController.provider.notifier) + .read(ClientController.provider.notifier) .registerClient( .new( clientName: "Nexus", diff --git a/lib/controllers/event.dart b/lib/controllers/event.dart index 5c6513b..51b791f 100644 --- a/lib/controllers/event.dart +++ b/lib/controllers/event.dart @@ -19,13 +19,13 @@ class EventController(final GetEventRequest request) if (event != null) return event; final fetchedEvent = await ref - .watch(ClientController.provider.notifier) + .read(ClientController.provider.notifier) .getEvent(request) .onError((_, _) => null); if (fetchedEvent != null) { ref - .watch(RoomsController.provider.notifier) + .read(RoomsController.provider.notifier) .update( .new({ request.roomId: .new( diff --git a/lib/controllers/gomuks_listener.dart b/lib/controllers/gomuks_listener.dart index 2a43676..d0cbf59 100644 --- a/lib/controllers/gomuks_listener.dart +++ b/lib/controllers/gomuks_listener.dart @@ -40,28 +40,23 @@ class GomuksListenerController extends AsyncNotifier { switch (muksEventType) { case "client_state": ref - .watch(ClientStateController.provider.notifier) + .read(ClientStateController.provider.notifier) .set(.fromJson(decodedMuksEvent)); - break; case "sync_status": ref - .watch(SyncStatusController.provider.notifier) + .read(SyncStatusController.provider.notifier) .set(.fromJson(decodedMuksEvent)); - break; case "init_complete": - ref.watch(InitCompleteController.provider.notifier).complete(); - break; + ref.read(InitCompleteController.provider.notifier).complete(); case "send_complete": final event = Event.fromJson(decodedMuksEvent["event"]); ref - .watch(RoomsController.provider.notifier) + .read(RoomsController.provider.notifier) .update( .new({ event.roomId: .new(events: .new({event.rowId: event})), }), ); - - break; case "sync_complete": final syncData = SyncData.fromJson(decodedMuksEvent); final roomProvider = RoomsController.provider; @@ -73,28 +68,27 @@ class GomuksListenerController extends AsyncNotifier { } ref - .watch(roomProvider.notifier) + .read(roomProvider.notifier) .update(syncData.rooms, syncData.leftRooms); ref - .watch(accountDataProvider.notifier) + .read(accountDataProvider.notifier) .update(syncData.accountData); - if (syncData.topLevelSpaces != null) { + if (syncData.topLevelSpaces case final topLevelSpaces?) { ref - .watch(TopLevelSpacesController.provider.notifier) - .set(syncData.topLevelSpaces!); + .read(TopLevelSpacesController.provider.notifier) + .set(topLevelSpaces); } - if (syncData.spaceEdges != null) { + if (syncData.spaceEdges case final spaceEdges?) { ref - .watch(SpaceEdgesController.provider.notifier) - .set(syncData.spaceEdges!); + .read(SpaceEdgesController.provider.notifier) + .set(spaceEdges); } - // ref - // .watch(SyncStatusController.provider.notifier) - // .set(SyncStatus.fromJson(decodedMuksEvent)); - break; + // ref + // .read(SyncStatusController.provider.notifier) + // .set(SyncStatus.fromJson(decodedMuksEvent)); default: debugPrint("Unhandled event: $muksEventType"); } diff --git a/lib/controllers/key.dart b/lib/controllers/key.dart index 77b4883..33c76da 100644 --- a/lib/controllers/key.dart +++ b/lib/controllers/key.dart @@ -11,7 +11,7 @@ class KeyController(final String key) extends AsyncNotifier { ref.watch(SharedPrefsController.provider).getString(key); Future set(String? value) async { - final prefs = ref.watch(SharedPrefsController.provider); + final prefs = ref.read(SharedPrefsController.provider); state = .data(value); if (value == null) { diff --git a/lib/controllers/member_list_opened.dart b/lib/controllers/member_list_opened.dart index f0295d8..60a17d5 100644 --- a/lib/controllers/member_list_opened.dart +++ b/lib/controllers/member_list_opened.dart @@ -10,7 +10,7 @@ class MemberListOpenedController extends AsyncNotifier { Future set(bool value) async { state = .data(value); - await ref.watch(SharedPrefsController.provider).setBool(key, value); + await ref.read(SharedPrefsController.provider).setBool(key, value); } static final provider = diff --git a/lib/controllers/members.dart b/lib/controllers/members.dart index d8172b1..8d0c989 100644 --- a/lib/controllers/members.dart +++ b/lib/controllers/members.dart @@ -18,7 +18,7 @@ class MembersController(final String roomId) if (!room.hasFetchedMembers) { final fetchedState = await ref - .watch(ClientController.provider.notifier) + .read(ClientController.provider.notifier) .getRoomState( GetRoomStateRequest( roomId: roomId, diff --git a/lib/controllers/notifications.dart b/lib/controllers/notifications.dart index cd10dff..a9bb89f 100644 --- a/lib/controllers/notifications.dart +++ b/lib/controllers/notifications.dart @@ -55,7 +55,7 @@ class NotificationsController(final NotificationsRequest request) final lastNotification = currentNotifications.lastOrNull; final lastTs = lastNotification == null ? null - : ref.watch( + : ref.read( RoomsController.provider.select( (rooms) => rooms[lastNotification.$2] ?.events[lastNotification.$1] diff --git a/lib/controllers/push_key.dart b/lib/controllers/push_key.dart index e3f6b82..24e9440 100644 --- a/lib/controllers/push_key.dart +++ b/lib/controllers/push_key.dart @@ -13,8 +13,8 @@ class PushKeyController(final String instance) extends AsyncNotifier { Future set(String? value) async { final provider = KeyController.provider(KeyController.pushKeyKey); - final notifier = ref.watch(provider.notifier); - final prefs = IMap(json.decode(await ref.watch(provider.future) ?? "{}")); + final notifier = ref.read(provider.notifier); + final prefs = IMap(json.decode(await ref.read(provider.future) ?? "{}")); state = .data(value); notifier.set( diff --git a/lib/controllers/reactions.dart b/lib/controllers/reactions.dart index 9a92843..b3584c8 100644 --- a/lib/controllers/reactions.dart +++ b/lib/controllers/reactions.dart @@ -18,7 +18,7 @@ class ReactionsController(final ReactionsConfig config) final reactionEvents = eventInfo?.$2.isNotEmpty == true ? await ref - .watch(ClientController.provider.notifier) + .read(ClientController.provider.notifier) .getRelatedEvents( .new( roomId: config.roomId, diff --git a/lib/controllers/recent_emoji.dart b/lib/controllers/recent_emoji.dart index d814a79..8dd3e59 100644 --- a/lib/controllers/recent_emoji.dart +++ b/lib/controllers/recent_emoji.dart @@ -27,7 +27,7 @@ class RecentEmojiController extends Notifier> { } Future add(String emoji) => ref - .watch(ClientController.provider.notifier) + .read(ClientController.provider.notifier) .setAccountData( .new( type: AccountData.recentEmojiKey, diff --git a/lib/controllers/room_chat.dart b/lib/controllers/room_chat.dart index 1576401..65dc203 100644 --- a/lib/controllers/room_chat.dart +++ b/lib/controllers/room_chat.dart @@ -88,7 +88,7 @@ class RoomChatController(final ChatParams info) } Future deleteMessage(Event event, {String? reason}) => ref - .watch(ClientController.provider.notifier) + .read(ClientController.provider.notifier) .redactEvent( RedactEventRequest( eventId: event.eventId, @@ -146,7 +146,7 @@ class RoomChatController(final ChatParams info) ); if (response.events.isEmpty) { - state = .data(state.value); + state = .data(chat); } ref @@ -234,7 +234,7 @@ class RoomChatController(final ChatParams info) Content? baseContent; final relatedEvent = relatedEventRowId == null ? null - : ref.watch( + : ref.read( RoomsController.provider.select( (rooms) => rooms[info.roomId]?.events[relatedEventRowId], ), @@ -267,9 +267,7 @@ class RoomChatController(final ChatParams info) baseContent: baseContent, mentions: Mentions( userIds: [ - if (shouldMention == true && - relatedEvent != null && - relationType == RelationType.reply) + if (shouldMention && relatedEvent != null && relationType == .reply) relatedEvent.sender, ].toIList(), room: taggedMessage.contains("@room"), @@ -307,24 +305,20 @@ class RoomChatController(final ChatParams info) ), ); - final reactionEvents = allReactionEvents - ?.where((event) => event.redactedBy == null) - .toIList(); - - final reactionEvent = reactionEvents?.firstWhereOrNull( - (event) => switch (event.content) { - ReactionContent(:final key) => - key == reaction && event.sender == userId, - _ => false, - }, + final reactionEvent = allReactionEvents?.firstWhereOrNull( + (event) => + event.redactedBy == null && + switch (event.content) { + ReactionContent(:final key) => + key == reaction && event.sender == userId, + _ => false, + }, ); if (reactionEvent != null) { - await ref - .watch(ClientController.provider.notifier) - .redactEvent( - .new(eventId: reactionEvent.eventId, roomId: info.roomId), - ); + await client.redactEvent( + .new(eventId: reactionEvent.eventId, roomId: info.roomId), + ); } } diff --git a/lib/controllers/rooms.dart b/lib/controllers/rooms.dart index 7b86f2a..7a5c181 100644 --- a/lib/controllers/rooms.dart +++ b/lib/controllers/rooms.dart @@ -48,7 +48,7 @@ class RoomsController extends Notifier> { hasMore: incoming.hasMore, clientSticky: incoming.resetClientSticky ? incoming.clientSticky - : (incoming.clientSticky.isEmpty == true + : (incoming.clientSticky.isEmpty ? existing.clientSticky : existing.clientSticky.addAll( incoming.clientSticky, @@ -73,9 +73,9 @@ class RoomsController extends Notifier> { incoming.hasFetchedMembers || existing.hasFetchedMembers, hasFetchedState: incoming.hasFetchedState || existing.hasFetchedState, - timeline: (incoming.reset + timeline: incoming.reset ? incoming.timeline - : existing.timeline.addAll(incoming.timeline)), + : existing.timeline.addAll(incoming.timeline), receipts: incoming.receipts.entries.fold( existing.receipts, (receiptAcc, event) => receiptAcc.add( @@ -88,12 +88,7 @@ class RoomsController extends Notifier> { ); }); - final prunedList = leftRooms.fold( - merged, - (acc, roomId) => acc.remove(roomId), - ); - - state = prunedList; + state = leftRooms.fold(merged, (acc, roomId) => acc.remove(roomId)); } static final provider = NotifierProvider>( diff --git a/lib/controllers/settings.dart b/lib/controllers/settings.dart index 8c107e3..1d6076a 100644 --- a/lib/controllers/settings.dart +++ b/lib/controllers/settings.dart @@ -1,4 +1,5 @@ import "dart:convert"; + import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/settings_file.dart"; import "package:nexus/models/settings.dart"; @@ -17,7 +18,7 @@ class SettingsController extends AsyncNotifier { Future set(Settings settings) async { state = .data(settings); - final file = await ref.watch(SettingsFileController.provider.future); + final file = await ref.read(SettingsFileController.provider.future); await file.writeAsString(json.encode(settings.toJson())); } diff --git a/lib/controllers/settings_file.dart b/lib/controllers/settings_file.dart index 2c050bc..9b57777 100644 --- a/lib/controllers/settings_file.dart +++ b/lib/controllers/settings_file.dart @@ -1,4 +1,5 @@ import "dart:io"; + import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:path/path.dart"; import "package:path_provider/path_provider.dart"; diff --git a/lib/controllers/settings_sections.dart b/lib/controllers/settings_sections.dart index e00778e..26288a9 100644 --- a/lib/controllers/settings_sections.dart +++ b/lib/controllers/settings_sections.dart @@ -47,7 +47,7 @@ class SettingsSectionsController options: ThemeMode.values, getName: (option) => toBeginningOfSentenceCase(option.name), onChanged: (value) => ref - .watch(SettingsController.provider.notifier) + .read(SettingsController.provider.notifier) .set(settings.copyWith(theme: value)) .onError(showError), ), @@ -67,7 +67,7 @@ class SettingsSectionsController Platform.isMacOS || Platform.isWindows) ? (value) => ref - .watch(SettingsController.provider.notifier) + .read(SettingsController.provider.notifier) .set(settings.copyWith(useDynamicTheming: value)) .onError(showError) : null, @@ -89,7 +89,7 @@ class SettingsSectionsController secondary: Icon(icon), value: settings.showAllInHome, onChanged: (value) => ref - .watch(SettingsController.provider.notifier) + .read(SettingsController.provider.notifier) .set(settings.copyWith(showAllInHome: value)) .onError(showError), ), @@ -106,7 +106,7 @@ class SettingsSectionsController secondary: Icon(icon), value: settings.showMinimizeButton, onChanged: (value) => ref - .watch(SettingsController.provider.notifier) + .read(SettingsController.provider.notifier) .set(settings.copyWith(showMinimizeButton: value)) .onError(showError), ), @@ -122,7 +122,7 @@ class SettingsSectionsController value: settings.linuxMobileMode, onChanged: Platform.isLinux ? (value) => ref - .watch(SettingsController.provider.notifier) + .read(SettingsController.provider.notifier) .set(settings.copyWith(linuxMobileMode: value)) .onError(showError) : null, @@ -190,14 +190,14 @@ class SettingsSectionsController loading.value = true; if (value != null) { if (await ref - .watch( + .read( NotificationController .provider .notifier, ) .requestPermissions()) { await ref - .watch( + .read( UnifiedPushController .provider .notifier, @@ -208,7 +208,7 @@ class SettingsSectionsController } } else { await ref - .watch( + .read( UnifiedPushController .provider .notifier, @@ -268,7 +268,7 @@ class SettingsSectionsController onChanged: specVersionsResponse.maybeWhen( data: (_) => (value) => ref - .watch(ClientController.provider.notifier) + .read(ClientController.provider.notifier) .setAccountData( .new( type: AccountData.invitePermissionConfigKey, diff --git a/lib/controllers/spaces.dart b/lib/controllers/spaces.dart index e86cfe1..02d6476 100644 --- a/lib/controllers/spaces.dart +++ b/lib/controllers/spaces.dart @@ -19,10 +19,10 @@ class SpacesController extends AsyncNotifier> { final spaceEdges = ref.watch(SpaceEdgesController.provider); final accountData = ref.watch(AccountDataController.provider); - final childrenById = { - for (final entry in spaceEdges.entries) - entry.key: entry.value.map((e) => e.childId).toList(), - }; + final childrenById = spaceEdges.map( + (spaceId, edges) => + MapEntry(spaceId, edges.map((edge) => edge.childId).toList()), + ); Set collectDescendants(String startId) { final visited = {}; @@ -78,21 +78,22 @@ class SpacesController extends AsyncNotifier> { final spaces = topLevelSpaceIds.map(buildSpace).toIList(); - final usedRooms = { - for (final space in spaces) ...[ - ...space.children, - ...space.subSpaces.expand((s) => s.children), - ], - }.nonNulls.toISet(); + final usedRooms = spaces + .expand( + (space) => [ + ...space.children, + ...space.subSpaces.expand((s) => s.children), + ], + ) + .toISet(); + final usedRoomIds = usedRooms.map((room) => room.metadata?.id).toSet(); - final directMessages = accountData.directMessages.values.flattened; + final directMessages = accountData.directMessages.values.flattened.toSet(); final otherRooms = rooms.entries .where( (e) => - !usedRooms - .map((element) => element.metadata?.id) - .contains(e.key) && + !usedRoomIds.contains(e.key) && !topLevelSpaceIds.contains(e.key) && !childrenById.containsKey(e.key), ) diff --git a/lib/controllers/unified_push_allowed.dart b/lib/controllers/unified_push_allowed.dart index 515239a..03ce3b0 100644 --- a/lib/controllers/unified_push_allowed.dart +++ b/lib/controllers/unified_push_allowed.dart @@ -20,7 +20,7 @@ class UnifiedPushAllowedController extends AsyncNotifier { } final capabilities = await ref - .watch(ClientController.provider.notifier) + .read(ClientController.provider.notifier) .getCapabilities(); if (capabilities.webpush?.vapid == null) { diff --git a/lib/controllers/url_preview.dart b/lib/controllers/url_preview.dart index 41c3ef4..12adca9 100644 --- a/lib/controllers/url_preview.dart +++ b/lib/controllers/url_preview.dart @@ -11,7 +11,7 @@ class UrlPreviewController(final Uri url) try { return await ref - .watch(ClientController.provider.notifier) + .read(ClientController.provider.notifier) .getUrlPreview(url); } catch (error, stackTrace) { debugPrintStack(label: error.toString(), stackTrace: stackTrace); diff --git a/lib/controllers/user.dart b/lib/controllers/user.dart index c69ebd0..bad8115 100644 --- a/lib/controllers/user.dart +++ b/lib/controllers/user.dart @@ -1,4 +1,5 @@ import "dart:async"; + import "package:collection/collection.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/members.dart"; diff --git a/lib/helpers/extensions/build_event_options.dart b/lib/helpers/extensions/build_event_options.dart index fdf3bdf..69d024f 100644 --- a/lib/helpers/extensions/build_event_options.dart +++ b/lib/helpers/extensions/build_event_options.dart @@ -34,7 +34,7 @@ extension BuildEventOptions on Event { )).notifier; final isPinned = ref - .watch(PinnedIdsController.provider(roomId)) + .read(PinnedIdsController.provider(roomId)) .contains(eventId); void showReasonDialog({ @@ -86,7 +86,7 @@ extension BuildEventOptions on Event { } return .new([ - if (ref.watch( + if (ref.read( PowerLevelController.provider( .new(eventType: .reaction, roomId: roomId), ), @@ -98,30 +98,33 @@ extension BuildEventOptions on Event { child: Row( mainAxisAlignment: .center, children: [ - for (final emoji in { - ...ref - .watch(RecentEmojiController.provider) - .map((entry) => entry.emoji), - "👍", - "🤣", - "😭", - "🤔", - }.take(3)) - IconButton( - icon: Text(emoji), - onPressed: () async { - Navigator.of(context).pop(); - await ref - .watch(notifier) - .sendReaction(emoji, this) - .onError(showError); + ...{ + ...ref + .read(RecentEmojiController.provider) + .map((entry) => entry.emoji), + "👍", + "🤣", + "😭", + "🤔", + } + .take(3) + .map( + (emoji) => IconButton( + icon: Text(emoji), + onPressed: () async { + Navigator.of(context).pop(); + await ref + .read(notifier) + .sendReaction(emoji, this) + .onError(showError); - await ref - .read(RecentEmojiController.provider.notifier) - .add(emoji) - .onError(showError); - }, - ), + await ref + .read(RecentEmojiController.provider.notifier) + .add(emoji) + .onError(showError); + }, + ), + ), IconButton( icon: const Icon(Icons.emoji_emotions), onPressed: () { @@ -135,7 +138,7 @@ extension BuildEventOptions on Event { onSelection: (emoji) { Navigator.of(context).pop(); ref - .watch(notifier) + .read(notifier) .sendReaction(emoji, this) .onError(showError); }, @@ -148,7 +151,7 @@ extension BuildEventOptions on Event { ), ), - if (ref.watch( + if (ref.read( PowerLevelController.provider( .new(eventType: .message, roomId: roomId), ), @@ -167,7 +170,7 @@ extension BuildEventOptions on Event { child: const ListTile(leading: Icon(Icons.edit), title: Text("Edit")), ), - if (ref.watch( + if (ref.read( PowerLevelController.provider( .state(eventType: .pinnedEvents, roomId: roomId), ), @@ -225,7 +228,7 @@ extension BuildEventOptions on Event { ), ), - if (ref.watch( + if (ref.read( PowerLevelController.provider( .redaction(targetUser: sender, roomId: roomId), ), @@ -239,7 +242,7 @@ extension BuildEventOptions on Event { "This cannot be reversed.", action: "Delete", onConfirm: (reason) => - ref.watch(notifier).deleteMessage(this, reason: reason), + ref.read(notifier).deleteMessage(this, reason: reason), ), child: ListTile( leading: Icon(Icons.delete, color: danger), @@ -249,7 +252,7 @@ extension BuildEventOptions on Event { if (eventId.isFake) PopupMenuItem( - onTap: () => ref.watch(notifier).hideClientSticky(rowId), + onTap: () => ref.read(notifier).hideClientSticky(rowId), child: ListTile( leading: Icon(Icons.delete, color: danger), title: Text("Hide", style: .new(color: danger)), @@ -260,7 +263,7 @@ extension BuildEventOptions on Event { onTap: () => showReasonDialog( title: "Report", description: - "Report this this to your server administrators, " + "Report this event to your server administrators, " "who can take action like banning this server or room.", action: "Report", onConfirm: (reason) => ref diff --git a/lib/helpers/extensions/room_destinations.dart b/lib/helpers/extensions/room_destinations.dart new file mode 100644 index 0000000..59bb3d2 --- /dev/null +++ b/lib/helpers/extensions/room_destinations.dart @@ -0,0 +1,23 @@ +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/avatar_or_hash.dart"; + +extension RoomDestinations on IList { + List toDestinations({required bool isDm}) => + map( + (room) => NavigationRailM3EDestination( + label: room.metadata?.name ?? "Unnamed Room", + badgeCount: switch (room.metadata?.unreadNotifications) { + 0 || null => room.metadata?.unreadMessages == 0 ? null : 0, + int unread => unread, + }, + icon: AvatarOrHash( + room.metadata?.avatar, + room.metadata?.name ?? "Unnamed Room", + fallback: isDm ? null : const Icon(Icons.numbers), + ), + ), + ).toList(); +} diff --git a/lib/helpers/extensions/show_about_dialog.dart b/lib/helpers/extensions/show_about_dialog.dart index 4729e99..e684dfd 100644 --- a/lib/helpers/extensions/show_about_dialog.dart +++ b/lib/helpers/extensions/show_about_dialog.dart @@ -51,7 +51,7 @@ extension ShowAboutDialog on BuildContext { ), M3ECardColumn( onTap: (index) => - ref.watch(LaunchHelper.provider).launchUrl(switch (index) { + ref.read(LaunchHelper.provider).launchUrl(switch (index) { 0 => .https("git.federated.nexus", "nexus/nexus"), _ => .https("liberapay.com", "QuadRadical"), }), diff --git a/lib/widgets/composer/composer.dart b/lib/widgets/composer/composer.dart index cc3a49a..7bf5d0e 100644 --- a/lib/widgets/composer/composer.dart +++ b/lib/widgets/composer/composer.dart @@ -11,6 +11,7 @@ import "package:fluttertagger/fluttertagger.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/attachment.dart"; import "package:nexus/controllers/image_picker.dart"; +import "package:image_picker/image_picker.dart"; import "package:nexus/controllers/power_level.dart"; import "package:nexus/models/content/message.dart"; import "package:nexus/models/relation_type.dart"; @@ -89,6 +90,14 @@ class const Composer( final attachment = ref.watch(AttachmentController.provider(roomId)); + Future attach(Future pick) async { + if (await pick case final file?) { + await ref + .read(AttachmentController.provider(roomId).notifier) + .add(file); + } + } + void send() { if (controller.value.text.isEmpty && attachment == null || attachment != null && attachment.$2 == null) { @@ -173,53 +182,33 @@ class const Composer( tooltip: "Add media", enabled: attachment == null, itemBuilder: (context) => [ - if (Platform.isAndroid || Platform.isIOS) + for (final (source, title, icon) in [ + if (Platform.isAndroid || Platform.isIOS) + ( + ImageSource.camera, + "Camera", + Icons.add_a_photo, + ), + ( + ImageSource.gallery, + "Gallery", + Icons.add_photo_alternate, + ), + ]) PopupMenuItem( child: ListTile( - title: Text("Camera"), - leading: Icon(Icons.add_a_photo), + title: Text(title), + leading: Icon(icon), ), - onTap: () async => ref - .read( - AttachmentController.provider(roomId) - .notifier, - ) - .add( - (await ref - .watch( - ImagePickerController.provider, - ) - .pickImage(source: .camera))!, - ) - .onError(showError), + onTap: () => attach( + ref + .read(ImagePickerController.provider) + .pickImage(source: source), + ).onError(showError), ), PopupMenuItem( - child: ListTile( - title: Text("Gallery"), - leading: Icon(Icons.add_photo_alternate), - ), - onTap: () async => ref - .read( - AttachmentController.provider(roomId) - .notifier, - ) - .add( - (await ref - .watch( - ImagePickerController.provider, - ) - .pickImage(source: .gallery))!, - ) - .onError(showError), - ), - PopupMenuItem( - onTap: () async => ref - .watch( - AttachmentController.provider(roomId) - .notifier, - ) - .add((await openFile())!) - .onError(showError), + onTap: () => + attach(openFile()).onError(showError), child: ListTile( title: Text("Files"), leading: Icon(Icons.attachment), diff --git a/lib/widgets/emoji_picker.dart b/lib/widgets/emoji_picker.dart index 453213a..7e49836 100644 --- a/lib/widgets/emoji_picker.dart +++ b/lib/widgets/emoji_picker.dart @@ -26,7 +26,7 @@ class const EmojiPicker({ if (!isFreeText) { await ref - .watch(RecentEmojiController.provider.notifier) + .read(RecentEmojiController.provider.notifier) .add(emoji) .onError(showError); } diff --git a/lib/widgets/html/html.dart b/lib/widgets/html/html.dart index 8eb60d8..325e449 100644 --- a/lib/widgets/html/html.dart +++ b/lib/widgets/html/html.dart @@ -149,6 +149,6 @@ class const Html( .nonNulls, ), }, - onTapUrl: (url) => ref.watch(LaunchHelper.provider).launchUrl(.parse(url)), + onTapUrl: (url) => ref.read(LaunchHelper.provider).launchUrl(.parse(url)), ); } diff --git a/lib/widgets/linkified_text.dart b/lib/widgets/linkified_text.dart index 77ba7a4..4e621d2 100644 --- a/lib/widgets/linkified_text.dart +++ b/lib/widgets/linkified_text.dart @@ -16,7 +16,7 @@ final class const LinkifiedText( style: style, options: .new(humanize: false), onOpen: (link) => - ref.watch(LaunchHelper.provider).launchUrl(.parse(link.url)), + ref.read(LaunchHelper.provider).launchUrl(.parse(link.url)), linkStyle: .new(color: Theme.of(context).colorScheme.primary), overflow: maxLines == null ? null : .ellipsis, ); diff --git a/lib/widgets/member_list.dart b/lib/widgets/member_list.dart index ac0cb2a..3b62ab6 100644 --- a/lib/widgets/member_list.dart +++ b/lib/widgets/member_list.dart @@ -79,9 +79,11 @@ class const MemberList(final String roomId, {super.key}) ) : Expanded( child: CustomScrollView( - slivers: [ - for (final MapEntry(key: powerLevel, value: members) - in value) ...[ + slivers: value.expand((group) { + final MapEntry(key: powerLevel, value: members) = + group; + + return [ SliverToBoxAdapter( child: Padding( padding: .symmetric(horizontal: 16), @@ -151,8 +153,8 @@ class const MemberList(final String roomId, {super.key}) } }, ), - ], - ], + ]; + }).toList(), ), ), AsyncLoading _ => Loading(), diff --git a/lib/widgets/pages/select_server.dart b/lib/widgets/pages/select_server.dart index c690dc1..431b7d4 100644 --- a/lib/widgets/pages/select_server.dart +++ b/lib/widgets/pages/select_server.dart @@ -18,7 +18,7 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final launch = ref.watch(LaunchHelper.provider).launchUrl; + final launch = ref.read(LaunchHelper.provider).launchUrl; final isLoading = useState(false); final homeserverUrl = useTextEditingController(); @@ -52,7 +52,7 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { AuthUrlController.provider(url).future, ); - await ref.watch(LaunchHelper.provider).launchUrl(codeResponse.url); + await ref.read(LaunchHelper.provider).launchUrl(codeResponse.url); AppLinks().uriLinkStream.listen((encodedUri) async { final state = encodedUri.queryParameters["state"]; @@ -64,7 +64,7 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { if (code != null) { await ref - .watch(ClientController.provider.notifier) + .read(ClientController.provider.notifier) .exchangeToken( .new( homeserverUrl: url, @@ -74,7 +74,7 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { path: "/", ), code: code, - clientId: await ref.watch( + clientId: await ref.read( ClientIdController.provider(url).future, ), ), diff --git a/lib/widgets/pages/settings.dart b/lib/widgets/pages/settings.dart index 50eb7f0..1760b89 100644 --- a/lib/widgets/pages/settings.dart +++ b/lib/widgets/pages/settings.dart @@ -30,7 +30,7 @@ class const SettingsPage({super.key}) extends ConsumerWidget { barHintText: "Search...", isFullScreen: categoriesArePages, suggestionsBuilder: (suggestionsContext, controller) async { - final categories = await ref.watch( + final categories = await ref.read( SettingsSectionsController.provider.future, ); final query = controller.text.toLowerCase(); diff --git a/lib/widgets/pages/verify.dart b/lib/widgets/pages/verify.dart index 44d276f..7e85b10 100644 --- a/lib/widgets/pages/verify.dart +++ b/lib/widgets/pages/verify.dart @@ -23,7 +23,7 @@ class const VerifyPage({super.key}) extends HookConsumerWidget { } inputError.value = await ref - .watch(ClientController.provider.notifier) + .read(ClientController.provider.notifier) .verify(passphraseController.text); } finally { isLoading.value = false; diff --git a/lib/widgets/players/audio.dart b/lib/widgets/players/audio.dart index bd50a8b..8e3d83d 100644 --- a/lib/widgets/players/audio.dart +++ b/lib/widgets/players/audio.dart @@ -27,7 +27,7 @@ class const AudioPlayer( player.platform?.state = player.platform!.state.copyWith(buffering: true); scheduleMicrotask(() async { final video = await ref - .watch(ClientController.provider.notifier) + .read(ClientController.provider.notifier) .downloadMedia(.new(mxc: uri, encrypted: encrypted)); await player.open(Media(video.path), play: false); diff --git a/lib/widgets/players/video.dart b/lib/widgets/players/video.dart index 024c30e..cf3c949 100644 --- a/lib/widgets/players/video.dart +++ b/lib/widgets/players/video.dart @@ -25,7 +25,7 @@ class const VideoPlayer( player.platform?.state = player.platform!.state.copyWith(buffering: true); scheduleMicrotask(() async { final video = await ref - .watch(ClientController.provider.notifier) + .read(ClientController.provider.notifier) .downloadMedia(.new(mxc: uri, encrypted: encrypted)); await player.open(Media(video.path), play: false); }); diff --git a/lib/widgets/renderers/event.dart b/lib/widgets/renderers/event.dart index 97c0265..bd0f5f3 100644 --- a/lib/widgets/renderers/event.dart +++ b/lib/widgets/renderers/event.dart @@ -81,6 +81,15 @@ class const EventRenderer( event.sender == previousEvent?.sender && event.pmp?.id == previousEvent?.pmp?.id; + GenericEventRenderer generic(IconData icon, String text) => + .new(icon, maxLines: maxLines, [ + WidgetSpan( + alignment: .middle, + child: MessageDisplayname(event, shouldWrap: maxLines == null), + ), + TextSpan(text: text), + ]); + final child = event.redactedBy != null || event.relationType == "m.replace" ? null : switch (event.content) { @@ -122,169 +131,67 @@ class const EventRenderer( _ => MembershipRenderer(event, maxLines: maxLines), }, - AvatarContent() => GenericEventRenderer( + AvatarContent() => generic( Icons.interests, - maxLines: maxLines, - [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan(text: "changed the room avatar"), - ], + "changed the room avatar", ), - - CreateContent() => GenericEventRenderer( - Icons.add, - maxLines: maxLines, - [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan(text: "created the room"), - ], - ), - - PowerLevelsContent() => GenericEventRenderer( + CreateContent() => generic(Icons.add, "created the room"), + PowerLevelsContent() => generic( Icons.power, - maxLines: maxLines, - [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan(text: "changed the room's power levels"), - ], + "changed the room's power levels", ), - - JoinRulesContent() => GenericEventRenderer( + JoinRulesContent() => generic( Icons.rule, - maxLines: maxLines, - [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan(text: "changed the room's join rules"), - ], + "changed the room's join rules", ), - - TopicContent() => GenericEventRenderer( + TopicContent() => generic( Icons.description, - maxLines: maxLines, - [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan(text: "updated the room topic"), - ], + "updated the room topic", ), - - HistoryVisibilityContent(:final historyVisibility) => - GenericEventRenderer(Icons.history, maxLines: maxLines, [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan( - text: - "changed the room's history visibility to ${switch (historyVisibility) { - .invited => "since invited", - .joined => "since joined", - .shared => "all history visible (shared)", - .worldReadable => "all history visible (world readable)", - }}", - ), - ]), - - PinnedEventsContent() => GenericEventRenderer( + HistoryVisibilityContent(:final historyVisibility) => generic( + Icons.history, + "changed the room's history visibility to ${switch (historyVisibility) { + .invited => "since invited", + .joined => "since joined", + .shared => "all history visible (shared)", + .worldReadable => "all history visible (world readable)", + }}", + ), + PinnedEventsContent() => generic( Icons.push_pin, - maxLines: maxLines, - [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan(text: "pinned/unpinned some events"), - ], + "pinned/unpinned some events", ), - - ServerACLContent() => GenericEventRenderer( + ServerACLContent() => generic( Icons.list, - maxLines: maxLines, - [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan(text: "updated the server ban list"), - ], + "updated the server ban list", ), + CanonicalAliasContent(:final alias, :final altAliases) => generic( + Icons.numbers, + switch ([ + if (event.previousContent case CanonicalAliasContent( + alias: final prevAlias, + altAliases: final prevAltAliases, + )) ...[ + if (prevAlias != alias) + if (alias == null) + "removed the room's canonical alias" + else + "changed the room's canonical alias to $alias", - CanonicalAliasContent(:final alias, :final altAliases) => - GenericEventRenderer(Icons.numbers, maxLines: maxLines, [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan( - text: switch ([ - if (event.previousContent case CanonicalAliasContent( - alias: final prevAlias, - altAliases: final prevAltAliases, - )) ...[ - if (prevAlias != alias) - if (alias == null) - "removed the room's canonical alias" - else - "changed the room's canonical alias to $alias", - - if (prevAltAliases - .remove(alias ?? "") - .remove(prevAlias ?? "") != - altAliases - .remove(alias ?? "") - .remove(prevAlias ?? "")) - "changed the room's aliases", - ] else ...[ - if (alias != null) "set the room's canonical alias", - if (altAliases.isNotEmpty) "set the room's aliases", - ], - ]) { - [] => "did something related to room aliases", - List prev => prev.join(" and "), - }, - ), - ]), + if (prevAltAliases + .remove(alias ?? "") + .remove(prevAlias ?? "") != + altAliases.remove(alias ?? "").remove(prevAlias ?? "")) + "changed the room's aliases", + ] else ...[ + if (alias != null) "set the room's canonical alias", + if (altAliases.isNotEmpty) "set the room's aliases", + ], + ]) { + [] => "did something related to room aliases", + List prev => prev.join(" and "), + }, + ), _ => null, }; diff --git a/lib/widgets/renderers/generic_event.dart b/lib/widgets/renderers/generic_event.dart index 5ff9dbc..101913c 100644 --- a/lib/widgets/renderers/generic_event.dart +++ b/lib/widgets/renderers/generic_event.dart @@ -1,3 +1,4 @@ +import "package:collection/collection.dart"; import "package:material_ui/material_ui.dart"; class const GenericEventRenderer( @@ -17,12 +18,14 @@ class const GenericEventRenderer( Flexible( child: Text.rich( TextSpan( - children: [ - for (final (index, child) in children.indexed) ...[ - if (index > 0) TextSpan(text: " "), - child, - ], - ], + children: children + .expandIndexed( + (index, child) => [ + if (index > 0) TextSpan(text: " "), + child, + ], + ) + .toList(), ), maxLines: maxLines ?? 999, overflow: .ellipsis, diff --git a/lib/widgets/room_chat/room_chat.dart b/lib/widgets/room_chat/room_chat.dart index a49e6f9..74a0977 100644 --- a/lib/widgets/room_chat/room_chat.dart +++ b/lib/widgets/room_chat/room_chat.dart @@ -271,8 +271,7 @@ final class const RoomChat({ ), ), - if (memberListOpened == true && showMembersByDefault) - MemberList(roomId), + if (memberListOpened && showMembersByDefault) MemberList(roomId), ], ), ), diff --git a/lib/widgets/room_list.dart b/lib/widgets/room_list.dart index b4f07f5..27aa8c1 100644 --- a/lib/widgets/room_list.dart +++ b/lib/widgets/room_list.dart @@ -4,6 +4,7 @@ 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:nexus/helpers/extensions/get_all_child_rooms.dart"; +import "package:nexus/helpers/extensions/room_destinations.dart"; import "package:nexus/models/nav_page.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/divider_widget.dart"; @@ -13,32 +14,11 @@ import "package:nexus/widgets/join_dialog.dart"; import "package:nexus/widgets/room_menu_button.dart"; import "package:nexus/controllers/key.dart"; import "package:nexus/controllers/spaces.dart"; -import "package:nexus/models/room.dart"; import "package:collection/collection.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; // Needed for navigation_rail_m3e (#65). import "package:flutter/material.dart" as old_mat; -List roomDestinations( - IList rooms, { - required bool isDm, -}) => [ - for (final room in rooms) - .new( - label: room.metadata?.name ?? "Unnamed Room", - badgeCount: switch (room.metadata?.unreadNotifications) { - 0 || null => room.metadata?.unreadMessages == 0 ? null : 0, - int unread => unread, - }, - icon: AvatarOrHash( - room.metadata?.avatar, - room.metadata?.name ?? "Unnamed Room", - fallback: isDm ? null : const Icon(Icons.numbers), - ), - ), -]; - class const RoomList({super.key}) extends ConsumerWidget implements NavPage { @override String get title => "Rooms"; @@ -265,13 +245,12 @@ class const RoomsPane({super.key}) extends ConsumerWidget { header: selectedSpace.room == null ? null : const DividerWidget(Text("Rooms")), - destinations: roomDestinations( - selectedSpace.children, + destinations: selectedSpace.children.toDestinations( isDm: isDm, ), ), - for (final subSpace in selectedSpace.subSpaces) - .new( + ...selectedSpace.subSpaces.map( + (subSpace) => NavigationRailM3ESection( header: DividerWidget( Row( mainAxisSize: .min, @@ -293,11 +272,9 @@ class const RoomsPane({super.key}) extends ConsumerWidget { ], ), ), - destinations: roomDestinations( - subSpace.children, - isDm: isDm, - ), + destinations: subSpace.children.toDestinations(isDm: isDm), ), + ), ], onDestinationSelected: (value) { ref diff --git a/lib/widgets/room_menu_button.dart b/lib/widgets/room_menu_button.dart index 7e6516d..a348540 100644 --- a/lib/widgets/room_menu_button.dart +++ b/lib/widgets/room_menu_button.dart @@ -33,7 +33,7 @@ final class const RoomMenuButton( if (room != null) ...[ PopupMenuItem( onTap: () async { - final vias = ref.watch(ViaController.provider(room!)); + final vias = ref.read(ViaController.provider(room!)); await Clipboard.setData( .new( @@ -126,8 +126,10 @@ final class const RoomMenuButton( duration: Duration(days: 1), ), ); - await client.leaveRoom(room!); - snackbar.close(); + await client + .leaveRoom(room!) + .onError(showError) + .whenComplete(snackbar.close); }, child: Text("Leave"), ), diff --git a/lib/widgets/url_preview.dart b/lib/widgets/url_preview.dart index 5eda427..6403427 100644 --- a/lib/widgets/url_preview.dart +++ b/lib/widgets/url_preview.dart @@ -15,7 +15,7 @@ class const UrlPreview(final Uri link, {super.key}) extends ConsumerWidget { data: (preview) => preview == null ? SizedBox.shrink() : InkWell( - onTap: () => ref.watch(LaunchHelper.provider).launchUrl(link), + onTap: () => ref.read(LaunchHelper.provider).launchUrl(link), child: Card( margin: .symmetric(vertical: 4), color: Theme.of(context) diff --git a/lib/widgets/user_bottom_sheet.dart b/lib/widgets/user_bottom_sheet.dart index d7cc55b..b6c35ae 100644 --- a/lib/widgets/user_bottom_sheet.dart +++ b/lib/widgets/user_bottom_sheet.dart @@ -26,6 +26,9 @@ final class const UserBottomSheet( Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final textTheme = theme.textTheme; + final subtitleStyle = textTheme.titleSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ); final client = ref.read(ClientController.provider.notifier); void showMembershipDialog(MembershipAction action) => showDialog( @@ -112,13 +115,7 @@ final class const UserBottomSheet( style: textTheme.headlineLarge, textAlign: .center, ), - SelectableText( - userId, - textAlign: .center, - style: textTheme.titleSmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), + SelectableText(userId, textAlign: .center, style: subtitleStyle), ref .watch(ProfileController.provider(userId)) @@ -151,9 +148,7 @@ final class const UserBottomSheet( Text( pronoun.summary, textAlign: .center, - style: textTheme.titleSmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), + style: subtitleStyle, ), ], ) @@ -172,9 +167,7 @@ final class const UserBottomSheet( Text( profileResponse.profile.timezone!, textAlign: .center, - style: textTheme.titleSmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), + style: subtitleStyle, ), ], ], @@ -212,7 +205,6 @@ final class const UserBottomSheet( Padding( padding: .only(top: 4), child: Row( - mainAxisSize: MainAxisSize.max, spacing: 8, children: [ if (member.status == .join || member.status == .invite)