/* * Famedly Matrix SDK * Copyright (C) 2019, 2020 Famedly GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import 'dart:async'; import 'package:famedlysdk/matrix_api.dart'; import 'event.dart'; import 'room.dart'; import 'utils/event_update.dart'; import 'utils/room_update.dart'; typedef onTimelineUpdateCallback = void Function(); typedef onTimelineInsertCallback = void Function(int insertID); /// Represents the timeline of a room. The callbacks [onUpdate], [onDelete], /// [onInsert] and [onResort] will be triggered automatically. The initial /// event list will be retreived when created by the [room.getTimeline] method. class Timeline { final Room room; List events = []; final onTimelineUpdateCallback onUpdate; final onTimelineInsertCallback onInsert; StreamSubscription sub; StreamSubscription roomSub; StreamSubscription sessionIdReceivedSub; bool _requestingHistoryLock = false; final Map _eventCache = {}; /// Searches for the event in this timeline. If not /// found, requests from the server. Requested events /// are cached. Future getEventById(String id) async { for (var i = 0; i < events.length; i++) { if (events[i].eventId == id) return events[i]; } if (_eventCache.containsKey(id)) return _eventCache[id]; final requestedEvent = await room.getEventById(id); if (requestedEvent == null) return null; _eventCache[id] = requestedEvent; return _eventCache[id]; } Future requestHistory( {int historyCount = Room.DefaultHistoryCount}) async { if (!_requestingHistoryLock) { _requestingHistoryLock = true; await room.requestHistory( historyCount: historyCount, onHistoryReceived: () { if (room.prev_batch.isEmpty || room.prev_batch == null) events = []; }, ); await Future.delayed(const Duration(seconds: 2)); _requestingHistoryLock = false; } } Timeline({this.room, this.events, this.onUpdate, this.onInsert}) { sub ??= room.client.onEvent.stream.listen(_handleEventUpdate); // if the timeline is limited we want to clear our events cache // as r.limitedTimeline can be "null" sometimes, we need to check for == true // as after receiving a limited timeline room update new events are expected // to be received via the onEvent stream, it is unneeded to call sortAndUpdate roomSub ??= room.client.onRoomUpdate.stream .where((r) => r.id == room.id && r.limitedTimeline == true) .listen((r) => events.clear()); sessionIdReceivedSub ??= room.onSessionKeyReceived.stream.listen(_sessionKeyReceived); } /// Don't forget to call this before you dismiss this object! void cancelSubscriptions() { sub?.cancel(); roomSub?.cancel(); sessionIdReceivedSub?.cancel(); } void _sessionKeyReceived(String sessionId) async { var decryptAtLeastOneEvent = false; final decryptFn = () async { if (!room.client.encryptionEnabled) { return; } for (var i = 0; i < events.length; i++) { if (events[i].type == EventTypes.Encrypted && events[i].messageType == MessageTypes.BadEncrypted && events[i].content['can_request_session'] == true && events[i].content['session_id'] == sessionId) { events[i] = await room.client.encryption .decryptRoomEvent(room.id, events[i], store: true); if (events[i].type != EventTypes.Encrypted) { decryptAtLeastOneEvent = true; } } } }; if (room.client.database != null) { await room.client.database.transaction(decryptFn); } else { await decryptFn(); } if (decryptAtLeastOneEvent) onUpdate(); } int _findEvent({String event_id, String unsigned_txid}) { int i; for (i = 0; i < events.length; i++) { if (events[i].eventId == event_id || (unsigned_txid != null && events[i].eventId == unsigned_txid)) break; } return i; } void _handleEventUpdate(EventUpdate eventUpdate) async { try { if (eventUpdate.roomID != room.id) return; if (eventUpdate.type == 'timeline' || eventUpdate.type == 'history') { // Redaction events are handled as modification for existing events. if (eventUpdate.eventType == EventTypes.Redaction) { final eventId = _findEvent(event_id: eventUpdate.content['redacts']); if (eventId != null) { events[eventId].setRedactionEvent(Event.fromJson( eventUpdate.content, room, eventUpdate.sortOrder)); } } else if (eventUpdate.content['status'] == -2) { var i = _findEvent(event_id: eventUpdate.content['event_id']); if (i < events.length) events.removeAt(i); } // Is this event already in the timeline? else if (eventUpdate.content.containsKey('unsigned') && eventUpdate.content['unsigned']['transaction_id'] is String) { var i = _findEvent( event_id: eventUpdate.content['event_id'], unsigned_txid: eventUpdate.content.containsKey('unsigned') ? eventUpdate.content['unsigned']['transaction_id'] : null); if (i < events.length) { events[i] = Event.fromJson( eventUpdate.content, room, eventUpdate.sortOrder); } } else { Event newEvent; var senderUser = room .getState( EventTypes.RoomMember, eventUpdate.content['sender']) ?.asUser ?? await room.client.database?.getUser( room.client.id, eventUpdate.content['sender'], room); if (senderUser != null) { eventUpdate.content['displayname'] = senderUser.displayName; eventUpdate.content['avatar_url'] = senderUser.avatarUrl.toString(); } newEvent = Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder); if (eventUpdate.type == 'history' && events.indexWhere( (e) => e.eventId == eventUpdate.content['event_id']) != -1) return; events.insert(0, newEvent); if (onInsert != null) onInsert(0); } } sortAndUpdate(); } catch (e) { if (room.client.debug) { print('[WARNING] (_handleEventUpdate) ${e.toString()}'); } } } bool sortLock = false; void sort() { if (sortLock || events.length < 2) return; sortLock = true; events?.sort((a, b) => b.sortOrder - a.sortOrder > 0 ? 1 : -1); sortLock = false; } void sortAndUpdate() async { sort(); if (onUpdate != null) onUpdate(); } }