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;
@ -1590,10 +1584,10 @@ class Client extends MatrixApi {
final timelineEvents = room.timeline?.events; final timelineEvents = room.timeline?.events;
if (timelineEvents != null && timelineEvents.isNotEmpty) { if (timelineEvents != null && timelineEvents.isNotEmpty) {
await _handleRoomEvents( await _handleRoomEvents(
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)? onRemove,
{void Function(int index)? onChange, void Function(int insertID)? onInsert,
void Function(int index)? onRemove, void Function()? onUpdate,
void Function(int insertID)? onInsert, }) async {
void Function()? onNewEvent,
void Function()? onUpdate,
String? eventContextId}) async {
await postLoad(); await postLoad();
final events = await client.database?.getEventList(
this,
limit: defaultHistoryCount,
) ??
<Event>[];
final _events = await client.database?.getEventList( // Try again to decrypt encrypted events and update the database.
this, if (encrypted && client.database != null && client.encryptionEnabled) {
limit: defaultHistoryCount, await client.database?.transaction(() async {
); for (var i = 0; i < events.length; i++) {
if (events[i].type == EventTypes.Encrypted &&
var chunk = TimelineChunk(events: _events ?? []); events[i].content['can_request_session'] == true) {
events[i] = await client.encryption!
if (_events != null) { .decryptRoomEvent(id, events[i], store: true);
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 {
for (var i = 0; i < chunk.events.length; i++) {
if (chunk.events[i].content['can_request_session'] == true) {
chunk.events[i] = await client.encryption!
.decryptRoomEvent(id, chunk.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 room,
: await room.client.database?.getEventList( start: events.length,
room, limit: Room.defaultHistoryCount,
start: events.length, );
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,37 +95,20 @@ 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; for (var i = startIndex; i < endIndex; i++) {
for (var i = startIndex; i < endIndex; i++) { onInsert?.call(i);
onInsert?.call(i);
}
} else {
events.insertAll(0, eventsFromStore);
final startIndex = eventsFromStore.length;
final endIndex = 0;
for (var i = startIndex; i > endIndex; i--) {
onInsert?.call(i);
}
} }
} else { } else {
Logs().i('No more events found in the store. Request from server...'); Logs().v('No more events found in the store. Request from server...');
if (isFragmentedTimeline) { await room.requestHistory(
await getRoomEvents( historyCount: historyCount,
historyCount: historyCount, onHistoryReceived: () {
direction: direction, _collectHistoryUpdates = true;
); },
} else { );
await room.requestHistory(
historyCount: historyCount,
direction: direction,
onHistoryReceived: () {
_collectHistoryUpdates = true;
},
);
}
} }
} finally { } finally {
_collectHistoryUpdates = false; _collectHistoryUpdates = 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. this.onUpdate,
Future<int> getRoomEvents( this.onChange,
{int historyCount = Room.defaultHistoryCount, this.onInsert,
direction = Direction.b}) async { this.onRemove,
final resp = await room.client.getRoomEvents( }) : events = events ?? [] {
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.onChange,
this.onInsert,
this.onRemove,
this.onNewEvent,
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++;
}, },