diff --git a/lib/controllers/events_controller.dart b/lib/controllers/events_controller.dart new file mode 100644 index 0000000..e3f0a9c --- /dev/null +++ b/lib/controllers/events_controller.dart @@ -0,0 +1,30 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/from_controller.dart"; + +class EventsController extends AsyncNotifier { + EventsController(this.room); + final Room room; + + @override + Future build({String? from}) async { + final response = await room.client.getRoomEvents( + room.id, + Direction.b, + from: from, + limit: 32, + ); + ref.watch(FromController.provider(room).notifier).set(response.end); + return response; + } + + Future prev() async { + final resp = await build(from: ref.read(FromController.provider(room))); + return resp; + } + + static final provider = AsyncNotifierProvider.autoDispose + .family( + EventsController.new, + ); +} diff --git a/lib/controllers/from_controller.dart b/lib/controllers/from_controller.dart new file mode 100644 index 0000000..54c850a --- /dev/null +++ b/lib/controllers/from_controller.dart @@ -0,0 +1,15 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:matrix/matrix.dart"; + +class FromController extends Notifier { + FromController(_); + @override + String? build() => null; + + void set(String? value) => state = value; + + static final provider = + NotifierProvider.family( + FromController.new, + ); +} diff --git a/lib/controllers/members_controller.dart b/lib/controllers/members_controller.dart index f362e64..fae5433 100644 --- a/lib/controllers/members_controller.dart +++ b/lib/controllers/members_controller.dart @@ -1,20 +1,22 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:matrix/matrix.dart"; -class MembersController extends AsyncNotifier> { +class MembersController extends AsyncNotifier> { final Room room; MembersController(this.room); @override - Future> build() async => - (await room.client.getMembersByRoom( - room.id, - notMembership: Membership.leave, - )) ?? - []; + Future> build() async => IList( + (await room.client.getMembersByRoom( + room.id, + notMembership: Membership.leave, + )) ?? + [], + ); static final provider = - AsyncNotifierProvider.family, Room>( + AsyncNotifierProvider.family, Room>( MembersController.new, ); } diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index bf0eabe..dab137b 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -3,7 +3,7 @@ import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_core/flutter_chat_core.dart" as chat; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:matrix/matrix.dart"; -import "package:nexus/controllers/timeline_controller.dart"; +import "package:nexus/controllers/events_controller.dart"; import "package:nexus/helpers/extension_helper.dart"; class RoomChatController extends AsyncNotifier { @@ -12,7 +12,7 @@ class RoomChatController extends AsyncNotifier { @override Future build() async { - final timeline = await ref.watch(TimelineController.provider(room).future); + final response = await ref.watch(EventsController.provider(room).future); ref.onDispose( room.client.onTimelineEvent.stream.listen((event) async { @@ -26,8 +26,10 @@ class RoomChatController extends AsyncNotifier { return InMemoryChatController( messages: (await Future.wait( - timeline.events.map((event) => event.toMessage()), - )).toList().reversed.nonNulls.toList(), + response.chunk.map( + (event) => Event.fromMatrixEvent(event, room).toMessage(), + ), + )).nonNulls.toList(), ); } @@ -46,13 +48,22 @@ class RoomChatController extends AsyncNotifier { } Future loadOlder() async { - await ref.watch(TimelineController.provider(room).notifier).prev(); + final controller = await future; + final response = await ref + .watch(EventsController.provider(room).notifier) + .prev(); + await controller.insertAllMessages( + (await Future.wait( + response.chunk.map( + (event) => Event.fromMatrixEvent(event, room).toMessage(), + ), + )).nonNulls.toList().reversed.toList(), + index: 0, + ); } - Future updateMessage(Message message, Message newMessage) async { - final controller = await future; - return controller.updateMessage(message, newMessage); - } + Future updateMessage(Message message, Message newMessage) async => + (await future).updateMessage(message, newMessage); Future send(String message, {Message? replyTo}) async => await room.sendTextEvent( diff --git a/lib/controllers/spaces_controller.dart b/lib/controllers/spaces_controller.dart index 76b0e8c..228f07c 100644 --- a/lib/controllers/spaces_controller.dart +++ b/lib/controllers/spaces_controller.dart @@ -1,13 +1,14 @@ import "package:collection/collection.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client_controller.dart"; import "package:nexus/helpers/extension_helper.dart"; import "package:nexus/models/space.dart"; -class SpacesController extends AsyncNotifier> { +class SpacesController extends AsyncNotifier> { @override - Future> build() async { + Future> build() async { final client = await ref.watch(ClientController.provider.future); final topLevel = await Future.wait( @@ -28,7 +29,7 @@ class SpacesController extends AsyncNotifier> { final topLevelSpaces = topLevel.where((r) => r.roomData.isSpace).toList(); final topLevelRooms = topLevel.where((r) => !r.roomData.isSpace).toList(); - return [ + return IList([ Space( client: client, title: "Home", @@ -66,10 +67,10 @@ class SpacesController extends AsyncNotifier> { ), ), )), - ]; + ]); } - static final provider = AsyncNotifierProvider>( + static final provider = AsyncNotifierProvider>( SpacesController.new, ); } diff --git a/lib/controllers/timeline_controller.dart b/lib/controllers/timeline_controller.dart deleted file mode 100644 index 447482d..0000000 --- a/lib/controllers/timeline_controller.dart +++ /dev/null @@ -1,21 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:matrix/matrix.dart"; - -class TimelineController extends AsyncNotifier { - TimelineController(this.room); - final Room room; - - @override - Future build() => room.getTimeline(); - - Future prev() async { - final timeline = await future; - await timeline.requestHistory(); - state = AsyncValue.data(timeline); - } - - static final provider = - AsyncNotifierProvider.family( - TimelineController.new, - ); -} diff --git a/lib/helpers/extension_helper.dart b/lib/helpers/extension_helper.dart index 95338ec..2ee41f4 100644 --- a/lib/helpers/extension_helper.dart +++ b/lib/helpers/extension_helper.dart @@ -39,7 +39,9 @@ extension ToMessage on Event { final metadata = { "formatted": formattedText.isEmpty ? body : formattedText, "eventType": type, - "displayName": senderFromMemoryOrFallback.displayName, + "displayName": + senderFromMemoryOrFallback.displayName ?? + senderFromMemoryOrFallback.id, "txnId": transactionId, }; diff --git a/lib/main.dart b/lib/main.dart index cfbf23a..6d6bc14 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,6 +29,7 @@ class App extends StatelessWidget { builder: (lightDynamic, darkDynamic) => LayoutBuilder( builder: (context, constraints) { final isDesktop = constraints.maxWidth > 650; + final showMembersByDefault = constraints.maxWidth > 1000; return MaterialApp( debugShowCheckedModeBanner: false, @@ -47,7 +48,12 @@ class App extends StatelessWidget { builder: (context) => Row( children: [ if (isDesktop) Sidebar(), - Expanded(child: RoomChat(isDesktop: isDesktop)), + Expanded( + child: RoomChat( + isDesktop: isDesktop, + showMembersByDefault: showMembersByDefault, + ), + ), ], ), ), diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart index f4ad046..e50ab4f 100644 --- a/lib/widgets/room_chat.dart +++ b/lib/widgets/room_chat.dart @@ -24,7 +24,12 @@ import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart class RoomChat extends HookConsumerWidget { final bool isDesktop; - const RoomChat({required this.isDesktop, super.key}); + final bool showMembersByDefault; + const RoomChat({ + required this.isDesktop, + required this.showMembersByDefault, + super.key, + }); void showContextMenu({ required BuildContext context, @@ -48,8 +53,7 @@ class RoomChat extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final replyToMessage = useState(null); - final memberListOpened = useState(isDesktop); - final urlRegex = RegExp(r"https?://[^\s\]\(\)]+"); + final memberListOpened = useState(showMembersByDefault); final theme = Theme.of(context); return ref .watch(CurrentRoomController.provider) @@ -105,14 +109,13 @@ class RoomChat extends HookConsumerWidget { onTap: () => replyToMessage.value = message, ), builders: Builders( - chatAnimatedListBuilder: (context, itemBuilder) { - return ChatAnimatedList( - itemBuilder: itemBuilder, - onEndReached: ref - .watch(controllerProvider.notifier) - .loadOlder, - ); - }, + chatAnimatedListBuilder: (_, itemBuilder) => + ChatAnimatedList( + itemBuilder: itemBuilder, + onEndReached: ref + .watch(controllerProvider.notifier) + .loadOlder, + ), composerBuilder: (_) => ChatBox( replyToMessage: replyToMessage.value, onDismiss: () => replyToMessage.value = null, @@ -133,14 +136,20 @@ class RoomChat extends HookConsumerWidget { return SizedBox.shrink(); } if (element.localName == "code") { - return SizedBox( - width: 400, - child: CodeField( - name: element.className - .replaceAll("language-", ""), - codes: element.text, - ), - ); + if (element.parent?.localName == + "pre") { + return SizedBox( + width: 400, + child: CodeField( + name: element.className + .replaceAll( + "language-", + "", + ), + codes: element.text, + ), + ); + } } if (element.localName == "img") { final src = Uri.tryParse( @@ -204,11 +213,7 @@ class RoomChat extends HookConsumerWidget { ), linkPreviewBuilder: (_, message, isSentByMe) => LinkPreview( - text: - urlRegex - .firstMatch(message.text) - ?.group(0) ?? - "", + text: message.text, backgroundColor: isSentByMe ? theme.colorScheme.inversePrimary : theme.colorScheme.surfaceContainerLow, @@ -306,11 +311,13 @@ class RoomChat extends HookConsumerWidget { ), ), - if (memberListOpened.value == true && isDesktop) + if (memberListOpened.value == true && showMembersByDefault) MemberList(room.roomData), ], ), - endDrawer: isDesktop ? null : MemberList(room.roomData), + endDrawer: showMembersByDefault + ? null + : MemberList(room.roomData), ); }, ); diff --git a/lib/widgets/top_widget.dart b/lib/widgets/top_widget.dart index 830eaa7..60d3e15 100644 --- a/lib/widgets/top_widget.dart +++ b/lib/widgets/top_widget.dart @@ -1,5 +1,4 @@ import "dart:math"; - import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_ui/flutter_chat_ui.dart"; @@ -29,13 +28,18 @@ class TopWidget extends ConsumerWidget { loading: SizedBox.shrink, data: (replyMessage) { if (replyMessage == null) return SizedBox.shrink(); + + // Black magic to limit reply preview length final replyText = message is TextMessage ? replyMessage.text.substring( 0, min( max( min( - (message as TextMessage).text.length - 20, + max( + (message as TextMessage).text.length - 20, + message.metadata?["displayName"].length, + ), replyMessage.text.length, ), 5,