import "dart:async"; import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; 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:fluttertagger/fluttertagger.dart"; import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/message_controller.dart"; import "package:nexus/controllers/messages_controller.dart"; import "package:nexus/controllers/new_events_controller.dart"; import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/models/message_config.dart"; import "package:nexus/models/messages_config.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"; class RoomChatController extends AsyncNotifier { final String roomId; RoomChatController(this.roomId); @override Future build() async { final client = ref.watch(ClientController.provider.notifier); var room = ref.read(RoomsController.provider)[roomId]; if (room == null) return InMemoryChatController(); final state = await client.getRoomState( GetRoomStateRequest( roomId: roomId, fetchMembers: room.metadata?.hasMemberList == false, includeMembers: true, ), ); ref .read(RoomsController.provider.notifier) .update( { roomId: Room( events: state, state: state.fold( const IMap.empty(), (previousValue, stateEvent) => previousValue.add( stateEvent.type, (previousValue[stateEvent.type] ?? const IMap.empty()).addAll( IMap({ if (stateEvent.stateKey != null) stateEvent.stateKey!: stateEvent.rowId, }), ), ), ), ), }.toIMap(), const ISet.empty(), ); room = ref.read(RoomsController.provider)[roomId]; if (room == null) return InMemoryChatController(); final messages = await ref.watch( MessagesController.provider( MessagesConfig( room: room, events: room.timeline .map( (timelineRowTuple) => room!.events.firstWhereOrNull( (event) => event.rowId == timelineRowTuple.eventRowId, ), ) .nonNulls .toIList(), ), ).future, ); final controller = InMemoryChatController(messages: messages.toList()); ref.onDispose( ref.listen(NewEventsController.provider(roomId), (_, next) async { final controller = await future; for (final event in next) { if (event.type == "m.room.redaction") { final controller = await future; final message = controller.messages.firstWhereOrNull( (message) => message.id == event.content["redacts"], ); if (message == null || !ref.mounted) return; await controller.removeMessage(message); } else { final message = await ref.watch( MessageController.provider( MessageConfig(event: event, room: room!, includeEdits: true), ).future, ); if (event.relationType == "m.replace") { final controller = await future; final oldMessage = controller.messages.firstWhereOrNull( (element) => element.id == event.relatesTo, ); if (oldMessage == null || message == null || !ref.mounted) return; return await controller.updateMessage( oldMessage, message.copyWith( id: oldMessage.id, replyToMessageId: oldMessage.replyToMessageId, metadata: { ...(oldMessage.metadata ?? {}), ...(message.metadata ?? {}) .toIMap() .where((key, value) => value != null) .unlock, }, ), ); } if (message != null && !controller.messages.any( (oldMessage) => oldMessage.id == message.id, ) && ref.mounted) { await controller.insertMessage(message); } } } }, weak: true).close, ); ref.onDispose(controller.dispose); // While there are under 20 messages, try up to two times to load more messages. for (var i = 0; i < 2 && messages.length < 20; i++) { await loadOlder(controller); } return controller; } Future insertMessage(Message message) async { final controller = await future; final oldMessage = message.metadata?["txnId"] == null ? null : controller.messages.firstWhereOrNull( (element) => element.metadata?["txnId"] == message.metadata?["txnId"], ); return oldMessage == null ? controller.insertMessage(message) : controller.updateMessage(oldMessage, message); } Future deleteMessage(Message message, {String? reason}) async { final controller = await future; await controller.removeMessage(message); await ref .watch(ClientController.provider.notifier) .redactEvent( RedactEventRequest( eventId: message.id, roomId: roomId, reason: reason, ), ); } Future loadOlder([InMemoryChatController? chatController]) async { final response = await ref .watch(ClientController.provider.notifier) .paginate( PaginateRequest( roomId: roomId, maxTimelineId: ref .read(RoomsController.provider)[roomId] ?.timeline .firstOrNull ?.timelineRowId, ), ); 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 room = ref.read(RoomsController.provider)[roomId]; if (room == null) return; final messages = await ref.watch( MessagesController.provider( MessagesConfig(room: room, events: response.events.reversed), ).future, ); final controller = chatController ?? await future; await controller.insertAllMessages( messages .where( (newMessage) => !controller.messages.any( (message) => message.id == newMessage.id, ), ) .toList(), index: 0, ); } Future send( String message, { bool shouldMention = true, required Iterable tags, required RelationType relationType, Message? relation, }) async { var taggedMessage = message; for (final tag in tags) { final escaped = RegExp.escape(tag.id); final pattern = RegExp(r"@+(" + escaped + r")(#[^#]*#)?"); taggedMessage = taggedMessage.replaceAllMapped( pattern, (match) => match.group(1)!, ); } final client = ref.watch(ClientController.provider.notifier); client.sendMessage( SendMessageRequest( roomId: roomId, mentions: Mentions( userIds: [ if (shouldMention == true && relation != null && relationType == RelationType.reply) relation.authorId, ].toIList(), room: taggedMessage.contains("@room"), ), text: taggedMessage, relation: relation == null ? null : Relation(eventId: relation.id, relationType: relationType), ), ); } Future resolveUser(String id) async { final user = await ref .watch(ClientController.provider.notifier) .getProfile(id); return chat.User( id: id, name: user.displayName, // imageSource: user.avatarUrl == null // ? null // : (await ref.watch( // AvatarController.provider(user.avatarUrl!.toString()).future, // )).toString(), ); } Future scrollToMessage(Message message) async { final controller = await future; Future setFlashing(bool flashing) => controller.updateMessage( message, message.copyWith( metadata: {...(message.metadata ?? {}), "flashing": flashing}, ), ); await setFlashing(true); Timer(Duration(seconds: 1), () => setFlashing(false)); return await controller.scrollToMessage(message.id); } static final provider = AsyncNotifierProvider.family .autoDispose( RoomChatController.new, ); }