import "dart:async"; import "dart:math"; import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:fluttertagger/fluttertagger.dart"; import "package:nexus/controllers/attachment.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/rooms.dart"; import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/reaction.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/requests/redact_event.dart"; import "package:nexus/models/relation_type.dart"; import "package:nexus/models/requests/send_message.dart"; import "package:nexus/models/room.dart"; class RoomChatController extends AsyncNotifier?> { final String roomId; Future? _loadingOlder; RoomChatController(this.roomId); @override Future?> build() async { final client = ref.watch(ClientController.provider.notifier); final initialRoom = ref.watch( RoomsController.provider.select((rooms) => rooms[roomId]), ); if (initialRoom == null) return null; Room room = initialRoom; if (!room.hasFetchedState) { final state = await client.getRoomState(.new(roomId: roomId)); await ref.read(RoomsController.provider.notifier).addState(roomId, state); room = ref.read(RoomsController.provider)[roomId] ?? room; } // Load one more page when the initial timeline is short. if (room.hasMore && room.timeline.length < 20) { await loadOlder(); room = ref.read(RoomsController.provider)[roomId] ?? room; } return room.timeline .toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0)) .map((element) => element.value) .toIList() .addAll(room.sticky) .map((entry) { final foundEvent = entry == null ? null : room.events[entry]; final editedEvent = foundEvent == null || foundEvent.lastEditRowId == 0 ? null : room.events[foundEvent.lastEditRowId]; return editedEvent == null ? foundEvent : foundEvent?.copyWith( content: editedEvent.content, localContent: editedEvent.localContent, ); }) .nonNulls .toIList(); } Future deleteMessage(Event event, {String? reason}) => ref .watch(ClientController.provider.notifier) .redactEvent( RedactEventRequest( eventId: event.eventId, roomId: roomId, reason: reason, ), ); Future loadOlder() => _loadingOlder ??= _loadOlder().whenComplete(() { _loadingOlder = null; }); Future _loadOlder() async { final room = ref.read(RoomsController.provider)[roomId]; if (room == null || !room.hasMore) return false; final timelineKeys = room.timeline.keys; final response = await ref .watch(ClientController.provider.notifier) .paginate( .new( roomId: roomId, maxTimelineId: timelineKeys.isNotEmpty ? timelineKeys.reduce(min) : null, ), ); ref .watch(RoomsController.provider.notifier) .update( IMap({ roomId: Room( events: IMap.fromIterable( response.events.addAll(response.relatedEvents), keyMapper: (event) => event.rowId, valueMapper: (event) => event, ), hasMore: response.hasMore, timeline: IMap.fromIterable( response.events, keyMapper: (event) => event.timelineRowId, valueMapper: (event) => event.rowId, ), ), }), .new(), ); return response.hasMore; } Future send( String text, { bool shouldMention = true, required IList tags, required RelationType relationType, Event? relation, }) async { Content? baseContent; if (relationType == .edit) { baseContent = relation?.content; } else { final provider = AttachmentController.provider(roomId); baseContent = ref.watch(provider)?.$2; ref.invalidate(provider); } var taggedMessage = text; 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); final event = await client.sendMessage( SendMessageRequest( roomId: roomId, baseContent: baseContent, mentions: Mentions( userIds: [ if (shouldMention == true && relation != null && relationType == RelationType.reply) relation.sender, ].toIList(), room: taggedMessage.contains("@room"), ), text: taggedMessage, relation: relation == null ? null : .new(eventId: relation.eventId, relationType: relationType), ), ); ref .watch(RoomsController.provider.notifier) .update( .new({ roomId: .new( events: .new({event.rowId: event}), sticky: .new({event.rowId}), ), }), .new(), ); } Future removeReaction( String reaction, Event event, String userId, ) async { final client = ref.watch(ClientController.provider.notifier); final allReactionEvents = await client.getRelatedEvents( .new( roomId: roomId, eventId: event.eventId, relationType: "m.annotation", ), ); 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, }, ); if (reactionEvent != null) { await ref .watch(ClientController.provider.notifier) .redactEvent(.new(eventId: reactionEvent.eventId, roomId: roomId)); } } Future sendReaction(String reaction, Event event) async { final client = ref.watch(ClientController.provider.notifier); await client.sendEvent( .new( roomId: roomId, type: EventType.reaction.type, content: ReactionContent(key: reaction), synchronous: true, disableEncryption: true, relatesTo: event.eventId, relationType: "m.annotation", ), ); } static final provider = AsyncNotifierProvider.family .autoDispose?, String>( RoomChatController.new, ); }