Merge branch 'main' into voip/add-voip-function

This commit is contained in:
cloudwebrtc 2021-11-30 22:31:10 +08:00
commit 7efe123274
20 changed files with 1823 additions and 126 deletions

View File

@ -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
- feat: More advanced create chat methods (encryption is now enabled by default)
- feat: Make waiting on init db optional

View File

@ -33,7 +33,6 @@ import 'utils/bootstrap.dart';
class Encryption {
final Client client;
final bool debug;
final bool enableE2eeRecovery;
bool get enabled => olmManager.enabled;
@ -53,7 +52,6 @@ class Encryption {
Encryption({
required this.client,
this.debug = false,
required this.enableE2eeRecovery,
}) {
ssss = SSSS(this);
keyManager = KeyManager(this);
@ -232,8 +230,7 @@ class Encryption {
decryptedPayload = json.decode(decryptResult.plaintext);
} catch (exception) {
// alright, if this was actually by our own outbound group session, we might as well clear it
if (client.enableE2eeRecovery &&
exception.toString() != DecryptException.unknownSession &&
if (exception.toString() != DecryptException.unknownSession &&
(keyManager
.getOutboundGroupSession(roomId)
?.outboundGroupSession

View File

@ -182,7 +182,7 @@ class KeyManager {
// attempt to decrypt the last event
final event = room.getState(EventTypes.Encrypted);
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
room.onSessionKeyReceived.add(sessionId);
@ -219,8 +219,7 @@ class KeyManager {
void maybeAutoRequest(String roomId, String sessionId, String senderKey) {
final room = client.getRoomById(roomId);
final requestIdent = '$roomId|$sessionId|$senderKey';
if (client.enableE2eeRecovery &&
room != null &&
if (room != null &&
!_requestedSessionIds.contains(requestIdent) &&
!client.isUnknownSession) {
// do e2ee recovery

View File

@ -514,10 +514,10 @@ class OlmManager {
return _decryptToDeviceEvent(event);
} 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
if (client.enableE2eeRecovery) {
// ignore: unawaited_futures
runInRoot(() => restoreOlmSession(event.senderId, senderKey));
}
// ignore: unawaited_futures
runInRoot(() => restoreOlmSession(event.senderId, senderKey));
rethrow;
}
}

View File

@ -24,6 +24,7 @@ export 'package:matrix_api_lite/matrix_api_lite.dart';
export 'src/client.dart';
export 'src/database/database_api.dart';
export 'src/database/hive_database.dart';
export 'src/database/fluffybox_database.dart';
export 'src/event.dart';
export 'src/event_status.dart';
export 'src/voip.dart';

View File

@ -74,8 +74,6 @@ class Client extends MatrixApi {
DatabaseApi? get database => _database;
bool enableE2eeRecovery;
@deprecated
MatrixApi get api => this;
@ -120,7 +118,6 @@ class Client extends MatrixApi {
/// [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
/// [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:
/// KeyVerificationMethod.numbers: Compare numbers. Most basic, should be supported
/// KeyVerificationMethod.emoji: Compare emojis
@ -157,7 +154,8 @@ class Client extends MatrixApi {
this.databaseDestroyer,
this.legacyDatabaseBuilder,
this.legacyDatabaseDestroyer,
this.enableE2eeRecovery = false,
@Deprecated('This is now always enabled by default.')
bool? enableE2eeRecovery,
Set<KeyVerificationMethod>? verificationMethods,
http.Client? httpClient,
Set<String>? importantStateEvents,
@ -568,7 +566,8 @@ class Client extends MatrixApi {
final directChatRoomId = getDirectChatFromUserId(mxid);
if (directChatRoomId != null) return directChatRoomId;
enableEncryption ??= await userOwnsEncryptionKeys(mxid);
enableEncryption ??=
encryptionEnabled && await userOwnsEncryptionKeys(mxid);
if (enableEncryption) {
initialState ??= [];
if (!initialState.any((s) => s.type == EventTypes.Encryption)) {
@ -609,6 +608,7 @@ class Client extends MatrixApi {
List<String>? invite,
CreateRoomPreset preset = CreateRoomPreset.privateChat,
List<StateEvent>? initialState,
Visibility? visibility,
bool waitForSync = true,
}) async {
enableEncryption ??=
@ -629,6 +629,7 @@ class Client extends MatrixApi {
preset: preset,
name: groupName,
initialState: initialState,
visibility: visibility,
);
if (waitForSync) {
@ -649,7 +650,7 @@ class Client extends MatrixApi {
return true;
}
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
@ -772,6 +773,7 @@ class Client extends MatrixApi {
leftRoom,
));
});
leftRoom.prev_batch = room.timeline?.prevBatch;
room.state?.forEach((event) {
leftRoom.setState(Event.fromMatrixEvent(
event,
@ -817,8 +819,15 @@ class Client extends MatrixApi {
}
}
/// Uploads a new user avatar for this user.
Future<void> setAvatar(MatrixFile file) async {
/// Uploads a new user avatar for this user. Leave file null to remove the
/// 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(
file.bytes,
filename: file.name,
@ -1063,8 +1072,7 @@ class Client extends MatrixApi {
// make sure to throw an exception if libolm doesn't exist
await olm.init();
olm.get_library_version();
encryption =
Encryption(client: this, enableE2eeRecovery: enableE2eeRecovery);
encryption = Encryption(client: this);
} catch (_) {
encryption?.dispose();
encryption = null;
@ -1806,7 +1814,13 @@ class Client extends MatrixApi {
final deviceKeysList =
_userDeviceKeys[userId] ??= DeviceKeysList(userId, this);
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 ||
DateTime.now()
.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...');
final userDeviceKeys = await legacyDatabase.getUserDeviceKeys(this);
for (final userId in userDeviceKeys.keys) {

View File

@ -67,8 +67,6 @@ abstract class DatabaseApi {
Future<Event?> getEventById(String eventId, Room room);
bool eventIsKnown(String eventId, String roomId);
Future<void> forgetRoom(String roomId);
Future<void> clearCache();
@ -272,6 +270,8 @@ abstract class DatabaseApi {
String userId,
);
Future<Map<String, Map>> getAllOlmSessions();
Future<List<OlmSession>> getOlmSessionsForDevices(
List<String> identityKeys,
String userId,

File diff suppressed because it is too large Load Diff

View File

@ -371,10 +371,6 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
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
Future<List<Event>> _getEventsByIds(List<String> eventIds, Room room) =>
Future.wait(eventIds
@ -489,6 +485,21 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
.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
Future<List<OlmSession>> getOlmSessionsForDevices(
List<String> identityKey, String userId) async {

View File

@ -187,7 +187,8 @@ class Room {
state.relationshipEventId != null &&
state.relationshipType == RelationshipTypes.edit &&
lastEvent != null &&
!lastEvent.matchesEventOrTransactionId(state.relationshipEventId)) {
!state.matchesEventOrTransactionId(lastEvent.eventId) &&
lastEvent.eventId != state.relationshipEventId) {
return;
}
@ -202,7 +203,8 @@ class Room {
final prevEvent = getState(state.type, stateKey);
if (prevEvent != null &&
prevEvent.eventId != state.eventId &&
client.database?.eventIsKnown(state.eventId, roomId) == true) {
prevEvent.originServerTs.millisecondsSinceEpoch >
state.originServerTs.millisecondsSinceEpoch) {
return;
}
@ -1354,15 +1356,18 @@ class Room {
}
/// Uploads a new user avatar for this room. Returns the event ID of the new
/// m.room.avatar event.
Future<String> setAvatar(MatrixFile file) async {
final uploadResp =
await client.uploadContent(file.bytes, filename: file.name);
/// m.room.avatar event. Leave empty to remove the current avatar.
Future<String> setAvatar(MatrixFile? file) async {
final uploadResp = file == null
? null
: await client.uploadContent(file.bytes, filename: file.name);
return await client.setRoomStateWithKey(
id,
EventTypes.RoomAvatar,
'',
{'url': uploadResp.toString()},
{
if (uploadResp != null) 'url': uploadResp.toString(),
},
);
}

View File

@ -120,13 +120,12 @@ class Timeline {
this.onRemove,
}) : 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();
});
.listen(_removeEventsNotInThisSync);
sessionIdReceivedSub =
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!
void cancelSubscriptions() {
sub?.cancel();

View File

@ -129,7 +129,7 @@ class User extends Event {
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
}
}
return words.join(' ');
return words.join(' ').trim();
}
return 'Unknown user';
}

View File

@ -98,6 +98,19 @@ extension CommandsClientExtension on Client {
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 {
return await args.room.sendTextEvent(
args.msg,
@ -202,6 +215,10 @@ extension CommandsClientExtension on Client {
.clearOrUseOutboundGroupSession(args.room.id, wipe: true);
return '';
});
addCommand('clearcache', (CommandArgs args) async {
await clearCache();
return '';
});
}
}

View File

@ -50,11 +50,13 @@ class HtmlToText {
.firstMatch(text);
if (match == null) {
text = HtmlUnescape().convert(text);
if (text[0] != '\n') {
text = '\n$text';
}
if (text[text.length - 1] != '\n') {
text += '\n';
if (text.isNotEmpty) {
if (text[0] != '\n') {
text = '\n$text';
}
if (text[text.length - 1] != '\n') {
text += '\n';
}
}
return text;
}
@ -64,11 +66,13 @@ class HtmlToText {
text = text.replaceAll(
RegExp(r'</code>$', multiLine: false, caseSensitive: false), '');
text = HtmlUnescape().convert(text);
if (text[0] != '\n') {
text = '\n$text';
}
if (text[text.length - 1] != '\n') {
text += '\n';
if (text.isNotEmpty) {
if (text[0] != '\n') {
text = '\n$text';
}
if (text[text.length - 1] != '\n') {
text += '\n';
}
}
final language =
RegExp(r'language-(\w+)', multiLine: false, caseSensitive: false)

View File

@ -1,6 +1,6 @@
name: matrix
description: Matrix Dart SDK
version: 0.7.0-nullsafety.4
version: 0.7.0-nullsafety.10
homepage: https://famedly.com
environment:
@ -25,6 +25,7 @@ dependencies:
collection: ^1.15.0
webrtc_interface: ^1.0.1
sdp_transform: ^0.3.2
fluffybox: ^0.3.5
dev_dependencies:
dart_code_metrics: ^4.4.0

View File

@ -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 {
if (olmEnabled) {
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 {
await client.dispose(closeDatabase: true);
});

View File

@ -26,7 +26,11 @@ import 'package:olm/olm.dart' as olm;
import 'fake_database.dart';
void main() {
/// All Tests related to the ChatTime
group('FluffyBox Database Test', () {
testDatabase(
getFluffyBoxDatabase(null),
);
});
group('Hive Database Test', () {
testDatabase(
getHiveDatabase(null),
@ -309,6 +313,42 @@ void testDatabase(
);
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 {
final olm = await database.getOlmSessionsForDevices(
['identityKeys'],

View File

@ -23,11 +23,25 @@ import 'package:matrix/matrix.dart';
import 'package:matrix/src/database/hive_database.dart';
import 'package:file/memory.dart';
import 'package:hive/hive.dart';
import 'package:matrix/src/database/fluffybox_database.dart';
Future<DatabaseApi> getDatabase(Client? _) => getHiveDatabase(_);
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 {
if (!hiveInitialized) {
final fileSystem = MemoryFileSystem();

View File

@ -21,80 +21,81 @@ import 'package:test/test.dart';
void main() {
group('htmlToText', () {
test('stuff', () async {
final testMap = <String, String>{
'': '',
'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':
'*That\'s* not a test, **this** is a test',
'Visit <del><a href="http://example.com">our website</a></del> (outdated)':
'Visit ~~🔗our website~~ (outdated)',
'(cw spiders) <span data-mx-spoiler>spiders are pretty cool</span>':
'(cw spiders) ███████████████████████',
'<span data-mx-spoiler="cw spiders">spiders are pretty cool</span>':
'(cw spiders) ███████████████████████',
'<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● 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*',
'<i>fox</i>': '*fox*',
'<strong>fox</i>': '**fox**',
'<b>fox</b>': '**fox**',
'<u>fox</u>': '__fox__',
'<ins>fox</ins>': '__fox__',
'<del>fox</del>': '~~fox~~',
'<strike>fox</strike>': '~~fox~~',
'<s>fox</s>': '~~fox~~',
'<code>&gt;fox</code>': '`>fox`',
'<pre>meep</pre>': '```\nmeep\n```',
'<pre>meep\n</pre>': '```\nmeep\n```',
'<pre><code class="language-floof">meep</code></pre>':
'```floof\nmeep\n```',
'before<pre>code</pre>after': 'before\n```\ncode\n```\nafter',
'<p>before</p><pre>code</pre><p>after</p>':
'before\n```\ncode\n```\nafter',
'<p>fox</p>': 'fox',
'<p>fox</p><p>floof</p>': 'fox\n\nfloof',
'<a href="https://example.org">website</a>': '🔗website',
'<a href="https://matrix.to/#/@user:example.org">fox</a>': 'fox',
'<a href="matrix:u/user:example.org">fox</a>': 'fox',
'<img alt=":wave:" src="mxc://fox">': ':wave:',
'fox<br>floof': 'fox\nfloof',
'<blockquote>fox</blockquote>floof': '> fox\nfloof',
'<blockquote><p>fox</p></blockquote>floof': '> fox\nfloof',
'<blockquote><p>fox</p></blockquote><p>floof</p>': '> fox\nfloof',
'a<blockquote>fox</blockquote>floof': 'a\n> fox\nfloof',
'<blockquote><blockquote>fox</blockquote>floof</blockquote>fluff':
'> > fox\n> floof\nfluff',
'<ul><li>hey<ul><li>a</li><li>b</li></ul></li><li>foxies</li></ul>':
'● hey\n ○ a\n ○ b\n● foxies',
'<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><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',
'<ol><li>a<ul><li>aa</li><li>bb</li></ul></li><li>b</li></ol>':
'1. a\n ○ aa\n ○ bb\n2. b',
'<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',
'<mx-reply>bunny</mx-reply>fox': 'fox',
'fox<hr>floof': 'fox\n----------\nfloof',
'<p>fox</p><hr><p>floof</p>': 'fox\n----------\nfloof',
'<h1>fox</h1>floof': '# fox\nfloof',
'<h1>fox</h1><p>floof</p>': '# fox\nfloof',
'floof<h1>fox</h1>': 'floof\n# fox',
'<p>floof</p><h1>fox</h1>': 'floof\n# fox',
'<h2>fox</h2>': '## fox',
'<h3>fox</h3>': '### fox',
'<h4>fox</h4>': '#### fox',
'<h5>fox</h5>': '##### fox',
'<h6>fox</h6>': '###### fox',
'<span>fox</span>': 'fox',
'<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',
};
for (final entry in testMap.entries) {
final testMap = <String, String>{
'': '',
'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':
'*That\'s* not a test, **this** is a test',
'Visit <del><a href="http://example.com">our website</a></del> (outdated)':
'Visit ~~🔗our website~~ (outdated)',
'(cw spiders) <span data-mx-spoiler>spiders are pretty cool</span>':
'(cw spiders) ███████████████████████',
'<span data-mx-spoiler="cw spiders">spiders are pretty cool</span>':
'(cw spiders) ███████████████████████',
'<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● 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*',
'<i>fox</i>': '*fox*',
'<strong>fox</i>': '**fox**',
'<b>fox</b>': '**fox**',
'<u>fox</u>': '__fox__',
'<ins>fox</ins>': '__fox__',
'<del>fox</del>': '~~fox~~',
'<strike>fox</strike>': '~~fox~~',
'<s>fox</s>': '~~fox~~',
'<code>&gt;fox</code>': '`>fox`',
'<pre>meep</pre>': '```\nmeep\n```',
'<pre>meep\n</pre>': '```\nmeep\n```',
'<pre><code class="language-floof">meep</code></pre>':
'```floof\nmeep\n```',
'before<pre>code</pre>after': 'before\n```\ncode\n```\nafter',
'<p>before</p><pre>code</pre><p>after</p>':
'before\n```\ncode\n```\nafter',
'<p>fox</p>': 'fox',
'<p>fox</p><p>floof</p>': 'fox\n\nfloof',
'<a href="https://example.org">website</a>': '🔗website',
'<a href="https://matrix.to/#/@user:example.org">fox</a>': 'fox',
'<a href="matrix:u/user:example.org">fox</a>': 'fox',
'<img alt=":wave:" src="mxc://fox">': ':wave:',
'fox<br>floof': 'fox\nfloof',
'<blockquote>fox</blockquote>floof': '> fox\nfloof',
'<blockquote><p>fox</p></blockquote>floof': '> fox\nfloof',
'<blockquote><p>fox</p></blockquote><p>floof</p>': '> fox\nfloof',
'a<blockquote>fox</blockquote>floof': 'a\n> fox\nfloof',
'<blockquote><blockquote>fox</blockquote>floof</blockquote>fluff':
'> > fox\n> floof\nfluff',
'<ul><li>hey<ul><li>a</li><li>b</li></ul></li><li>foxies</li></ul>':
'● hey\n ○ a\n ○ b\n● foxies',
'<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><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',
'<ol><li>a<ul><li>aa</li><li>bb</li></ul></li><li>b</li></ol>':
'1. a\n ○ aa\n ○ bb\n2. b',
'<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',
'<mx-reply>bunny</mx-reply>fox': 'fox',
'fox<hr>floof': 'fox\n----------\nfloof',
'<p>fox</p><hr><p>floof</p>': 'fox\n----------\nfloof',
'<h1>fox</h1>floof': '# fox\nfloof',
'<h1>fox</h1><p>floof</p>': '# fox\nfloof',
'floof<h1>fox</h1>': 'floof\n# fox',
'<p>floof</p><h1>fox</h1>': 'floof\n# fox',
'<h2>fox</h2>': '## fox',
'<h3>fox</h3>': '### fox',
'<h4>fox</h4>': '#### fox',
'<h5>fox</h5>': '##### fox',
'<h6>fox</h6>': '###### fox',
'<span>fox</span>': 'fox',
'<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) {
test(entry.key, () async {
expect(HtmlToText.convert(entry.key), entry.value);
}
});
});
}
});
}

View File

@ -234,9 +234,35 @@ void main() {
'm.relates_to': {'rel_type': 'm.replace', 'event_id': '2'},
},
stateKey: '',
status: EventStatus.sending,
),
);
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 {
room.setState(