2019-06-21 10:18:54 +00:00
|
|
|
/*
|
|
|
|
* Copyright (c) 2019 Zender & Kurtz GbR.
|
|
|
|
*
|
|
|
|
* Authors:
|
|
|
|
* Christian Pauly <krille@famedly.com>
|
|
|
|
* Marcel Radzio <mtrnord@famedly.com>
|
|
|
|
*
|
|
|
|
* This file is part of famedlysdk.
|
|
|
|
*
|
|
|
|
* famedlysdk is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* famedlysdk 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 General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with famedlysdk. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import 'dart:async';
|
2019-07-12 09:26:07 +00:00
|
|
|
|
Update lib/src/client.dart, lib/src/user.dart, lib/src/timeline.dart, lib/src/room.dart, lib/src/presence.dart, lib/src/event.dart, lib/src/utils/profile.dart, lib/src/utils/receipt.dart, test/client_test.dart, test/event_test.dart, test/presence_test.dart, test/room_test.dart, test/timeline_test.dart, test/user_test.dart files
2020-01-04 17:56:17 +00:00
|
|
|
import 'event.dart';
|
|
|
|
import 'room.dart';
|
|
|
|
import 'sync/event_update.dart';
|
2020-05-21 14:52:14 +00:00
|
|
|
import 'sync/room_update.dart';
|
2019-06-21 10:18:54 +00:00
|
|
|
|
2019-09-02 08:33:32 +00:00
|
|
|
typedef onTimelineUpdateCallback = void Function();
|
|
|
|
typedef onTimelineInsertCallback = void Function(int insertID);
|
|
|
|
|
2019-06-21 10:18:54 +00:00
|
|
|
/// 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<Event> events = [];
|
|
|
|
|
2019-06-25 10:06:26 +00:00
|
|
|
final onTimelineUpdateCallback onUpdate;
|
|
|
|
final onTimelineInsertCallback onInsert;
|
2019-06-21 10:18:54 +00:00
|
|
|
|
|
|
|
StreamSubscription<EventUpdate> sub;
|
2020-05-21 14:52:14 +00:00
|
|
|
StreamSubscription<RoomUpdate> roomSub;
|
2020-02-21 15:05:19 +00:00
|
|
|
StreamSubscription<String> sessionIdReceivedSub;
|
2019-09-30 09:21:57 +00:00
|
|
|
bool _requestingHistoryLock = false;
|
2019-06-21 10:18:54 +00:00
|
|
|
|
2020-03-30 09:08:38 +00:00
|
|
|
final Map<String, Event> _eventCache = {};
|
2019-11-29 11:12:04 +00:00
|
|
|
|
|
|
|
/// Searches for the event in this timeline. If not
|
|
|
|
/// found, requests from the server. Requested events
|
|
|
|
/// are cached.
|
|
|
|
Future<Event> getEventById(String id) async {
|
2020-03-30 09:08:38 +00:00
|
|
|
for (var i = 0; i < events.length; i++) {
|
2019-11-29 11:12:04 +00:00
|
|
|
if (events[i].eventId == id) return events[i];
|
|
|
|
}
|
|
|
|
if (_eventCache.containsKey(id)) return _eventCache[id];
|
2020-03-30 09:08:38 +00:00
|
|
|
final requestedEvent = await room.getEventById(id);
|
2019-11-29 11:12:04 +00:00
|
|
|
if (requestedEvent == null) return null;
|
|
|
|
_eventCache[id] = requestedEvent;
|
|
|
|
return _eventCache[id];
|
|
|
|
}
|
|
|
|
|
2019-09-30 09:21:57 +00:00
|
|
|
Future<void> requestHistory(
|
|
|
|
{int historyCount = Room.DefaultHistoryCount}) async {
|
|
|
|
if (!_requestingHistoryLock) {
|
|
|
|
_requestingHistoryLock = true;
|
|
|
|
await room.requestHistory(
|
2019-09-26 09:30:07 +00:00
|
|
|
historyCount: historyCount,
|
|
|
|
onHistoryReceived: () {
|
|
|
|
if (room.prev_batch.isEmpty || room.prev_batch == null) events = [];
|
2019-09-30 09:21:57 +00:00
|
|
|
},
|
|
|
|
);
|
2020-01-08 14:20:42 +00:00
|
|
|
await Future.delayed(const Duration(seconds: 2));
|
2019-09-30 09:21:57 +00:00
|
|
|
_requestingHistoryLock = false;
|
|
|
|
}
|
2019-09-26 09:30:07 +00:00
|
|
|
}
|
|
|
|
|
2019-06-21 10:18:54 +00:00
|
|
|
Timeline({this.room, this.events, this.onUpdate, this.onInsert}) {
|
2020-01-02 14:09:49 +00:00
|
|
|
sub ??= room.client.onEvent.stream.listen(_handleEventUpdate);
|
2020-05-21 14:52:14 +00:00
|
|
|
// 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
|
2020-05-22 10:12:18 +00:00
|
|
|
roomSub ??= room.client.onRoomUpdate.stream
|
|
|
|
.where((r) => r.id == room.id && r.limitedTimeline == true)
|
|
|
|
.listen((r) => events.clear());
|
2020-02-21 15:05:19 +00:00
|
|
|
sessionIdReceivedSub ??=
|
|
|
|
room.onSessionKeyReceived.stream.listen(_sessionKeyReceived);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Don't forget to call this before you dismiss this object!
|
|
|
|
void cancelSubscriptions() {
|
|
|
|
sub?.cancel();
|
2020-05-21 14:52:14 +00:00
|
|
|
roomSub?.cancel();
|
2020-02-21 15:05:19 +00:00
|
|
|
sessionIdReceivedSub?.cancel();
|
|
|
|
}
|
|
|
|
|
2020-05-22 11:15:48 +00:00
|
|
|
void _sessionKeyReceived(String sessionId) async {
|
2020-03-30 09:08:38 +00:00
|
|
|
var decryptAtLeastOneEvent = false;
|
2020-05-22 11:15:48 +00:00
|
|
|
final decryptFn = () async {
|
|
|
|
for (var i = 0; i < events.length; i++) {
|
|
|
|
if (events[i].type == EventTypes.Encrypted &&
|
|
|
|
events[i].messageType == MessageTypes.BadEncrypted &&
|
|
|
|
events[i].content['body'] == DecryptError.UNKNOWN_SESSION &&
|
|
|
|
events[i].content['session_id'] == sessionId) {
|
|
|
|
events[i] = await events[i].decryptAndStore();
|
|
|
|
if (events[i].type != EventTypes.Encrypted) {
|
|
|
|
decryptAtLeastOneEvent = true;
|
|
|
|
}
|
2020-02-21 15:05:19 +00:00
|
|
|
}
|
|
|
|
}
|
2020-05-22 11:15:48 +00:00
|
|
|
};
|
|
|
|
if (room.client.database != null) {
|
|
|
|
await room.client.database.transaction(decryptFn);
|
|
|
|
} else {
|
|
|
|
await decryptFn();
|
2020-02-21 15:05:19 +00:00
|
|
|
}
|
|
|
|
if (decryptAtLeastOneEvent) onUpdate();
|
2019-06-21 10:18:54 +00:00
|
|
|
}
|
|
|
|
|
2019-07-23 09:09:13 +00:00
|
|
|
int _findEvent({String event_id, String unsigned_txid}) {
|
2019-06-27 08:12:39 +00:00
|
|
|
int i;
|
|
|
|
for (i = 0; i < events.length; i++) {
|
2019-08-07 09:38:51 +00:00
|
|
|
if (events[i].eventId == event_id ||
|
|
|
|
(unsigned_txid != null && events[i].eventId == unsigned_txid)) break;
|
2019-06-27 08:12:39 +00:00
|
|
|
}
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
|
2019-06-21 10:18:54 +00:00
|
|
|
void _handleEventUpdate(EventUpdate eventUpdate) async {
|
|
|
|
try {
|
|
|
|
if (eventUpdate.roomID != room.id) return;
|
2020-02-15 07:48:41 +00:00
|
|
|
|
2020-03-30 09:08:38 +00:00
|
|
|
if (eventUpdate.type == 'timeline' || eventUpdate.type == 'history') {
|
2019-12-12 12:19:18 +00:00
|
|
|
// Redaction events are handled as modification for existing events.
|
2020-03-30 09:08:38 +00:00
|
|
|
if (eventUpdate.eventType == 'm.room.redaction') {
|
|
|
|
final eventId = _findEvent(event_id: eventUpdate.content['redacts']);
|
2019-12-12 12:19:18 +00:00
|
|
|
if (eventId != null) {
|
2020-05-22 10:12:18 +00:00
|
|
|
events[eventId].setRedactionEvent(Event.fromJson(
|
|
|
|
eventUpdate.content, room, eventUpdate.sortOrder));
|
2019-12-12 12:19:18 +00:00
|
|
|
}
|
2020-03-30 09:08:38 +00:00
|
|
|
} else if (eventUpdate.content['status'] == -2) {
|
|
|
|
var i = _findEvent(event_id: eventUpdate.content['event_id']);
|
2019-06-27 08:12:39 +00:00
|
|
|
if (i < events.length) events.removeAt(i);
|
|
|
|
}
|
2019-06-26 14:36:34 +00:00
|
|
|
// Is this event already in the timeline?
|
2020-03-30 09:08:38 +00:00
|
|
|
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']
|
2019-06-27 08:12:39 +00:00
|
|
|
: null);
|
|
|
|
|
2019-06-26 14:36:34 +00:00
|
|
|
if (i < events.length) {
|
2020-05-22 10:12:18 +00:00
|
|
|
events[i] = Event.fromJson(
|
|
|
|
eventUpdate.content, room, eventUpdate.sortOrder);
|
2019-06-26 14:36:34 +00:00
|
|
|
}
|
|
|
|
} else {
|
2019-07-11 20:17:40 +00:00
|
|
|
Event newEvent;
|
2020-05-22 10:12:18 +00:00
|
|
|
var senderUser = room
|
|
|
|
.getState('m.room.member', eventUpdate.content['sender'])
|
|
|
|
?.asUser ??
|
|
|
|
await room.client.database?.getUser(
|
|
|
|
room.client.id, eventUpdate.content['sender'], room);
|
2019-07-11 20:17:40 +00:00
|
|
|
if (senderUser != null) {
|
2020-03-30 09:08:38 +00:00
|
|
|
eventUpdate.content['displayname'] = senderUser.displayName;
|
2020-04-24 07:24:06 +00:00
|
|
|
eventUpdate.content['avatar_url'] = senderUser.avatarUrl.toString();
|
2019-06-26 14:36:34 +00:00
|
|
|
}
|
2019-06-21 10:18:54 +00:00
|
|
|
|
2020-05-22 10:12:18 +00:00
|
|
|
newEvent =
|
|
|
|
Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder);
|
2019-06-21 10:18:54 +00:00
|
|
|
|
2020-03-30 09:08:38 +00:00
|
|
|
if (eventUpdate.type == 'history' &&
|
2019-09-30 09:21:57 +00:00
|
|
|
events.indexWhere(
|
2020-03-30 09:08:38 +00:00
|
|
|
(e) => e.eventId == eventUpdate.content['event_id']) !=
|
2019-09-30 09:21:57 +00:00
|
|
|
-1) return;
|
|
|
|
|
2019-06-26 14:36:34 +00:00
|
|
|
events.insert(0, newEvent);
|
|
|
|
if (onInsert != null) onInsert(0);
|
|
|
|
}
|
2019-06-21 10:18:54 +00:00
|
|
|
}
|
2019-06-21 11:30:39 +00:00
|
|
|
sortAndUpdate();
|
2019-06-21 10:18:54 +00:00
|
|
|
} catch (e) {
|
2019-07-12 09:26:07 +00:00
|
|
|
if (room.client.debug) {
|
2020-03-30 09:08:38 +00:00
|
|
|
print('[WARNING] (_handleEventUpdate) ${e.toString()}');
|
2019-07-12 09:26:07 +00:00
|
|
|
}
|
2019-06-21 10:18:54 +00:00
|
|
|
}
|
|
|
|
}
|
2019-06-21 11:30:39 +00:00
|
|
|
|
2019-10-01 09:39:15 +00:00
|
|
|
bool sortLock = false;
|
|
|
|
|
2020-03-30 09:08:38 +00:00
|
|
|
void sort() {
|
2019-10-24 09:53:53 +00:00
|
|
|
if (sortLock || events.length < 2) return;
|
2019-10-01 09:39:15 +00:00
|
|
|
sortLock = true;
|
2020-05-15 18:40:17 +00:00
|
|
|
events?.sort((a, b) => b.sortOrder - a.sortOrder > 0 ? 1 : -1);
|
2019-10-01 09:39:15 +00:00
|
|
|
sortLock = false;
|
|
|
|
}
|
|
|
|
|
2020-03-30 09:08:38 +00:00
|
|
|
void sortAndUpdate() async {
|
2019-10-01 09:39:15 +00:00
|
|
|
sort();
|
2019-06-25 10:34:03 +00:00
|
|
|
if (onUpdate != null) onUpdate();
|
2019-06-21 11:30:39 +00:00
|
|
|
}
|
2019-06-21 10:18:54 +00:00
|
|
|
}
|