Merge branch 'soru/fix-too-early-event-update' into 'master'

fix issue with sending messages

See merge request famedly/famedlysdk!393
This commit is contained in:
Christian Pauly 2020-07-23 08:09:00 +00:00
commit ad8135990d
11 changed files with 587 additions and 82 deletions

View File

@ -787,7 +787,7 @@ class MatrixApi {
String stateKey = '',
]) async {
final response = await request(RequestType.PUT,
'/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/state/${Uri.encodeQueryComponent(eventType)}/${Uri.encodeQueryComponent(stateKey)}',
'/client/r0/rooms/${Uri.encodeComponent(roomId)}/state/${Uri.encodeComponent(eventType)}/${Uri.encodeComponent(stateKey)}',
data: content);
return response['event_id'];
}
@ -803,7 +803,7 @@ class MatrixApi {
Map<String, dynamic> content,
) async {
final response = await request(RequestType.PUT,
'/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/send/${Uri.encodeQueryComponent(eventType)}/${Uri.encodeQueryComponent(txnId)}',
'/client/r0/rooms/${Uri.encodeComponent(roomId)}/send/${Uri.encodeComponent(eventType)}/${Uri.encodeComponent(txnId)}',
data: content);
return response['event_id'];
}
@ -818,7 +818,7 @@ class MatrixApi {
String reason,
}) async {
final response = await request(RequestType.PUT,
'/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/redact/${Uri.encodeQueryComponent(eventId)}/${Uri.encodeQueryComponent(txnId)}',
'/client/r0/rooms/${Uri.encodeComponent(roomId)}/redact/${Uri.encodeComponent(eventId)}/${Uri.encodeComponent(txnId)}',
data: {
if (reason != null) 'reason': reason,
});
@ -1734,7 +1734,7 @@ class MatrixApi {
Future<Map<String, Tag>> requestRoomTags(String userId, String roomId) async {
final response = await request(
RequestType.GET,
'/client/r0/user/${Uri.encodeQueryComponent(userId)}/rooms/${Uri.encodeQueryComponent(roomId)}/tags',
'/client/r0/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/tags',
);
return (response['tags'] as Map).map(
(k, v) => MapEntry(k, Tag.fromJson(v)),
@ -1750,7 +1750,7 @@ class MatrixApi {
double order,
}) async {
await request(RequestType.PUT,
'/client/r0/user/${Uri.encodeQueryComponent(userId)}/rooms/${Uri.encodeQueryComponent(roomId)}/tags/${Uri.encodeQueryComponent(tag)}',
'/client/r0/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/tags/${Uri.encodeComponent(tag)}',
data: {
if (order != null) 'order': order,
});
@ -1762,7 +1762,7 @@ class MatrixApi {
Future<void> removeRoomTag(String userId, String roomId, String tag) async {
await request(
RequestType.DELETE,
'/client/r0/user/${Uri.encodeQueryComponent(userId)}/rooms/${Uri.encodeQueryComponent(roomId)}/tags/${Uri.encodeQueryComponent(tag)}',
'/client/r0/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/tags/${Uri.encodeComponent(tag)}',
);
return;
}
@ -1777,7 +1777,7 @@ class MatrixApi {
) async {
await request(
RequestType.PUT,
'/client/r0/user/${Uri.encodeQueryComponent(userId)}/account_data/${Uri.encodeQueryComponent(type)}',
'/client/r0/user/${Uri.encodeComponent(userId)}/account_data/${Uri.encodeComponent(type)}',
data: content,
);
return;
@ -1791,7 +1791,7 @@ class MatrixApi {
) async {
return await request(
RequestType.GET,
'/client/r0/user/${Uri.encodeQueryComponent(userId)}/account_data/${Uri.encodeQueryComponent(type)}',
'/client/r0/user/${Uri.encodeComponent(userId)}/account_data/${Uri.encodeComponent(type)}',
);
}
@ -1806,7 +1806,7 @@ class MatrixApi {
) async {
await request(
RequestType.PUT,
'/client/r0/user/${Uri.encodeQueryComponent(userId)}/rooms/${Uri.encodeQueryComponent(roomId)}/account_data/${Uri.encodeQueryComponent(type)}',
'/client/r0/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/account_data/${Uri.encodeComponent(type)}',
data: content,
);
return;
@ -1821,7 +1821,7 @@ class MatrixApi {
) async {
return await request(
RequestType.GET,
'/client/r0/user/${Uri.encodeQueryComponent(userId)}/rooms/${Uri.encodeQueryComponent(roomId)}/account_data/${Uri.encodeQueryComponent(type)}',
'/client/r0/user/${Uri.encodeComponent(userId)}/rooms/${Uri.encodeComponent(roomId)}/account_data/${Uri.encodeComponent(type)}',
);
}
@ -1830,7 +1830,7 @@ class MatrixApi {
Future<WhoIsInfo> requestWhoIsInfo(String userId) async {
final response = await request(
RequestType.GET,
'/client/r0/admin/whois/${Uri.encodeQueryComponent(userId)}',
'/client/r0/admin/whois/${Uri.encodeComponent(userId)}',
);
return WhoIsInfo.fromJson(response);
}
@ -1845,7 +1845,7 @@ class MatrixApi {
String filter,
}) async {
final response = await request(RequestType.GET,
'/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/context/${Uri.encodeQueryComponent(eventId)}',
'/client/r0/rooms/${Uri.encodeComponent(roomId)}/context/${Uri.encodeComponent(eventId)}',
query: {
if (filter != null) 'filter': filter,
if (limit != null) 'limit': limit.toString(),
@ -1862,7 +1862,7 @@ class MatrixApi {
int score,
) async {
await request(RequestType.POST,
'/client/r0/rooms/${Uri.encodeQueryComponent(roomId)}/report/${Uri.encodeQueryComponent(eventId)}',
'/client/r0/rooms/${Uri.encodeComponent(roomId)}/report/${Uri.encodeComponent(eventId)}',
data: {
'reason': reason,
'score': score,

View File

@ -361,9 +361,32 @@ class Database extends _$Database {
if ((status == 1 || status == -1) &&
eventContent['unsigned'] is Map<String, dynamic> &&
eventContent['unsigned']['transaction_id'] is String) {
// status changed and we have an old transaction id --> update event id and stuffs
await updateEventStatus(status, eventContent['event_id'], clientId,
eventContent['unsigned']['transaction_id'], chatId);
final allOldEvents =
await getEvent(clientId, eventContent['event_id'], chatId).get();
if (allOldEvents.isNotEmpty) {
// we were likely unable to change transaction_id -> event_id.....because the event ID already exists!
// So, we try to fetch the old event
// the transaction id event will automatically be deleted further down
final oldEvent = allOldEvents.first;
// do we update the status? We should allow 0 -> -1 updates and status increases
if (status > oldEvent.status ||
(oldEvent.status == 0 && status == -1)) {
// update the status
await updateEventStatusOnly(
status, clientId, eventContent['event_id'], chatId);
}
} else {
// status changed and we have an old transaction id --> update event id and stuffs
try {
await updateEventStatus(status, eventContent['event_id'], clientId,
eventContent['unsigned']['transaction_id'], chatId);
} catch (err) {
// we could not update the transaction id to the event id....so it already exists
// as we just tried to fetch the event previously this is a race condition if the event comes down sync in the mean time
// that means that the status we already have in the database is likely more accurate
// than our status. So, we just ignore this error
}
}
} else {
DbEvent oldEvent;
if (type == 'history') {

View File

@ -6033,6 +6033,21 @@ abstract class _$Database extends GeneratedDatabase {
);
}
Future<int> updateEventStatusOnly(
int status, int client_id, String event_id, String room_id) {
return customUpdate(
'UPDATE events SET status = :status WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id',
variables: [
Variable.withInt(status),
Variable.withInt(client_id),
Variable.withString(event_id),
Variable.withString(room_id)
],
updates: {events},
updateKind: UpdateKind.update,
);
}
DbRoomState _rowToDbRoomState(QueryRow row) {
return DbRoomState(
clientId: row.readInt('client_id'),

View File

@ -208,6 +208,7 @@ getAllAccountData: SELECT * FROM account_data WHERE client_id = :client_id;
storeAccountData: INSERT OR REPLACE INTO account_data (client_id, type, content) VALUES (:client_id, :type, :content);
updateEvent: UPDATE events SET unsigned = :unsigned, content = :content, prev_content = :prev_content WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id;
updateEventStatus: UPDATE events SET status = :status, event_id = :new_event_id WHERE client_id = :client_id AND event_id = :old_event_id AND room_id = :room_id;
updateEventStatusOnly: UPDATE events SET status = :status WHERE client_id = :client_id AND event_id = :event_id AND room_id = :room_id;
getImportantRoomStates: SELECT * FROM room_states WHERE client_id = :client_id AND type IN :events;
getAllRoomStates: SELECT * FROM room_states WHERE client_id = :client_id;
getUnimportantRoomStatesForRoom: SELECT * FROM room_states WHERE client_id = :client_id AND room_id = :room_id AND type NOT IN :events;

View File

@ -312,7 +312,8 @@ class Event extends MatrixEvent {
/// Try to send this event again. Only works with events of status -1.
Future<String> sendAgain({String txid}) async {
if (status != -1) return null;
await remove();
// we do not remove the event here. It will automatically be updated
// in the `sendEvent` method to transition -1 -> 0 -> 1 -> 2
final eventID = await room.sendEvent(
content,
txid: txid ?? unsigned['transaction_id'],

View File

@ -122,10 +122,31 @@ class Timeline {
}
int _findEvent({String event_id, String unsigned_txid}) {
// we want to find any existing event where either the passed event_id or the passed unsigned_txid
// matches either the event_id or transaction_id of the existing event.
// For that we create two sets, searchNeedle, what we search, and searchHaystack, where we check if there is a match.
// Now, after having these two sets, if the intersect between them is non-empty, we know that we have at least one match in one pair,
// thus meaning we found our element.
final searchNeedle = <String>{};
if (event_id != null) {
searchNeedle.add(event_id);
}
if (unsigned_txid != null) {
searchNeedle.add(unsigned_txid);
}
int i;
for (i = 0; i < events.length; i++) {
if (events[i].eventId == event_id ||
(unsigned_txid != null && events[i].eventId == unsigned_txid)) break;
final searchHaystack = <String>{};
if (events[i].eventId != null) {
searchHaystack.add(events[i].eventId);
}
if (events[i].unsigned != null &&
events[i].unsigned['transaction_id'] != null) {
searchHaystack.add(events[i].unsigned['transaction_id']);
}
if (searchNeedle.intersection(searchHaystack).isNotEmpty) {
break;
}
}
return i;
}
@ -135,6 +156,7 @@ class Timeline {
if (eventUpdate.roomID != room.id) return;
if (eventUpdate.type == 'timeline' || eventUpdate.type == 'history') {
var status = eventUpdate.content['status'] ?? 2;
// Redaction events are handled as modification for existing events.
if (eventUpdate.eventType == EventTypes.Redaction) {
final eventId = _findEvent(event_id: eventUpdate.content['redacts']);
@ -142,13 +164,10 @@ class Timeline {
events[eventId].setRedactionEvent(Event.fromJson(
eventUpdate.content, room, eventUpdate.sortOrder));
}
} else if (eventUpdate.content['status'] == -2) {
} else if (status == -2) {
var i = _findEvent(event_id: eventUpdate.content['event_id']);
if (i < events.length) events.removeAt(i);
}
// Is this event already in the timeline?
else if (eventUpdate.content['unsigned'] is Map &&
eventUpdate.content['unsigned']['transaction_id'] is String) {
} else {
var i = _findEvent(
event_id: eventUpdate.content['event_id'],
unsigned_txid: eventUpdate.content['unsigned'] is Map
@ -156,34 +175,29 @@ class Timeline {
: null);
if (i < events.length) {
// we want to preserve the old sort order
final tempSortOrder = events[i].sortOrder;
// if the old status is larger than the new one, we also want to preserve the old status
final oldStatus = events[i].status;
events[i] = Event.fromJson(
eventUpdate.content, room, eventUpdate.sortOrder);
events[i].sortOrder = tempSortOrder;
// do we preserve the status? we should allow 0 -> -1 updates and status increases
if (status < oldStatus && !(status == -1 && oldStatus == 0)) {
events[i].status = oldStatus;
}
} else {
var newEvent = Event.fromJson(
eventUpdate.content, room, eventUpdate.sortOrder);
if (eventUpdate.type == 'history' &&
events.indexWhere(
(e) => e.eventId == eventUpdate.content['event_id']) !=
-1) return;
events.insert(0, newEvent);
if (onInsert != null) onInsert(0);
}
} else {
Event newEvent;
var senderUser = room
.getState(
EventTypes.RoomMember, eventUpdate.content['sender'])
?.asUser ??
await room.client.database?.getUser(
room.client.id, eventUpdate.content['sender'], room);
if (senderUser != null) {
eventUpdate.content['displayname'] = senderUser.displayName;
eventUpdate.content['avatar_url'] = senderUser.avatarUrl.toString();
}
newEvent =
Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder);
if (eventUpdate.type == 'history' &&
events.indexWhere(
(e) => e.eventId == eventUpdate.content['event_id']) !=
-1) return;
events.insert(0, newEvent);
if (onInsert != null) onInsert(0);
}
}
sortAndUpdate();

View File

@ -89,7 +89,7 @@ void main() {
// account_data for this test
final content = FakeMatrixApi
.calledEndpoints[
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal']
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best%20animal']
.first;
client.accountData['best animal'] = BasicEvent.fromJson({
'type': 'best animal',

View File

@ -80,7 +80,7 @@ class FakeMatrixApi extends MockClient {
res = {'displayname': ''};
} else if (method == 'PUT' &&
action.contains(
'/client/r0/rooms/%211234%3AfakeServer.notExisting/send/')) {
'/client/r0/rooms/!1234%3AfakeServer.notExisting/send/')) {
res = {'event_id': '\$event${FakeMatrixApi.eventCounter++}'};
} else {
res = {
@ -748,7 +748,7 @@ class FakeMatrixApi extends MockClient {
'app_url': 'https://custom.app.example.org'
}
},
'/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags':
'/client/r0/user/%40alice%3Aexample.com/rooms/!localpart%3Aexample.com/tags':
(var req) => {
'tags': {
'm.favourite': {'order': 0.1},
@ -1982,21 +1982,21 @@ class FakeMatrixApi extends MockClient {
(var req) => {},
'/client/r0/pushrules/global/content/nocake/enabled': (var req) => {},
'/client/r0/pushrules/global/content/nocake/actions': (var req) => {},
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.history_visibility':
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.history_visibility':
(var req) => {},
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.join_rules':
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.join_rules':
(var req) => {},
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.guest_access':
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.guest_access':
(var req) => {},
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.call.invite/1234':
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.room.call.invite/1234':
(var req) => {},
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.call.answer/1234':
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.room.call.answer/1234':
(var req) => {},
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.call.candidates/1234':
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.room.call.candidates/1234':
(var req) => {},
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.call.hangup/1234':
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.room.call.hangup/1234':
(var req) => {},
'/client/r0/rooms/%211234%3Aexample.com/redact/1143273582443PhrSn%3Aexample.org/1234':
'/client/r0/rooms/!1234%3Aexample.com/redact/1143273582443PhrSn%3Aexample.org/1234':
(var req) => {'event_id': '1234'},
'/client/r0/pushrules/global/room/!localpart%3Aserver.abc': (var req) =>
{},
@ -2006,23 +2006,27 @@ class FakeMatrixApi extends MockClient {
(var req) => {},
'/client/r0/devices/QBUAZIFURK': (var req) => {},
'/client/r0/directory/room/%23testalias%3Aexample.com': (var reqI) => {},
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.message/testtxid':
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.room.message/testtxid':
(var reqI) => {
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
},
'/client/r0/rooms/!localpart%3Aexample.com/typing/%40alice%3Aexample.com':
(var req) => {},
'/client/r0/rooms/%211234%3Aexample.com/send/m.room.message/1234':
'/client/r0/rooms/!1234%3Aexample.com/send/m.room.message/1234':
(var reqI) => {
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
},
'/client/r0/user/%40test%3AfakeServer.notExisting/rooms/%21localpart%3Aserver.abc/tags/m.favourite':
'/client/r0/rooms/!1234%3Aexample.com/send/m.room.message/newresend':
(var reqI) => {
'event_id': '\$event${FakeMatrixApi.eventCounter++}',
},
'/client/r0/user/%40test%3AfakeServer.notExisting/rooms/!localpart%3Aserver.abc/tags/m.favourite':
(var req) => {},
'/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags/testtag':
'/client/r0/user/%40alice%3Aexample.com/rooms/!localpart%3Aexample.com/tags/testtag':
(var req) => {},
'/client/r0/user/%40alice%3Aexample.com/account_data/test.account.data':
(var req) => {},
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best+animal':
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best%20animal':
(var req) => {},
'/client/r0/user/%40alice%3Aexample.com/rooms/1234/account_data/test.account.data':
(var req) => {},
@ -2034,27 +2038,27 @@ class FakeMatrixApi extends MockClient {
'/client/r0/profile/%40alice%3Aexample.com/avatar_url': (var reqI) => {},
'/client/r0/profile/%40test%3AfakeServer.notExisting/avatar_url':
(var reqI) => {},
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.encryption':
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.encryption':
(var reqI) => {'event_id': 'YUwRidLecu:example.com'},
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.avatar':
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.avatar':
(var reqI) => {'event_id': 'YUwRidLecu:example.com'},
'/client/r0/rooms/%21localpart%3Aserver.abc/send/m.room.message/1234':
'/client/r0/rooms/!localpart%3Aserver.abc/send/m.room.message/1234':
(var reqI) => {'event_id': 'YUwRidLecu:example.com'},
'/client/r0/rooms/%21localpart%3Aserver.abc/redact/1234/1234':
(var reqI) => {'event_id': 'YUwRidLecu:example.com'},
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.name':
'/client/r0/rooms/!localpart%3Aserver.abc/redact/1234/1234': (var reqI) =>
{'event_id': 'YUwRidLecu:example.com'},
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.name':
(var reqI) => {
'event_id': '42',
},
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.topic':
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.topic':
(var reqI) => {
'event_id': '42',
},
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.pinned_events':
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.pinned_events':
(var reqI) => {
'event_id': '42',
},
'/client/r0/rooms/%21localpart%3Aserver.abc/state/m.room.power_levels':
'/client/r0/rooms/!localpart%3Aserver.abc/state/m.room.power_levels':
(var reqI) => {
'event_id': '42',
},
@ -2083,9 +2087,9 @@ class FakeMatrixApi extends MockClient {
'/client/r0/pushrules/global/content/nocake': (var req) => {},
'/client/r0/pushrules/global/override/!localpart%3Aserver.abc':
(var req) => {},
'/client/r0/user/%40test%3AfakeServer.notExisting/rooms/%21localpart%3Aserver.abc/tags/m.favourite':
'/client/r0/user/%40test%3AfakeServer.notExisting/rooms/!localpart%3Aserver.abc/tags/m.favourite':
(var req) => {},
'/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags/testtag':
'/client/r0/user/%40alice%3Aexample.com/rooms/!localpart%3Aexample.com/tags/testtag':
(var req) => {},
'/client/unstable/room_keys/version/5': (var req) => {},
'/client/unstable/room_keys/keys/${Uri.encodeComponent('!726s6s6q:example.com')}/${Uri.encodeComponent('ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU')}?version=5':

View File

@ -1377,7 +1377,7 @@ void main() {
'@alice:example.com', '!localpart:example.com');
expect(
FakeMatrixApi.api['GET'][
'/client/r0/user/%40alice%3Aexample.com/rooms/%21localpart%3Aexample.com/tags']({}),
'/client/r0/user/%40alice%3Aexample.com/rooms/!localpart%3Aexample.com/tags']({}),
{'tags': response.map((k, v) => MapEntry(k, v.toJson()))},
);

View File

@ -0,0 +1,184 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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:famedlysdk/famedlysdk.dart';
import 'package:test/test.dart';
import 'fake_database.dart';
void main() {
group('Databse', () {
final database = getDatabase();
var clientId = -1;
var room = Room(id: '!room:blubb');
test('setupDatabase', () async {
clientId = await database.insertClient(
'testclient',
'https://example.org',
'blubb',
'@test:example.org',
null,
null,
null,
null);
});
test('storeEventUpdate', () async {
// store a simple update
var update = EventUpdate(
type: 'timeline',
roomID: room.id,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'origin_server_ts': 100,
'content': {'blah': 'blubb'},
'event_id': '\$event-1',
'sender': '@blah:blubb',
},
sortOrder: 0.0,
);
await database.storeEventUpdate(clientId, update);
var event = await database.getEventById(clientId, '\$event-1', room);
expect(event.eventId, '\$event-1');
// insert a transaction id
update = EventUpdate(
type: 'timeline',
roomID: room.id,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'origin_server_ts': 100,
'content': {'blah': 'blubb'},
'event_id': 'transaction-1',
'sender': '@blah:blubb',
'status': 0,
},
sortOrder: 0.0,
);
await database.storeEventUpdate(clientId, update);
event = await database.getEventById(clientId, 'transaction-1', room);
expect(event.eventId, 'transaction-1');
update = EventUpdate(
type: 'timeline',
roomID: room.id,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'origin_server_ts': 100,
'content': {'blah': 'blubb'},
'event_id': '\$event-2',
'sender': '@blah:blubb',
'unsigned': {
'transaction_id': 'transaction-1',
},
'status': 1,
},
sortOrder: 0.0,
);
await database.storeEventUpdate(clientId, update);
event = await database.getEventById(clientId, 'transaction-1', room);
expect(event, null);
event = await database.getEventById(clientId, '\$event-2', room);
// insert a transaction id if the event id for it already exists
update = EventUpdate(
type: 'timeline',
roomID: room.id,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'origin_server_ts': 100,
'content': {'blah': 'blubb'},
'event_id': '\$event-3',
'sender': '@blah:blubb',
'status': 0,
},
sortOrder: 0.0,
);
await database.storeEventUpdate(clientId, update);
event = await database.getEventById(clientId, '\$event-3', room);
expect(event.eventId, '\$event-3');
update = EventUpdate(
type: 'timeline',
roomID: room.id,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'origin_server_ts': 100,
'content': {'blah': 'blubb'},
'event_id': '\$event-3',
'sender': '@blah:blubb',
'status': 1,
'unsigned': {
'transaction_id': 'transaction-2',
},
},
sortOrder: 0.0,
);
await database.storeEventUpdate(clientId, update);
event = await database.getEventById(clientId, '\$event-3', room);
expect(event.eventId, '\$event-3');
expect(event.status, 1);
event = await database.getEventById(clientId, 'transaction-2', room);
expect(event, null);
// insert transaction id and not update status
update = EventUpdate(
type: 'timeline',
roomID: room.id,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'origin_server_ts': 100,
'content': {'blah': 'blubb'},
'event_id': '\$event-4',
'sender': '@blah:blubb',
'status': 2,
},
sortOrder: 0.0,
);
await database.storeEventUpdate(clientId, update);
event = await database.getEventById(clientId, '\$event-4', room);
expect(event.eventId, '\$event-4');
update = EventUpdate(
type: 'timeline',
roomID: room.id,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'origin_server_ts': 100,
'content': {'blah': 'blubb'},
'event_id': '\$event-4',
'sender': '@blah:blubb',
'status': 1,
'unsigned': {
'transaction_id': 'transaction-3',
},
},
sortOrder: 0.0,
);
await database.storeEventUpdate(clientId, update);
event = await database.getEventById(clientId, '\$event-4', room);
expect(event.eventId, '\$event-4');
expect(event.status, 2);
event = await database.getEventById(clientId, 'transaction-3', room);
expect(event, null);
});
});
}

View File

@ -214,14 +214,29 @@ void main() {
});
test('Resend message', () async {
await timeline.events[0].sendAgain(txid: '1234');
client.onEvent.add(EventUpdate(
type: 'timeline',
roomID: roomID,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': -1,
'event_id': 'new-test-event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'newresend'},
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
await timeline.events[0].sendAgain();
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 17);
expect(insertList, [0, 0, 0, 0, 0, 0, 0, 0]);
expect(timeline.events.length, 6);
expect(timeline.events.length, 7);
expect(timeline.events[0].status, 1);
});
@ -231,12 +246,12 @@ void main() {
await Future.delayed(Duration(milliseconds: 50));
expect(updateCount, 20);
expect(timeline.events.length, 9);
expect(timeline.events[6].eventId, '3143273582443PhrSn:example.org');
expect(timeline.events[7].eventId, '2143273582443PhrSn:example.org');
expect(timeline.events[8].eventId, '1143273582443PhrSn:example.org');
expect(timeline.events.length, 10);
expect(timeline.events[7].eventId, '3143273582443PhrSn:example.org');
expect(timeline.events[8].eventId, '2143273582443PhrSn:example.org');
expect(timeline.events[9].eventId, '1143273582443PhrSn:example.org');
expect(room.prev_batch, 't47409-4357353_219380_26003_2265');
await timeline.events[8].redact(reason: 'test', txid: '1234');
await timeline.events[9].redact(reason: 'test', txid: '1234');
});
test('Clear cache on limited timeline', () async {
@ -251,5 +266,253 @@ void main() {
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events.isEmpty, true);
});
test('sending event to failed update', () async {
timeline.events.clear();
client.onEvent.add(EventUpdate(
type: 'timeline',
roomID: roomID,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 0,
'event_id': 'will-fail',
'origin_server_ts': testTimeStamp
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 0);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: 'timeline',
roomID: roomID,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': -1,
'event_id': 'will-fail',
'origin_server_ts': testTimeStamp
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, -1);
expect(timeline.events.length, 1);
});
test('sending an event and the http request finishes first, 0 -> 1 -> 2',
() async {
timeline.events.clear();
client.onEvent.add(EventUpdate(
type: 'timeline',
roomID: roomID,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 0,
'event_id': 'transaction',
'origin_server_ts': testTimeStamp
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 0);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: 'timeline',
roomID: roomID,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 1,
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'}
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 1);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: 'timeline',
roomID: roomID,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 2,
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'}
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 2);
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: 'timeline',
roomID: roomID,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 0,
'event_id': 'transaction',
'origin_server_ts': testTimeStamp
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 0);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: 'timeline',
roomID: roomID,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 2,
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'}
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 2);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: 'timeline',
roomID: roomID,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 1,
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'}
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 2);
expect(timeline.events.length, 1);
});
test('sending an event 0 -> -1 -> 2', () async {
timeline.events.clear();
client.onEvent.add(EventUpdate(
type: 'timeline',
roomID: roomID,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 0,
'event_id': 'transaction',
'origin_server_ts': testTimeStamp
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 0);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: 'timeline',
roomID: roomID,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': -1,
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'},
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, -1);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: 'timeline',
roomID: roomID,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 2,
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'},
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 2);
expect(timeline.events.length, 1);
});
test('sending an event 0 -> 2 -> -1', () async {
timeline.events.clear();
client.onEvent.add(EventUpdate(
type: 'timeline',
roomID: roomID,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 0,
'event_id': 'transaction',
'origin_server_ts': testTimeStamp
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 0);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: 'timeline',
roomID: roomID,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 2,
'event_id': '\$event',
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'},
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 2);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: 'timeline',
roomID: roomID,
eventType: 'm.room.message',
content: {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': -1,
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'},
},
sortOrder: room.newSortOrder));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 2);
expect(timeline.events.length, 1);
});
});
}