Merge branch 'main' into voip/add-voip-function
This commit is contained in:
commit
7efe123274
37
CHANGELOG.md
37
CHANGELOG.md
|
|
@ -1,3 +1,40 @@
|
||||||
|
## [0.7.0-nullsafety.10] - 26nd Nov 2021
|
||||||
|
- feat: Migrate olm sessions on database migration
|
||||||
|
- chore: Enable E2EE recovery by default
|
||||||
|
|
||||||
|
## [0.7.0-nullsafety.9] - 25nd Nov 2021
|
||||||
|
- fix: Limited timeline clean up on web
|
||||||
|
- fix: Remove account avatar
|
||||||
|
|
||||||
|
## [0.7.0-nullsafety.8] - 24nd Nov 2021
|
||||||
|
- chore: Update FluffyBox
|
||||||
|
|
||||||
|
## [0.7.0-nullsafety.7] - 23nd Nov 2021
|
||||||
|
- feat: Add commands to create chats
|
||||||
|
- feat: Add clear cache command
|
||||||
|
- feat: Implement new FluffyBox database API implementation
|
||||||
|
- fix: Workaround for a null exception for a non nullable boolean while user device key updating
|
||||||
|
- fix: Limited timeline clears too many events
|
||||||
|
- fix: Ability to remove avatar from room and account
|
||||||
|
- fix: Request history in archived rooms
|
||||||
|
- fix: Decrypt last event of a room
|
||||||
|
- refactor: Remove Sembast database implementation
|
||||||
|
|
||||||
|
## [0.7.0-nullsafety.6] - 16nd Nov 2021
|
||||||
|
- feat: Implement sembast store
|
||||||
|
- fix: HtmlToText crashes with an empty code block
|
||||||
|
- fix: use originServerTs to check if state event is old
|
||||||
|
- fix: Dont enable e2ee in new direct chats without encryption support
|
||||||
|
- fix: Change eventstatus of edits in prevEvent
|
||||||
|
- chore: Trim formatted username fallback
|
||||||
|
|
||||||
|
## [0.7.0-nullsafety.5] - 10nd Nov 2021
|
||||||
|
- fix: Edits as lastEvent do not update
|
||||||
|
- fix: JSON parsing in decryptRoomEvent method
|
||||||
|
- fix: Wrong null check in hive database
|
||||||
|
- fix: crash on invalid displaynames
|
||||||
|
- chore: Update matrix_api_lite
|
||||||
|
|
||||||
## [0.7.0-nullsafety.4] - 09nd Nov 2021
|
## [0.7.0-nullsafety.4] - 09nd Nov 2021
|
||||||
- feat: More advanced create chat methods (encryption is now enabled by default)
|
- feat: More advanced create chat methods (encryption is now enabled by default)
|
||||||
- feat: Make waiting on init db optional
|
- feat: Make waiting on init db optional
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ import 'utils/bootstrap.dart';
|
||||||
class Encryption {
|
class Encryption {
|
||||||
final Client client;
|
final Client client;
|
||||||
final bool debug;
|
final bool debug;
|
||||||
final bool enableE2eeRecovery;
|
|
||||||
|
|
||||||
bool get enabled => olmManager.enabled;
|
bool get enabled => olmManager.enabled;
|
||||||
|
|
||||||
|
|
@ -53,7 +52,6 @@ class Encryption {
|
||||||
Encryption({
|
Encryption({
|
||||||
required this.client,
|
required this.client,
|
||||||
this.debug = false,
|
this.debug = false,
|
||||||
required this.enableE2eeRecovery,
|
|
||||||
}) {
|
}) {
|
||||||
ssss = SSSS(this);
|
ssss = SSSS(this);
|
||||||
keyManager = KeyManager(this);
|
keyManager = KeyManager(this);
|
||||||
|
|
@ -232,8 +230,7 @@ class Encryption {
|
||||||
decryptedPayload = json.decode(decryptResult.plaintext);
|
decryptedPayload = json.decode(decryptResult.plaintext);
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
// alright, if this was actually by our own outbound group session, we might as well clear it
|
// alright, if this was actually by our own outbound group session, we might as well clear it
|
||||||
if (client.enableE2eeRecovery &&
|
if (exception.toString() != DecryptException.unknownSession &&
|
||||||
exception.toString() != DecryptException.unknownSession &&
|
|
||||||
(keyManager
|
(keyManager
|
||||||
.getOutboundGroupSession(roomId)
|
.getOutboundGroupSession(roomId)
|
||||||
?.outboundGroupSession
|
?.outboundGroupSession
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@ class KeyManager {
|
||||||
// attempt to decrypt the last event
|
// attempt to decrypt the last event
|
||||||
final event = room.getState(EventTypes.Encrypted);
|
final event = room.getState(EventTypes.Encrypted);
|
||||||
if (event != null && event.content['session_id'] == sessionId) {
|
if (event != null && event.content['session_id'] == sessionId) {
|
||||||
encryption.decryptRoomEvent(roomId, event, store: true);
|
room.setState(encryption.decryptRoomEventSync(roomId, event));
|
||||||
}
|
}
|
||||||
// and finally broadcast the new session
|
// and finally broadcast the new session
|
||||||
room.onSessionKeyReceived.add(sessionId);
|
room.onSessionKeyReceived.add(sessionId);
|
||||||
|
|
@ -219,8 +219,7 @@ class KeyManager {
|
||||||
void maybeAutoRequest(String roomId, String sessionId, String senderKey) {
|
void maybeAutoRequest(String roomId, String sessionId, String senderKey) {
|
||||||
final room = client.getRoomById(roomId);
|
final room = client.getRoomById(roomId);
|
||||||
final requestIdent = '$roomId|$sessionId|$senderKey';
|
final requestIdent = '$roomId|$sessionId|$senderKey';
|
||||||
if (client.enableE2eeRecovery &&
|
if (room != null &&
|
||||||
room != null &&
|
|
||||||
!_requestedSessionIds.contains(requestIdent) &&
|
!_requestedSessionIds.contains(requestIdent) &&
|
||||||
!client.isUnknownSession) {
|
!client.isUnknownSession) {
|
||||||
// do e2ee recovery
|
// do e2ee recovery
|
||||||
|
|
|
||||||
|
|
@ -514,10 +514,10 @@ class OlmManager {
|
||||||
return _decryptToDeviceEvent(event);
|
return _decryptToDeviceEvent(event);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// okay, the thing errored while decrypting. It is safe to assume that the olm session is corrupt and we should generate a new one
|
// okay, the thing errored while decrypting. It is safe to assume that the olm session is corrupt and we should generate a new one
|
||||||
if (client.enableE2eeRecovery) {
|
|
||||||
// ignore: unawaited_futures
|
// ignore: unawaited_futures
|
||||||
runInRoot(() => restoreOlmSession(event.senderId, senderKey));
|
runInRoot(() => restoreOlmSession(event.senderId, senderKey));
|
||||||
}
|
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export 'package:matrix_api_lite/matrix_api_lite.dart';
|
||||||
export 'src/client.dart';
|
export 'src/client.dart';
|
||||||
export 'src/database/database_api.dart';
|
export 'src/database/database_api.dart';
|
||||||
export 'src/database/hive_database.dart';
|
export 'src/database/hive_database.dart';
|
||||||
|
export 'src/database/fluffybox_database.dart';
|
||||||
export 'src/event.dart';
|
export 'src/event.dart';
|
||||||
export 'src/event_status.dart';
|
export 'src/event_status.dart';
|
||||||
export 'src/voip.dart';
|
export 'src/voip.dart';
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,6 @@ class Client extends MatrixApi {
|
||||||
|
|
||||||
DatabaseApi? get database => _database;
|
DatabaseApi? get database => _database;
|
||||||
|
|
||||||
bool enableE2eeRecovery;
|
|
||||||
|
|
||||||
@deprecated
|
@deprecated
|
||||||
MatrixApi get api => this;
|
MatrixApi get api => this;
|
||||||
|
|
||||||
|
|
@ -120,7 +118,6 @@ class Client extends MatrixApi {
|
||||||
/// [databaseBuilder]: A function that creates the database instance, that will be used.
|
/// [databaseBuilder]: A function that creates the database instance, that will be used.
|
||||||
/// [legacyDatabaseBuilder]: Use this for your old database implementation to perform an automatic migration
|
/// [legacyDatabaseBuilder]: Use this for your old database implementation to perform an automatic migration
|
||||||
/// [databaseDestroyer]: A function that can be used to destroy a database instance, for example by deleting files from disk.
|
/// [databaseDestroyer]: A function that can be used to destroy a database instance, for example by deleting files from disk.
|
||||||
/// [enableE2eeRecovery]: Enable additional logic to try to recover from bad e2ee sessions
|
|
||||||
/// [verificationMethods]: A set of all the verification methods this client can handle. Includes:
|
/// [verificationMethods]: A set of all the verification methods this client can handle. Includes:
|
||||||
/// KeyVerificationMethod.numbers: Compare numbers. Most basic, should be supported
|
/// KeyVerificationMethod.numbers: Compare numbers. Most basic, should be supported
|
||||||
/// KeyVerificationMethod.emoji: Compare emojis
|
/// KeyVerificationMethod.emoji: Compare emojis
|
||||||
|
|
@ -157,7 +154,8 @@ class Client extends MatrixApi {
|
||||||
this.databaseDestroyer,
|
this.databaseDestroyer,
|
||||||
this.legacyDatabaseBuilder,
|
this.legacyDatabaseBuilder,
|
||||||
this.legacyDatabaseDestroyer,
|
this.legacyDatabaseDestroyer,
|
||||||
this.enableE2eeRecovery = false,
|
@Deprecated('This is now always enabled by default.')
|
||||||
|
bool? enableE2eeRecovery,
|
||||||
Set<KeyVerificationMethod>? verificationMethods,
|
Set<KeyVerificationMethod>? verificationMethods,
|
||||||
http.Client? httpClient,
|
http.Client? httpClient,
|
||||||
Set<String>? importantStateEvents,
|
Set<String>? importantStateEvents,
|
||||||
|
|
@ -568,7 +566,8 @@ class Client extends MatrixApi {
|
||||||
final directChatRoomId = getDirectChatFromUserId(mxid);
|
final directChatRoomId = getDirectChatFromUserId(mxid);
|
||||||
if (directChatRoomId != null) return directChatRoomId;
|
if (directChatRoomId != null) return directChatRoomId;
|
||||||
|
|
||||||
enableEncryption ??= await userOwnsEncryptionKeys(mxid);
|
enableEncryption ??=
|
||||||
|
encryptionEnabled && await userOwnsEncryptionKeys(mxid);
|
||||||
if (enableEncryption) {
|
if (enableEncryption) {
|
||||||
initialState ??= [];
|
initialState ??= [];
|
||||||
if (!initialState.any((s) => s.type == EventTypes.Encryption)) {
|
if (!initialState.any((s) => s.type == EventTypes.Encryption)) {
|
||||||
|
|
@ -609,6 +608,7 @@ class Client extends MatrixApi {
|
||||||
List<String>? invite,
|
List<String>? invite,
|
||||||
CreateRoomPreset preset = CreateRoomPreset.privateChat,
|
CreateRoomPreset preset = CreateRoomPreset.privateChat,
|
||||||
List<StateEvent>? initialState,
|
List<StateEvent>? initialState,
|
||||||
|
Visibility? visibility,
|
||||||
bool waitForSync = true,
|
bool waitForSync = true,
|
||||||
}) async {
|
}) async {
|
||||||
enableEncryption ??=
|
enableEncryption ??=
|
||||||
|
|
@ -629,6 +629,7 @@ class Client extends MatrixApi {
|
||||||
preset: preset,
|
preset: preset,
|
||||||
name: groupName,
|
name: groupName,
|
||||||
initialState: initialState,
|
initialState: initialState,
|
||||||
|
visibility: visibility,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (waitForSync) {
|
if (waitForSync) {
|
||||||
|
|
@ -649,7 +650,7 @@ class Client extends MatrixApi {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
final keys = await queryKeys({userId: []});
|
final keys = await queryKeys({userId: []});
|
||||||
return keys.deviceKeys?.isNotEmpty ?? false;
|
return keys.deviceKeys?[userId]?.isNotEmpty ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new space and returns the Room ID. The parameters are mostly
|
/// Creates a new space and returns the Room ID. The parameters are mostly
|
||||||
|
|
@ -772,6 +773,7 @@ class Client extends MatrixApi {
|
||||||
leftRoom,
|
leftRoom,
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
leftRoom.prev_batch = room.timeline?.prevBatch;
|
||||||
room.state?.forEach((event) {
|
room.state?.forEach((event) {
|
||||||
leftRoom.setState(Event.fromMatrixEvent(
|
leftRoom.setState(Event.fromMatrixEvent(
|
||||||
event,
|
event,
|
||||||
|
|
@ -817,8 +819,15 @@ class Client extends MatrixApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Uploads a new user avatar for this user.
|
/// Uploads a new user avatar for this user. Leave file null to remove the
|
||||||
Future<void> setAvatar(MatrixFile file) async {
|
/// current avatar.
|
||||||
|
Future<void> setAvatar(MatrixFile? file) async {
|
||||||
|
if (file == null) {
|
||||||
|
// We send an empty String to remove the avatar. Sending Null **should**
|
||||||
|
// work but it doesn't with Synapse. See:
|
||||||
|
// https://gitlab.com/famedly/company/frontend/famedlysdk/-/issues/254
|
||||||
|
return setAvatarUrl(userID!, Uri.parse(''));
|
||||||
|
}
|
||||||
final uploadResp = await uploadContent(
|
final uploadResp = await uploadContent(
|
||||||
file.bytes,
|
file.bytes,
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
|
|
@ -1063,8 +1072,7 @@ class Client extends MatrixApi {
|
||||||
// make sure to throw an exception if libolm doesn't exist
|
// make sure to throw an exception if libolm doesn't exist
|
||||||
await olm.init();
|
await olm.init();
|
||||||
olm.get_library_version();
|
olm.get_library_version();
|
||||||
encryption =
|
encryption = Encryption(client: this);
|
||||||
Encryption(client: this, enableE2eeRecovery: enableE2eeRecovery);
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
encryption?.dispose();
|
encryption?.dispose();
|
||||||
encryption = null;
|
encryption = null;
|
||||||
|
|
@ -1806,7 +1814,13 @@ class Client extends MatrixApi {
|
||||||
final deviceKeysList =
|
final deviceKeysList =
|
||||||
_userDeviceKeys[userId] ??= DeviceKeysList(userId, this);
|
_userDeviceKeys[userId] ??= DeviceKeysList(userId, this);
|
||||||
final failure = _keyQueryFailures[userId.domain];
|
final failure = _keyQueryFailures[userId.domain];
|
||||||
if (deviceKeysList.outdated &&
|
|
||||||
|
// deviceKeysList.outdated is not nullable but we have seen this error
|
||||||
|
// in production: `Failed assertion: boolean expression must not be null`
|
||||||
|
// So this could either be a null safety bug in Dart or a result of
|
||||||
|
// using unsound null safety. The extra equal check `!= false` should
|
||||||
|
// save us here.
|
||||||
|
if (deviceKeysList.outdated != false &&
|
||||||
(failure == null ||
|
(failure == null ||
|
||||||
DateTime.now()
|
DateTime.now()
|
||||||
.subtract(Duration(minutes: 5))
|
.subtract(Duration(minutes: 5))
|
||||||
|
|
@ -2381,6 +2395,24 @@ class Client extends MatrixApi {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Logs().d('Migrate OLM sessions...');
|
||||||
|
try {
|
||||||
|
final olmSessions = await legacyDatabase.getAllOlmSessions();
|
||||||
|
for (final identityKey in olmSessions.keys) {
|
||||||
|
final sessions = olmSessions[identityKey]!;
|
||||||
|
for (final sessionId in sessions.keys) {
|
||||||
|
final session = sessions[sessionId]!;
|
||||||
|
await database.storeOlmSession(
|
||||||
|
identityKey,
|
||||||
|
session['session_id'] as String,
|
||||||
|
session['pickle'] as String,
|
||||||
|
session['last_received'] as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs().e('Unable to migrate OLM sessions!', e, s);
|
||||||
|
}
|
||||||
Logs().d('Migrate Device Keys...');
|
Logs().d('Migrate Device Keys...');
|
||||||
final userDeviceKeys = await legacyDatabase.getUserDeviceKeys(this);
|
final userDeviceKeys = await legacyDatabase.getUserDeviceKeys(this);
|
||||||
for (final userId in userDeviceKeys.keys) {
|
for (final userId in userDeviceKeys.keys) {
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,6 @@ abstract class DatabaseApi {
|
||||||
|
|
||||||
Future<Event?> getEventById(String eventId, Room room);
|
Future<Event?> getEventById(String eventId, Room room);
|
||||||
|
|
||||||
bool eventIsKnown(String eventId, String roomId);
|
|
||||||
|
|
||||||
Future<void> forgetRoom(String roomId);
|
Future<void> forgetRoom(String roomId);
|
||||||
|
|
||||||
Future<void> clearCache();
|
Future<void> clearCache();
|
||||||
|
|
@ -272,6 +270,8 @@ abstract class DatabaseApi {
|
||||||
String userId,
|
String userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<Map<String, Map>> getAllOlmSessions();
|
||||||
|
|
||||||
Future<List<OlmSession>> getOlmSessionsForDevices(
|
Future<List<OlmSession>> getOlmSessionsForDevices(
|
||||||
List<String> identityKeys,
|
List<String> identityKeys,
|
||||||
String userId,
|
String userId,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -371,10 +371,6 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
|
||||||
return Event.fromJson(convertToJson(raw), room);
|
return Event.fromJson(convertToJson(raw), room);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
bool eventIsKnown(String eventId, String roomId) =>
|
|
||||||
_eventsBox.keys.contains(MultiKey(roomId, eventId).toString());
|
|
||||||
|
|
||||||
/// Loads a whole list of events at once from the store for a specific room
|
/// Loads a whole list of events at once from the store for a specific room
|
||||||
Future<List<Event>> _getEventsByIds(List<String> eventIds, Room room) =>
|
Future<List<Event>> _getEventsByIds(List<String> eventIds, Room room) =>
|
||||||
Future.wait(eventIds
|
Future.wait(eventIds
|
||||||
|
|
@ -489,6 +485,21 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, Map>> getAllOlmSessions() async {
|
||||||
|
final backup = Map.fromEntries(
|
||||||
|
await Future.wait(
|
||||||
|
_olmSessionsBox.keys.map(
|
||||||
|
(key) async => MapEntry(
|
||||||
|
key,
|
||||||
|
await _olmSessionsBox.get(key),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return backup.cast<String, Map>();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<OlmSession>> getOlmSessionsForDevices(
|
Future<List<OlmSession>> getOlmSessionsForDevices(
|
||||||
List<String> identityKey, String userId) async {
|
List<String> identityKey, String userId) async {
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,8 @@ class Room {
|
||||||
state.relationshipEventId != null &&
|
state.relationshipEventId != null &&
|
||||||
state.relationshipType == RelationshipTypes.edit &&
|
state.relationshipType == RelationshipTypes.edit &&
|
||||||
lastEvent != null &&
|
lastEvent != null &&
|
||||||
!lastEvent.matchesEventOrTransactionId(state.relationshipEventId)) {
|
!state.matchesEventOrTransactionId(lastEvent.eventId) &&
|
||||||
|
lastEvent.eventId != state.relationshipEventId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,7 +203,8 @@ class Room {
|
||||||
final prevEvent = getState(state.type, stateKey);
|
final prevEvent = getState(state.type, stateKey);
|
||||||
if (prevEvent != null &&
|
if (prevEvent != null &&
|
||||||
prevEvent.eventId != state.eventId &&
|
prevEvent.eventId != state.eventId &&
|
||||||
client.database?.eventIsKnown(state.eventId, roomId) == true) {
|
prevEvent.originServerTs.millisecondsSinceEpoch >
|
||||||
|
state.originServerTs.millisecondsSinceEpoch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1354,15 +1356,18 @@ class Room {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Uploads a new user avatar for this room. Returns the event ID of the new
|
/// Uploads a new user avatar for this room. Returns the event ID of the new
|
||||||
/// m.room.avatar event.
|
/// m.room.avatar event. Leave empty to remove the current avatar.
|
||||||
Future<String> setAvatar(MatrixFile file) async {
|
Future<String> setAvatar(MatrixFile? file) async {
|
||||||
final uploadResp =
|
final uploadResp = file == null
|
||||||
await client.uploadContent(file.bytes, filename: file.name);
|
? null
|
||||||
|
: await client.uploadContent(file.bytes, filename: file.name);
|
||||||
return await client.setRoomStateWithKey(
|
return await client.setRoomStateWithKey(
|
||||||
id,
|
id,
|
||||||
EventTypes.RoomAvatar,
|
EventTypes.RoomAvatar,
|
||||||
'',
|
'',
|
||||||
{'url': uploadResp.toString()},
|
{
|
||||||
|
if (uploadResp != null) 'url': uploadResp.toString(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,13 +120,12 @@ class Timeline {
|
||||||
this.onRemove,
|
this.onRemove,
|
||||||
}) : events = events ?? [] {
|
}) : events = events ?? [] {
|
||||||
sub = room.client.onEvent.stream.listen(_handleEventUpdate);
|
sub = room.client.onEvent.stream.listen(_handleEventUpdate);
|
||||||
|
|
||||||
// If the timeline is limited we want to clear our events cache
|
// If the timeline is limited we want to clear our events cache
|
||||||
roomSub = room.client.onSync.stream
|
roomSub = room.client.onSync.stream
|
||||||
.where((sync) => sync.rooms?.join?[room.id]?.timeline?.limited == true)
|
.where((sync) => sync.rooms?.join?[room.id]?.timeline?.limited == true)
|
||||||
.listen((_) {
|
.listen(_removeEventsNotInThisSync);
|
||||||
this.events.clear();
|
|
||||||
aggregatedEvents.clear();
|
|
||||||
});
|
|
||||||
sessionIdReceivedSub =
|
sessionIdReceivedSub =
|
||||||
room.onSessionKeyReceived.stream.listen(_sessionKeyReceived);
|
room.onSessionKeyReceived.stream.listen(_sessionKeyReceived);
|
||||||
|
|
||||||
|
|
@ -136,6 +135,13 @@ class Timeline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes all entries from [events] which are not in this SyncUpdate.
|
||||||
|
void _removeEventsNotInThisSync(SyncUpdate sync) {
|
||||||
|
final newSyncEvents = sync.rooms?.join?[room.id]?.timeline?.events ?? [];
|
||||||
|
final keepEventIds = newSyncEvents.map((e) => e.eventId);
|
||||||
|
events.removeWhere((e) => !keepEventIds.contains(e.eventId));
|
||||||
|
}
|
||||||
|
|
||||||
/// Don't forget to call this before you dismiss this object!
|
/// Don't forget to call this before you dismiss this object!
|
||||||
void cancelSubscriptions() {
|
void cancelSubscriptions() {
|
||||||
sub?.cancel();
|
sub?.cancel();
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ class User extends Event {
|
||||||
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
|
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return words.join(' ');
|
return words.join(' ').trim();
|
||||||
}
|
}
|
||||||
return 'Unknown user';
|
return 'Unknown user';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,19 @@ extension CommandsClientExtension on Client {
|
||||||
txid: args.txid,
|
txid: args.txid,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
addCommand('dm', (CommandArgs args) async {
|
||||||
|
final parts = args.msg.split(' ');
|
||||||
|
return await args.room.client.startDirectChat(
|
||||||
|
parts.first,
|
||||||
|
enableEncryption: !parts.any((part) => part == '--no-encryption'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
addCommand('create', (CommandArgs args) async {
|
||||||
|
final parts = args.msg.split(' ');
|
||||||
|
return await args.room.client.createGroupChat(
|
||||||
|
enableEncryption: !parts.any((part) => part == '--no-encryption'),
|
||||||
|
);
|
||||||
|
});
|
||||||
addCommand('plain', (CommandArgs args) async {
|
addCommand('plain', (CommandArgs args) async {
|
||||||
return await args.room.sendTextEvent(
|
return await args.room.sendTextEvent(
|
||||||
args.msg,
|
args.msg,
|
||||||
|
|
@ -202,6 +215,10 @@ extension CommandsClientExtension on Client {
|
||||||
.clearOrUseOutboundGroupSession(args.room.id, wipe: true);
|
.clearOrUseOutboundGroupSession(args.room.id, wipe: true);
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
addCommand('clearcache', (CommandArgs args) async {
|
||||||
|
await clearCache();
|
||||||
|
return '';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,11 +50,13 @@ class HtmlToText {
|
||||||
.firstMatch(text);
|
.firstMatch(text);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
text = HtmlUnescape().convert(text);
|
text = HtmlUnescape().convert(text);
|
||||||
if (text[0] != '\n') {
|
if (text.isNotEmpty) {
|
||||||
text = '\n$text';
|
if (text[0] != '\n') {
|
||||||
}
|
text = '\n$text';
|
||||||
if (text[text.length - 1] != '\n') {
|
}
|
||||||
text += '\n';
|
if (text[text.length - 1] != '\n') {
|
||||||
|
text += '\n';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
@ -64,11 +66,13 @@ class HtmlToText {
|
||||||
text = text.replaceAll(
|
text = text.replaceAll(
|
||||||
RegExp(r'</code>$', multiLine: false, caseSensitive: false), '');
|
RegExp(r'</code>$', multiLine: false, caseSensitive: false), '');
|
||||||
text = HtmlUnescape().convert(text);
|
text = HtmlUnescape().convert(text);
|
||||||
if (text[0] != '\n') {
|
if (text.isNotEmpty) {
|
||||||
text = '\n$text';
|
if (text[0] != '\n') {
|
||||||
}
|
text = '\n$text';
|
||||||
if (text[text.length - 1] != '\n') {
|
}
|
||||||
text += '\n';
|
if (text[text.length - 1] != '\n') {
|
||||||
|
text += '\n';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
final language =
|
final language =
|
||||||
RegExp(r'language-(\w+)', multiLine: false, caseSensitive: false)
|
RegExp(r'language-(\w+)', multiLine: false, caseSensitive: false)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
name: matrix
|
name: matrix
|
||||||
description: Matrix Dart SDK
|
description: Matrix Dart SDK
|
||||||
version: 0.7.0-nullsafety.4
|
version: 0.7.0-nullsafety.10
|
||||||
homepage: https://famedly.com
|
homepage: https://famedly.com
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|
@ -25,6 +25,7 @@ dependencies:
|
||||||
collection: ^1.15.0
|
collection: ^1.15.0
|
||||||
webrtc_interface: ^1.0.1
|
webrtc_interface: ^1.0.1
|
||||||
sdp_transform: ^0.3.2
|
sdp_transform: ^0.3.2
|
||||||
|
fluffybox: ^0.3.5
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
dart_code_metrics: ^4.4.0
|
dart_code_metrics: ^4.4.0
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,28 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('dm', () async {
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
await room.sendTextEvent('/dm @alice:example.com --no-encryption');
|
||||||
|
expect(
|
||||||
|
json.decode(
|
||||||
|
FakeMatrixApi.calledEndpoints['/client/r0/createRoom']?.first),
|
||||||
|
{
|
||||||
|
'invite': ['@alice:example.com'],
|
||||||
|
'is_direct': true,
|
||||||
|
'preset': 'trusted_private_chat'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create', () async {
|
||||||
|
FakeMatrixApi.calledEndpoints.clear();
|
||||||
|
await room.sendTextEvent('/create @alice:example.com --no-encryption');
|
||||||
|
expect(
|
||||||
|
json.decode(
|
||||||
|
FakeMatrixApi.calledEndpoints['/client/r0/createRoom']?.first),
|
||||||
|
{'preset': 'private_chat'});
|
||||||
|
});
|
||||||
|
|
||||||
test('discardsession', () async {
|
test('discardsession', () async {
|
||||||
if (olmEnabled) {
|
if (olmEnabled) {
|
||||||
await client.encryption?.keyManager.createOutboundGroupSession(room.id);
|
await client.encryption?.keyManager.createOutboundGroupSession(room.id);
|
||||||
|
|
@ -289,6 +311,11 @@ void main() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('create', () async {
|
||||||
|
await room.sendTextEvent('/clearcache');
|
||||||
|
expect(room.client.prevBatch, null);
|
||||||
|
});
|
||||||
|
|
||||||
test('dispose client', () async {
|
test('dispose client', () async {
|
||||||
await client.dispose(closeDatabase: true);
|
await client.dispose(closeDatabase: true);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,11 @@ import 'package:olm/olm.dart' as olm;
|
||||||
import 'fake_database.dart';
|
import 'fake_database.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
/// All Tests related to the ChatTime
|
group('FluffyBox Database Test', () {
|
||||||
|
testDatabase(
|
||||||
|
getFluffyBoxDatabase(null),
|
||||||
|
);
|
||||||
|
});
|
||||||
group('Hive Database Test', () {
|
group('Hive Database Test', () {
|
||||||
testDatabase(
|
testDatabase(
|
||||||
getHiveDatabase(null),
|
getHiveDatabase(null),
|
||||||
|
|
@ -309,6 +313,42 @@ void testDatabase(
|
||||||
);
|
);
|
||||||
expect(olm.isEmpty, true);
|
expect(olm.isEmpty, true);
|
||||||
});
|
});
|
||||||
|
test('getAllOlmSessions', () async {
|
||||||
|
var sessions = await database.getAllOlmSessions();
|
||||||
|
expect(sessions.isEmpty, true);
|
||||||
|
await database.storeOlmSession(
|
||||||
|
'identityKey',
|
||||||
|
'sessionId',
|
||||||
|
'pickle',
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
await database.storeOlmSession(
|
||||||
|
'identityKey',
|
||||||
|
'sessionId2',
|
||||||
|
'pickle',
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
sessions = await database.getAllOlmSessions();
|
||||||
|
expect(
|
||||||
|
sessions,
|
||||||
|
{
|
||||||
|
'identityKey': {
|
||||||
|
'sessionId': {
|
||||||
|
'identity_key': 'identityKey',
|
||||||
|
'pickle': 'pickle',
|
||||||
|
'session_id': 'sessionId',
|
||||||
|
'last_received': 0
|
||||||
|
},
|
||||||
|
'sessionId2': {
|
||||||
|
'identity_key': 'identityKey',
|
||||||
|
'pickle': 'pickle',
|
||||||
|
'session_id': 'sessionId2',
|
||||||
|
'last_received': 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
test('getOlmSessionsForDevices', () async {
|
test('getOlmSessionsForDevices', () async {
|
||||||
final olm = await database.getOlmSessionsForDevices(
|
final olm = await database.getOlmSessionsForDevices(
|
||||||
['identityKeys'],
|
['identityKeys'],
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,25 @@ import 'package:matrix/matrix.dart';
|
||||||
import 'package:matrix/src/database/hive_database.dart';
|
import 'package:matrix/src/database/hive_database.dart';
|
||||||
import 'package:file/memory.dart';
|
import 'package:file/memory.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:matrix/src/database/fluffybox_database.dart';
|
||||||
|
|
||||||
Future<DatabaseApi> getDatabase(Client? _) => getHiveDatabase(_);
|
Future<DatabaseApi> getDatabase(Client? _) => getHiveDatabase(_);
|
||||||
|
|
||||||
bool hiveInitialized = false;
|
bool hiveInitialized = false;
|
||||||
|
|
||||||
|
Future<FluffyBoxDatabase> getFluffyBoxDatabase(Client? c) async {
|
||||||
|
final fileSystem = MemoryFileSystem();
|
||||||
|
final testHivePath =
|
||||||
|
'${fileSystem.path}/build/.test_store/${Random().nextDouble()}';
|
||||||
|
Directory(testHivePath).createSync(recursive: true);
|
||||||
|
final db = FluffyBoxDatabase(
|
||||||
|
'unit_test.${c?.hashCode}',
|
||||||
|
testHivePath,
|
||||||
|
);
|
||||||
|
await db.open();
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
Future<FamedlySdkHiveDatabase> getHiveDatabase(Client? c) async {
|
Future<FamedlySdkHiveDatabase> getHiveDatabase(Client? c) async {
|
||||||
if (!hiveInitialized) {
|
if (!hiveInitialized) {
|
||||||
final fileSystem = MemoryFileSystem();
|
final fileSystem = MemoryFileSystem();
|
||||||
|
|
|
||||||
|
|
@ -21,80 +21,81 @@ import 'package:test/test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('htmlToText', () {
|
group('htmlToText', () {
|
||||||
test('stuff', () async {
|
final testMap = <String, String>{
|
||||||
final testMap = <String, String>{
|
'': '',
|
||||||
'': '',
|
'hello world\nthis is a test': 'hello world\nthis is a test',
|
||||||
'hello world\nthis is a test': 'hello world\nthis is a test',
|
'<em>That\'s</em> not a test, <strong>this</strong> is a test':
|
||||||
'<em>That\'s</em> not a test, <strong>this</strong> is a test':
|
'*That\'s* not a test, **this** is a test',
|
||||||
'*That\'s* not a test, **this** is a test',
|
'Visit <del><a href="http://example.com">our website</a></del> (outdated)':
|
||||||
'Visit <del><a href="http://example.com">our website</a></del> (outdated)':
|
'Visit ~~🔗our website~~ (outdated)',
|
||||||
'Visit ~~🔗our website~~ (outdated)',
|
'(cw spiders) <span data-mx-spoiler>spiders are pretty cool</span>':
|
||||||
'(cw spiders) <span data-mx-spoiler>spiders are pretty cool</span>':
|
'(cw spiders) ███████████████████████',
|
||||||
'(cw spiders) ███████████████████████',
|
'<span data-mx-spoiler="cw spiders">spiders are pretty cool</span>':
|
||||||
'<span data-mx-spoiler="cw spiders">spiders are pretty cool</span>':
|
'(cw spiders) ███████████████████████',
|
||||||
'(cw spiders) ███████████████████████',
|
'<img src="test.gif" alt="a test case" />': 'a test case',
|
||||||
'<img src="test.gif" alt="a test case" />': 'a test case',
|
'List of cute animals:\n<ul>\n<li>Kittens</li>\n<li>Puppies</li>\n<li>Snakes<br/>(I think they\'re cute!)</li>\n</ul>\n(This list is incomplete, you can help by adding to it!)':
|
||||||
'List of cute animals:\n<ul>\n<li>Kittens</li>\n<li>Puppies</li>\n<li>Snakes<br/>(I think they\'re cute!)</li>\n</ul>\n(This list is incomplete, you can help by adding to it!)':
|
'List of cute animals:\n● Kittens\n● Puppies\n● Snakes\n (I think they\'re cute!)\n(This list is incomplete, you can help by adding to it!)',
|
||||||
'List of cute animals:\n● Kittens\n● Puppies\n● Snakes\n (I think they\'re cute!)\n(This list is incomplete, you can help by adding to it!)',
|
'<em>fox</em>': '*fox*',
|
||||||
'<em>fox</em>': '*fox*',
|
'<i>fox</i>': '*fox*',
|
||||||
'<i>fox</i>': '*fox*',
|
'<strong>fox</i>': '**fox**',
|
||||||
'<strong>fox</i>': '**fox**',
|
'<b>fox</b>': '**fox**',
|
||||||
'<b>fox</b>': '**fox**',
|
'<u>fox</u>': '__fox__',
|
||||||
'<u>fox</u>': '__fox__',
|
'<ins>fox</ins>': '__fox__',
|
||||||
'<ins>fox</ins>': '__fox__',
|
'<del>fox</del>': '~~fox~~',
|
||||||
'<del>fox</del>': '~~fox~~',
|
'<strike>fox</strike>': '~~fox~~',
|
||||||
'<strike>fox</strike>': '~~fox~~',
|
'<s>fox</s>': '~~fox~~',
|
||||||
'<s>fox</s>': '~~fox~~',
|
'<code>>fox</code>': '`>fox`',
|
||||||
'<code>>fox</code>': '`>fox`',
|
'<pre>meep</pre>': '```\nmeep\n```',
|
||||||
'<pre>meep</pre>': '```\nmeep\n```',
|
'<pre>meep\n</pre>': '```\nmeep\n```',
|
||||||
'<pre>meep\n</pre>': '```\nmeep\n```',
|
'<pre><code class="language-floof">meep</code></pre>':
|
||||||
'<pre><code class="language-floof">meep</code></pre>':
|
'```floof\nmeep\n```',
|
||||||
'```floof\nmeep\n```',
|
'before<pre>code</pre>after': 'before\n```\ncode\n```\nafter',
|
||||||
'before<pre>code</pre>after': 'before\n```\ncode\n```\nafter',
|
'<p>before</p><pre>code</pre><p>after</p>':
|
||||||
'<p>before</p><pre>code</pre><p>after</p>':
|
'before\n```\ncode\n```\nafter',
|
||||||
'before\n```\ncode\n```\nafter',
|
'<p>fox</p>': 'fox',
|
||||||
'<p>fox</p>': 'fox',
|
'<p>fox</p><p>floof</p>': 'fox\n\nfloof',
|
||||||
'<p>fox</p><p>floof</p>': 'fox\n\nfloof',
|
'<a href="https://example.org">website</a>': '🔗website',
|
||||||
'<a href="https://example.org">website</a>': '🔗website',
|
'<a href="https://matrix.to/#/@user:example.org">fox</a>': 'fox',
|
||||||
'<a href="https://matrix.to/#/@user:example.org">fox</a>': 'fox',
|
'<a href="matrix:u/user:example.org">fox</a>': 'fox',
|
||||||
'<a href="matrix:u/user:example.org">fox</a>': 'fox',
|
'<img alt=":wave:" src="mxc://fox">': ':wave:',
|
||||||
'<img alt=":wave:" src="mxc://fox">': ':wave:',
|
'fox<br>floof': 'fox\nfloof',
|
||||||
'fox<br>floof': 'fox\nfloof',
|
'<blockquote>fox</blockquote>floof': '> fox\nfloof',
|
||||||
'<blockquote>fox</blockquote>floof': '> fox\nfloof',
|
'<blockquote><p>fox</p></blockquote>floof': '> fox\nfloof',
|
||||||
'<blockquote><p>fox</p></blockquote>floof': '> fox\nfloof',
|
'<blockquote><p>fox</p></blockquote><p>floof</p>': '> fox\nfloof',
|
||||||
'<blockquote><p>fox</p></blockquote><p>floof</p>': '> fox\nfloof',
|
'a<blockquote>fox</blockquote>floof': 'a\n> fox\nfloof',
|
||||||
'a<blockquote>fox</blockquote>floof': 'a\n> fox\nfloof',
|
'<blockquote><blockquote>fox</blockquote>floof</blockquote>fluff':
|
||||||
'<blockquote><blockquote>fox</blockquote>floof</blockquote>fluff':
|
'> > fox\n> floof\nfluff',
|
||||||
'> > fox\n> floof\nfluff',
|
'<ul><li>hey<ul><li>a</li><li>b</li></ul></li><li>foxies</li></ul>':
|
||||||
'<ul><li>hey<ul><li>a</li><li>b</li></ul></li><li>foxies</li></ul>':
|
'● hey\n ○ a\n ○ b\n● foxies',
|
||||||
'● hey\n ○ a\n ○ b\n● foxies',
|
'<ol><li>a</li><li>b</li></ol>': '1. a\n2. b',
|
||||||
'<ol><li>a</li><li>b</li></ol>': '1. a\n2. b',
|
'<ol start="42"><li>a</li><li>b</li></ol>': '42. a\n43. b',
|
||||||
'<ol start="42"><li>a</li><li>b</li></ol>': '42. a\n43. b',
|
'<ol><li>a<ol><li>aa</li><li>bb</li></ol></li><li>b</li></ol>':
|
||||||
'<ol><li>a<ol><li>aa</li><li>bb</li></ol></li><li>b</li></ol>':
|
'1. a\n 1. aa\n 2. bb\n2. b',
|
||||||
'1. a\n 1. aa\n 2. bb\n2. b',
|
'<ol><li>a<ul><li>aa</li><li>bb</li></ul></li><li>b</li></ol>':
|
||||||
'<ol><li>a<ul><li>aa</li><li>bb</li></ul></li><li>b</li></ol>':
|
'1. a\n ○ aa\n ○ bb\n2. b',
|
||||||
'1. a\n ○ aa\n ○ bb\n2. b',
|
'<ul><li>a<ol><li>aa</li><li>bb</li></ol></li><li>b</li></ul>':
|
||||||
'<ul><li>a<ol><li>aa</li><li>bb</li></ol></li><li>b</li></ul>':
|
'● a\n 1. aa\n 2. bb\n● b',
|
||||||
'● a\n 1. aa\n 2. bb\n● b',
|
'<mx-reply>bunny</mx-reply>fox': 'fox',
|
||||||
'<mx-reply>bunny</mx-reply>fox': 'fox',
|
'fox<hr>floof': 'fox\n----------\nfloof',
|
||||||
'fox<hr>floof': 'fox\n----------\nfloof',
|
'<p>fox</p><hr><p>floof</p>': 'fox\n----------\nfloof',
|
||||||
'<p>fox</p><hr><p>floof</p>': 'fox\n----------\nfloof',
|
'<h1>fox</h1>floof': '# fox\nfloof',
|
||||||
'<h1>fox</h1>floof': '# fox\nfloof',
|
'<h1>fox</h1><p>floof</p>': '# fox\nfloof',
|
||||||
'<h1>fox</h1><p>floof</p>': '# fox\nfloof',
|
'floof<h1>fox</h1>': 'floof\n# fox',
|
||||||
'floof<h1>fox</h1>': 'floof\n# fox',
|
'<p>floof</p><h1>fox</h1>': 'floof\n# fox',
|
||||||
'<p>floof</p><h1>fox</h1>': 'floof\n# fox',
|
'<h2>fox</h2>': '## fox',
|
||||||
'<h2>fox</h2>': '## fox',
|
'<h3>fox</h3>': '### fox',
|
||||||
'<h3>fox</h3>': '### fox',
|
'<h4>fox</h4>': '#### fox',
|
||||||
'<h4>fox</h4>': '#### fox',
|
'<h5>fox</h5>': '##### fox',
|
||||||
'<h5>fox</h5>': '##### fox',
|
'<h6>fox</h6>': '###### fox',
|
||||||
'<h6>fox</h6>': '###### fox',
|
'<span>fox</span>': 'fox',
|
||||||
'<span>fox</span>': 'fox',
|
'<p>fox</p>\n<p>floof</p>': 'fox\n\nfloof',
|
||||||
'<p>fox</p>\n<p>floof</p>': 'fox\n\nfloof',
|
'<mx-reply>beep</mx-reply><p>fox</p>\n<p>floof</p>': 'fox\n\nfloof',
|
||||||
'<mx-reply>beep</mx-reply><p>fox</p>\n<p>floof</p>': 'fox\n\nfloof',
|
'<pre><code></code></pre>': '``````',
|
||||||
};
|
};
|
||||||
for (final entry in testMap.entries) {
|
for (final entry in testMap.entries) {
|
||||||
|
test(entry.key, () async {
|
||||||
expect(HtmlToText.convert(entry.key), entry.value);
|
expect(HtmlToText.convert(entry.key), entry.value);
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -234,9 +234,35 @@ void main() {
|
||||||
'm.relates_to': {'rel_type': 'm.replace', 'event_id': '2'},
|
'm.relates_to': {'rel_type': 'm.replace', 'event_id': '2'},
|
||||||
},
|
},
|
||||||
stateKey: '',
|
stateKey: '',
|
||||||
|
status: EventStatus.sending,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(room.lastEvent?.body, 'edited cdc');
|
expect(room.lastEvent?.body, 'edited cdc');
|
||||||
|
expect(room.lastEvent?.status, EventStatus.sending);
|
||||||
|
expect(room.lastEvent?.eventId, '4');
|
||||||
|
|
||||||
|
// Status update on edits working?
|
||||||
|
room.setState(
|
||||||
|
Event(
|
||||||
|
senderId: '@test:example.com',
|
||||||
|
type: 'm.room.encrypted',
|
||||||
|
room: room,
|
||||||
|
eventId: '5',
|
||||||
|
unsigned: {'transaction_id': '4'},
|
||||||
|
originServerTs: DateTime.now(),
|
||||||
|
content: {
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'body': 'edited cdc',
|
||||||
|
'm.new_content': {'msgtype': 'm.text', 'body': 'edited cdc'},
|
||||||
|
'm.relates_to': {'rel_type': 'm.replace', 'event_id': '2'},
|
||||||
|
},
|
||||||
|
stateKey: '',
|
||||||
|
status: EventStatus.sent,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(room.lastEvent?.eventId, '5');
|
||||||
|
expect(room.lastEvent?.body, 'edited cdc');
|
||||||
|
expect(room.lastEvent?.status, EventStatus.sent);
|
||||||
});
|
});
|
||||||
test('lastEvent when reply parent edited', () async {
|
test('lastEvent when reply parent edited', () async {
|
||||||
room.setState(
|
room.setState(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue