// Version: 1.0.1 | Created: 2026-04-01 // Chat repository. Bridges Matrix SDK timeline to app domain models. // Uses room.getTimeline() — timeline is async in matrix 0.33.0. import 'package:matrix/matrix.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../../core/network/matrix_client.dart'; import '../domain/message_model.dart'; part 'chat_repository.g.dart'; @riverpod ChatRepository chatRepository(Ref ref) { return ChatRepository(client: ref.watch(matrixClientProvider)); } class ChatRepository { ChatRepository({required Client client}) : _client = client; final Client _client; Room? _getRoom(String roomId) => _client.getRoomById(roomId); /// Returns a stream of message lists for [roomId]. /// /// Opens the room's timeline once and then emits on every update. /// The timeline object is closed when the stream subscription is cancelled. Stream> watchTimeline(String roomId) async* { final room = _getRoom(roomId); if (room == null) return; final timeline = await room.getTimeline( onUpdate: () { // Handled by the stream controller below. }, ); // Emit the initial state. yield _mapTimeline(timeline, room); // Emit on subsequent sync events that affect this room. await for (final update in _client.onSync.stream) { final updatesThisRoom = update.rooms?.join?.containsKey(roomId) ?? false; if (updatesThisRoom) { yield _mapTimeline(timeline, room); } } // Clean up timeline subscriptions when the stream is cancelled. timeline.cancelSubscriptions(); } /// Sends a plain text message to [roomId]. Future sendTextMessage(String roomId, String text) async { final room = _getRoom(roomId); if (room == null) return; await room.sendTextEvent(text); } /// Sends a read receipt for the latest event in [roomId]. Future markAsRead(String roomId) async { final room = _getRoom(roomId); if (room == null) return; final lastEventId = room.lastEvent?.eventId ?? ''; if (lastEventId.isEmpty) return; await room.setReadMarker(lastEventId, mRead: lastEventId); } /// Requests older messages be loaded (pagination). Future loadMoreMessages(String roomId) async { final room = _getRoom(roomId); if (room == null) return; await room.requestHistory(); } List _mapTimeline(Timeline timeline, Room room) { final myUserId = _client.userID ?? ''; return timeline.events .map((e) => _toModel(e, timeline, myUserId)) .toList() .reversed .toList(); } MessageModel _toModel(Event event, Timeline timeline, String myUserId) { final senderProfile = event.room.unsafeGetUserFromMemoryOrFallback( event.senderId, ); return MessageModel( eventId: event.eventId, roomId: event.roomId ?? '', senderId: event.senderId, senderDisplayName: senderProfile.displayName ?? event.senderId.split(':').first, senderAvatarUrl: senderProfile.avatarUrl?.toString(), timestamp: event.originServerTs, type: _messageType(event), body: event.redacted ? null : event.body, mxcUrl: _extractMxcUrl(event), inReplyToEventId: event.relationshipEventId, isMine: event.senderId == myUserId, isEdited: event.hasAggregatedEvents(timeline, RelationshipTypes.edit), ); } MessageType _messageType(Event event) { if (event.redacted) return MessageType.redacted; return switch (event.messageType) { MessageTypes.Text => MessageType.text, MessageTypes.Image => MessageType.image, MessageTypes.File => MessageType.file, MessageTypes.Audio => MessageType.audio, MessageTypes.Video => MessageType.video, MessageTypes.Sticker => MessageType.sticker, _ => MessageType.unsupported, }; } String? _extractMxcUrl(Event event) { final content = event.content; if (content.containsKey('url')) { return content['url'] as String?; } return null; } }