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/utils/http_timeout.dart';
export 'src/client.dart'; export 'src/client.dart';
export 'src/event.dart'; export 'src/event.dart';
export 'src/event_status.dart';
export 'src/room.dart'; export 'src/room.dart';
export 'src/voip_content.dart'; export 'src/voip_content.dart';
export 'src/timeline.dart'; export 'src/timeline.dart';

View File

@ -19,16 +19,17 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; 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 '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: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'; import 'package:matrix/src/utils/run_benchmarked.dart';
/// This is a basic database for the Matrix SDK using the hive store. You need /// 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; late LazyBox _seenDeviceKeysBox;
String get _clientBoxName => '$name.box.client'; String get _clientBoxName => '$name.box.client';
String get _accountDataBoxName => '$name.box.account_data'; String get _accountDataBoxName => '$name.box.account_data';
String get _roomsBoxName => '$name.box.rooms'; String get _roomsBoxName => '$name.box.rooms';
String get _toDeviceQueueBoxName => '$name.box.to_device_queue'; String get _toDeviceQueueBoxName => '$name.box.to_device_queue';
String get _roomStateBoxName => '$name.box.room_states'; String get _roomStateBoxName => '$name.box.room_states';
String get _roomMembersBoxName => '$name.box.room_members'; String get _roomMembersBoxName => '$name.box.room_members';
String get _roomAccountDataBoxName => '$name.box.room_account_data'; String get _roomAccountDataBoxName => '$name.box.room_account_data';
String get _inboundGroupSessionsBoxName => '$name.box.inbound_group_session'; String get _inboundGroupSessionsBoxName => '$name.box.inbound_group_session';
String get _outboundGroupSessionsBoxName => String get _outboundGroupSessionsBoxName =>
'$name.box.outbound_group_session'; '$name.box.outbound_group_session';
String get _olmSessionsBoxName => '$name.box.olm_session'; String get _olmSessionsBoxName => '$name.box.olm_session';
String get _userDeviceKeysBoxName => '$name.box.user_device_keys'; String get _userDeviceKeysBoxName => '$name.box.user_device_keys';
String get _userDeviceKeysOutdatedBoxName => String get _userDeviceKeysOutdatedBoxName =>
'$name.box.user_device_keys_outdated'; '$name.box.user_device_keys_outdated';
String get _userCrossSigningKeysBoxName => '$name.box.cross_signing_keys'; String get _userCrossSigningKeysBoxName => '$name.box.cross_signing_keys';
String get _ssssCacheBoxName => '$name.box.ssss_cache'; String get _ssssCacheBoxName => '$name.box.ssss_cache';
String get _presencesBoxName => '$name.box.presences'; String get _presencesBoxName => '$name.box.presences';
String get _timelineFragmentsBoxName => '$name.box.timeline_fragments'; String get _timelineFragmentsBoxName => '$name.box.timeline_fragments';
String get _eventsBoxName => '$name.box.events'; String get _eventsBoxName => '$name.box.events';
String get _seenDeviceIdsBoxName => '$name.box.seen_device_ids'; String get _seenDeviceIdsBoxName => '$name.box.seen_device_ids';
String get _seenDeviceKeysBoxName => '$name.box.seen_device_keys'; String get _seenDeviceKeysBoxName => '$name.box.seen_device_keys';
final HiveCipher? encryptionCipher; final HiveCipher? encryptionCipher;
@ -872,27 +891,34 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
: null; : null;
// calculate the status // calculate the status
final newStatus = eventUpdate.content.tryGet<int>('status') ?? final newStatus = eventStatusFromInt(
eventUpdate.content.tryGet<int>('status') ??
eventUpdate.content eventUpdate.content
.tryGetMap<String, dynamic>('unsigned') .tryGetMap<String, dynamic>('unsigned')
?.tryGet<int>(messageSendingStatusKey) ?? ?.tryGet<int>(messageSendingStatusKey) ??
2; EventStatus.synced.intValue,
);
// Is this the response to a sending event which is already synced? Then // Is this the response to a sending event which is already synced? Then
// there is nothing to do here. // there is nothing to do here.
if (newStatus != 2 && prevEvent?.status == 2) { if (!newStatus.isSynced &&
prevEvent != null &&
prevEvent.status.isSynced) {
return; return;
} }
final status = final status =
newStatus == -1 || prevEvent == null || prevEvent.status == null newStatus.isError || prevEvent == null || prevEvent.status != null
? newStatus ? newStatus
: max(prevEvent.status, newStatus); : latestEventStatus(
prevEvent.status,
newStatus,
);
// Add the status and the sort order to the content so it get stored // Add the status and the sort order to the content so it get stored
eventUpdate.content['unsigned'] ??= <String, dynamic>{}; eventUpdate.content['unsigned'] ??= <String, dynamic>{};
eventUpdate.content['unsigned'][messageSendingStatusKey] = 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 // In case this event has sent from this account we have a transaction ID
final transactionId = eventUpdate.content final transactionId = eventUpdate.content
@ -903,8 +929,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
eventUpdate.content); eventUpdate.content);
// Update timeline fragments // Update timeline fragments
final key = final key = MultiKey(eventUpdate.roomID, status.isSent ? '' : 'SENDING')
MultiKey(eventUpdate.roomID, status >= 1 ? '' : 'SENDING').toString(); .toString();
final List eventIds = (await _timelineFragmentsBox.get(key) ?? []); final List eventIds = (await _timelineFragmentsBox.get(key) ?? []);
if (!eventIds.contains(eventId)) { if (!eventIds.contains(eventId)) {
@ -914,8 +941,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
eventIds.insert(0, eventId); eventIds.insert(0, eventId);
} }
await _timelineFragmentsBox.put(key, eventIds); await _timelineFragmentsBox.put(key, eventIds);
} else if (status == 2 && } else if (status.isSynced &&
prevEvent?.status == 1 && prevEvent != null &&
prevEvent.status.isSent &&
eventUpdate.type != EventUpdateType.history) { eventUpdate.type != EventUpdateType.history) {
// Status changes from 1 -> 2? Make sure event is correctly sorted. // Status changes from 1 -> 2? Make sure event is correctly sorted.
eventIds.remove(eventId); eventIds.remove(eventId);
@ -923,7 +951,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
} }
// If event comes from server timeline, remove sending events with this ID // 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 key = MultiKey(eventUpdate.roomID, 'SENDING').toString();
final List eventIds = (await _timelineFragmentsBox.get(key) ?? []); final List eventIds = (await _timelineFragmentsBox.get(key) ?? []);
final i = eventIds.indexWhere((id) => id == eventId); 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. // 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); await removeEvent(transactionId, eventUpdate.roomID);
} }
} }
@ -1348,12 +1376,14 @@ Map<String, dynamic> convertToJson(Map map) {
class MultiKey { class MultiKey {
final List<String> parts; final List<String> parts;
MultiKey(String key1, [String? key2, String? key3]) MultiKey(String key1, [String? key2, String? key3])
: parts = [ : parts = [
key1, key1,
if (key2 != null) key2, if (key2 != null) key2,
if (key3 != null) key3, if (key3 != null) key3,
]; ];
const MultiKey.byParts(this.parts); const MultiKey.byParts(this.parts);
MultiKey.fromString(String multiKeyString) 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 'package:http/http.dart' as http;
import '../matrix.dart'; import '../matrix.dart';
import 'event_status.dart';
import 'room.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/matrix_localizations.dart';
import 'utils/receipt.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 { abstract class RelationshipTypes {
static const String reply = 'm.in_reply_to'; static const String reply = 'm.in_reply_to';
@ -53,21 +54,9 @@ class Event extends MatrixEvent {
final Room room; final Room room;
/// The status of this event. /// The status of this event.
/// -1=ERROR EventStatus status;
/// 0=SENDING
/// 1=SENT
/// 2=TIMELINE
/// 3=ROOM_STATE
int status;
static const int defaultStatus = 2; static const EventStatus defaultStatus = EventStatus.synced;
static const Map<String, int> statusType = {
'error': -1,
'sending': 0,
'sent': 1,
'timeline': 2,
'roomState': 3,
};
/// Optional. The event that redacted this event, if any. Otherwise null. /// Optional. The event that redacted this event, if any. Otherwise null.
Event get redactedBecause => Event get redactedBecause =>
@ -98,7 +87,7 @@ class Event extends MatrixEvent {
this.roomId = roomId ?? room?.id; this.roomId = roomId ?? room?.id;
this.senderId = senderId; this.senderId = senderId;
this.unsigned = unsigned; 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. // into the unsigned block.
// Currently we are facing a very strange bug in web which is impossible to debug. // 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. // 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 // 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 // than the timeout. This should not happen with the deprecated Moor
// database! // database!
if (status == 0 && room?.client?.database != null) { if (status.isSending && room?.client?.database != null) {
// Age of this event in milliseconds // Age of this event in milliseconds
final age = DateTime.now().millisecondsSinceEpoch - final age = DateTime.now().millisecondsSinceEpoch -
originServerTs.millisecondsSinceEpoch; originServerTs.millisecondsSinceEpoch;
@ -128,7 +117,7 @@ class Event extends MatrixEvent {
// Update this event in database and open timelines // Update this event in database and open timelines
final json = toJson(); final json = toJson();
json['unsigned'] ??= <String, dynamic>{}; json['unsigned'] ??= <String, dynamic>{};
json['unsigned'][messageSendingStatusKey] = -1; json['unsigned'][messageSendingStatusKey] = EventStatus.error.intValue;
room.client.handleSync(SyncUpdate(nextBatch: '') room.client.handleSync(SyncUpdate(nextBatch: '')
..rooms = (RoomsUpdate() ..rooms = (RoomsUpdate()
..join = (<String, JoinedRoomUpdate>{}..[room.id] = ..join = (<String, JoinedRoomUpdate>{}..[room.id] =
@ -154,7 +143,7 @@ class Event extends MatrixEvent {
factory Event.fromMatrixEvent( factory Event.fromMatrixEvent(
MatrixEvent matrixEvent, MatrixEvent matrixEvent,
Room room, { Room room, {
int status, EventStatus status,
}) => }) =>
Event( Event(
status: status, status: status,
@ -179,9 +168,9 @@ class Event extends MatrixEvent {
final unsigned = Event.getMapFromPayload(jsonPayload['unsigned']); final unsigned = Event.getMapFromPayload(jsonPayload['unsigned']);
final prevContent = Event.getMapFromPayload(jsonPayload['prev_content']); final prevContent = Event.getMapFromPayload(jsonPayload['prev_content']);
return Event( return Event(
status: jsonPayload['status'] ?? status: eventStatusFromInt(jsonPayload['status'] ??
unsigned[messageSendingStatusKey] ?? unsigned[messageSendingStatusKey] ??
defaultStatus, defaultStatus.intValue),
stateKey: jsonPayload['state_key'], stateKey: jsonPayload['state_key'],
prevContent: prevContent, prevContent: prevContent,
content: content, content: content,
@ -301,10 +290,11 @@ class Event extends MatrixEvent {
.toList(); .toList();
} }
/// Removes this event if the status is < 1. This event will just be removed /// Removes this event if the status is [sending], [error] or [removed].
/// from the database and the timelines. Returns false if not removed. /// This event will just be removed from the database and the timelines.
/// Returns [false] if not removed.
Future<bool> remove() async { Future<bool> remove() async {
if (status < 1) { if (!status.isSent) {
await room.client.database?.removeEvent(eventId, room.id); await room.client.database?.removeEvent(eventId, room.id);
room.client.onEvent.add(EventUpdate( room.client.onEvent.add(EventUpdate(
@ -312,7 +302,7 @@ class Event extends MatrixEvent {
type: EventUpdateType.timeline, type: EventUpdateType.timeline,
content: { content: {
'event_id': eventId, 'event_id': eventId,
'status': -2, 'status': EventStatus.removed.intValue,
'content': {'body': 'Removed...'} 'content': {'body': 'Removed...'}
}, },
)); ));
@ -321,9 +311,9 @@ class Event extends MatrixEvent {
return false; 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 { 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 // we do not remove the event here. It will automatically be updated
// in the `sendEvent` method to transition -1 -> 0 -> 1 -> 2 // in the `sendEvent` method to transition -1 -> 0 -> 1 -> 2
final newEventId = await room.sendEvent( final newEventId = await room.sendEvent(
@ -382,7 +372,7 @@ class Event extends MatrixEvent {
/// Returns if a file events thumbnail is encrypted /// Returns if a file events thumbnail is encrypted
bool get isThumbnailEncrypted => infoMap['thumbnail_file'] is Map; 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 String get attachmentMimetype => infoMap['mimetype'] is String
? infoMap['mimetype'].toLowerCase() ? infoMap['mimetype'].toLowerCase()
: (content['file'] is Map && content['file']['mimetype'] is String : (content['file'] is Map && content['file']['mimetype'] is String
@ -397,16 +387,16 @@ class Event extends MatrixEvent {
? infoMap['thumbnail_file']['mimetype'] ? 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( Uri get attachmentMxcUrl => Uri.parse(
isAttachmentEncrypted ? content['file']['url'] : content['url']); 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 Uri get thumbnailMxcUrl => Uri.parse(isThumbnailEncrypted
? infoMap['thumbnail_file']['url'] ? infoMap['thumbnail_file']['url']
: infoMap['thumbnail_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}) { Uri attachmentOrThumbnailMxcUrl({bool getThumbnail = false}) {
if (getThumbnail && if (getThumbnail &&
infoMap['size'] is int && infoMap['size'] is int &&
@ -493,7 +483,7 @@ class Event extends MatrixEvent {
return uint8list != null; 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 /// event and returns it as a [MatrixFile]. If this event doesn't
/// contain an attachment, this throws an error. Set [getThumbnail] to /// contain an attachment, this throws an error. Set [getThumbnail] to
/// true to download the thumbnail instead. /// true to download the thumbnail instead.
@ -671,7 +661,7 @@ class Event extends MatrixEvent {
return null; 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] /// To be able to do that you need to pass a [timeline]
bool hasAggregatedEvents(Timeline timeline, String type) => bool hasAggregatedEvents(Timeline timeline, String type) =>
timeline.aggregatedEvents.containsKey(eventId) && 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 '../matrix.dart';
import 'client.dart'; import 'client.dart';
import 'event.dart'; import 'event.dart';
import 'event_status.dart';
import 'timeline.dart'; import 'timeline.dart';
import 'user.dart'; import 'user.dart';
import 'voip_content.dart'; import 'voip_content.dart';
@ -819,7 +820,7 @@ class Room {
senderId: client.userID, senderId: client.userID,
originServerTs: sentDate, originServerTs: sentDate,
unsigned: { unsigned: {
messageSendingStatusKey: 0, messageSendingStatusKey: EventStatus.sending.intValue,
'transaction_id': messageID, 'transaction_id': messageID,
}, },
) )
@ -846,14 +847,14 @@ class Room {
} else { } else {
Logs().w('[Client] Problem while sending message', e, s); Logs().w('[Client] Problem while sending message', e, s);
syncUpdate.rooms.join.values.first.timeline.events.first syncUpdate.rooms.join.values.first.timeline.events.first
.unsigned[messageSendingStatusKey] = -1; .unsigned[messageSendingStatusKey] = EventStatus.error.intValue;
await _handleFakeSync(syncUpdate); await _handleFakeSync(syncUpdate);
return null; return null;
} }
} }
} }
syncUpdate.rooms.join.values.first.timeline.events.first 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; syncUpdate.rooms.join.values.first.timeline.events.first.eventId = res;
await _handleFakeSync(syncUpdate); await _handleFakeSync(syncUpdate);

View File

@ -20,6 +20,7 @@ import 'dart:async';
import '../matrix.dart'; import '../matrix.dart';
import 'event.dart'; import 'event.dart';
import 'event_status.dart';
import 'room.dart'; import 'room.dart';
import 'utils/event_update.dart'; import 'utils/event_update.dart';
@ -243,11 +244,11 @@ class Timeline {
eventUpdate.type != EventUpdateType.history) { eventUpdate.type != EventUpdateType.history) {
return; return;
} }
final status = 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]
: null) ?? : null) ??
2; EventStatus.synced.intValue);
// Redaction events are handled as modification for existing events. // Redaction events are handled as modification for existing events.
if (eventUpdate.content['type'] == EventTypes.Redaction) { if (eventUpdate.content['type'] == EventTypes.Redaction) {
final eventId = _findEvent(event_id: eventUpdate.content['redacts']); final eventId = _findEvent(event_id: eventUpdate.content['redacts']);
@ -258,7 +259,7 @@ class Timeline {
room, room,
)); ));
} }
} else if (status == -2) { } else if (status.isRemoved) {
final i = _findEvent(event_id: eventUpdate.content['event_id']); final i = _findEvent(event_id: eventUpdate.content['event_id']);
if (i < events.length) { if (i < events.length) {
removeAggregatedEvent(events[i]); removeAggregatedEvent(events[i]);
@ -279,7 +280,8 @@ class Timeline {
room, room,
); );
// do we preserve the status? we should allow 0 -> -1 updates and status increases // 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; events[i].status = oldStatus;
} }
addAggregatedEvent(events[i]); addAggregatedEvent(events[i]);
@ -315,7 +317,7 @@ class Timeline {
extension on List<Event> { extension on List<Event> {
int get firstIndexWhereNotError { int get firstIndexWhereNotError {
if (isEmpty) return 0; if (isEmpty) return 0;
final index = indexWhere((e) => e.status != -1); final index = indexWhere((event) => !event.status.isError);
if (index == -1) return length; if (index == -1) return length;
return index; 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/encryption.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:matrix/src/event.dart'; import 'package:matrix/src/event.dart';
import 'package:matrix/src/event_status.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';
@ -53,7 +54,7 @@ void main() {
'origin_server_ts': timestamp, 'origin_server_ts': timestamp,
'type': type, 'type': type,
'room_id': '1234', 'room_id': '1234',
'status': 2, 'status': EventStatus.synced.intValue,
'content': contentJson, 'content': contentJson,
}; };
final client = Client('testclient', httpClient: FakeMatrixApi()); final client = Client('testclient', httpClient: FakeMatrixApi());
@ -79,7 +80,7 @@ void main() {
expect(event.eventId, id); expect(event.eventId, id);
expect(event.senderId, senderID); expect(event.senderId, senderID);
expect(event.status, 2); expect(event.status, EventStatus.synced);
expect(event.text, body); expect(event.text, body);
expect(event.formattedText, formatted_body); expect(event.formattedText, formatted_body);
expect(event.body, body); expect(event.body, body);
@ -89,7 +90,7 @@ void main() {
final state = Event.fromJson(jsonObj, null); final state = Event.fromJson(jsonObj, null);
expect(state.eventId, id); expect(state.eventId, id);
expect(state.stateKey, ''); expect(state.stateKey, '');
expect(state.status, 2); expect(state.status, EventStatus.synced);
}); });
test('Test all EventTypes', () async { test('Test all EventTypes', () async {
Event event; Event event;
@ -259,7 +260,7 @@ void main() {
final event = Event.fromJson( final event = Event.fromJson(
jsonObj, Room(id: '1234', client: Client('testclient'))); jsonObj, Room(id: '1234', client: Client('testclient')));
final removed1 = await event.remove(); final removed1 = await event.remove();
event.status = 0; event.status = EventStatus.sending;
final removed2 = await event.remove(); final removed2 = await event.remove();
expect(removed1, false); expect(removed1, false);
expect(removed2, true); expect(removed2, true);
@ -276,7 +277,7 @@ void main() {
final event = Event.fromJson( final event = Event.fromJson(
jsonObj, Room(id: '!1234:example.com', client: matrix)); jsonObj, Room(id: '!1234:example.com', client: matrix));
final resp1 = await event.sendAgain(); final resp1 = await event.sendAgain();
event.status = -1; event.status = EventStatus.error;
final resp2 = await event.sendAgain(txid: '1234'); final resp2 = await event.sendAgain(txid: '1234');
expect(resp1, null); expect(resp1, null);
expect(resp2.startsWith('\$event'), true); expect(resp2.startsWith('\$event'), true);
@ -308,7 +309,7 @@ void main() {
'origin_server_ts': timestamp, 'origin_server_ts': timestamp,
'type': 'm.room.encrypted', 'type': 'm.room.encrypted',
'room_id': '1234', 'room_id': '1234',
'status': 2, 'status': EventStatus.synced.intValue,
'content': json.encode({ 'content': json.encode({
'msgtype': 'm.bad.encrypted', 'msgtype': 'm.bad.encrypted',
'body': DecryptException.unknownSession, 'body': DecryptException.unknownSession,

View File

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

View File

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