Revert "feat: non cached fragmented timeline"

This reverts commit 09b324674e
This commit is contained in:
Krille Fear 2022-05-12 06:46:48 +00:00 committed by Christian Pauly
parent e769f84b34
commit 9a87850092
15 changed files with 137 additions and 1033 deletions

View File

@ -1389,7 +1389,7 @@ class Client extends MatrixApi {
await roomsLoading; await roomsLoading;
await _accountDataLoading; await _accountDataLoading;
_currentTransaction = database.transaction(() async { _currentTransaction = database.transaction(() async {
await _handleSync(syncResp, direction: Direction.f); await _handleSync(syncResp);
if (prevBatch != syncResp.nextBatch) { if (prevBatch != syncResp.nextBatch) {
await database.storePrevBatch(syncResp.nextBatch); await database.storePrevBatch(syncResp.nextBatch);
} }
@ -1401,7 +1401,7 @@ class Client extends MatrixApi {
); );
onSyncStatus.add(SyncStatusUpdate(SyncStatus.cleaningUp)); onSyncStatus.add(SyncStatusUpdate(SyncStatus.cleaningUp));
} else { } else {
await _handleSync(syncResp, direction: Direction.f); await _handleSync(syncResp);
} }
if (_disposed || _aborted) return; if (_disposed || _aborted) return;
if (prevBatch == null) { if (prevBatch == null) {
@ -1445,13 +1445,13 @@ class Client extends MatrixApi {
} }
/// Use this method only for testing utilities! /// Use this method only for testing utilities!
Future<void> handleSync(SyncUpdate sync, {Direction? direction}) async { Future<void> handleSync(SyncUpdate sync, {bool sortAtTheEnd = false}) async {
// ensure we don't upload keys because someone forgot to set a key count // ensure we don't upload keys because someone forgot to set a key count
sync.deviceOneTimeKeysCount ??= {'signed_curve25519': 100}; sync.deviceOneTimeKeysCount ??= {'signed_curve25519': 100};
await _handleSync(sync, direction: direction); await _handleSync(sync, sortAtTheEnd: sortAtTheEnd);
} }
Future<void> _handleSync(SyncUpdate sync, {Direction? direction}) async { Future<void> _handleSync(SyncUpdate sync, {bool sortAtTheEnd = false}) async {
final syncToDevice = sync.toDevice; final syncToDevice = sync.toDevice;
if (syncToDevice != null) { if (syncToDevice != null) {
await _handleToDeviceEvents(syncToDevice); await _handleToDeviceEvents(syncToDevice);
@ -1460,15 +1460,15 @@ class Client extends MatrixApi {
if (sync.rooms != null) { if (sync.rooms != null) {
final join = sync.rooms?.join; final join = sync.rooms?.join;
if (join != null) { if (join != null) {
await _handleRooms(join, direction: direction); await _handleRooms(join, sortAtTheEnd: sortAtTheEnd);
} }
final invite = sync.rooms?.invite; final invite = sync.rooms?.invite;
if (invite != null) { if (invite != null) {
await _handleRooms(invite, direction: direction); await _handleRooms(invite, sortAtTheEnd: sortAtTheEnd);
} }
final leave = sync.rooms?.leave; final leave = sync.rooms?.leave;
if (leave != null) { if (leave != null) {
await _handleRooms(leave, direction: direction); await _handleRooms(leave, sortAtTheEnd: sortAtTheEnd);
} }
} }
for (final newPresence in sync.presence ?? []) { for (final newPresence in sync.presence ?? []) {
@ -1532,7 +1532,7 @@ class Client extends MatrixApi {
} }
Future<void> _handleRooms(Map<String, SyncRoomUpdate> rooms, Future<void> _handleRooms(Map<String, SyncRoomUpdate> rooms,
{Direction? direction}) async { {bool sortAtTheEnd = false}) async {
var handledRooms = 0; var handledRooms = 0;
for (final entry in rooms.entries) { for (final entry in rooms.entries) {
onSyncStatus.add(SyncStatusUpdate( onSyncStatus.add(SyncStatusUpdate(
@ -1548,14 +1548,11 @@ class Client extends MatrixApi {
/// Handle now all room events and save them in the database /// Handle now all room events and save them in the database
if (room is JoinedRoomUpdate) { if (room is JoinedRoomUpdate) {
final state = room.state; final state = room.state;
if (state != null && state.isNotEmpty) { if (state != null && state.isNotEmpty) {
// TODO: This method seems to be comperatively slow for some updates // TODO: This method seems to be comperatively slow for some updates
await _handleRoomEvents( await _handleRoomEvents(
id, id, state.map((i) => i.toJson()).toList(), EventUpdateType.state,
state.map((i) => i.toJson()).toList(), sortAtTheEnd: sortAtTheEnd);
EventUpdateType.state,
);
} }
final timelineEvents = room.timeline?.events; final timelineEvents = room.timeline?.events;
@ -1563,11 +1560,8 @@ class Client extends MatrixApi {
await _handleRoomEvents( await _handleRoomEvents(
id, id,
timelineEvents.map((i) => i.toJson()).toList(), timelineEvents.map((i) => i.toJson()).toList(),
direction != null sortAtTheEnd ? EventUpdateType.history : EventUpdateType.timeline,
? (direction == Direction.b sortAtTheEnd: sortAtTheEnd);
? EventUpdateType.history
: EventUpdateType.timeline)
: EventUpdateType.timeline);
} }
final ephemeral = room.ephemeral; final ephemeral = room.ephemeral;
@ -1593,7 +1587,7 @@ class Client extends MatrixApi {
id, id,
timelineEvents.map((i) => i.toJson()).toList(), timelineEvents.map((i) => i.toJson()).toList(),
EventUpdateType.timeline, EventUpdateType.timeline,
); sortAtTheEnd: sortAtTheEnd);
} }
final accountData = room.accountData; final accountData = room.accountData;
if (accountData != null && accountData.isNotEmpty) { if (accountData != null && accountData.isNotEmpty) {
@ -1664,14 +1658,16 @@ class Client extends MatrixApi {
} }
Future<void> _handleRoomEvents( Future<void> _handleRoomEvents(
String chat_id, List<dynamic> events, EventUpdateType type) async { String chat_id, List<dynamic> events, EventUpdateType type,
{bool sortAtTheEnd = false}) async {
for (final event in events) { for (final event in events) {
await _handleEvent(event, chat_id, type); await _handleEvent(event, chat_id, type, sortAtTheEnd: sortAtTheEnd);
} }
} }
Future<void> _handleEvent( Future<void> _handleEvent(
Map<String, dynamic> event, String roomID, EventUpdateType type) async { Map<String, dynamic> event, String roomID, EventUpdateType type,
{bool sortAtTheEnd = false}) async {
if (event['type'] is String && event['content'] is Map<String, dynamic>) { if (event['type'] is String && event['content'] is Map<String, dynamic>) {
// The client must ignore any new m.room.encryption event to prevent // The client must ignore any new m.room.encryption event to prevent
// man-in-the-middle attacks! // man-in-the-middle attacks!
@ -1684,7 +1680,11 @@ class Client extends MatrixApi {
return; return;
} }
var update = EventUpdate(roomID: roomID, type: type, content: event); var update = EventUpdate(
roomID: roomID,
type: type,
content: event,
);
if (event['type'] == EventTypes.Encrypted && encryptionEnabled) { if (event['type'] == EventTypes.Encrypted && encryptionEnabled) {
update = await update.decrypt(room); update = await update.decrypt(room);
} }

View File

@ -83,7 +83,6 @@ abstract class DatabaseApi {
Future<List<Event>> getEventList( Future<List<Event>> getEventList(
Room room, { Room room, {
int start = 0, int start = 0,
bool onlySending,
int limit, int limit,
}); });

View File

@ -345,7 +345,6 @@ class FluffyBoxDatabase extends DatabaseApi {
Future<List<Event>> getEventList( Future<List<Event>> getEventList(
Room room, { Room room, {
int start = 0, int start = 0,
bool onlySending = false,
int? limit, int? limit,
}) => }) =>
runBenchmarked<List<Event>>('Get event list', () async { runBenchmarked<List<Event>>('Get event list', () async {
@ -367,11 +366,10 @@ class FluffyBoxDatabase extends DatabaseApi {
// Combine those two lists while respecting the start and limit parameters. // Combine those two lists while respecting the start and limit parameters.
final end = min(timelineEventIds.length, final end = min(timelineEventIds.length,
start + (limit ?? timelineEventIds.length)); start + (limit ?? timelineEventIds.length));
final eventIds = sendingEventIds +
final eventIds = sendingEventIds; (start < timelineEventIds.length
if (start < timelineEventIds.length && !onlySending) { ? timelineEventIds.getRange(start, end).toList()
eventIds.addAll(timelineEventIds.getRange(start, end).toList()); : []);
}
return await _getEventsByIds(eventIds.cast<String>(), room); return await _getEventsByIds(eventIds.cast<String>(), room);
}); });

View File

@ -387,7 +387,6 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
Future<List<Event>> getEventList( Future<List<Event>> getEventList(
Room room, { Room room, {
int start = 0, int start = 0,
bool onlySending = false,
int? limit, int? limit,
}) => }) =>
runBenchmarked<List<Event>>('Get event list', () async { runBenchmarked<List<Event>>('Get event list', () async {
@ -411,7 +410,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
final end = min(timelineEventIds.length, final end = min(timelineEventIds.length,
start + (limit ?? timelineEventIds.length)); start + (limit ?? timelineEventIds.length));
final eventIds = sendingEventIds + final eventIds = sendingEventIds +
(start < timelineEventIds.length && !onlySending (start < timelineEventIds.length
? timelineEventIds.getRange(start, end).toList() ? timelineEventIds.getRange(start, end).toList()
: []); : []);

View File

@ -1,10 +0,0 @@
import '../../matrix.dart';
class TimelineChunk {
String prevBatch; // pos of the first event of the database timeline chunk
String nextBatch;
List<Event> events;
TimelineChunk(
{required this.events, this.prevBatch = '', this.nextBatch = ''});
}

View File

@ -22,7 +22,6 @@ import 'dart:typed_data';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:html_unescape/html_unescape.dart'; import 'package:html_unescape/html_unescape.dart';
import 'package:matrix/src/models/timeline_chunk.dart';
import 'package:matrix/src/utils/crypto/crypto.dart'; import 'package:matrix/src/utils/crypto/crypto.dart';
import 'package:matrix/src/utils/file_send_request_credentials.dart'; import 'package:matrix/src/utils/file_send_request_credentials.dart';
import 'package:matrix/src/utils/space_child.dart'; import 'package:matrix/src/utils/space_child.dart';
@ -32,8 +31,11 @@ import 'utils/markdown.dart';
import 'utils/marked_unread.dart'; import 'utils/marked_unread.dart';
enum PushRuleState { notify, mentionsOnly, dontNotify } enum PushRuleState { notify, mentionsOnly, dontNotify }
enum JoinRules { public, knock, invite, private } enum JoinRules { public, knock, invite, private }
enum GuestAccess { canJoin, forbidden } enum GuestAccess { canJoin, forbidden }
enum HistoryVisibility { invited, joined, shared, worldReadable } enum HistoryVisibility { invited, joined, shared, worldReadable }
const Map<GuestAccess, String> _guestAccessMap = { const Map<GuestAccess, String> _guestAccessMap = {
@ -1081,8 +1083,7 @@ class Room {
/// Returns the actual count of received timeline events. /// Returns the actual count of received timeline events.
Future<int> requestHistory( Future<int> requestHistory(
{int historyCount = defaultHistoryCount, {int historyCount = defaultHistoryCount,
void Function()? onHistoryReceived, void Function()? onHistoryReceived}) async {
direction = Direction.b}) async {
final prev_batch = this.prev_batch; final prev_batch = this.prev_batch;
if (prev_batch == null) { if (prev_batch == null) {
throw 'Tried to request history without a prev_batch token'; throw 'Tried to request history without a prev_batch token';
@ -1090,7 +1091,7 @@ class Room {
final resp = await client.getRoomEvents( final resp = await client.getRoomEvents(
id, id,
prev_batch, prev_batch,
direction, Direction.b,
limit: historyCount, limit: historyCount,
filter: jsonEncode(StateFilter(lazyLoadMembers: true).toJson()), filter: jsonEncode(StateFilter(lazyLoadMembers: true).toJson()),
); );
@ -1111,12 +1112,8 @@ class Room {
state: resp.state, state: resp.state,
timeline: TimelineUpdate( timeline: TimelineUpdate(
limited: false, limited: false,
events: direction == Direction.b events: resp.chunk,
? resp.chunk prevBatch: resp.end,
: resp.chunk?.reversed.toList(),
prevBatch: direction == Direction.b
? resp.end
: resp.start,
), ),
) )
} }
@ -1128,15 +1125,13 @@ class Room {
timeline: TimelineUpdate( timeline: TimelineUpdate(
limited: false, limited: false,
events: resp.chunk, events: resp.chunk,
prevBatch: direction == Direction.b prevBatch: resp.end,
? resp.end
: resp.start,
), ),
), ),
} }
: null), : null),
), ),
direction: Direction.b); sortAtTheEnd: true);
}; };
if (client.database != null) { if (client.database != null) {
@ -1215,34 +1210,6 @@ class Room {
return; return;
} }
Future<TimelineChunk?> getEventContext(String eventId) async {
final resp = await client.getEventContext(id, eventId,
limit: Room.defaultHistoryCount
// filter: jsonEncode(StateFilter(lazyLoadMembers: true).toJson()),
);
final events = [
if (resp.eventsAfter != null) ...resp.eventsAfter!.reversed.toList(),
if (resp.event != null) resp.event!,
if (resp.eventsBefore != null) ...resp.eventsBefore!
].map((e) => Event.fromMatrixEvent(e, this)).toList();
// Try again to decrypt encrypted events but don't update the database.
if (encrypted && client.database != null && client.encryptionEnabled) {
for (var i = 0; i < events.length; i++) {
if (events[i].type == EventTypes.Encrypted &&
events[i].content['can_request_session'] == true) {
events[i] = await client.encryption!.decryptRoomEvent(id, events[i]);
}
}
}
final chunk = TimelineChunk(
nextBatch: resp.end ?? '', prevBatch: resp.start ?? '', events: events);
return chunk;
}
/// This API updates the marker for the given receipt type to the event ID /// This API updates the marker for the given receipt type to the event ID
/// specified. /// specified.
Future<void> postReceipt(String eventId) async { Future<void> postReceipt(String eventId) async {
@ -1261,80 +1228,47 @@ class Room {
/// just want to update the whole timeline on every change, use the [onUpdate] /// just want to update the whole timeline on every change, use the [onUpdate]
/// callback. For updating only the parts that have changed, use the /// callback. For updating only the parts that have changed, use the
/// [onChange], [onRemove], [onInsert] and the [onHistoryReceived] callbacks. /// [onChange], [onRemove], [onInsert] and the [onHistoryReceived] callbacks.
/// This method can also retrieve the timeline at a specific point by setting Future<Timeline> getTimeline({
/// the [eventContextId] void Function(int index)? onChange,
Future<Timeline> getTimeline(
{void Function(int index)? onChange,
void Function(int index)? onRemove, void Function(int index)? onRemove,
void Function(int insertID)? onInsert, void Function(int insertID)? onInsert,
void Function()? onNewEvent,
void Function()? onUpdate, void Function()? onUpdate,
String? eventContextId}) async { }) async {
await postLoad(); await postLoad();
final events = await client.database?.getEventList(
final _events = await client.database?.getEventList(
this, this,
limit: defaultHistoryCount, limit: defaultHistoryCount,
); ) ??
<Event>[];
var chunk = TimelineChunk(events: _events ?? []); // Try again to decrypt encrypted events and update the database.
if (encrypted && client.database != null && client.encryptionEnabled) {
if (_events != null) {
if (eventContextId != null) {
if (_events
.firstWhereOrNull((event) => event.eventId == eventContextId) !=
null) {
chunk = TimelineChunk(events: _events);
} else {
chunk = await getEventContext(eventContextId) ??
TimelineChunk(events: []);
}
}
// Fetch all users from database we have got here.
if (eventContextId != null) {
for (final event in _events) {
if (getState(EventTypes.RoomMember, event.senderId) != null) continue;
final dbUser = await client.database?.getUser(event.senderId, this);
if (dbUser != null) setState(dbUser);
}
}
}
if (encrypted && client.encryptionEnabled) {
// decrypt messages
for (var i = 0; i < chunk.events.length; i++) {
if (chunk.events[i].type == EventTypes.Encrypted) {
if (eventContextId != null) {
// for the fragmented timeline, we don't cache the decrypted
//message in the database
chunk.events[i] = await client.encryption!.decryptRoomEvent(
id,
chunk.events[i],
);
} else if (client.database != null) {
// else, we need the database
await client.database?.transaction(() async { await client.database?.transaction(() async {
for (var i = 0; i < chunk.events.length; i++) { for (var i = 0; i < events.length; i++) {
if (chunk.events[i].content['can_request_session'] == true) { if (events[i].type == EventTypes.Encrypted &&
chunk.events[i] = await client.encryption! events[i].content['can_request_session'] == true) {
.decryptRoomEvent(id, chunk.events[i], store: true); events[i] = await client.encryption!
.decryptRoomEvent(id, events[i], store: true);
} }
} }
}); });
} }
}
} // Fetch all users from database we have got here.
for (final event in events) {
if (getState(EventTypes.RoomMember, event.senderId) != null) continue;
final dbUser = await client.database?.getUser(event.senderId, this);
if (dbUser != null) setState(dbUser);
} }
final timeline = Timeline( final timeline = Timeline(
room: this, room: this,
chunk: chunk, events: events,
onChange: onChange, onChange: onChange,
onRemove: onRemove, onRemove: onRemove,
onInsert: onInsert, onInsert: onInsert,
onNewEvent: onNewEvent, onUpdate: onUpdate,
onUpdate: onUpdate); );
return timeline; return timeline;
} }
@ -1862,13 +1796,13 @@ class Room {
} }
Future<void> _handleFakeSync(SyncUpdate syncUpdate, Future<void> _handleFakeSync(SyncUpdate syncUpdate,
{Direction? direction}) async { {bool sortAtTheEnd = false}) async {
if (client.database != null) { if (client.database != null) {
await client.database?.transaction(() async { await client.database?.transaction(() async {
await client.handleSync(syncUpdate, direction: direction); await client.handleSync(syncUpdate, sortAtTheEnd: sortAtTheEnd);
}); });
} else { } else {
await client.handleSync(syncUpdate, direction: direction); await client.handleSync(syncUpdate, sortAtTheEnd: sortAtTheEnd);
} }
} }

View File

@ -17,20 +17,17 @@
*/ */
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:collection/src/iterable_extensions.dart'; import 'package:collection/src/iterable_extensions.dart';
import '../matrix.dart'; import '../matrix.dart';
import 'models/timeline_chunk.dart';
/// Represents the timeline of a room. The callback [onUpdate] will be triggered /// Represents the timeline of a room. The callback [onUpdate] will be triggered
/// automatically. The initial /// automatically. The initial
/// event list will be retreived when created by the `room.getTimeline()` method. /// event list will be retreived when created by the `room.getTimeline()` method.
class Timeline { class Timeline {
final Room room; final Room room;
List<Event> get events => chunk.events; final List<Event> events;
/// Map of event ID to map of type to set of aggregated events /// Map of event ID to map of type to set of aggregated events
final Map<String, Map<String, Set<Event>>> aggregatedEvents = {}; final Map<String, Map<String, Set<Event>>> aggregatedEvents = {};
@ -39,21 +36,14 @@ class Timeline {
final void Function(int index)? onChange; final void Function(int index)? onChange;
final void Function(int index)? onInsert; final void Function(int index)? onInsert;
final void Function(int index)? onRemove; final void Function(int index)? onRemove;
final void Function()? onNewEvent;
StreamSubscription<EventUpdate>? sub; StreamSubscription<EventUpdate>? sub;
StreamSubscription<SyncUpdate>? roomSub; StreamSubscription<SyncUpdate>? roomSub;
StreamSubscription<String>? sessionIdReceivedSub; StreamSubscription<String>? sessionIdReceivedSub;
bool isRequestingHistory = false; bool isRequestingHistory = false;
bool isRequestingFuture = false;
bool allowNewEvent = true;
bool isFragmentedTimeline = false;
final Map<String, Event> _eventCache = {}; final Map<String, Event> _eventCache = {};
TimelineChunk chunk;
/// Searches for the event in this timeline. If not /// Searches for the event in this timeline. If not
/// found, requests from the server. Requested events /// found, requests from the server. Requested events
/// are cached. /// are cached.
@ -84,41 +74,16 @@ class Timeline {
if (isRequestingHistory) { if (isRequestingHistory) {
return; return;
} }
isRequestingHistory = true; isRequestingHistory = true;
await _requestEvents(direction: Direction.b, historyCount: historyCount);
isRequestingHistory = false;
}
bool get canRequestFuture => !allowNewEvent;
Future<void> requestFuture(
{int historyCount = Room.defaultHistoryCount}) async {
if (allowNewEvent) {
return; // we shouldn't force to add new events if they will autatically be added
}
if (isRequestingFuture) return;
isRequestingFuture = true;
await _requestEvents(direction: Direction.f, historyCount: historyCount);
isRequestingFuture = false;
}
Future<void> _requestEvents(
{int historyCount = Room.defaultHistoryCount,
required Direction direction}) async {
onUpdate?.call(); onUpdate?.call();
try { try {
// Look up for events in the database first. With fragmented view, we should delete the database cache // Look up for events in hive first
final eventsFromStore = isFragmentedTimeline final eventsFromStore = await room.client.database?.getEventList(
? null
: await room.client.database?.getEventList(
room, room,
start: events.length, start: events.length,
limit: Room.defaultHistoryCount, limit: Room.defaultHistoryCount,
); );
if (eventsFromStore != null && eventsFromStore.isNotEmpty) { if (eventsFromStore != null && eventsFromStore.isNotEmpty) {
// Fetch all users from database we have got here. // Fetch all users from database we have got here.
for (final event in events) { for (final event in events) {
@ -130,7 +95,6 @@ class Timeline {
if (dbUser != null) room.setState(dbUser); if (dbUser != null) room.setState(dbUser);
} }
if (direction == Direction.b) {
events.addAll(eventsFromStore); events.addAll(eventsFromStore);
final startIndex = events.length - eventsFromStore.length; final startIndex = events.length - eventsFromStore.length;
final endIndex = events.length; final endIndex = events.length;
@ -138,30 +102,14 @@ class Timeline {
onInsert?.call(i); onInsert?.call(i);
} }
} else { } else {
events.insertAll(0, eventsFromStore); Logs().v('No more events found in the store. Request from server...');
final startIndex = eventsFromStore.length;
final endIndex = 0;
for (var i = startIndex; i > endIndex; i--) {
onInsert?.call(i);
}
}
} else {
Logs().i('No more events found in the store. Request from server...');
if (isFragmentedTimeline) {
await getRoomEvents(
historyCount: historyCount,
direction: direction,
);
} else {
await room.requestHistory( await room.requestHistory(
historyCount: historyCount, historyCount: historyCount,
direction: direction,
onHistoryReceived: () { onHistoryReceived: () {
_collectHistoryUpdates = true; _collectHistoryUpdates = true;
}, },
); );
} }
}
} finally { } finally {
_collectHistoryUpdates = false; _collectHistoryUpdates = false;
isRequestingHistory = false; isRequestingHistory = false;
@ -169,103 +117,14 @@ class Timeline {
} }
} }
/// Request more previous events from the server. [historyCount] defines how much events should Timeline({
/// be received maximum. When the request is answered, [onHistoryReceived] will be triggered **before** required this.room,
/// the historical events will be published in the onEvent stream. List<Event>? events,
/// Returns the actual count of received timeline events.
Future<int> getRoomEvents(
{int historyCount = Room.defaultHistoryCount,
direction = Direction.b}) async {
final resp = await room.client.getRoomEvents(
room.id,
direction == Direction.b ? chunk.prevBatch : chunk.nextBatch,
direction,
limit: historyCount,
filter: jsonEncode(StateFilter(lazyLoadMembers: true).toJson()),
);
if (resp.end == null || resp.start == null) {
Logs().w('end or start parameters where not set in the response');
}
final newNextBatch = direction == Direction.b ? resp.start : resp.end;
final newPrevBatch = direction == Direction.b ? resp.end : resp.start;
final type = direction == Direction.b
? EventUpdateType.history
: EventUpdateType.timeline;
if ((resp.state?.length ?? 0) == 0 && resp.start != resp.end) {
if (type == EventUpdateType.history) {
Logs().w(
'[nav] we can still request history prevBatch: $type $newPrevBatch');
} else {
Logs().w(
'[nav] we can still request history nextBatch: $type $newNextBatch');
}
}
final newEvents =
resp.chunk?.map((e) => Event.fromMatrixEvent(e, room)).toList() ?? [];
if (!allowNewEvent) {
if (resp.start == resp.end) allowNewEvent = true;
if (allowNewEvent) {
Logs().d('We now allow sync update into the timeline.');
allowNewEvent = true;
newEvents.addAll(
await room.client.database?.getEventList(room, onlySending: true) ??
[]);
}
}
// Try to decrypt encrypted events but don't update the database.
if (room.encrypted &&
room.client.database != null &&
room.client.encryptionEnabled) {
for (var i = 0; i < newEvents.length; i++) {
if (newEvents[i].type == EventTypes.Encrypted) {
newEvents[i] = await room.client.encryption!
.decryptRoomEvent(room.id, newEvents[i]);
}
}
}
// update chunk anchors
if (type == EventUpdateType.history) {
chunk.prevBatch = newPrevBatch ?? '';
final offset = chunk.events.length;
chunk.events.addAll(newEvents);
for (var i = 0; i < newEvents.length; i++) {
onInsert?.call(i + offset);
}
} else {
chunk.nextBatch = newNextBatch ?? '';
chunk.events.insertAll(0, newEvents.reversed);
for (var i = 0; i < newEvents.length; i++) {
onInsert?.call(i);
}
}
if (onUpdate != null) {
onUpdate!();
}
return resp.chunk?.length ?? 0;
}
Timeline(
{required this.room,
this.onUpdate, this.onUpdate,
this.onChange, this.onChange,
this.onInsert, this.onInsert,
this.onRemove, this.onRemove,
this.onNewEvent, }) : events = events ?? [] {
required this.chunk}) {
sub = room.client.onEvent.stream.listen(_handleEventUpdate); sub = room.client.onEvent.stream.listen(_handleEventUpdate);
// If the timeline is limited we want to clear our events cache // If the timeline is limited we want to clear our events cache
@ -277,15 +136,9 @@ class Timeline {
room.onSessionKeyReceived.stream.listen(_sessionKeyReceived); room.onSessionKeyReceived.stream.listen(_sessionKeyReceived);
// we want to populate our aggregated events // we want to populate our aggregated events
for (final e in events) { for (final e in this.events) {
addAggregatedEvent(e); addAggregatedEvent(e);
} }
// we are using a fragmented timeline
if (chunk.nextBatch != '') {
allowNewEvent = false;
isFragmentedTimeline = true;
}
} }
/// Removes all entries from [events] which are not in this SyncUpdate. /// Removes all entries from [events] which are not in this SyncUpdate.
@ -428,13 +281,6 @@ class Timeline {
eventUpdate.type != EventUpdateType.history) { eventUpdate.type != EventUpdateType.history) {
return; return;
} }
if (eventUpdate.type == EventUpdateType.timeline) {
onNewEvent?.call();
}
if (!allowNewEvent) return;
final status = eventStatusFromInt(eventUpdate.content['status'] ?? final status = eventStatusFromInt(eventUpdate.content['status'] ??
(eventUpdate.content['unsigned'] is Map<String, dynamic> (eventUpdate.content['unsigned'] is Map<String, dynamic>
? eventUpdate.content['unsigned'][messageSendingStatusKey] ? eventUpdate.content['unsigned'][messageSendingStatusKey]

View File

@ -39,8 +39,11 @@ class EventUpdate {
// The json payload of the content of this event. // The json payload of the content of this event.
final Map<String, dynamic> content; final Map<String, dynamic> content;
EventUpdate( EventUpdate({
{required this.roomID, required this.type, required this.content}); required this.roomID,
required this.type,
required this.content,
});
Future<EventUpdate> decrypt(Room room, {bool store = false}) async { Future<EventUpdate> decrypt(Room room, {bool store = false}) async {
final encryption = room.client.encryption; final encryption = room.client.encryption;
@ -54,7 +57,10 @@ class EventUpdate {
room.id, Event.fromJson(content, room), room.id, Event.fromJson(content, room),
store: store, updateType: type); store: store, updateType: type);
return EventUpdate( return EventUpdate(
roomID: roomID, type: type, content: decrpytedEvent.toJson()); roomID: roomID,
type: type,
content: decrpytedEvent.toJson(),
);
} catch (e, s) { } catch (e, s) {
Logs().e('[LibOlm] Could not decrypt megolm event', e, s); Logs().e('[LibOlm] Could not decrypt megolm event', e, s);
return this; return this;

View File

@ -23,5 +23,5 @@ fi
if which flutter >/dev/null; then if which flutter >/dev/null; then
flutter pub get flutter pub get
else else
pub get dart pub get
fi fi

View File

@ -1,2 +1,2 @@
#!/bin/sh -e #!/bin/sh -e
pub run test_driver/matrixsdk_test.dart -p vm dart pub run test_driver/matrixsdk_test.dart -p vm

View File

@ -232,8 +232,7 @@ void testDatabase(
}); });
test('getEventList', () async { test('getEventList', () async {
final events = await database.getEventList( final events = await database.getEventList(
Room(id: '!testroom:example.com', client: Client('testclient')), Room(id: '!testroom:example.com', client: Client('testclient')));
);
expect(events.single.type, EventTypes.Message); expect(events.single.type, EventTypes.Message);
}); });
test('getUser', () async { test('getUser', () async {

View File

@ -21,7 +21,6 @@ import 'dart:typed_data';
import 'package:matrix/encryption.dart'; import 'package:matrix/encryption.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:matrix/src/models/timeline_chunk.dart';
import 'package:olm/olm.dart' as olm; import 'package:olm/olm.dart' as olm;
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -1108,9 +1107,8 @@ void main() {
'sender': '@example:example.org', 'sender': '@example:example.org',
'event_id': '\$edit2', 'event_id': '\$edit2',
}, room); }, room);
final timeline = Timeline( final timeline =
chunk: TimelineChunk(events: <Event>[event, edit1, edit2]), Timeline(events: <Event>[event, edit1, edit2], room: room);
room: room);
expect(event.hasAggregatedEvents(timeline, RelationshipTypes.edit), true); expect(event.hasAggregatedEvents(timeline, RelationshipTypes.edit), true);
expect(event.aggregatedEvents(timeline, RelationshipTypes.edit), expect(event.aggregatedEvents(timeline, RelationshipTypes.edit),
{edit1, edit2}); {edit1, edit2});
@ -1206,26 +1204,24 @@ void main() {
'sender': '@bob:example.org', 'sender': '@bob:example.org',
}, room); }, room);
// no edits // no edits
var displayEvent = event.getDisplayEvent( var displayEvent =
Timeline(chunk: TimelineChunk(events: <Event>[event]), room: room)); event.getDisplayEvent(Timeline(events: <Event>[event], room: room));
expect(displayEvent.body, 'blah'); expect(displayEvent.body, 'blah');
// one edit // one edit
displayEvent = event.getDisplayEvent(Timeline( displayEvent = event
chunk: TimelineChunk(events: <Event>[event, edit1]), room: room)); .getDisplayEvent(Timeline(events: <Event>[event, edit1], room: room));
expect(displayEvent.body, 'edit 1'); expect(displayEvent.body, 'edit 1');
// two edits // two edits
displayEvent = event.getDisplayEvent(Timeline( displayEvent = event.getDisplayEvent(
chunk: TimelineChunk(events: <Event>[event, edit1, edit2]), Timeline(events: <Event>[event, edit1, edit2], room: room));
room: room));
expect(displayEvent.body, 'edit 2'); expect(displayEvent.body, 'edit 2');
// foreign edit // foreign edit
displayEvent = event.getDisplayEvent(Timeline( displayEvent = event
chunk: TimelineChunk(events: <Event>[event, edit3]), room: room)); .getDisplayEvent(Timeline(events: <Event>[event, edit3], room: room));
expect(displayEvent.body, 'blah'); expect(displayEvent.body, 'blah');
// mixed foreign and non-foreign // mixed foreign and non-foreign
displayEvent = event.getDisplayEvent(Timeline( displayEvent = event.getDisplayEvent(
chunk: TimelineChunk(events: <Event>[event, edit1, edit2, edit3]), Timeline(events: <Event>[event, edit1, edit2, edit3], room: room));
room: room));
expect(displayEvent.body, 'edit 2'); expect(displayEvent.body, 'edit 2');
event = Event.fromJson({ event = Event.fromJson({
'type': EventTypes.Message, 'type': EventTypes.Message,
@ -1243,9 +1239,8 @@ void main() {
}, },
}, },
}, room); }, room);
displayEvent = event.getDisplayEvent(Timeline( displayEvent = event.getDisplayEvent(
chunk: TimelineChunk(events: <Event>[event, edit1, edit2, edit3]), Timeline(events: <Event>[event, edit1, edit2, edit3], room: room));
room: room));
expect(displayEvent.body, 'Redacted'); expect(displayEvent.body, 'Redacted');
}); });
test('attachments', () async { test('attachments', () async {

View File

@ -148,7 +148,7 @@ class FakeMatrixApi extends MockClient {
return Response.bytes(utf8.encode(json.encode(res)), statusCode); return Response.bytes(utf8.encode(json.encode(res)), statusCode);
}); });
static Map<String, dynamic> messagesResponsePast = { static Map<String, dynamic> messagesResponse = {
'start': 't47429-4392820_219380_26003_2265', 'start': 't47429-4392820_219380_26003_2265',
'end': 't47409-4357353_219380_26003_2265', 'end': 't47409-4357353_219380_26003_2265',
'chunk': [ 'chunk': [
@ -206,70 +206,6 @@ class FakeMatrixApi extends MockClient {
], ],
'state': [], 'state': [],
}; };
static Map<String, dynamic> messagesResponseFuture = {
'start': 't456',
'end': 't789',
'chunk': [
{
'content': {
'body': 'Gangnam Style',
'url': 'mxc://example.org/a526eYUSFFxlgbQYZmo442',
'info': {
'thumbnail_url': 'mxc://example.org/FHyPlCeYUSFFxlgbQYZmoEoe',
'thumbnail_info': {
'mimetype': 'image/jpeg',
'size': 46144,
'w': 300,
'h': 300
},
'w': 480,
'h': 320,
'duration': 2140786,
'size': 1563685,
'mimetype': 'video/mp4'
},
'msgtype': 'm.video'
},
'type': 'm.room.message',
'event_id': '1143273582443PhrSn:example.org',
'room_id': '!1234:example.com',
'sender': '@example:example.org',
'origin_server_ts': 1432735824653,
'unsigned': {'age': 1234}
},
{
'content': {'name': 'The room name'},
'type': 'm.room.name',
'event_id': '2143273582443PhrSn:example.org',
'room_id': '!1234:example.com',
'sender': '@example:example.org',
'origin_server_ts': 1432735824653,
'unsigned': {'age': 1234},
'state_key': ''
},
{
'content': {
'body': 'This is an example text message',
'msgtype': 'm.text',
'format': 'org.matrix.custom.html',
'formatted_body': '<b>This is an example text message</b>'
},
'type': 'm.room.message',
'event_id': '3143273582443PhrSn:example.org',
'room_id': '!1234:example.com',
'sender': '@example:example.org',
'origin_server_ts': 1432735824653,
'unsigned': {'age': 1234}
}
],
'state': [],
};
static Map<String, dynamic> messagesResponseFutureEnd = {
'start': 't789',
'end': 't789',
'chunk': [],
'state': [],
};
static Map<String, dynamic> syncResponse = { static Map<String, dynamic> syncResponse = {
'next_batch': Random().nextDouble().toString(), 'next_batch': Random().nextDouble().toString(),
@ -1401,19 +1337,11 @@ class FakeMatrixApi extends MockClient {
'unsigned': {'age': 1234} 'unsigned': {'age': 1234}
}, },
'/client/r0/rooms/!localpart%3Aserver.abc/messages?from=1234&dir=b&to=1234&limit=10&filter=%7B%22lazy_load_members%22%3Atrue%7D': '/client/r0/rooms/!localpart%3Aserver.abc/messages?from=1234&dir=b&to=1234&limit=10&filter=%7B%22lazy_load_members%22%3Atrue%7D':
(var req) => messagesResponsePast, (var req) => messagesResponse,
'/client/r0/rooms/!localpart%3Aserver.abc/messages?from=&dir=b&limit=10&filter=%7B%22lazy_load_members%22%3Atrue%7D': '/client/r0/rooms/!localpart%3Aserver.abc/messages?from=&dir=b&limit=10&filter=%7B%22lazy_load_members%22%3Atrue%7D':
(var req) => messagesResponsePast, (var req) => messagesResponse,
'/client/r0/rooms/!1234%3Aexample.com/messages?from=1234&dir=b&limit=30&filter=%7B%22lazy_load_members%22%3Atrue%7D': '/client/r0/rooms/!1234%3Aexample.com/messages?from=1234&dir=b&limit=30&filter=%7B%22lazy_load_members%22%3Atrue%7D':
(var req) => messagesResponsePast, (var req) => messagesResponse,
'/client/r0/rooms/!localpart%3Aserver.abc/messages?from=t456&dir=f&to=1234&limit=10&filter=%7B%22lazy_load_members%22%3Atrue%7D':
(var req) => messagesResponseFuture,
'/client/r0/rooms/!1234%3Aexample.com/messages?from=t456&dir=f&limit=30&filter=%7B%22lazy_load_members%22%3Atrue%7D':
(var req) => messagesResponseFuture,
'/client/r0/rooms/!localpart%3Aserver.abc/messages?from=t789&dir=f&to=1234&limit=10&filter=%7B%22lazy_load_members%22%3Atrue%7D':
(var req) => messagesResponseFutureEnd,
'/client/r0/rooms/!1234%3Aexample.com/messages?from=t789&dir=f&limit=30&filter=%7B%22lazy_load_members%22%3Atrue%7D':
(var req) => messagesResponseFutureEnd,
'/client/versions': (var req) => { '/client/versions': (var req) => {
'versions': [ 'versions': [
'r0.0.1', 'r0.0.1',

View File

@ -1,589 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import 'package:matrix/matrix.dart';
import 'package:matrix/src/models/timeline_chunk.dart';
import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm;
import 'fake_client.dart';
void main() {
group('Timeline context', () {
Logs().level = Level.error;
final roomID = '!1234:example.com';
final testTimeStamp = DateTime.now().millisecondsSinceEpoch;
var updateCount = 0;
final insertList = <int>[];
final changeList = <int>[];
final removeList = <int>[];
var olmEnabled = true;
late Client client;
late Room room;
late Timeline timeline;
test('create stuff', () async {
try {
await olm.init();
olm.get_library_version();
} catch (e) {
olmEnabled = false;
Logs().w('[LibOlm] Failed to load LibOlm', e);
}
Logs().i('[LibOlm] Enabled: $olmEnabled');
client = await getClient();
client.sendMessageTimeoutSeconds = 5;
room = Room(
id: roomID, client: client, prev_batch: 't123', roomAccountData: {});
timeline = Timeline(
room: room,
chunk: TimelineChunk(events: [], nextBatch: 't456', prevBatch: 't123'),
onUpdate: () {
updateCount++;
},
onInsert: insertList.add,
onChange: changeList.add,
onRemove: removeList.add,
);
expect(timeline.isFragmentedTimeline, true);
expect(timeline.allowNewEvent, false);
});
test('Request future', () async {
timeline.events.clear();
await timeline.requestFuture();
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 3);
expect(insertList, [0, 1, 2]);
expect(timeline.events.length, 3);
expect(timeline.events[0].eventId, '3143273582443PhrSn:example.org');
expect(timeline.events[1].eventId, '2143273582443PhrSn:example.org');
expect(timeline.events[2].eventId, '1143273582443PhrSn:example.org');
expect(timeline.chunk.nextBatch, 't789');
expect(timeline.isFragmentedTimeline, true);
expect(timeline.allowNewEvent, false);
});
/// We send a message in a fragmented timeline, it didn't reached the end so we shouldn't be displayed.
test('Send message not displayed', () async {
updateCount = 0;
await room.sendTextEvent('test', txid: '1234');
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 0);
expect(insertList, [0, 1, 2]);
expect(insertList.length,
timeline.events.length); // expect no new events to have been added
final eventId = '1844295642248BcDkn:example.org';
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'test'},
'sender': '@alice:example.com',
'status': EventStatus.synced.intValue,
'event_id': eventId,
'unsigned': {'transaction_id': '1234'},
'origin_server_ts': DateTime.now().millisecondsSinceEpoch
},
)); // just assume that it was on the server for this call but not for the following.
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 0);
expect(insertList, [0, 1, 2]);
expect(timeline.events.length,
3); // we still expect the timeline to contain the same numbre of elements
});
test('Request future end of timeline', () async {
await timeline.requestFuture();
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 3);
expect(insertList, [0, 1, 2]);
expect(insertList.length, timeline.events.length);
expect(timeline.events[0].eventId, '3143273582443PhrSn:example.org');
expect(timeline.events[1].eventId, '2143273582443PhrSn:example.org');
expect(timeline.events[2].eventId, '1143273582443PhrSn:example.org');
expect(timeline.chunk.nextBatch, 't789');
expect(timeline.isFragmentedTimeline, true);
expect(timeline.allowNewEvent, true);
});
test('Send message', () async {
await room.sendTextEvent('test', txid: '1234');
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 5);
expect(insertList, [0, 1, 2, 0]);
expect(insertList.length, timeline.events.length);
final eventId = timeline.events[0].eventId;
expect(eventId.startsWith('\$event'), true);
expect(timeline.events[0].status, EventStatus.sent);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'test'},
'sender': '@alice:example.com',
'status': EventStatus.synced.intValue,
'event_id': eventId,
'unsigned': {'transaction_id': '1234'},
'origin_server_ts': DateTime.now().millisecondsSinceEpoch
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 6);
expect(insertList, [0, 1, 2, 0]);
expect(insertList.length, timeline.events.length);
expect(timeline.events[0].eventId, eventId);
expect(timeline.events[0].status, EventStatus.synced);
});
test('Send message with error', () async {
updateCount = 0;
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': EventStatus.sending.intValue,
'event_id': 'abc',
'origin_server_ts': testTimeStamp
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 1);
await room.sendTextEvent('test', txid: 'errortxid');
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 3);
await room.sendTextEvent('test', txid: 'errortxid2');
await Future.delayed(Duration(milliseconds: 50));
await room.sendTextEvent('test', txid: 'errortxid3');
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 7);
expect(insertList, [0, 1, 2, 0, 0, 0, 1, 2]);
expect(insertList.length, timeline.events.length);
expect(changeList, [0, 0, 0, 1, 2]);
expect(removeList, []);
expect(timeline.events[0].status, EventStatus.error);
expect(timeline.events[1].status, EventStatus.error);
expect(timeline.events[2].status, EventStatus.error);
});
test('Remove message', () async {
updateCount = 0;
await timeline.events[0].remove();
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 1);
expect(insertList, [0, 1, 2, 0, 0, 0, 1, 2]);
expect(changeList, [0, 0, 0, 1, 2]);
expect(removeList, [0]);
expect(timeline.events.length, 7);
expect(timeline.events[0].status, EventStatus.error);
});
test('getEventById', () async {
var event = await timeline.getEventById('abc');
expect(event?.content, {'msgtype': 'm.text', 'body': 'Testcase'});
event = await timeline.getEventById('not_found');
expect(event, null);
event = await timeline.getEventById('unencrypted_event');
expect(event?.body, 'This is an example text message');
if (olmEnabled) {
event = await timeline.getEventById('encrypted_event');
// the event is invalid but should have traces of attempting to decrypt
expect(event?.messageType, MessageTypes.BadEncrypted);
}
});
test('Resend message', () async {
timeline.events.clear();
updateCount = 0;
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': EventStatus.error.intValue,
'event_id': 'new-test-event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'newresend'},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.error);
await timeline.events[0].sendAgain();
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 3);
expect(insertList, [0, 1, 2, 0, 0, 0, 1, 2, 0]);
expect(changeList, [0, 0, 0, 1, 2, 0, 0]);
expect(removeList, [0]);
expect(timeline.events.length, 1);
expect(timeline.events[0].status, EventStatus.sent);
});
test('Clear cache on limited timeline', () async {
client.onSync.add(
SyncUpdate(
nextBatch: '1234',
rooms: RoomsUpdate(
join: {
roomID: JoinedRoomUpdate(
timeline: TimelineUpdate(
limited: true,
prevBatch: 'blah',
),
unreadNotifications: UnreadNotificationCounts(
highlightCount: 0,
notificationCount: 0,
),
),
},
),
),
);
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events.isEmpty, true);
});
test('sort errors on top', () async {
timeline.events.clear();
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': EventStatus.error.intValue,
'event_id': 'abc',
'origin_server_ts': testTimeStamp
},
));
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': EventStatus.synced.intValue,
'event_id': 'def',
'origin_server_ts': testTimeStamp + 5
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.error);
expect(timeline.events[1].status, EventStatus.synced);
});
test('sending event to failed update', () async {
timeline.events.clear();
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': EventStatus.sending.intValue,
'event_id': 'will-fail',
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.sending);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': EventStatus.error.intValue,
'event_id': 'will-fail',
'origin_server_ts': testTimeStamp
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.error);
expect(timeline.events.length, 1);
});
test('setReadMarker', () async {
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': EventStatus.synced.intValue,
'event_id': 'will-work',
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
},
));
await Future.delayed(Duration(milliseconds: 50));
room.notificationCount = 1;
await timeline.setReadMarker();
expect(room.notificationCount, 0);
});
test('sending an event and the http request finishes first, 0 -> 1 -> 2',
() async {
timeline.events.clear();
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': EventStatus.sending.intValue,
'event_id': 'transaction',
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.sending);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': EventStatus.sent.intValue,
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'}
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.sent);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': EventStatus.synced.intValue,
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'}
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
});
test('sending an event where the sync reply arrives first, 0 -> 2 -> 1',
() async {
timeline.events.clear();
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'event_id': 'transaction',
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
'unsigned': {
messageSendingStatusKey: EventStatus.sending.intValue,
'transaction_id': 'transaction',
},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.sending);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {
'transaction_id': 'transaction',
messageSendingStatusKey: EventStatus.synced.intValue,
},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {
'transaction_id': 'transaction',
messageSendingStatusKey: EventStatus.sent.intValue,
},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
});
test('sending an event 0 -> -1 -> 2', () async {
timeline.events.clear();
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': EventStatus.sending.intValue,
'event_id': 'transaction',
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.sending);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': EventStatus.error.intValue,
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.error);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': EventStatus.synced.intValue,
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
});
test('sending an event 0 -> 2 -> -1', () async {
timeline.events.clear();
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': EventStatus.sending.intValue,
'event_id': 'transaction',
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.sending);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': EventStatus.synced.intValue,
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
roomID: roomID,
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': EventStatus.error.intValue,
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
});
test('logout', () async {
await client.logout();
});
});
}

View File

@ -17,7 +17,6 @@
*/ */
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:matrix/src/models/timeline_chunk.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm; import 'package:olm/olm.dart' as olm;
@ -53,7 +52,7 @@ void main() {
id: roomID, client: client, prev_batch: '1234', roomAccountData: {}); id: roomID, client: client, prev_batch: '1234', roomAccountData: {});
timeline = Timeline( timeline = Timeline(
room: room, room: room,
chunk: TimelineChunk(events: []), events: [],
onUpdate: () { onUpdate: () {
updateCount++; updateCount++;
}, },