diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index 15d06c9..fd7600f 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -15,6 +15,7 @@ import "package:nexus/models/event.dart"; import "package:nexus/models/paginate.dart"; import "package:nexus/models/requests/get_event_request.dart"; import "package:nexus/models/requests/get_related_events_request.dart"; +import "package:nexus/models/requests/get_room_state_request.dart"; import "package:nexus/models/requests/login_request.dart"; import "package:nexus/models/profile.dart"; import "package:nexus/models/requests/paginate_request.dart"; @@ -117,7 +118,9 @@ class ClientController extends AsyncNotifier { calloc.free(bufferPointer); - return response.buf.toJson(); + final json = response.buf.toJson(); + if (json is String) throw json; + return json; } Future redactEvent(RedactEventRequest report) => @@ -140,6 +143,12 @@ class ClientController extends AsyncNotifier { await _sendCommand("leave_room", {"room_id": room.metadata!.id}); } + Future> getRoomState(GetRoomStateRequest request) async { + final response = + (await _sendCommand("get_room_state", request.toJson())) as List; + return response.map((event) => Event.fromJson(event)).toIList(); + } + Future?> getRelatedEvents( GetRelatedEventsRequest request, ) async { @@ -157,11 +166,8 @@ class ClientController extends AsyncNotifier { Future paginate(PaginateRequest request) async => Paginate.fromJson(await _sendCommand("paginate", request.toJson())); - Future getProfile(String userId) async { - final json = await _sendCommand("get_profile", {"user_id": userId}); - - return json == null ? null : Profile.fromJson(json); - } + Future getProfile(String userId) async => + Profile.fromJson(await _sendCommand("get_profile", {"user_id": userId})); Future reportEvent(ReportRequest report) => _sendCommand("report_event", report.toJson()); diff --git a/lib/controllers/members_controller.dart b/lib/controllers/members_controller.dart index df15c1c..5f88f2b 100644 --- a/lib/controllers/members_controller.dart +++ b/lib/controllers/members_controller.dart @@ -1,22 +1,26 @@ +import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:matrix/matrix.dart"; +import "package:nexus/models/event.dart"; +import "package:nexus/models/room.dart"; -class MembersController extends AsyncNotifier> { +class MembersController extends AsyncNotifier> { final Room room; MembersController(this.room); @override - Future> build() async => IList( - (await room.client.getMembersByRoom( - room.id, - notMembership: Membership.leave, - )) ?? - [], - ); + Future> build() async => + (room.state["m.room.member"]?.values ?? []) + .map( + (eventRowId) => room.events.firstWhereOrNull( + (event) => event.rowId == eventRowId, + ), + ) + .nonNulls + .toIList(); static final provider = AsyncNotifierProvider.family - .autoDispose, Room>( + .autoDispose, Room>( MembersController.new, ); } diff --git a/lib/controllers/profile_controller.dart b/lib/controllers/profile_controller.dart new file mode 100644 index 0000000..e825593 --- /dev/null +++ b/lib/controllers/profile_controller.dart @@ -0,0 +1,17 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/models/profile.dart"; + +class ProfileController extends AsyncNotifier { + final String userId; + ProfileController(this.userId); + + @override + Future build() => + ref.watch(ClientController.provider.notifier).getProfile(userId); + + static final provider = + AsyncNotifierProvider.family( + ProfileController.new, + ); +} diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index 4490fdd..1428df1 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -6,14 +6,17 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:fluttertagger/fluttertagger.dart" as tagger; import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/new_events_controller.dart"; +import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/controllers/selected_room_controller.dart"; import "package:nexus/helpers/extensions/event_to_message.dart"; import "package:nexus/helpers/extensions/list_to_messages.dart"; +import "package:nexus/models/event.dart"; +import "package:nexus/models/requests/get_room_state_request.dart"; +import "package:nexus/models/requests/paginate_request.dart"; import "package:nexus/models/requests/redact_event_request.dart"; import "package:nexus/models/relation_type.dart"; import "package:nexus/models/requests/send_message_request.dart"; import "package:nexus/models/room.dart"; -import "package:nexus/models/sync_data.dart"; class RoomChatController extends AsyncNotifier { final String roomId; @@ -22,9 +25,8 @@ class RoomChatController extends AsyncNotifier { @override Future build() async { final client = ref.watch(ClientController.provider.notifier); - final events = - ref.read(SelectedRoomController.provider)?.events ?? - const IList.empty(); + final room = ref.read(SelectedRoomController.provider); + if (room == null) return InMemoryChatController(); ref.onDispose( ref.listen(NewEventsController.provider(roomId), (_, next) async { @@ -38,7 +40,7 @@ class RoomChatController extends AsyncNotifier { await controller.removeMessage(message); } else { - final message = await event.toMessage(client, includeEdits: true); + final message = await event.toMessage(ref, includeEdits: true); if (event.relationType == "m.replace") { final controller = await future; final oldMessage = controller.messages.firstWhereOrNull( @@ -69,11 +71,26 @@ class RoomChatController extends AsyncNotifier { }).close, ); - final messages = await events.toMessages(client); + final messages = await room.timeline + .map( + (timelineRowTuple) => room.events.firstWhereOrNull( + (event) => event.rowId == timelineRowTuple.eventRowId, + ), + ) + .nonNulls + .toMessages(ref); final controller = InMemoryChatController(messages: messages); + ref.onDispose(controller.dispose); if (messages.length < 20) await loadOlder(controller); + await client.getRoomState( + GetRoomStateRequest( + roomId: roomId, + fetchMembers: room.metadata?.hasMemberList == false, + ), + ); + return controller; } @@ -109,23 +126,47 @@ class RoomChatController extends AsyncNotifier { final controller = chatController ?? await future; final client = ref.watch(ClientController.provider.notifier); - client. - // await ref.watchInMemoryChatController? chatController(EventsController.provider(room).notifier).prev(); - // final timeline = await ref.watch(EventsController.provider(room).future); + final response = await client.paginate( + PaginateRequest( + roomId: roomId, + maxTimelineId: controller.messages.firstOrNull?.metadata?["timelineId"], + ), + ); - // final controller = await future; - // await controller.insertAllMessages( - // await timeline.events - // .where( - // (event) => !currentEvents.messages.any( - // (existingEvent) => existingEvent.id == event.eventId, - // ), - // ) - // .toList() - // .toMessages(room, timeline), - // index: 0, - // ); - // ref.notifyListeners(); + ref + .watch(RoomsController.provider.notifier) + .update( + IMap({ + roomId: Room( + events: response.events.addAll(response.relatedEvents), + hasMore: response.hasMore, + timeline: response.events + .map( + (event) => TimelineRowTuple( + timelineRowId: event.timelineRowId, + eventRowId: event.rowId, + ), + ) + .toIList(), + ), + }), + const ISet.empty(), + ); + + final existingIds = controller.messages.map((m) => m.id).toSet(); + + final messages = await response.events + .where((event) => !existingIds.contains(event.eventId)) + .fold({}, (acc, event) { + acc[event.eventId] = + event; // overwrites duplicates in response.events + return acc; + }) + .values + .toIList() + .reversed + .toMessages(ref); + await controller.insertAllMessages(messages, index: 0); } Future updateMessage(Message message, Message newMessage) async => @@ -167,7 +208,7 @@ class RoomChatController extends AsyncNotifier { .getProfile(id); return chat.User( id: id, - name: user?.displayName, + name: user.displayName, // imageSource: user.avatarUrl == null // ? null // : (await ref.watch( diff --git a/lib/controllers/rooms_controller.dart b/lib/controllers/rooms_controller.dart index bbeb402..bc109c9 100644 --- a/lib/controllers/rooms_controller.dart +++ b/lib/controllers/rooms_controller.dart @@ -1,3 +1,4 @@ +import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/new_events_controller.dart"; @@ -28,16 +29,22 @@ class RoomsController extends Notifier> { ), state: incoming.state.entries.fold( existing.state, - (stateAcc, event) => stateAcc.add( + (previousValue, event) => previousValue.add( event.key, - (stateAcc[event.key] ?? IMap()).addAll( + (previousValue[event.key] ?? const IMap.empty()).addAll( event.value, ), ), ), - timeline: incoming.reset - ? incoming.timeline - : existing.timeline.addAll(incoming.timeline), + timeline: + (incoming.reset + ? incoming.timeline + : existing.timeline.updateById( + incoming.timeline, + (item) => item.timelineRowId, + )) + .sortedBy((element) => element.timelineRowId) + .toIList(), receipts: incoming.receipts.entries.fold( existing.receipts, (receiptAcc, event) => receiptAcc.add( diff --git a/lib/helpers/extensions/event_to_message.dart b/lib/helpers/extensions/event_to_message.dart index 699b9f3..231774b 100644 --- a/lib/helpers/extensions/event_to_message.dart +++ b/lib/helpers/extensions/event_to_message.dart @@ -1,16 +1,19 @@ import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/controllers/profile_controller.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/requests/get_event_request.dart"; import "package:nexus/models/requests/get_related_events_request.dart"; extension EventToMessage on Event { Future toMessage( - ClientController client, { + Ref ref, { bool mustBeText = false, bool includeEdits = false, }) async { if (relationType == "m.replace" && !includeEdits) return null; + final client = ref.watch(ClientController.provider.notifier); final newEvents = await client.getRelatedEvents( GetRelatedEventsRequest( @@ -28,22 +31,25 @@ extension EventToMessage on Event { GetEventRequest(roomId: roomId, eventId: replyId), ); - final author = await client.getProfile(event.authorId); + final author = await ref.watch( + ProfileController.provider(event.authorId).future, + ); final content = (decrypted ?? this.content); final type = (decryptedType ?? this.type); final newContent = content["m.new_content"] as Map?; final metadata = { + "timelineId": event.timelineRowId, "formatted": newContent?["formatted_body"] ?? newContent?["body"] ?? content["formatted_body"] ?? content["body"] ?? "", - "reply": await replyEvent?.toMessage(client, mustBeText: true), + "reply": await replyEvent?.toMessage(ref, mustBeText: true), "body": newContent?["body"] ?? content["body"], "eventType": type, - "avatarUrl": author?.avatarUrl, - "displayName": author?.displayName ?? authorId, + "avatarUrl": author.avatarUrl, + "displayName": author.displayName ?? authorId, "txnId": transactionId, }; diff --git a/lib/helpers/extensions/list_to_messages.dart b/lib/helpers/extensions/list_to_messages.dart index 725f9c3..78648ba 100644 --- a/lib/helpers/extensions/list_to_messages.dart +++ b/lib/helpers/extensions/list_to_messages.dart @@ -1,12 +1,10 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; -import "package:nexus/controllers/client_controller.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/helpers/extensions/event_to_message.dart"; import "package:nexus/models/event.dart"; -extension ListToMessages on IList { - Future> toMessages(ClientController client) async => - (await Future.wait( - map((event) => event.toMessage(client)), - )).nonNulls.toList(); +extension ListToMessages on Iterable { + Future> toMessages(Ref ref) async => (await Future.wait( + map((event) => event.toMessage(ref)), + )).nonNulls.toList(); } diff --git a/lib/models/paginate.dart b/lib/models/paginate.dart index 4faf4e9..df0a0f6 100644 --- a/lib/models/paginate.dart +++ b/lib/models/paginate.dart @@ -8,6 +8,7 @@ part "paginate.g.dart"; abstract class Paginate with _$Paginate { const factory Paginate({ required IList events, + required IList relatedEvents, required bool hasMore, }) = _Paginate; diff --git a/lib/models/requests/get_room_state_request.dart b/lib/models/requests/get_room_state_request.dart new file mode 100644 index 0000000..a154d5f --- /dev/null +++ b/lib/models/requests/get_room_state_request.dart @@ -0,0 +1,15 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "get_room_state_request.freezed.dart"; +part "get_room_state_request.g.dart"; + +@freezed +abstract class GetRoomStateRequest with _$GetRoomStateRequest { + const factory GetRoomStateRequest({ + required String roomId, + required bool fetchMembers, + @Default(false) bool includeMembers, + }) = _GetRoomStateRequest; + + factory GetRoomStateRequest.fromJson(Map json) => + _$GetRoomStateRequestFromJson(json); +} diff --git a/lib/models/requests/paginate_request.dart b/lib/models/requests/paginate_request.dart index 5941499..44cf8ec 100644 --- a/lib/models/requests/paginate_request.dart +++ b/lib/models/requests/paginate_request.dart @@ -6,6 +6,7 @@ part "paginate_request.g.dart"; abstract class PaginateRequest with _$PaginateRequest { const factory PaginateRequest({ required String roomId, + required int? maxTimelineId, @Default(20) int limit, }) = _PaginateRequest; diff --git a/lib/models/requests/send_message_request.dart b/lib/models/requests/send_message_request.dart index 6825d85..883c585 100644 --- a/lib/models/requests/send_message_request.dart +++ b/lib/models/requests/send_message_request.dart @@ -9,8 +9,8 @@ abstract class SendMessageRequest with _$SendMessageRequest { const factory SendMessageRequest({ required String roomId, required String text, - @Default(Mentions()) @JsonKey(name: "m.mentions") Mentions mentions, - @JsonKey(name: "m.relates_to") Relation? relation, + @Default(Mentions()) @JsonKey(name: "mentions") Mentions mentions, + @JsonKey(name: "relates_to") Relation? relation, }) = _SendMessageRequest; factory SendMessageRequest.fromJson(Map json) => @@ -28,17 +28,16 @@ abstract class Mentions with _$Mentions { _$MentionsFromJson(json); } -@freezed +@Freezed(toJson: false) abstract class Relation with _$Relation { - const Relation._(); // required for custom methods + const Relation._(); const factory Relation({ required String eventId, required RelationType relationType, }) = _Relation; - @override - Map toJson() { + Map toJson() { switch (relationType) { case RelationType.reply: return { @@ -50,6 +49,6 @@ abstract class Relation with _$Relation { } } - factory Relation.fromJson(Map json) => + factory Relation.fromJson(Map json) => _$RelationFromJson(json); } diff --git a/lib/models/room.dart b/lib/models/room.dart index 91ab952..b7ff81d 100644 --- a/lib/models/room.dart +++ b/lib/models/room.dart @@ -11,12 +11,13 @@ abstract class Room with _$Room { const factory Room({ @JsonKey(name: "meta") RoomMetadata? metadata, @Default(IList.empty()) IList timeline, - required bool reset, - required IMap state, + @Default(false) bool reset, + @Default(IMap.empty()) IMap> state, // required IMap accountData, - required IList events, + @Default(IList.empty()) IList events, @Default(IMap.empty()) IMap> receipts, - required bool dismissNotifications, + @Default(false) bool dismissNotifications, + @Default(true) bool hasMore, // required IList notifications, }) = _Room; diff --git a/lib/widgets/chat_page/member_list.dart b/lib/widgets/chat_page/member_list.dart index 8130de8..2d8f54f 100644 --- a/lib/widgets/chat_page/member_list.dart +++ b/lib/widgets/chat_page/member_list.dart @@ -1,11 +1,8 @@ import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:matrix/matrix.dart"; -import "package:nexus/controllers/avatar_controller.dart"; import "package:nexus/controllers/members_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/helpers/extensions/get_headers.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/models/room.dart"; class MemberList extends ConsumerWidget { final Room room; @@ -36,26 +33,30 @@ class MemberList extends ConsumerWidget { .where( (membership) => membership.content["membership"] == - Membership.join.name, + "join", // TODO: Show invites seperately ) .map( (member) => ListTile( onTap: () {}, - leading: AvatarOrHash( - ref - .watch( - AvatarController.provider( - member.content["avatar_url"].toString(), - ), - ) - .whenOrNull(data: (data) => data), - member.content["displayname"].toString(), - headers: room.client.headers, - ), + // leading: AvatarOrHash( TODO + // ref + // .watch( + // AvatarController.provider( + // member.content["avatar_url"].toString(), + // ), + // ) + // .whenOrNull(data: (data) => data), + // member.content["displayname"].toString(), + // headers: room.client.headers, + // ), title: Text( member.content["displayname"].toString(), overflow: TextOverflow.ellipsis, ), + subtitle: Text( + member.authorId, + overflow: TextOverflow.ellipsis, + ), ), ), ], diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index 4214e89..d8d99c4 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -21,12 +21,12 @@ import "package:nexus/models/relation_type.dart"; import "package:nexus/models/requests/report_request.dart"; import "package:nexus/widgets/chat_page/chat_box.dart"; import "package:nexus/widgets/chat_page/html/html.dart"; +import "package:nexus/widgets/chat_page/member_list.dart"; import "package:nexus/widgets/chat_page/room_appbar.dart"; import "package:nexus/widgets/chat_page/top_widget.dart"; import "package:nexus/widgets/form_text_input.dart"; import "package:nexus/widgets/loading.dart"; // import "package:dynamic_polls/dynamic_polls.dart"; -// import "package:matrix/matrix.dart"; class RoomChat extends HookConsumerWidget { final bool isDesktop; @@ -534,12 +534,12 @@ class RoomChat extends HookConsumerWidget { ), ), - // if (memberListOpened.value == true && showMembersByDefault) TODO: Member list - // MemberList(room), + if (memberListOpened.value == true && showMembersByDefault) + MemberList(room), ], ), - // endDrawer: showMembersByDefault ? null : MemberList(room), + endDrawer: showMembersByDefault ? null : MemberList(room), ); } } diff --git a/lib/widgets/chat_page/sidebar.dart b/lib/widgets/chat_page/sidebar.dart index a8157cf..a883228 100644 --- a/lib/widgets/chat_page/sidebar.dart +++ b/lib/widgets/chat_page/sidebar.dart @@ -186,7 +186,7 @@ class Sidebar extends HookConsumerWidget { ? null : Icon(selectedSpace.icon), - selectedSpace.title, // TODO RM + selectedSpace.title, headers: {}, // space.client.headers, TODO ),