refactor: Change event status to enum

This commit is contained in:
Jindřich Pikora 2021-10-14 13:31:07 +00:00
parent d5e5500ac5
commit c6e0359522
12 changed files with 2201 additions and 124 deletions

View File

@ -36,6 +36,7 @@ export 'src/utils/commands_extension.dart';
export 'src/utils/http_timeout.dart';
export 'src/client.dart';
export 'src/event.dart';
export 'src/event_status.dart';
export 'src/room.dart';
export 'src/voip_content.dart';
export 'src/timeline.dart';

View File

@ -19,16 +19,17 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:matrix/encryption/utils/stored_inbound_group_session.dart';
import 'package:matrix/encryption/utils/ssss_cache.dart';
import 'package:matrix/encryption/utils/outbound_group_session.dart';
import 'package:matrix/encryption/utils/olm_session.dart';
import 'dart:typed_data';
import 'package:matrix/matrix.dart';
import 'package:matrix/src/utils/queued_to_device_event.dart';
import 'package:hive/hive.dart';
import 'package:matrix/encryption/utils/olm_session.dart';
import 'package:matrix/encryption/utils/outbound_group_session.dart';
import 'package:matrix/encryption/utils/ssss_cache.dart';
import 'package:matrix/encryption/utils/stored_inbound_group_session.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix/src/event_status.dart';
import 'package:matrix/src/utils/queued_to_device_event.dart';
import 'package:matrix/src/utils/run_benchmarked.dart';
import 'package:matrix/src/utils/run_benchmarked.dart';
/// This is a basic database for the Matrix SDK using the hive store. You need
@ -81,25 +82,43 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
late LazyBox _seenDeviceKeysBox;
String get _clientBoxName => '$name.box.client';
String get _accountDataBoxName => '$name.box.account_data';
String get _roomsBoxName => '$name.box.rooms';
String get _toDeviceQueueBoxName => '$name.box.to_device_queue';
String get _roomStateBoxName => '$name.box.room_states';
String get _roomMembersBoxName => '$name.box.room_members';
String get _roomAccountDataBoxName => '$name.box.room_account_data';
String get _inboundGroupSessionsBoxName => '$name.box.inbound_group_session';
String get _outboundGroupSessionsBoxName =>
'$name.box.outbound_group_session';
String get _olmSessionsBoxName => '$name.box.olm_session';
String get _userDeviceKeysBoxName => '$name.box.user_device_keys';
String get _userDeviceKeysOutdatedBoxName =>
'$name.box.user_device_keys_outdated';
String get _userCrossSigningKeysBoxName => '$name.box.cross_signing_keys';
String get _ssssCacheBoxName => '$name.box.ssss_cache';
String get _presencesBoxName => '$name.box.presences';
String get _timelineFragmentsBoxName => '$name.box.timeline_fragments';
String get _eventsBoxName => '$name.box.events';
String get _seenDeviceIdsBoxName => '$name.box.seen_device_ids';
String get _seenDeviceKeysBoxName => '$name.box.seen_device_keys';
final HiveCipher? encryptionCipher;
@ -872,27 +891,34 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
: null;
// calculate the status
final newStatus = eventUpdate.content.tryGet<int>('status') ??
eventUpdate.content
.tryGetMap<String, dynamic>('unsigned')
?.tryGet<int>(messageSendingStatusKey) ??
2;
final newStatus = eventStatusFromInt(
eventUpdate.content.tryGet<int>('status') ??
eventUpdate.content
.tryGetMap<String, dynamic>('unsigned')
?.tryGet<int>(messageSendingStatusKey) ??
EventStatus.synced.intValue,
);
// Is this the response to a sending event which is already synced? Then
// there is nothing to do here.
if (newStatus != 2 && prevEvent?.status == 2) {
if (!newStatus.isSynced &&
prevEvent != null &&
prevEvent.status.isSynced) {
return;
}
final status =
newStatus == -1 || prevEvent == null || prevEvent.status == null
newStatus.isError || prevEvent == null || prevEvent.status != null
? newStatus
: max(prevEvent.status, newStatus);
: latestEventStatus(
prevEvent.status,
newStatus,
);
// Add the status and the sort order to the content so it get stored
eventUpdate.content['unsigned'] ??= <String, dynamic>{};
eventUpdate.content['unsigned'][messageSendingStatusKey] =
eventUpdate.content['status'] = status;
eventUpdate.content['status'] = status.intValue;
// In case this event has sent from this account we have a transaction ID
final transactionId = eventUpdate.content
@ -903,8 +929,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
eventUpdate.content);
// Update timeline fragments
final key =
MultiKey(eventUpdate.roomID, status >= 1 ? '' : 'SENDING').toString();
final key = MultiKey(eventUpdate.roomID, status.isSent ? '' : 'SENDING')
.toString();
final List eventIds = (await _timelineFragmentsBox.get(key) ?? []);
if (!eventIds.contains(eventId)) {
@ -914,8 +941,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
eventIds.insert(0, eventId);
}
await _timelineFragmentsBox.put(key, eventIds);
} else if (status == 2 &&
prevEvent?.status == 1 &&
} else if (status.isSynced &&
prevEvent != null &&
prevEvent.status.isSent &&
eventUpdate.type != EventUpdateType.history) {
// Status changes from 1 -> 2? Make sure event is correctly sorted.
eventIds.remove(eventId);
@ -923,7 +951,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
}
// If event comes from server timeline, remove sending events with this ID
if (status >= 1) {
if (status.isSent) {
final key = MultiKey(eventUpdate.roomID, 'SENDING').toString();
final List eventIds = (await _timelineFragmentsBox.get(key) ?? []);
final i = eventIds.indexWhere((id) => id == eventId);
@ -933,7 +961,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
}
// Is there a transaction id? Then delete the event with this id.
if (status != -1 && status != 0 && transactionId != null) {
if (!status.isError && !status.isSending && transactionId != null) {
await removeEvent(transactionId, eventUpdate.roomID);
}
}
@ -1348,12 +1376,14 @@ Map<String, dynamic> convertToJson(Map map) {
class MultiKey {
final List<String> parts;
MultiKey(String key1, [String? key2, String? key3])
: parts = [
key1,
if (key2 != null) key2,
if (key3 != null) key3,
];
const MultiKey.byParts(this.parts);
MultiKey.fromString(String multiKeyString)

File diff suppressed because it is too large Load Diff

View File

@ -23,12 +23,13 @@ import 'dart:typed_data';
import 'package:http/http.dart' as http;
import '../matrix.dart';
import 'event_status.dart';
import 'room.dart';
import 'utils/crypto/encrypted_file.dart';
import 'utils/event_localizations.dart';
import 'utils/html_to_text.dart';
import 'utils/matrix_localizations.dart';
import 'utils/receipt.dart';
import 'utils/event_localizations.dart';
import 'utils/crypto/encrypted_file.dart';
import 'utils/html_to_text.dart';
abstract class RelationshipTypes {
static const String reply = 'm.in_reply_to';
@ -53,21 +54,9 @@ class Event extends MatrixEvent {
final Room room;
/// The status of this event.
/// -1=ERROR
/// 0=SENDING
/// 1=SENT
/// 2=TIMELINE
/// 3=ROOM_STATE
int status;
EventStatus status;
static const int defaultStatus = 2;
static const Map<String, int> statusType = {
'error': -1,
'sending': 0,
'sent': 1,
'timeline': 2,
'roomState': 3,
};
static const EventStatus defaultStatus = EventStatus.synced;
/// Optional. The event that redacted this event, if any. Otherwise null.
Event get redactedBecause =>
@ -98,7 +87,7 @@ class Event extends MatrixEvent {
this.roomId = roomId ?? room?.id;
this.senderId = senderId;
this.unsigned = unsigned;
// synapse unfortunatley isn't following the spec and tosses the prev_content
// synapse unfortunately isn't following the spec and tosses the prev_content
// into the unsigned block.
// Currently we are facing a very strange bug in web which is impossible to debug.
// It may be because of this line so we put this in try-catch until we can fix it.
@ -119,7 +108,7 @@ class Event extends MatrixEvent {
// Mark event as failed to send if status is `sending` and event is older
// than the timeout. This should not happen with the deprecated Moor
// database!
if (status == 0 && room?.client?.database != null) {
if (status.isSending && room?.client?.database != null) {
// Age of this event in milliseconds
final age = DateTime.now().millisecondsSinceEpoch -
originServerTs.millisecondsSinceEpoch;
@ -128,7 +117,7 @@ class Event extends MatrixEvent {
// Update this event in database and open timelines
final json = toJson();
json['unsigned'] ??= <String, dynamic>{};
json['unsigned'][messageSendingStatusKey] = -1;
json['unsigned'][messageSendingStatusKey] = EventStatus.error.intValue;
room.client.handleSync(SyncUpdate(nextBatch: '')
..rooms = (RoomsUpdate()
..join = (<String, JoinedRoomUpdate>{}..[room.id] =
@ -154,7 +143,7 @@ class Event extends MatrixEvent {
factory Event.fromMatrixEvent(
MatrixEvent matrixEvent,
Room room, {
int status,
EventStatus status,
}) =>
Event(
status: status,
@ -179,9 +168,9 @@ class Event extends MatrixEvent {
final unsigned = Event.getMapFromPayload(jsonPayload['unsigned']);
final prevContent = Event.getMapFromPayload(jsonPayload['prev_content']);
return Event(
status: jsonPayload['status'] ??
status: eventStatusFromInt(jsonPayload['status'] ??
unsigned[messageSendingStatusKey] ??
defaultStatus,
defaultStatus.intValue),
stateKey: jsonPayload['state_key'],
prevContent: prevContent,
content: content,
@ -301,10 +290,11 @@ class Event extends MatrixEvent {
.toList();
}
/// Removes this event if the status is < 1. This event will just be removed
/// from the database and the timelines. Returns false if not removed.
/// Removes this event if the status is [sending], [error] or [removed].
/// This event will just be removed from the database and the timelines.
/// Returns [false] if not removed.
Future<bool> remove() async {
if (status < 1) {
if (!status.isSent) {
await room.client.database?.removeEvent(eventId, room.id);
room.client.onEvent.add(EventUpdate(
@ -312,7 +302,7 @@ class Event extends MatrixEvent {
type: EventUpdateType.timeline,
content: {
'event_id': eventId,
'status': -2,
'status': EventStatus.removed.intValue,
'content': {'body': 'Removed...'}
},
));
@ -321,9 +311,9 @@ class Event extends MatrixEvent {
return false;
}
/// Try to send this event again. Only works with events of status -1.
/// Try to send this event again. Only works with events of `EventStatus.isError`.
Future<String> sendAgain({String txid}) async {
if (status != -1) return null;
if (!status.isError) return null;
// we do not remove the event here. It will automatically be updated
// in the `sendEvent` method to transition -1 -> 0 -> 1 -> 2
final newEventId = await room.sendEvent(
@ -382,7 +372,7 @@ class Event extends MatrixEvent {
/// Returns if a file events thumbnail is encrypted
bool get isThumbnailEncrypted => infoMap['thumbnail_file'] is Map;
/// Gets the mimetipe of the attachment of a file event, or a blank string if not present
/// Gets the mimetype of the attachment of a file event, or a blank string if not present
String get attachmentMimetype => infoMap['mimetype'] is String
? infoMap['mimetype'].toLowerCase()
: (content['file'] is Map && content['file']['mimetype'] is String
@ -397,16 +387,16 @@ class Event extends MatrixEvent {
? infoMap['thumbnail_file']['mimetype']
: '');
/// Gets the underyling mxc url of an attachment of a file event, or null if not present
/// Gets the underlying mxc url of an attachment of a file event, or null if not present
Uri get attachmentMxcUrl => Uri.parse(
isAttachmentEncrypted ? content['file']['url'] : content['url']);
/// Gets the underyling mxc url of a thumbnail of a file event, or null if not present
/// Gets the underlying mxc url of a thumbnail of a file event, or null if not present
Uri get thumbnailMxcUrl => Uri.parse(isThumbnailEncrypted
? infoMap['thumbnail_file']['url']
: infoMap['thumbnail_url']);
/// Gets the mxc url of an attachemnt/thumbnail of a file event, taking sizes into account, or null if not present
/// Gets the mxc url of an attachment/thumbnail of a file event, taking sizes into account, or null if not present
Uri attachmentOrThumbnailMxcUrl({bool getThumbnail = false}) {
if (getThumbnail &&
infoMap['size'] is int &&
@ -493,7 +483,7 @@ class Event extends MatrixEvent {
return uint8list != null;
}
/// Downloads (and decryptes if necessary) the attachment of this
/// Downloads (and decrypts if necessary) the attachment of this
/// event and returns it as a [MatrixFile]. If this event doesn't
/// contain an attachment, this throws an error. Set [getThumbnail] to
/// true to download the thumbnail instead.
@ -671,7 +661,7 @@ class Event extends MatrixEvent {
return null;
}
/// Get wether this event has aggregated events from a certain [type]
/// Get whether this event has aggregated events from a certain [type]
/// To be able to do that you need to pass a [timeline]
bool hasAggregatedEvents(Timeline timeline, String type) =>
timeline.aggregatedEvents.containsKey(eventId) &&

70
lib/src/event_status.dart Normal file
View File

@ -0,0 +1,70 @@
/// Defines event status:
/// - removed
/// - error: (http request failed)
/// - sending: (http request started)
/// - sent: (http request successful)
/// - synced: (event came from sync loop)
/// - roomState
enum EventStatus {
removed,
error,
sending,
sent,
synced,
roomState,
}
/// Returns `EventStatusEnum` value from `intValue`.
///
/// - -2 == removed;
/// - -1 == error;
/// - 0 == sending;
/// - 1 == sent;
/// - 2 == synced;
/// - 3 == roomState;
EventStatus eventStatusFromInt(int intValue) =>
EventStatus.values[intValue + 2];
/// Takes two [EventStatus] values and returns the one with higher
/// (better in terms of message sending) status.
EventStatus latestEventStatus(EventStatus status1, EventStatus status2) =>
status1.intValue > status2.intValue ? status1 : status2;
extension EventStatusExtension on EventStatus {
/// Returns int value of the event status.
///
/// - -2 == removed;
/// - -1 == error;
/// - 0 == sending;
/// - 1 == sent;
/// - 2 == synced;
/// - 3 == roomState;
int get intValue => (index - 2);
/// Return `true` if the `EventStatus` equals `removed`.
bool get isRemoved => this == EventStatus.removed;
/// Return `true` if the `EventStatus` equals `error`.
bool get isError => this == EventStatus.error;
/// Return `true` if the `EventStatus` equals `sending`.
bool get isSending => this == EventStatus.sending;
/// Return `true` if the `EventStatus` equals `roomState`.
bool get isRoomState => this == EventStatus.roomState;
/// Returns `true` if the status is sent or later:
/// [EventStatus.sent], [EventStatus.synced] or [EventStatus.roomState].
bool get isSent => [
EventStatus.sent,
EventStatus.synced,
EventStatus.roomState
].contains(this);
/// Returns `true` if the status is `synced` or `roomState`:
/// [EventStatus.synced] or [EventStatus.roomState].
bool get isSynced => [
EventStatus.synced,
EventStatus.roomState,
].contains(this);
}

View File

@ -26,6 +26,7 @@ import 'package:matrix/src/utils/space_child.dart';
import '../matrix.dart';
import 'client.dart';
import 'event.dart';
import 'event_status.dart';
import 'timeline.dart';
import 'user.dart';
import 'voip_content.dart';
@ -819,7 +820,7 @@ class Room {
senderId: client.userID,
originServerTs: sentDate,
unsigned: {
messageSendingStatusKey: 0,
messageSendingStatusKey: EventStatus.sending.intValue,
'transaction_id': messageID,
},
)
@ -846,14 +847,14 @@ class Room {
} else {
Logs().w('[Client] Problem while sending message', e, s);
syncUpdate.rooms.join.values.first.timeline.events.first
.unsigned[messageSendingStatusKey] = -1;
.unsigned[messageSendingStatusKey] = EventStatus.error.intValue;
await _handleFakeSync(syncUpdate);
return null;
}
}
}
syncUpdate.rooms.join.values.first.timeline.events.first
.unsigned[messageSendingStatusKey] = 1;
.unsigned[messageSendingStatusKey] = EventStatus.sent.intValue;
syncUpdate.rooms.join.values.first.timeline.events.first.eventId = res;
await _handleFakeSync(syncUpdate);

View File

@ -20,6 +20,7 @@ import 'dart:async';
import '../matrix.dart';
import 'event.dart';
import 'event_status.dart';
import 'room.dart';
import 'utils/event_update.dart';
@ -243,11 +244,11 @@ class Timeline {
eventUpdate.type != EventUpdateType.history) {
return;
}
final status = eventUpdate.content['status'] ??
final status = eventStatusFromInt(eventUpdate.content['status'] ??
(eventUpdate.content['unsigned'] is Map<String, dynamic>
? eventUpdate.content['unsigned'][messageSendingStatusKey]
: null) ??
2;
EventStatus.synced.intValue);
// Redaction events are handled as modification for existing events.
if (eventUpdate.content['type'] == EventTypes.Redaction) {
final eventId = _findEvent(event_id: eventUpdate.content['redacts']);
@ -258,7 +259,7 @@ class Timeline {
room,
));
}
} else if (status == -2) {
} else if (status.isRemoved) {
final i = _findEvent(event_id: eventUpdate.content['event_id']);
if (i < events.length) {
removeAggregatedEvent(events[i]);
@ -279,7 +280,8 @@ class Timeline {
room,
);
// do we preserve the status? we should allow 0 -> -1 updates and status increases
if (status < oldStatus && !(status == -1 && oldStatus == 0)) {
if ((latestEventStatus(status, oldStatus) == oldStatus) &&
!(status.isError && oldStatus.isSending)) {
events[i].status = oldStatus;
}
addAggregatedEvent(events[i]);
@ -315,7 +317,7 @@ class Timeline {
extension on List<Event> {
int get firstIndexWhereNotError {
if (isEmpty) return 0;
final index = indexWhere((e) => e.status != -1);
final index = indexWhere((event) => !event.status.isError);
if (index == -1) return length;
return index;
}

329
lib/src/timeline.dart.orig Normal file
View File

@ -0,0 +1,329 @@
/*
* Famedly Matrix SDK
* Copyright (C) 2019, 2020, 2021 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 'dart:async';
import '../matrix.dart';
import 'event.dart';
import 'event_status.dart';
import 'room.dart';
import 'utils/event_update.dart';
/// Represents the timeline of a room. The callback [onUpdate] will be triggered
/// automatically. The initial
/// event list will be retreived when created by the `room.getTimeline()` method.
class Timeline {
final Room room;
final List<Event> events;
/// Map of event ID to map of type to set of aggregated events
final Map<String, Map<String, Set<Event>>> aggregatedEvents = {};
final void Function()? onUpdate;
final void Function(int insertID)? onInsert;
StreamSubscription<EventUpdate>? sub;
StreamSubscription<SyncUpdate>? roomSub;
StreamSubscription<String>? sessionIdReceivedSub;
bool isRequestingHistory = false;
final Map<String, Event> _eventCache = {};
/// Searches for the event in this timeline. If not
/// found, requests from the server. Requested events
/// are cached.
Future<Event?> getEventById(String id) async {
for (final event in events) {
if (event.eventId == id) return event;
}
if (_eventCache.containsKey(id)) return _eventCache[id];
final requestedEvent = await room.getEventById(id);
if (requestedEvent == null) return null;
_eventCache[id] = requestedEvent;
return _eventCache[id];
}
// When fetching history, we will collect them into the `_historyUpdates` set
// first, and then only process all events at once, once we have the full history.
// This ensures that the entire history fetching only triggers `onUpdate` only *once*,
// even if /sync's complete while history is being proccessed.
bool _collectHistoryUpdates = false;
bool get canRequestHistory {
if (events.isEmpty) return true;
return events.last.type != EventTypes.RoomCreate;
}
Future<void> requestHistory(
{int historyCount = Room.defaultHistoryCount}) async {
if (isRequestingHistory) {
return;
}
isRequestingHistory = true;
onUpdate?.call();
try {
// Look up for events in hive first
final eventsFromStore = await room.client.database?.getEventList(
room,
start: events.length,
limit: Room.defaultHistoryCount,
);
if (eventsFromStore != null && eventsFromStore.isNotEmpty) {
events.addAll(eventsFromStore);
} else {
Logs().v('No more events found in the store. Request from server...');
await room.requestHistory(
historyCount: historyCount,
onHistoryReceived: () {
_collectHistoryUpdates = true;
},
);
}
} finally {
_collectHistoryUpdates = false;
isRequestingHistory = false;
onUpdate?.call();
}
}
Timeline(
{required this.room, List<Event>? events, this.onUpdate, this.onInsert})
: events = events ?? [] {
sub = room.client.onEvent.stream.listen(_handleEventUpdate);
// If the timeline is limited we want to clear our events cache
roomSub = room.client.onSync.stream
.where((sync) => sync.rooms?.join?[room.id]?.timeline?.limited == true)
.listen((_) {
this.events.clear();
aggregatedEvents.clear();
});
sessionIdReceivedSub =
room.onSessionKeyReceived.stream.listen(_sessionKeyReceived);
// we want to populate our aggregated events
for (final e in this.events) {
addAggregatedEvent(e);
}
}
/// Don't forget to call this before you dismiss this object!
void cancelSubscriptions() {
sub?.cancel();
roomSub?.cancel();
sessionIdReceivedSub?.cancel();
}
void _sessionKeyReceived(String sessionId) async {
var decryptAtLeastOneEvent = false;
final decryptFn = () async {
if (!room.client.encryptionEnabled) {
return;
}
for (var i = 0; i < events.length; i++) {
if (events[i].type == EventTypes.Encrypted &&
events[i].messageType == MessageTypes.BadEncrypted &&
events[i].content['session_id'] == sessionId) {
events[i] = await room.client.encryption
.decryptRoomEvent(room.id, events[i], store: true);
if (events[i].type != EventTypes.Encrypted) {
decryptAtLeastOneEvent = true;
}
}
}
};
if (room.client.database != null) {
await room.client.database.transaction(decryptFn);
} else {
await decryptFn();
}
if (decryptAtLeastOneEvent) onUpdate?.call();
}
/// Request the keys for undecryptable events of this timeline
void requestKeys() {
for (final event in events) {
if (event.type == EventTypes.Encrypted &&
event.messageType == MessageTypes.BadEncrypted &&
event.content['can_request_session'] == true) {
try {
room.client.encryption.keyManager.maybeAutoRequest(room.id,
event.content['session_id'], event.content['sender_key']);
} catch (_) {
// dispose
}
}
}
}
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++) {
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;
}
void _removeEventFromSet(Set<Event> eventSet, Event event) {
eventSet.removeWhere((e) =>
e.matchesEventOrTransactionId(event.eventId) ||
(event.unsigned != null &&
e.matchesEventOrTransactionId(event.unsigned['transaction_id'])));
}
void addAggregatedEvent(Event event) {
// we want to add an event to the aggregation tree
if (event.relationshipType == null || event.relationshipEventId == null) {
return; // nothing to do
}
if (!aggregatedEvents.containsKey(event.relationshipEventId)) {
aggregatedEvents[event.relationshipEventId] = <String, Set<Event>>{};
}
final events = (aggregatedEvents[event.relationshipEventId] ??=
<String, Set<Event>>{})[event.relationshipType] ??= <Event>{};
// remove a potential old event
_removeEventFromSet(events, event);
// add the new one
events.add(event);
}
void removeAggregatedEvent(Event event) {
aggregatedEvents.remove(event.eventId);
if (event.unsigned != null) {
aggregatedEvents.remove(event.unsigned['transaction_id']);
}
for (final types in aggregatedEvents.values) {
for (final events in types.values) {
_removeEventFromSet(events, event);
}
}
}
void _handleEventUpdate(EventUpdate eventUpdate, {bool update = true}) {
try {
if (eventUpdate.roomID != room.id) return;
if (eventUpdate.type != EventUpdateType.timeline &&
eventUpdate.type != EventUpdateType.history) {
return;
}
final status = eventStatusFromInt(eventUpdate.content['status'] ??
(eventUpdate.content['unsigned'] is Map<String, dynamic>
? eventUpdate.content['unsigned'][messageSendingStatusKey]
: null) ??
EventStatus.synced.intValue);
// Redaction events are handled as modification for existing events.
if (eventUpdate.content['type'] == EventTypes.Redaction) {
final eventId = _findEvent(event_id: eventUpdate.content['redacts']);
if (eventId < events.length) {
removeAggregatedEvent(events[eventId]);
events[eventId].setRedactionEvent(Event.fromJson(
eventUpdate.content,
room,
));
}
} else if (status.isRemoved) {
final i = _findEvent(event_id: eventUpdate.content['event_id']);
if (i < events.length) {
removeAggregatedEvent(events[i]);
events.removeAt(i);
}
} else {
final i = _findEvent(
event_id: eventUpdate.content['event_id'],
unsigned_txid: eventUpdate.content['unsigned'] is Map
? eventUpdate.content['unsigned']['transaction_id']
: null);
if (i < events.length) {
// 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,
);
// do we preserve the status? we should allow 0 -> -1 updates and status increases
if ((latestEventStatus(status, oldStatus) == oldStatus) &&
!(status.isError && oldStatus.isSending)) {
events[i].status = oldStatus;
}
addAggregatedEvent(events[i]);
} else {
final newEvent = Event.fromJson(
eventUpdate.content,
room,
);
if (eventUpdate.type == EventUpdateType.history &&
events.indexWhere(
(e) => e.eventId == eventUpdate.content['event_id']) !=
-1) return;
if (eventUpdate.type == EventUpdateType.history) {
events.add(newEvent);
<<<<<<< HEAD
=======
} else if (status.isError) {
events.insert(events.firstIndexWhereNotError, newEvent);
>>>>>>> 8fe85aca09b947ce7417672ce57ebfce80f3133c
} else {
events.insert(events.firstIndexWhereNotError, newEvent);
}
addAggregatedEvent(newEvent);
onInsert?.call(0);
}
}
if (update && !_collectHistoryUpdates) {
onUpdate?.call();
}
} catch (e, s) {
Logs().w('Handle event update failed', e, s);
}
}
}
extension on List<Event> {
int get firstIndexWhereNotError {
if (isEmpty) return 0;
final index = indexWhere((event) => !event.status.isError);
if (index == -1) return length;
return index;
}
}

241
lib/src/user.dart.orig Normal file
View File

@ -0,0 +1,241 @@
/*
* Famedly Matrix SDK
* Copyright (C) 2019, 2020, 2021 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 '../matrix.dart';
import 'event.dart';
import 'room.dart';
/// Represents a Matrix User which may be a participant in a Matrix Room.
class User extends Event {
factory User(
String id, {
String? membership,
String? displayName,
String? avatarUrl,
Room? room,
}) {
return User.fromState(
stateKey: id,
content: {
if (membership != null) 'membership': membership,
if (displayName != null) 'displayname': displayName,
if (avatarUrl != null) 'avatar_url': avatarUrl,
},
typeKey: EventTypes.RoomMember,
roomId: room?.id,
room: room,
originServerTs: DateTime.now(),
);
}
User.fromState(
{dynamic prevContent,
required String stateKey,
dynamic content,
required String typeKey,
String? eventId,
String? roomId,
String? senderId,
required DateTime originServerTs,
dynamic unsigned,
Room? room})
: super(
stateKey: stateKey,
prevContent: prevContent,
content: content,
type: typeKey,
eventId: eventId,
roomId: roomId,
senderId: senderId,
originServerTs: originServerTs,
unsigned: unsigned,
room: room);
/// The full qualified Matrix ID in the format @username:server.abc.
String get id => stateKey;
/// The displayname of the user if the user has set one.
<<<<<<< HEAD
String? get displayName =>
=======
String get displayName =>
>>>>>>> 8fe85aca09b947ce7417672ce57ebfce80f3133c
content?.tryGet<String>('displayname') ??
prevContent?.tryGet<String>('displayname');
/// Returns the power level of this user.
int get powerLevel => room?.getPowerLevelByUserId(id) ?? 0;
/// The membership status of the user. One of:
/// join
/// invite
/// leave
/// ban
Membership get membership => Membership.values.firstWhere((e) {
if (content['membership'] != null) {
return e.toString() == 'Membership.' + content['membership'];
}
return false;
}, orElse: () => Membership.join);
/// The avatar if the user has one.
Uri? get avatarUrl => content != null && content.containsKey('avatar_url')
? (content['avatar_url'] is String
? Uri.tryParse(content['avatar_url'])
: null)
: (prevContent != null && prevContent['avatar_url'] is String
? Uri.tryParse(prevContent['avatar_url'])
: null);
/// Returns the displayname or the local part of the Matrix ID if the user
/// has no displayname. If [formatLocalpart] is true, then the localpart will
/// be formatted in the way, that all "_" characters are becomming white spaces and
/// the first character of each word becomes uppercase.
/// If [mxidLocalPartFallback] is true, then the local part of the mxid will be shown
/// if there is no other displayname available. If not then this will return "Unknown user".
String calcDisplayname({
bool? formatLocalpart,
bool? mxidLocalPartFallback,
}) {
formatLocalpart ??= room?.client?.formatLocalpart ?? true;
mxidLocalPartFallback ??= room?.client?.mxidLocalPartFallback ?? true;
final displayName = this.displayName;
if (displayName != null && displayName.isNotEmpty) {
return displayName;
}
final stateKey = this.stateKey;
if (stateKey != null && mxidLocalPartFallback) {
if (!formatLocalpart) {
return stateKey.localpart ?? '';
}
final words = stateKey.localpart?.replaceAll('_', ' ').split(' ') ?? [];
for (var i = 0; i < words.length; i++) {
if (words[i].isNotEmpty) {
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
}
}
return words.join(' ');
}
return 'Unknown user';
}
/// Call the Matrix API to kick this user from this room.
Future<void> kick() => room.kick(id);
/// Call the Matrix API to ban this user from this room.
Future<void> ban() => room.ban(id);
/// Call the Matrix API to unban this banned user from this room.
Future<void> unban() => room.unban(id);
/// Call the Matrix API to change the power level of this user.
Future<void> setPower(int power) => room.setPower(id, power);
/// Returns an existing direct chat ID with this user or creates a new one.
/// Returns null on error.
Future<String?> startDirectChat() => room.client.startDirectChat(id);
/// The newest presence of this user if there is any and null if not.
Presence? get presence => room.client.presences[id];
/// Whether the client is able to ban/unban this user.
bool get canBan => room.canBan && powerLevel < room.ownPowerLevel;
/// Whether the client is able to kick this user.
bool get canKick =>
[Membership.join, Membership.invite].contains(membership) &&
room.canKick &&
powerLevel < room.ownPowerLevel;
/// Whether the client is allowed to change the power level of this user.
/// Please be aware that you can only set the power level to at least your own!
bool get canChangePowerLevel =>
room.canChangePowerLevel && powerLevel < room.ownPowerLevel;
@override
bool operator ==(dynamic other) => (other is User &&
other.id == id &&
other.room == room &&
other.membership == membership);
/// Get the mention text to use in a plain text body to mention this specific user
/// in this specific room
String get mention {
// if the displayname has [ or ] or : we can't build our more fancy stuff, so fall back to the id
// [] is used for the delimitors
// If we allowed : we could get collissions with the mxid fallbacks
final displayName = this.displayName;
if (displayName == null ||
displayName.isEmpty ||
{'[', ']', ':'}.any(displayName.contains)) {
return id;
}
final identifier = '@' +
// if we have non-word characters we need to surround with []
(RegExp(r'^\w+$').hasMatch(displayName)
? displayName
: '[$displayName]');
// get all the users with the same display name
final allUsersWithSameDisplayname = room.getParticipants();
allUsersWithSameDisplayname.removeWhere((user) =>
user.id == id ||
(user.displayName?.isEmpty ?? true) ||
user.displayName != displayName);
if (allUsersWithSameDisplayname.isEmpty) {
return identifier;
}
// ok, we have multiple users with the same display name....time to calculate a hash
final hashes = allUsersWithSameDisplayname.map((u) => _hash(u.id));
final ourHash = _hash(id);
// hash collission...just return our own mxid again
if (hashes.contains(ourHash)) {
return id;
}
return '$identifier#$ourHash';
}
/// Get the mention fragments for this user.
Set<String> get mentionFragments {
final displayName = this.displayName;
if (displayName == null ||
displayName.isEmpty ||
{'[', ']', ':'}.any(displayName.contains)) {
return {};
}
final identifier = '@' +
// if we have non-word characters we need to surround with []
(RegExp(r'^\w+$').hasMatch(displayName)
? displayName
: '[$displayName]');
final hash = _hash(id);
return {identifier, '$identifier#$hash'};
}
}
<<<<<<< HEAD
const _maximumHashLength = 10000;
String _hash(String s) =>
(s.codeUnits.fold<int>(0, (a, b) => a + b) % _maximumHashLength).toString();
=======
String _hash(String s) =>
(s.codeUnits.fold<int>(0, (a, b) => a + b) % 10000).toString();
>>>>>>> 8fe85aca09b947ce7417672ce57ebfce80f3133c

View File

@ -23,6 +23,7 @@ import 'dart:typed_data';
import 'package:matrix/encryption.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix/src/event.dart';
import 'package:matrix/src/event_status.dart';
import 'package:olm/olm.dart' as olm;
import 'package:test/test.dart';
@ -53,7 +54,7 @@ void main() {
'origin_server_ts': timestamp,
'type': type,
'room_id': '1234',
'status': 2,
'status': EventStatus.synced.intValue,
'content': contentJson,
};
final client = Client('testclient', httpClient: FakeMatrixApi());
@ -79,7 +80,7 @@ void main() {
expect(event.eventId, id);
expect(event.senderId, senderID);
expect(event.status, 2);
expect(event.status, EventStatus.synced);
expect(event.text, body);
expect(event.formattedText, formatted_body);
expect(event.body, body);
@ -89,7 +90,7 @@ void main() {
final state = Event.fromJson(jsonObj, null);
expect(state.eventId, id);
expect(state.stateKey, '');
expect(state.status, 2);
expect(state.status, EventStatus.synced);
});
test('Test all EventTypes', () async {
Event event;
@ -259,7 +260,7 @@ void main() {
final event = Event.fromJson(
jsonObj, Room(id: '1234', client: Client('testclient')));
final removed1 = await event.remove();
event.status = 0;
event.status = EventStatus.sending;
final removed2 = await event.remove();
expect(removed1, false);
expect(removed2, true);
@ -276,7 +277,7 @@ void main() {
final event = Event.fromJson(
jsonObj, Room(id: '!1234:example.com', client: matrix));
final resp1 = await event.sendAgain();
event.status = -1;
event.status = EventStatus.error;
final resp2 = await event.sendAgain(txid: '1234');
expect(resp1, null);
expect(resp2.startsWith('\$event'), true);
@ -308,7 +309,7 @@ void main() {
'origin_server_ts': timestamp,
'type': 'm.room.encrypted',
'room_id': '1234',
'status': 2,
'status': EventStatus.synced.intValue,
'content': json.encode({
'msgtype': 'm.bad.encrypted',
'body': DecryptException.unknownSession,

View File

@ -19,6 +19,7 @@
*/
import 'package:matrix/matrix.dart';
import 'package:matrix/src/event_status.dart';
import 'package:test/test.dart';
import 'fake_database.dart';
@ -69,7 +70,7 @@ void main() {
'content': <String, dynamic>{'blah': 'blubb'},
'event_id': 'transaction-1',
'sender': '@blah:blubb',
'status': 0,
'status': EventStatus.sending.intValue,
},
);
await database.storeEventUpdate(update);
@ -87,7 +88,7 @@ void main() {
'unsigned': <String, dynamic>{
'transaction_id': 'transaction-1',
},
'status': 1,
'status': EventStatus.sent.intValue,
},
);
await database.storeEventUpdate(update);
@ -105,7 +106,7 @@ void main() {
'content': {'blah': 'blubb'},
'event_id': '\$event-3',
'sender': '@blah:blubb',
'status': 0,
'status': EventStatus.sending.intValue,
},
);
await database.storeEventUpdate(update);
@ -120,7 +121,7 @@ void main() {
'content': {'blah': 'blubb'},
'event_id': '\$event-3',
'sender': '@blah:blubb',
'status': 1,
'status': EventStatus.sent.intValue,
'unsigned': <String, dynamic>{
'transaction_id': 'transaction-2',
},
@ -129,7 +130,7 @@ void main() {
await database.storeEventUpdate(update);
event = await database.getEventById('\$event-3', room);
expect(event.eventId, '\$event-3');
expect(event.status, 1);
expect(event.status, EventStatus.sent);
event = await database.getEventById('transaction-2', room);
expect(event, null);
@ -143,7 +144,7 @@ void main() {
'content': {'blah': 'blubb'},
'event_id': '\$event-4',
'sender': '@blah:blubb',
'status': 2,
'status': EventStatus.synced.intValue,
},
);
await database.storeEventUpdate(update);
@ -158,7 +159,7 @@ void main() {
'content': {'blah': 'blubb'},
'event_id': '\$event-4',
'sender': '@blah:blubb',
'status': 1,
'status': EventStatus.sent.intValue,
'unsigned': <String, dynamic>{
'transaction_id': 'transaction-3',
},
@ -167,7 +168,7 @@ void main() {
await database.storeEventUpdate(update);
event = await database.getEventById('\$event-4', room);
expect(event.eventId, '\$event-4');
expect(event.status, 2);
expect(event.status, EventStatus.synced);
event = await database.getEventById('transaction-3', room);
expect(event, null);
});

View File

@ -18,6 +18,7 @@
*/
import 'package:matrix/matrix.dart';
import 'package:matrix/src/event_status.dart';
import 'package:test/test.dart';
import 'package:matrix/src/client.dart';
@ -75,7 +76,7 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 2,
'status': EventStatus.synced.intValue,
'event_id': '2',
'origin_server_ts': testTimeStamp - 1000
},
@ -87,7 +88,7 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 2,
'status': EventStatus.synced.intValue,
'event_id': '1',
'origin_server_ts': testTimeStamp
},
@ -160,7 +161,7 @@ void main() {
expect(insertList.length, timeline.events.length);
final eventId = timeline.events[0].eventId;
expect(eventId.startsWith('\$event'), true);
expect(timeline.events[0].status, 1);
expect(timeline.events[0].status, EventStatus.sent);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
@ -169,7 +170,7 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'test'},
'sender': '@alice:example.com',
'status': 2,
'status': EventStatus.synced.intValue,
'event_id': eventId,
'unsigned': {'transaction_id': '1234'},
'origin_server_ts': DateTime.now().millisecondsSinceEpoch
@ -182,7 +183,7 @@ void main() {
expect(insertList, [0, 0, 0]);
expect(insertList.length, timeline.events.length);
expect(timeline.events[0].eventId, eventId);
expect(timeline.events[0].status, 2);
expect(timeline.events[0].status, EventStatus.synced);
});
test('Send message with error', () async {
@ -193,7 +194,7 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 0,
'status': EventStatus.sending.intValue,
'event_id': 'abc',
'origin_server_ts': testTimeStamp
},
@ -213,9 +214,9 @@ void main() {
expect(updateCount, 13);
expect(insertList, [0, 0, 0, 0, 0, 0, 0]);
expect(insertList.length, timeline.events.length);
expect(timeline.events[0].status, -1);
expect(timeline.events[1].status, -1);
expect(timeline.events[2].status, -1);
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 {
@ -227,7 +228,7 @@ void main() {
expect(insertList, [0, 0, 0, 0, 0, 0, 0]);
expect(timeline.events.length, 6);
expect(timeline.events[0].status, -1);
expect(timeline.events[0].status, EventStatus.error);
});
test('getEventById', () async {
@ -256,14 +257,14 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': -1,
'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, -1);
expect(timeline.events[0].status, EventStatus.error);
await timeline.events[0].sendAgain();
await Future.delayed(Duration(milliseconds: 50));
@ -272,7 +273,7 @@ void main() {
expect(insertList, [0, 0, 0, 0, 0, 0, 0, 0]);
expect(timeline.events.length, 1);
expect(timeline.events[0].status, 1);
expect(timeline.events[0].status, EventStatus.sent);
});
test('Request history', () async {
@ -317,7 +318,7 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': -1,
'status': EventStatus.error.intValue,
'event_id': 'abc',
'origin_server_ts': testTimeStamp
},
@ -329,14 +330,14 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 2,
'status': EventStatus.synced.intValue,
'event_id': 'def',
'origin_server_ts': testTimeStamp + 5
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, -1);
expect(timeline.events[1].status, 2);
expect(timeline.events[0].status, EventStatus.error);
expect(timeline.events[1].status, EventStatus.synced);
});
test('sending event to failed update', () async {
@ -348,13 +349,13 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 0,
'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, 0);
expect(timeline.events[0].status, EventStatus.sending);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
@ -363,13 +364,13 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': -1,
'status': EventStatus.error.intValue,
'event_id': 'will-fail',
'origin_server_ts': testTimeStamp
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, -1);
expect(timeline.events[0].status, EventStatus.error);
expect(timeline.events.length, 1);
});
test('sending an event and the http request finishes first, 0 -> 1 -> 2',
@ -382,13 +383,13 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 0,
'status': EventStatus.sending.intValue,
'event_id': 'transaction',
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 0);
expect(timeline.events[0].status, EventStatus.sending);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
@ -397,14 +398,14 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 1,
'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, 1);
expect(timeline.events[0].status, EventStatus.sent);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
@ -413,14 +414,14 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 2,
'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, 2);
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',
@ -436,13 +437,13 @@ void main() {
'event_id': 'transaction',
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
'unsigned': {
messageSendingStatusKey: 0,
messageSendingStatusKey: EventStatus.sending.intValue,
'transaction_id': 'transaction',
},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 0);
expect(timeline.events[0].status, EventStatus.sending);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
@ -455,12 +456,12 @@ void main() {
'origin_server_ts': testTimeStamp,
'unsigned': {
'transaction_id': 'transaction',
messageSendingStatusKey: 2,
messageSendingStatusKey: EventStatus.synced.intValue,
},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 2);
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
@ -473,12 +474,12 @@ void main() {
'origin_server_ts': testTimeStamp,
'unsigned': {
'transaction_id': 'transaction',
messageSendingStatusKey: 1,
messageSendingStatusKey: EventStatus.sent.intValue,
},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 2);
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
});
test('sending an event 0 -> -1 -> 2', () async {
@ -490,13 +491,13 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 0,
'status': EventStatus.sending.intValue,
'event_id': 'transaction',
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 0);
expect(timeline.events[0].status, EventStatus.sending);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
@ -505,13 +506,13 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': -1,
'status': EventStatus.error.intValue,
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, -1);
expect(timeline.events[0].status, EventStatus.error);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
@ -520,14 +521,14 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 2,
'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, 2);
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
});
test('sending an event 0 -> 2 -> -1', () async {
@ -539,13 +540,13 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 0,
'status': EventStatus.sending.intValue,
'event_id': 'transaction',
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 0);
expect(timeline.events[0].status, EventStatus.sending);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
@ -554,14 +555,14 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': 2,
'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, 2);
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
client.onEvent.add(EventUpdate(
type: EventUpdateType.timeline,
@ -570,13 +571,13 @@ void main() {
'type': 'm.room.message',
'content': {'msgtype': 'm.text', 'body': 'Testcase'},
'sender': '@alice:example.com',
'status': -1,
'status': EventStatus.error.intValue,
'origin_server_ts': testTimeStamp,
'unsigned': {'transaction_id': 'transaction'},
},
));
await Future.delayed(Duration(milliseconds: 50));
expect(timeline.events[0].status, 2);
expect(timeline.events[0].status, EventStatus.synced);
expect(timeline.events.length, 1);
});
test('logout', () async {