refactor: Change event status to enum
This commit is contained in:
parent
d5e5500ac5
commit
c6e0359522
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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) &&
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue