diff --git a/lib/encryption/encryption.dart b/lib/encryption/encryption.dart index 78cee03b..1714c786 100644 --- a/lib/encryption/encryption.dart +++ b/lib/encryption/encryption.dart @@ -312,6 +312,7 @@ class Encryption { roomID: roomId, type: updateType, ), + client, ); } return event; diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index 45874747..e2229f45 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -63,7 +63,8 @@ class KeyManager { _requestedSessionIds.clear(); for (final room in client.rooms) { final lastEvent = room.lastEvent; - if (lastEvent.type == EventTypes.Encrypted && + if (lastEvent != null && + lastEvent.type == EventTypes.Encrypted && lastEvent.content['can_request_session'] == true) { try { maybeAutoRequest(room.id, lastEvent.content['session_id'], diff --git a/lib/src/client.dart b/lib/src/client.dart index 75ff1d87..ea79b443 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -316,8 +316,8 @@ class Client extends MatrixApi { } for (final room in rooms) { if (room.membership == Membership.invite && - room.getState(EventTypes.RoomMember, userID)?.senderId == userId && - room.getState(EventTypes.RoomMember, userID).content['is_direct'] == + room.getState(EventTypes.RoomMember, userID!)?.senderId == userId && + room.getState(EventTypes.RoomMember, userID!)?.content['is_direct'] == true) { return room.id; } @@ -613,7 +613,7 @@ class Client extends MatrixApi { if (rooms.isNotEmpty) { final profileSet = {}; for (final room in rooms) { - final user = room.getUserByMXIDSync(userID); + final user = room.getUserByMXIDSync(userID!); profileSet.add(Profile.fromJson(user.content)); } if (profileSet.length == 1) return profileSet.first; @@ -1297,7 +1297,7 @@ class Client extends MatrixApi { final id = entry.key; final room = entry.value; - await database?.storeRoomUpdate(id, room); + await database?.storeRoomUpdate(id, room, this); _updateRoomsByRoomUpdate(id, room); /// Handle now all room events and save them in the database @@ -1377,7 +1377,7 @@ class Client extends MatrixApi { // there. if (event['type'] == 'm.receipt') { var room = getRoomById(id); - room ??= Room(id: id); + room ??= Room(id: id, client: this); final receiptStateContent = room.roomAccountData['m.receipt']?.content ?? {}; @@ -1455,7 +1455,7 @@ class Client extends MatrixApi { } _updateRoomsByEventUpdate(update); if (type != EventUpdateType.ephemeral) { - await database?.storeEventUpdate(update); + await database?.storeEventUpdate(update, this); } if (encryptionEnabled) { await encryption?.handleEventUpdate(update); @@ -1521,9 +1521,10 @@ class Client extends MatrixApi { id: roomId, membership: membership, prev_batch: chatUpdate.timeline?.prevBatch, - highlightCount: chatUpdate.unreadNotifications?.highlightCount, + highlightCount: + chatUpdate.unreadNotifications?.highlightCount ?? 0, notificationCount: - chatUpdate.unreadNotifications?.notificationCount, + chatUpdate.unreadNotifications?.notificationCount ?? 0, summary: chatUpdate.summary, client: this, ) @@ -1593,10 +1594,10 @@ class Client extends MatrixApi { } else { if (stateEvent.type != EventTypes.Message || stateEvent.relationshipType != RelationshipTypes.edit || - stateEvent.relationshipEventId == room.lastEvent.eventId || - ((room.lastEvent.relationshipType == RelationshipTypes.edit && + stateEvent.relationshipEventId == room.lastEvent?.eventId || + ((room.lastEvent?.relationshipType == RelationshipTypes.edit && stateEvent.relationshipEventId == - room.lastEvent.relationshipEventId))) { + room.lastEvent?.relationshipEventId))) { room.setState(stateEvent); } } diff --git a/lib/src/database/database_api.dart b/lib/src/database/database_api.dart index ca9ff333..368f1ce8 100644 --- a/lib/src/database/database_api.dart +++ b/lib/src/database/database_api.dart @@ -58,11 +58,12 @@ abstract class DatabaseApi { /// Stores a RoomUpdate object in the database. Must be called inside of /// [transaction]. - Future storeRoomUpdate(String roomId, SyncRoomUpdate roomUpdate); + Future storeRoomUpdate( + String roomId, SyncRoomUpdate roomUpdate, Client client); /// Stores an EventUpdate object in the database. Must be called inside of /// [transaction]. - Future storeEventUpdate(EventUpdate eventUpdate); + Future storeEventUpdate(EventUpdate eventUpdate, Client client); Future getEventById(String eventId, Room room); @@ -232,6 +233,7 @@ abstract class DatabaseApi { Future setRoomPrevBatch( String prevBatch, String roomId, + Client client, ); Future resetNotificationCount(String roomId); diff --git a/lib/src/database/hive_database.dart b/lib/src/database/hive_database.dart index 625f70f7..84dcdfac 100644 --- a/lib/src/database/hive_database.dart +++ b/lib/src/database/hive_database.dart @@ -518,10 +518,12 @@ class FamedlySdkHiveDatabase extends DatabaseApi { // We always need the member event for ourself final membersToPostload = {if (userID != null) userID}; // If the room is a direct chat, those IDs should be there too - if (room.isDirectChat) membersToPostload.add(room.directChatMatrixID); + if (room.isDirectChat) + membersToPostload.add(room.directChatMatrixID!); // the lastEvent message preview might have an author we need to fetch, if it is a group chat - if (room.getState(EventTypes.Message) != null && !room.isDirectChat) { - membersToPostload.add(room.getState(EventTypes.Message).senderId); + final lastEvent = room.getState(EventTypes.Message); + if (lastEvent != null && !room.isDirectChat) { + membersToPostload.add(lastEvent.senderId); } // if the room has no name and no canonical alias, its name is calculated // based on the heroes of the room @@ -529,7 +531,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi { room.getState(EventTypes.RoomCanonicalAlias) == null) { // we don't have a name and no canonical alias, so we'll need to // post-load the heroes - membersToPostload.addAll(room.summary?.mHeroes ?? []); + membersToPostload.addAll(room.summary.mHeroes ?? []); } // Load members for (final userId in membersToPostload) { @@ -817,10 +819,11 @@ class FamedlySdkHiveDatabase extends DatabaseApi { } @override - Future setRoomPrevBatch(String prevBatch, String roomId) async { + Future setRoomPrevBatch( + String prevBatch, String roomId, Client client) async { final raw = await _roomsBox.get(roomId.toHiveKey); if (raw == null) return; - final room = Room.fromJson(convertToJson(raw)); + final room = Room.fromJson(convertToJson(raw), client); room.prev_batch = prevBatch; await _roomsBox.put(roomId.toHiveKey, room.toJson()); return; @@ -861,13 +864,13 @@ class FamedlySdkHiveDatabase extends DatabaseApi { } @override - Future storeEventUpdate(EventUpdate eventUpdate) async { + Future storeEventUpdate(EventUpdate eventUpdate, Client client) async { // Ephemerals should not be stored if (eventUpdate.type == EventUpdateType.ephemeral) return; // In case of this is a redaction event if (eventUpdate.content['type'] == EventTypes.Redaction) { - final tmpRoom = Room(id: eventUpdate.roomID); + final tmpRoom = Room(id: eventUpdate.roomID, client: client); final event = await getEventById(eventUpdate.content['redacts'], tmpRoom); if (event != null) { event.setRedactionEvent(Event.fromJson(eventUpdate.content, tmpRoom)); @@ -1077,7 +1080,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi { } @override - Future storeRoomUpdate(String roomId, SyncRoomUpdate roomUpdate) async { + Future storeRoomUpdate( + String roomId, SyncRoomUpdate roomUpdate, Client client) async { // Leave room if membership is leave if (roomUpdate is LeftRoomUpdate) { await forgetRoom(roomId); @@ -1094,26 +1098,31 @@ class FamedlySdkHiveDatabase extends DatabaseApi { roomId.toHiveKey, roomUpdate is JoinedRoomUpdate ? Room( + client: client, id: roomId, membership: membership, highlightCount: - roomUpdate.unreadNotifications?.highlightCount?.toInt(), + roomUpdate.unreadNotifications?.highlightCount?.toInt() ?? + 0, notificationCount: roomUpdate - .unreadNotifications?.notificationCount - ?.toInt(), + .unreadNotifications?.notificationCount + ?.toInt() ?? + 0, prev_batch: roomUpdate.timeline?.prevBatch, summary: roomUpdate.summary, ).toJson() : Room( + client: client, id: roomId, membership: membership, ).toJson()); } else if (roomUpdate is JoinedRoomUpdate) { final currentRawRoom = await _roomsBox.get(roomId.toHiveKey); - final currentRoom = Room.fromJson(convertToJson(currentRawRoom)); + final currentRoom = Room.fromJson(convertToJson(currentRawRoom), client); await _roomsBox.put( roomId.toHiveKey, Room( + client: client, id: roomId, membership: membership, highlightCount: diff --git a/lib/src/event.dart b/lib/src/event.dart index 94ed3d78..55694efc 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -74,7 +74,7 @@ class Event extends MatrixEvent { bool get redacted => redactedBecause != null; - User? get stateKeyUser => room?.getUserByMXIDSync(stateKey); + User? get stateKeyUser => room?.getUserByMXIDSync(stateKey!); Event({ this.status = defaultStatus, diff --git a/lib/src/room.dart b/lib/src/room.dart index f2cb7a9a..453e6ae0 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -1,4 +1,3 @@ -// @dart=2.9 /* * Famedly Matrix SDK * Copyright (C) 2019, 2020, 2021 Famedly GmbH @@ -22,6 +21,7 @@ import 'dart:convert'; import 'package:html_unescape/html_unescape.dart'; import 'package:matrix/src/utils/space_child.dart'; +import 'package:collection/collection.dart'; import '../matrix.dart'; import 'client.dart'; @@ -78,18 +78,18 @@ class Room { int highlightCount; /// A token that can be supplied to the from parameter of the rooms/{roomId}/messages endpoint. - String prev_batch; + String? prev_batch; RoomSummary summary; @deprecated - List get mHeroes => summary.mHeroes; + List? get mHeroes => summary.mHeroes; @deprecated - int get mJoinedMemberCount => summary.mJoinedMemberCount; + int? get mJoinedMemberCount => summary.mJoinedMemberCount; @deprecated - int get mInvitedMemberCount => summary.mInvitedMemberCount; + int? get mInvitedMemberCount => summary.mInvitedMemberCount; /// The room states are a key value store of the key (`type`,`state_key`) => State(event). /// In a lot of cases the `state_key` might be an empty string. You **should** use the @@ -113,7 +113,7 @@ class Room { 'oldest_sort_order': 0, }; - factory Room.fromJson(Map json, [Client client]) => Room( + factory Room.fromJson(Map json, Client client) => Room( client: client, id: json['id'], membership: Membership.values.singleWhere( @@ -136,23 +136,25 @@ class Room { /// This load all the missing state events for the room from the database /// If the room has already been loaded, this does nothing. Future postLoad() async { - if (!partial || client.database == null) { + if (!partial) { return; } final allStates = await client.database - .getUnimportantRoomEventStatesForRoom( + ?.getUnimportantRoomEventStatesForRoom( client.importantStateEvents.toList(), this); - for (final state in allStates) { - setState(state); + if (allStates != null) { + for (final state in allStates) { + setState(state); + } } partial = false; } /// Returns the [Event] for the given [typeKey] and optional [stateKey]. /// If no [stateKey] is provided, it defaults to an empty string. - Event getState(String typeKey, [String stateKey = '']) => - states[typeKey] != null ? states[typeKey][stateKey] : null; + Event? getState(String typeKey, [String stateKey = '']) => + states[typeKey]?[stateKey]; /// Adds the [state] to this room and overwrites a state with the same /// typeKey/stateKey key pair if there is one. @@ -160,7 +162,7 @@ class Room { // Decrypt if necessary if (state.type == EventTypes.Encrypted && client.encryptionEnabled) { try { - state = client.encryption.decryptRoomEventSync(id, state); + state = client.encryption?.decryptRoomEventSync(id, state) ?? state; } catch (e, s) { Logs().e('[LibOlm] Could not decrypt room state', e, s); } @@ -188,27 +190,26 @@ class Room { } // Ignore other non-state events - if (!isMessageEvent && state.stateKey == null) { + final stateKey = isMessageEvent ? '' : state.stateKey; + final roomId = state.roomId; + if (stateKey == null || roomId == null) { return; } // Do not set old events as state events - final prevEvent = getState(state.type, state.stateKey); + final prevEvent = getState(state.type, stateKey); if (prevEvent != null && prevEvent.eventId != state.eventId && - client.database != null && - client.database.eventIsKnown(state.eventId, state.roomId)) { + client.database?.eventIsKnown(state.eventId, roomId) == true) { return; } - states[state.type] ??= {}; - states[state.type][state.stateKey ?? ''] = state; + (states[state.type] ??= {})[stateKey] = state; } /// ID of the fully read marker event. - String get fullyRead => roomAccountData['m.fully_read'] != null - ? roomAccountData['m.fully_read'].content['event_id'] - : ''; + String get fullyRead => + roomAccountData['m.fully_read']?.content['event_id'] ?? ''; /// If something changes, this callback will be triggered. Will return the /// room id. @@ -220,19 +221,17 @@ class Room { StreamController.broadcast(); /// The name of the room if set by a participant. - String get name => getState(EventTypes.RoomName) != null && - getState(EventTypes.RoomName).content['name'] is String - ? getState(EventTypes.RoomName).content['name'] - : ''; + String get name { + final n = getState(EventTypes.RoomName)?.content['name']; + return (n is String) ? n : ''; + } /// The pinned events for this room. If there are none this returns an empty /// list. - List get pinnedEventIds => getState(EventTypes.RoomPinnedEvents) != - null - ? (getState(EventTypes.RoomPinnedEvents).content['pinned'] is List - ? getState(EventTypes.RoomPinnedEvents).content['pinned'] - : []) - : []; + List get pinnedEventIds { + final pinned = getState(EventTypes.RoomPinnedEvents)?.content['pinned']; + return pinned is List ? pinned : []; + } /// Returns a localized displayname for this server. If the room is a groupchat /// without a name, then it will return the localized version of 'Group with Alice' instead @@ -243,7 +242,7 @@ class Room { if ((name?.isEmpty ?? true) && (canonicalAlias?.isEmpty ?? true) && !isDirectChat && - (summary.mHeroes != null && summary.mHeroes.isNotEmpty)) { + (summary.mHeroes != null && summary.mHeroes?.isNotEmpty == true)) { return i18n.groupWith(displayname); } if (displayname?.isNotEmpty ?? false) { @@ -253,23 +252,24 @@ class Room { } /// The topic of the room if set by a participant. - String get topic => getState(EventTypes.RoomTopic) != null && - getState(EventTypes.RoomTopic).content['topic'] is String - ? getState(EventTypes.RoomTopic).content['topic'] - : ''; + String get topic { + final t = getState(EventTypes.RoomTopic)?.content['topic']; + return t is String ? t : ''; + } /// The avatar of the room if set by a participant. - Uri get avatar { - if (getState(EventTypes.RoomAvatar) != null && - getState(EventTypes.RoomAvatar).content['url'] is String) { - return Uri.tryParse(getState(EventTypes.RoomAvatar).content['url']); + Uri? get avatar { + final avatarUrl = getState(EventTypes.RoomAvatar)?.content['url']; + if (avatarUrl is String) { + return Uri.tryParse(avatarUrl); } - if (summary.mHeroes != null && - summary.mHeroes.length == 1 && - getState(EventTypes.RoomMember, summary.mHeroes.first) != null) { - return getState(EventTypes.RoomMember, summary.mHeroes.first) - .asUser - .avatarUrl; + + final heroes = summary.mHeroes; + if (heroes != null && heroes.length == 1) { + final hero = getState(EventTypes.RoomMember, heroes.first); + if (hero != null) { + return hero.asUser.avatarUrl; + } } if (isDirectChat) { final user = directChatMatrixID; @@ -277,19 +277,17 @@ class Room { return getUserByMXIDSync(user).avatarUrl; } } - if (membership == Membership.invite && - getState(EventTypes.RoomMember, client.userID) != null) { - return getState(EventTypes.RoomMember, client.userID).sender.avatarUrl; + if (membership == Membership.invite) { + return getState(EventTypes.RoomMember, client.userID!)?.sender.avatarUrl; } return null; } /// The address in the format: #roomname:homeserver.org. - String get canonicalAlias => - getState(EventTypes.RoomCanonicalAlias) != null && - getState(EventTypes.RoomCanonicalAlias).content['alias'] is String - ? getState(EventTypes.RoomCanonicalAlias).content['alias'] - : ''; + String get canonicalAlias { + final alias = getState(EventTypes.RoomCanonicalAlias)?.content['alias']; + return (alias is String) ? alias : ''; + } /// Sets the canonical alias. If the [canonicalAlias] is not yet an alias of /// this room, it will create one. @@ -305,31 +303,31 @@ class Room { /// If this room is a direct chat, this is the matrix ID of the user. /// Returns null otherwise. - String get directChatMatrixID { - String returnUserId; + String? get directChatMatrixID { if (membership == Membership.invite) { - final invitation = getState(EventTypes.RoomMember, client.userID); + final invitation = getState(EventTypes.RoomMember, client.userID!); if (invitation != null && invitation.content['is_direct'] == true) { return invitation.senderId; } } + if (client.directChats is Map) { - client.directChats.forEach((String userId, dynamic roomIds) { - if (roomIds is List && roomIds.contains(id)) { - returnUserId = userId; - } - }); + return client.directChats.entries + .firstWhereOrNull((MapEntry e) { + final roomIds = e.value; + return roomIds is List && roomIds.contains(id); + })?.key; } - return returnUserId; + return null; } /// Wheither this is a direct chat or not bool get isDirectChat => directChatMatrixID != null; /// Must be one of [all, mention] - String notificationSettings; + String? notificationSettings; - Event get lastEvent { + Event? get lastEvent { // as lastEvent calculation is based on the state events we unfortunately cannot // use sortOrder here: With many state events we just know which ones are the // newest ones, without knowing in which order they actually happened. As such, @@ -338,7 +336,7 @@ class Room { // said room list, so it should be good enough. var lastTime = DateTime.fromMillisecondsSinceEpoch(0); final lastEvents = - client.roomPreviewLastEvents.map(getState).where((e) => e != null); + client.roomPreviewLastEvents.map(getState).whereType(); var lastEvent = lastEvents.isEmpty ? null @@ -356,8 +354,8 @@ class Room { }); if (lastEvent == null) { states.forEach((final String key, final entry) { - if (!entry.containsKey('')) return; final state = entry['']; + if (state == null) return; if (state.originServerTs != null && state.originServerTs.millisecondsSinceEpoch > lastTime.millisecondsSinceEpoch) { @@ -371,29 +369,28 @@ class Room { /// Returns a list of all current typing users. List get typingUsers { - if (!ephemerals.containsKey('m.typing')) return []; - final List typingMxid = ephemerals['m.typing'].content['user_ids']; - return typingMxid.cast().map(getUserByMXIDSync).toList(); + final typingMxid = ephemerals['m.typing']?.content['user_ids']; + return (typingMxid is List) + ? typingMxid.cast().map(getUserByMXIDSync).toList() + : []; } /// Your current client instance. final Client client; Room({ - this.id, + required this.id, this.membership = Membership.join, - int notificationCount, - int highlightCount, + this.notificationCount = 0, + this.highlightCount = 0, this.prev_batch, - this.client, + required this.client, this.notificationSettings, - Map roomAccountData, + Map? roomAccountData, double newestSortOrder = 0.0, double oldestSortOrder = 0.0, - RoomSummary summary, - }) : notificationCount = notificationCount ?? 0, - highlightCount = highlightCount ?? 0, - roomAccountData = roomAccountData ?? {}, + RoomSummary? summary, + }) : roomAccountData = roomAccountData ?? {}, summary = summary ?? RoomSummary.fromJson({ 'm.joined_member_count': 0, @@ -409,13 +406,15 @@ class Room { /// then generates a name from the heroes. String get displayname { if (name != null && name.isNotEmpty) return name; - if (canonicalAlias != null && - canonicalAlias.isNotEmpty && - canonicalAlias.length > 3) { - return canonicalAlias.localpart; + + final canonicalAlias = this.canonicalAlias?.localpart; + if (canonicalAlias != null && canonicalAlias.isNotEmpty) { + return canonicalAlias; } - if (summary.mHeroes != null && summary.mHeroes.isNotEmpty) { - return summary.mHeroes + + final heroes = summary.mHeroes; + if (heroes != null && heroes.isNotEmpty) { + return heroes .where((hero) => hero.isNotEmpty) .map((hero) => getUserByMXIDSync(hero).calcDisplayname()) .join(', '); @@ -423,34 +422,23 @@ class Room { if (isDirectChat) { final user = directChatMatrixID; if (user != null) { - return getUserByMXIDSync(user).displayName; + return getUserByMXIDSync(user).calcDisplayname(); } } - if (membership == Membership.invite && - getState(EventTypes.RoomMember, client.userID) != null) { - return getState(EventTypes.RoomMember, client.userID) - .sender + if (membership == Membership.invite) { + final sender = getState(EventTypes.RoomMember, client.userID!) + ?.sender .calcDisplayname(); + if (sender != null) return sender; } return 'Empty chat'; } @Deprecated('Use [lastEvent.body] instead') - String get lastMessage { - if (lastEvent != null) { - return lastEvent.body; - } else { - return ''; - } - } + String get lastMessage => lastEvent?.body ?? ''; /// When the last message received. - DateTime get timeCreated { - if (lastEvent != null) { - return lastEvent.originServerTs; - } - return DateTime.now(); - } + DateTime get timeCreated => lastEvent?.originServerTs ?? DateTime.now(); /// Call the Matrix API to change the name of this room. Returns the event ID of the /// new m.room.name event. @@ -470,8 +458,8 @@ class Room { ); /// Add a tag to the room. - Future addTag(String tag, {double order}) => client.setRoomTag( - client.userID, + Future addTag(String tag, {double? order}) => client.setRoomTag( + client.userID!, id, tag, order: order, @@ -479,7 +467,7 @@ class Room { /// Removes a tag from the room. Future removeTag(String tag) => client.deleteRoomTag( - client.userID, + client.userID!, id, tag, ); @@ -498,14 +486,16 @@ class Room { /// Returns all tags for this room. Map get tags { - if (roomAccountData['m.tag'] == null || - !(roomAccountData['m.tag'].content['tags'] is Map)) { - return {}; + final tags = roomAccountData['m.tag']?.content['tags']; + + if (tags is Map) { + final parsedTags = + tags.map((k, v) => MapEntry(k, _tryTagFromJson(v))); + parsedTags.removeWhere((k, v) => !TagType.isValid(k)); + return parsedTags; } - final tags = (roomAccountData['m.tag'].content['tags'] as Map) - .map((k, v) => MapEntry(k, _tryTagFromJson(v))); - tags.removeWhere((k, v) => !TagType.isValid(k)); - return tags; + + return {}; } bool get markedUnread { @@ -533,11 +523,12 @@ class Room { ) ]))))); await client.setAccountDataPerRoom( - client.userID, + client.userID!, id, EventType.markedUnread, content, ); + final lastEvent = this.lastEvent; if (unread == false && lastEvent != null) { await setReadMarker( lastEvent.eventId, @@ -570,19 +561,18 @@ class Room { getImagePacksFlat(ImagePackUsage.emoticon); /// returns the resolved mxid for a mention string, or null if none found - String getMention(String mention) => getParticipants() - .firstWhere((u) => u.mentionFragments.contains(mention), - orElse: () => null) + String? getMention(String mention) => getParticipants() + .firstWhereOrNull((u) => u.mentionFragments.contains(mention)) ?.id; /// Sends a normal text message to this room. Returns the event ID generated /// by the server for this message. - Future sendTextEvent(String message, - {String txid, - Event inReplyTo, - String editEventId, + Future sendTextEvent(String message, + {required String? txid, + Event? inReplyTo, + String? editEventId, bool parseMarkdown = true, - @deprecated Map> emotePacks, + @deprecated Map>? emotePacks, bool parseCommands = true, String msgtype = MessageTypes.Text}) { if (parseCommands) { @@ -610,7 +600,7 @@ class Room { /// Sends a reaction to an event with an [eventId] and the content [key] into a room. /// Returns the event ID generated by the server for this reaction. - Future sendReaction(String eventId, String key, {String txid}) { + Future sendReaction(String eventId, String key, {String? txid}) { return sendEvent({ 'm.relates_to': { 'rel_type': RelationshipTypes.reaction, @@ -622,7 +612,7 @@ class Room { /// Sends the location with description [body] and geo URI [geoUri] into a room. /// Returns the event ID generated by the server for this message. - Future sendLocation(String body, String geoUri, {String txid}) { + Future sendLocation(String body, String geoUri, {String? txid}) { final event = { 'msgtype': 'm.location', 'body': body, @@ -638,17 +628,18 @@ class Room { /// Optionally specify [extraContent] to tack on to the event. Future sendFileEvent( MatrixFile file, { - String txid, - Event inReplyTo, - String editEventId, + String? txid, + Event? inReplyTo, + String? editEventId, bool waitUntilSent = false, - MatrixImageFile thumbnail, - Map extraContent, + MatrixImageFile? thumbnail, + Map? extraContent, }) async { MatrixFile uploadFile = file; // ignore: omit_local_variable_types - MatrixFile uploadThumbnail = thumbnail; // ignore: omit_local_variable_types - EncryptedFile encryptedFile; - EncryptedFile encryptedThumbnail; + MatrixFile? uploadThumbnail = + thumbnail; // ignore: omit_local_variable_types + EncryptedFile? encryptedFile; + EncryptedFile? encryptedThumbnail; if (encrypted && client.fileEncryptionEnabled) { encryptedFile = await file.encrypt(); uploadFile = encryptedFile.toMatrixFile(); @@ -727,15 +718,15 @@ class Room { return uploadResp; } - Future _sendContent( + Future _sendContent( String type, Map content, { - String txid, + String? txid, }) async { txid ??= client.generateUniqueTransactionId(); final mustEncrypt = encrypted && client.encryptionEnabled; final sendMessageContent = mustEncrypt - ? await client.encryption + ? await client.encryption! .encryptGroupMessagePayload(id, content, type: type) : content; return await client.sendMessage( @@ -748,15 +739,13 @@ class Room { /// Sends an event to this room with this json as a content. Returns the /// event ID generated from the server. - Future sendEvent( + Future sendEvent( Map content, { - String type, - String txid, - Event inReplyTo, - String editEventId, + String type = EventTypes.Message, + String? txid, + Event? inReplyTo, + String? editEventId, }) async { - type = type ?? EventTypes.Message; - // Create new transaction id String messageID; if (txid == null) { @@ -782,7 +771,7 @@ class Room { .convert(content.tryGet('body') ?? '') .replaceAll('\n', '
'); content['formatted_body'] = - '
In reply to ${inReplyTo.senderId}
$replyHtml
$repliedHtml'; + '
In reply to ${inReplyTo.senderId}
$replyHtml
$repliedHtml'; // We escape all @room-mentions here to prevent accidental room pings when an admin // replies to a message containing that! content['body'] = @@ -817,7 +806,7 @@ class Room { content: content, type: type, eventId: messageID, - senderId: client.userID, + senderId: client.userID!, originServerTs: sentDate, unsigned: { messageSendingStatusKey: EventStatus.sending.intValue, @@ -828,7 +817,7 @@ class Room { await _handleFakeSync(syncUpdate); // Send the text and on success, store and display a *sent* event. - String res; + String? res; while (res == null) { try { res = await _sendContent( @@ -846,16 +835,16 @@ class Room { await Future.delayed(Duration(seconds: 1)); } else { Logs().w('[Client] Problem while sending message', e, s); - syncUpdate.rooms.join.values.first.timeline.events.first - .unsigned[messageSendingStatusKey] = EventStatus.error.intValue; + syncUpdate.rooms!.join!.values.first.timeline!.events!.first + .unsigned![messageSendingStatusKey] = EventStatus.error.intValue; await _handleFakeSync(syncUpdate); return null; } } } - syncUpdate.rooms.join.values.first.timeline.events.first - .unsigned[messageSendingStatusKey] = EventStatus.sent.intValue; - syncUpdate.rooms.join.values.first.timeline.events.first.eventId = res; + syncUpdate.rooms!.join!.values.first.timeline!.events!.first + .unsigned![messageSendingStatusKey] = EventStatus.sent.intValue; + syncUpdate.rooms!.join!.values.first.timeline!.events!.first.eventId = res; await _handleFakeSync(syncUpdate); return res; @@ -867,7 +856,7 @@ class Room { Future join({bool leaveIfNotFound = true}) async { try { await client.joinRoomById(id); - final invitation = getState(EventTypes.RoomMember, client.userID); + final invitation = getState(EventTypes.RoomMember, client.userID!); if (invitation != null && invitation.content['is_direct'] is bool && invitation.content['is_direct']) { @@ -926,11 +915,11 @@ class Room { /// Returns the event ID of the new state event. If there is no known /// power level event, there might something broken and this returns null. Future setPower(String userID, int power) async { - if (getState(EventTypes.RoomPowerLevels) == null) return null; - final powerMap = {} - ..addAll(getState(EventTypes.RoomPowerLevels).content); - if (powerMap['users'] == null) powerMap['users'] = {}; - powerMap['users'][userID] = power; + var powerMap = getState(EventTypes.RoomPowerLevels)?.content; + if (!(powerMap is Map)) { + powerMap = {}; + } + (powerMap['users'] ??= {})[userID] = power; return await client.setRoomStateWithKey( id, @@ -948,7 +937,8 @@ class Room { /// the historical events will be published in the onEvent stream. Future requestHistory( {int historyCount = defaultHistoryCount, - void Function() onHistoryReceived}) async { + void Function()? onHistoryReceived}) async { + final prev_batch = this.prev_batch; if (prev_batch == null) { throw 'Tried to request history without a prev_batch token'; } @@ -961,7 +951,7 @@ class Room { ); if (onHistoryReceived != null) onHistoryReceived(); - prev_batch = resp.end; + this.prev_batch = resp.end; final loadFn = () async { if (!((resp.chunk?.isNotEmpty ?? false) && resp.end != null)) return; @@ -989,8 +979,8 @@ class Room { }; if (client.database != null) { - await client.database.transaction(() async { - await client.database.setRoomPrevBatch(resp.end, id); + await client.database?.transaction(() async { + await client.database?.setRoomPrevBatch(resp.end!, id, client); await loadFn(); }); } else { @@ -1012,7 +1002,7 @@ class Room { } await client.setAccountData( - client.userID, + client.userID!, 'm.direct', directChats, ); @@ -1030,7 +1020,7 @@ class Room { } // Nothing to do here await client.setAccountDataPerRoom( - client.userID, + client.userID!, id, 'm.direct', directChats, @@ -1040,7 +1030,7 @@ class Room { /// Sets the position of the read marker for a given room, and optionally the /// read receipt's location. - Future setReadMarker(String eventId, {String mRead}) async { + Future setReadMarker(String eventId, {String? mRead}) async { if (mRead != null) { notificationCount = 0; await client.database?.resetNotificationCount(id); @@ -1082,26 +1072,24 @@ class Room { /// Creates a timeline from the store. Returns a [Timeline] object. Future getTimeline( - {void Function() onUpdate, void Function(int insertID) onInsert}) async { + {void Function()? onUpdate, + void Function(int insertID)? onInsert}) async { await postLoad(); var events; - if (client.database != null) { - events = await client.database.getEventList( - this, - limit: defaultHistoryCount, - ); - } else { - events = []; - } + events = await client.database?.getEventList( + this, + limit: defaultHistoryCount, + ) ?? + []; // Try again to decrypt encrypted events and update the database. if (encrypted && client.database != null && client.encryptionEnabled) { - await client.database.transaction(() async { + await client.database?.transaction(() async { for (var i = 0; i < events.length; i++) { if (events[i].type == EventTypes.Encrypted && events[i].content['can_request_session'] == true) { events[i] = await client.encryption - .decryptRoomEvent(id, events[i], store: true); + ?.decryptRoomEvent(id, events[i], store: true); } } }); @@ -1124,8 +1112,9 @@ class Room { /// case. List getParticipants() { final userList = []; - if (states[EventTypes.RoomMember] is Map) { - for (final entry in states[EventTypes.RoomMember].entries) { + final members = states[EventTypes.RoomMember]; + if (members != null && members is Map) { + for (final entry in members.entries) { final state = entry.value; if (state.type == EventTypes.RoomMember) userList.add(state.asUser); } @@ -1138,9 +1127,9 @@ class Room { /// Request the full list of participants from the server. The local list /// from the store is not complete if the client uses lazy loading. Future> requestParticipants() async { - if (!participantListComplete && partial && client.database != null) { + if (!participantListComplete && partial) { // we aren't fully loaded, maybe the users are in the database - final users = await client.database.getUsers(this); + final users = await client.database?.getUsers(this) ?? []; for (final user in users) { setState(user); } @@ -1149,8 +1138,10 @@ class Room { return getParticipants(); } final matrixEvents = await client.getMembersByRoom(id); - final users = - matrixEvents.map((e) => Event.fromMatrixEvent(e, this).asUser).toList(); + final users = matrixEvents + ?.map((e) => Event.fromMatrixEvent(e, this).asUser) + .toList() ?? + []; for (final user in users) { setState(user); // at *least* cache this in-memory } @@ -1172,18 +1163,15 @@ class Room { /// Returns the [User] object for the given [mxID] or requests it from /// the homeserver and waits for a response. @Deprecated('Use [requestUser] instead') - Future getUserByMXID(String mxID) async { - if (getState(EventTypes.RoomMember, mxID) != null) { - return getState(EventTypes.RoomMember, mxID).asUser; - } - return requestUser(mxID); - } + Future getUserByMXID(String mxID) async => + getState(EventTypes.RoomMember, mxID)?.asUser ?? await requestUser(mxID); /// Returns the [User] object for the given [mxID] or requests it from /// the homeserver and returns a default [User] object while waiting. User getUserByMXIDSync(String mxID) { - if (getState(EventTypes.RoomMember, mxID) != null) { - return getState(EventTypes.RoomMember, mxID).asUser; + final user = getState(EventTypes.RoomMember, mxID); + if (user != null) { + return user.asUser; } else { requestUser(mxID, ignoreErrors: true); return User(mxID, room: this); @@ -1195,7 +1183,7 @@ class Room { /// Requests a missing [User] for this room. Important for clients using /// lazy loading. If the user can't be found this method tries to fetch /// the displayname and avatar from the profile if [requestProfile] is true. - Future requestUser( + Future requestUser( String mxID, { bool ignoreErrors = false, bool requestProfile = true, @@ -1210,13 +1198,14 @@ class Room { } return null; } - - if (getState(EventTypes.RoomMember, mxID) != null) { - return getState(EventTypes.RoomMember, mxID).asUser; + final stateUser = getState(EventTypes.RoomMember, mxID); + if (stateUser != null) { + return stateUser.asUser; } - if (client.database != null) { + + { // it may be in the database - final user = await client.database.getUser(mxID, this); + final user = await client.database?.getUser(mxID, this); if (user != null) { setState(user); onUpdate.add(id); @@ -1224,7 +1213,7 @@ class Room { } } if (!_requestingMatrixIds.add(mxID)) return null; - Map resp; + Map? resp; try { Logs().v( 'Request missing user $mxID in room $displayname from the server...'); @@ -1272,12 +1261,13 @@ class Room { 'content': resp, 'state_key': mxID, }; - await client.database.storeEventUpdate( + await client.database?.storeEventUpdate( EventUpdate( content: content, roomID: id, type: EventUpdateType.state, ), + client, ); }); onUpdate.add(id); @@ -1286,14 +1276,14 @@ class Room { } /// Searches for the event on the server. Returns null if not found. - Future getEventById(String eventID) async { + Future getEventById(String eventID) async { try { final matrixEvent = await client.getOneRoomEvent(id, eventID); final event = Event.fromMatrixEvent(matrixEvent, this); if (event.type == EventTypes.Encrypted && client.encryptionEnabled) { // attempt decryption return await client.encryption - .decryptRoomEvent(id, event, store: false); + ?.decryptRoomEvent(id, event, store: false); } return event; } on MatrixException catch (err) { @@ -1322,15 +1312,13 @@ class Room { } /// Returns the user's own power level. - int get ownPowerLevel => getPowerLevelByUserId(client.userID); + int get ownPowerLevel => getPowerLevelByUserId(client.userID!); /// Returns the power levels from all users for this room or null if not given. - Map get powerLevels { - final powerLevelState = getState(EventTypes.RoomPowerLevels); - if (powerLevelState.content['users'] is Map) { - return powerLevelState.content['users']; - } - return null; + Map? get powerLevels { + final powerLevelState = + getState(EventTypes.RoomPowerLevels)?.content['users']; + return (powerLevelState is Map) ? powerLevelState : null; } /// Uploads a new user avatar for this room. Returns the event ID of the new @@ -1347,12 +1335,11 @@ class Room { } bool _hasPermissionFor(String action) { - if (getState(EventTypes.RoomPowerLevels) == null || - getState(EventTypes.RoomPowerLevels).content[action] == null) { + final pl = getState(EventTypes.RoomPowerLevels)?.content[action]; + if (pl == null) { return true; } - return ownPowerLevel >= - getState(EventTypes.RoomPowerLevels).content[action]; + return ownPowerLevel >= pl; } /// The level required to ban a user. @@ -1376,28 +1363,24 @@ class Room { bool get canChangePowerLevel => canSendEvent(EventTypes.RoomPowerLevels); bool canSendEvent(String eventType) { - if (getState(EventTypes.RoomPowerLevels) == null) return true; - if (getState(EventTypes.RoomPowerLevels).content['events'] == null || - getState(EventTypes.RoomPowerLevels).content['events'][eventType] == - null) { + final pl = + getState(EventTypes.RoomPowerLevels)?.content['events']?[eventType]; + if (pl == null) { return eventType == EventTypes.Message ? canSendDefaultMessages : canSendDefaultStates; } - return ownPowerLevel >= - getState(EventTypes.RoomPowerLevels).content['events'][eventType]; + return ownPowerLevel >= pl; } /// Returns the [PushRuleState] for this room, based on the m.push_rules stored in /// the account_data. PushRuleState get pushRuleState { - if (!client.accountData.containsKey('m.push_rules') || - !(client.accountData['m.push_rules'].content['global'] is Map)) { + final globalPushRules = + client.accountData['m.push_rules']?.content['global']; + if (!(globalPushRules is Map)) { return PushRuleState.notify; } - final Map globalPushRules = - client.accountData['m.push_rules'].content['global']; - if (globalPushRules == null) return PushRuleState.notify; if (globalPushRules['override'] is List) { for (final pushRule in globalPushRules['override']) { @@ -1476,8 +1459,8 @@ class Room { } /// Redacts this event. Throws `ErrorResponse` on error. - Future redactEvent(String eventId, - {String reason, String txid}) async { + Future redactEvent(String eventId, + {String? reason, String? txid}) async { // Create new transaction id String messageID; final now = DateTime.now().millisecondsSinceEpoch; @@ -1499,11 +1482,11 @@ class Room { /// This tells the server that the user is typing for the next N milliseconds /// where N is the value specified in the timeout key. Alternatively, if typing is false, /// it tells the server that the user has stopped typing. - Future setTyping(bool isTyping, {int timeout}) => - client.setTyping(client.userID, id, isTyping, timeout: timeout); + Future setTyping(bool isTyping, {int? timeout}) => + client.setTyping(client.userID!, id, isTyping, timeout: timeout); @Deprecated('Use sendTypingNotification instead') - Future sendTypingInfo(bool isTyping, {int timeout}) => + Future sendTypingInfo(bool isTyping, {int? timeout}) => setTyping(isTyping, timeout: timeout); /// This is sent by the caller when they wish to establish a call. @@ -1516,13 +1499,13 @@ class Room { /// [invitee] The user ID of the person who is being invited. Invites without an invitee field are defined to be /// intended for any member of the room other than the sender of the event. /// [party_id] The party ID for call, Can be set to client.deviceId. - Future inviteToCall( + Future inviteToCall( String callId, int lifetime, String party_id, String invitee, String sdp, {String type = 'offer', String version = voipProtoVersion, - String txid, - CallCapabilities capabilities, - SDPStreamMetadata metadata}) async { + String? txid, + CallCapabilities? capabilities, + SDPStreamMetadata? metadata}) async { txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}'; final content = { @@ -1551,9 +1534,9 @@ class Room { /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1. /// [party_id] The party ID for call, Can be set to client.deviceId. /// [selected_party_id] The party ID for the selected answer. - Future selectCallAnswer( + Future selectCallAnswer( String callId, int lifetime, String party_id, String selected_party_id, - {String version = voipProtoVersion, String txid}) async { + {String version = voipProtoVersion, String? txid}) async { txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}'; final content = { @@ -1575,8 +1558,8 @@ class Room { /// [callId] is a unique identifier for the call. /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1. /// [party_id] The party ID for call, Can be set to client.deviceId. - Future sendCallReject(String callId, int lifetime, String party_id, - {String version = voipProtoVersion, String txid}) async { + Future sendCallReject(String callId, int lifetime, String party_id, + {String version = voipProtoVersion, String? txid}) async { txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}'; final content = { @@ -1598,13 +1581,13 @@ class Room { /// [callId] is a unique identifier for the call. /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1. /// [party_id] The party ID for call, Can be set to client.deviceId. - Future sendCallNegotiate( + Future sendCallNegotiate( String callId, int lifetime, String party_id, String sdp, {String type = 'offer', String version = voipProtoVersion, - String txid, - CallCapabilities capabilities, - SDPStreamMetadata metadata}) async { + String? txid, + CallCapabilities? capabilities, + SDPStreamMetadata? metadata}) async { txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}'; final content = { 'call_id': callId, @@ -1642,12 +1625,12 @@ class Room { /// } /// ], /// ``` - Future sendCallCandidates( + Future sendCallCandidates( String callId, String party_id, List> candidates, { String version = voipProtoVersion, - String txid, + String? txid, }) async { txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}'; final content = { @@ -1669,12 +1652,12 @@ class Room { /// [type] The type of session description. Must be 'answer'. /// [sdp] The SDP text of the session description. /// [party_id] The party ID for call, Can be set to client.deviceId. - Future answerCall(String callId, String sdp, String party_id, + Future answerCall(String callId, String sdp, String party_id, {String type = 'answer', String version = voipProtoVersion, - String txid, - CallCapabilities capabilities, - SDPStreamMetadata metadata}) async { + String? txid, + CallCapabilities? capabilities, + SDPStreamMetadata? metadata}) async { txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}'; final content = { 'call_id': callId, @@ -1695,8 +1678,8 @@ class Room { /// [callId] The ID of the call this event relates to. /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1. /// [party_id] The party ID for call, Can be set to client.deviceId. - Future hangupCall(String callId, String party_id, String hangupCause, - {String version = voipProtoVersion, String txid}) async { + Future hangupCall(String callId, String party_id, String hangupCause, + {String version = voipProtoVersion, String? txid}) async { txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}'; final content = { @@ -1726,9 +1709,9 @@ class Room { /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1. /// [party_id] The party ID for call, Can be set to client.deviceId. /// [metadata] The sdp_stream_metadata object. - Future sendSDPStreamMetadataChanged( + Future sendSDPStreamMetadataChanged( String callId, String party_id, SDPStreamMetadata metadata, - {String version = voipProtoVersion, String txid}) async { + {String version = voipProtoVersion, String? txid}) async { txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}'; final content = { 'call_id': callId, @@ -1749,9 +1732,9 @@ class Room { /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1. /// [party_id] The party ID for call, Can be set to client.deviceId. /// [callReplaces] transfer info - Future sendCallReplaces( + Future sendCallReplaces( String callId, String party_id, CallReplaces callReplaces, - {String version = voipProtoVersion, String txid}) async { + {String version = voipProtoVersion, String? txid}) async { txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}'; final content = { 'call_id': callId, @@ -1772,9 +1755,9 @@ class Room { /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1. /// [party_id] The party ID for call, Can be set to client.deviceId. /// [assertedIdentity] the asserted identity - Future sendAssertedIdentity( + Future sendAssertedIdentity( String callId, String party_id, AssertedIdentity assertedIdentity, - {String version = voipProtoVersion, String txid}) async { + {String version = voipProtoVersion, String? txid}) async { txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}'; final content = { 'call_id': callId, @@ -1793,13 +1776,13 @@ class Room { /// it can be invite meaning that a user who wishes to join the room must first receive an invite /// to the room from someone already inside of the room. Currently, knock and private are reserved /// keywords which are not implemented. - JoinRules get joinRules => getState(EventTypes.RoomJoinRules) != null - ? JoinRules.values.firstWhere( - (r) => - r.toString().replaceAll('JoinRules.', '') == - getState(EventTypes.RoomJoinRules).content['join_rule'], - orElse: () => null) - : null; + JoinRules? get joinRules { + final joinRule = getState(EventTypes.RoomJoinRules)?.content['join_rule']; + return joinRule != null + ? JoinRules.values.firstWhereOrNull( + (r) => r.toString().replaceAll('JoinRules.', '') == joinRule) + : null; + } /// Changes the join rules. You should check first if the user is able to change it. Future setJoinRules(JoinRules joinRules) async { @@ -1819,10 +1802,13 @@ class Room { /// This event controls whether guest users are allowed to join rooms. If this event /// is absent, servers should act as if it is present and has the guest_access value "forbidden". - GuestAccess get guestAccess => getState(EventTypes.GuestAccess) != null - ? _guestAccessMap.map((k, v) => MapEntry(v, k))[ - getState(EventTypes.GuestAccess).content['guest_access']] - : GuestAccess.forbidden; + GuestAccess get guestAccess { + final ga = getState(EventTypes.GuestAccess)?.content['guest_access']; + return ga != null + ? (_guestAccessMap.map((k, v) => MapEntry(v, k))[ga] ?? + GuestAccess.forbidden) + : GuestAccess.forbidden; + } /// Changes the guest access. You should check first if the user is able to change it. Future setGuestAccess(GuestAccess guestAccess) async { @@ -1841,12 +1827,13 @@ class Room { bool get canChangeGuestAccess => canSendEvent(EventTypes.GuestAccess); /// This event controls whether a user can see the events that happened in a room from before they joined. - HistoryVisibility get historyVisibility => - getState(EventTypes.HistoryVisibility) != null - ? _historyVisibilityMap.map((k, v) => MapEntry(v, k))[ - getState(EventTypes.HistoryVisibility) - .content['history_visibility']] - : null; + HistoryVisibility? get historyVisibility { + final hv = + getState(EventTypes.HistoryVisibility)?.content['history_visibility']; + return hv != null + ? _historyVisibilityMap.map((k, v) => MapEntry(v, k))[hv] + : null; + } /// Changes the history visibility. You should check first if the user is able to change it. Future setHistoryVisibility(HistoryVisibility historyVisibility) async { @@ -1867,7 +1854,7 @@ class Room { /// Returns the encryption algorithm. Currently only `m.megolm.v1.aes-sha2` is supported. /// Returns null if there is no encryption algorithm. - String get encryptionAlgorithm => + String? get encryptionAlgorithm => getState(EventTypes.Encryption)?.parsedRoomEncryptionContent?.algorithm; /// Checks if this room is encrypted. @@ -1892,10 +1879,10 @@ class Room { final deviceKeys = []; final users = await requestParticipants(); for (final user in users) { + final userDeviceKeys = client.userDeviceKeys[user.id]?.deviceKeys.values; if ([Membership.invite, Membership.join].contains(user.membership) && - client.userDeviceKeys.containsKey(user.id)) { - for (final deviceKeyEntry - in client.userDeviceKeys[user.id].deviceKeys.values) { + userDeviceKeys != null) { + for (final deviceKeyEntry in userDeviceKeys) { deviceKeys.add(deviceKeyEntry); } } @@ -1907,13 +1894,13 @@ class Room { if (!client.encryptionEnabled) { return; } - await client.encryption.keyManager.request(this, sessionId, senderKey); + await client.encryption?.keyManager.request(this, sessionId, senderKey); } Future _handleFakeSync(SyncUpdate syncUpdate, {bool sortAtTheEnd = false}) async { if (client.database != null) { - await client.database.transaction(() async { + await client.database?.transaction(() async { await client.handleSync(syncUpdate, sortAtTheEnd: sortAtTheEnd); }); } else { @@ -1927,9 +1914,8 @@ class Room { bool get isExtinct => getState(EventTypes.RoomTombstone) != null; /// Returns informations about how this room is - TombstoneContent get extinctInformations => isExtinct - ? getState(EventTypes.RoomTombstone).parsedTombstoneContent - : null; + TombstoneContent? get extinctInformations => + getState(EventTypes.RoomTombstone)?.parsedTombstoneContent; /// Checks if the `m.room.create` state has a `type` key with the value /// `m.space`. @@ -1968,12 +1954,12 @@ class Room { /// Adds or edits a child of this space. Future setSpaceChild( String roomId, { - List via, - String order, - bool suggested, + List? via, + String? order, + bool? suggested, }) async { if (!isSpace) throw Exception('Room is not a space!'); - via ??= [roomId.domain]; + via ??= [client.userID!.domain!]; await client.setRoomStateWithKey(id, EventTypes.spaceChild, roomId, { 'via': via, if (order != null) 'order': order, diff --git a/lib/src/utils/commands_extension.dart b/lib/src/utils/commands_extension.dart index 7c4cc2ee..c86b6d46 100644 --- a/lib/src/utils/commands_extension.dart +++ b/lib/src/utils/commands_extension.dart @@ -171,9 +171,10 @@ extension CommandsClientExtension on Client { }); addCommand('myroomnick', (CommandArgs args) async { final currentEventJson = args.room - .getState(EventTypes.RoomMember, args.room.client.userID) - .content - .copy(); + .getState(EventTypes.RoomMember, args.room.client.userID!) + ?.content + .copy() ?? + {}; currentEventJson['displayname'] = args.msg; return await args.room.client.setRoomStateWithKey( args.room.id, @@ -184,9 +185,10 @@ extension CommandsClientExtension on Client { }); addCommand('myroomavatar', (CommandArgs args) async { final currentEventJson = args.room - .getState(EventTypes.RoomMember, args.room.client.userID) - .content - .copy(); + .getState(EventTypes.RoomMember, args.room.client.userID!) + ?.content + .copy() ?? + {}; currentEventJson['avatar_url'] = args.msg; return await args.room.client.setRoomStateWithKey( args.room.id, diff --git a/lib/src/utils/markdown.dart b/lib/src/utils/markdown.dart index 857cd3f4..0924e9dc 100644 --- a/lib/src/utils/markdown.dart +++ b/lib/src/utils/markdown.dart @@ -183,7 +183,7 @@ class PillSyntax extends InlineSyntax { } class MentionSyntax extends InlineSyntax { - final String Function(String)? getMention; + final String? Function(String)? getMention; MentionSyntax(this.getMention) : super(r'(@(?:\[[^\]:]+\]|\w+)(?:#\w+)?)'); @override @@ -206,7 +206,7 @@ class MentionSyntax extends InlineSyntax { String markdown( String text, { Map> Function()? getEmotePacks, - String Function(String)? getMention, + String? Function(String)? getMention, }) { var ret = markdownToHtml( text, diff --git a/test/database_api_test.dart b/test/database_api_test.dart index ca405984..9a5e7419 100644 --- a/test/database_api_test.dart +++ b/test/database_api_test.dart @@ -115,8 +115,9 @@ void testDatabase( 'limited_timeline': false, 'membership': Membership.join, }); - await database.storeRoomUpdate('!testroom', roomUpdate); - final rooms = await database.getRoomList(Client('testclient')); + final client = Client('testclient'); + await database.storeRoomUpdate('!testroom', roomUpdate, client); + final rooms = await database.getRoomList(client); expect(rooms.single.id, '!testroom'); }); test('getRoomList', () async { @@ -124,8 +125,9 @@ void testDatabase( expect(list.single.id, '!testroom'); }); test('setRoomPrevBatch', () async { - await database.setRoomPrevBatch('1234', '!testroom'); - final rooms = await database.getRoomList(Client('testclient')); + final client = Client('testclient'); + await database.setRoomPrevBatch('1234', '!testroom', client); + final rooms = await database.getRoomList(client); expect(rooms.single.prev_batch, '1234'); }); test('forgetRoom', () async { @@ -192,28 +194,28 @@ void testDatabase( }); test('storeEventUpdate', () async { await database.storeEventUpdate( - EventUpdate( - roomID: '!testroom:example.com', - type: EventUpdateType.timeline, - content: { - 'type': EventTypes.Message, - 'content': { - 'body': '* edit 3', - 'msgtype': 'm.text', - 'm.new_content': { - 'body': 'edit 3', + EventUpdate( + roomID: '!testroom:example.com', + type: EventUpdateType.timeline, + content: { + 'type': EventTypes.Message, + 'content': { + 'body': '* edit 3', 'msgtype': 'm.text', + 'm.new_content': { + 'body': 'edit 3', + 'msgtype': 'm.text', + }, + 'm.relates_to': { + 'event_id': '\$source', + 'rel_type': RelationshipTypes.edit, + }, }, - 'm.relates_to': { - 'event_id': '\$source', - 'rel_type': RelationshipTypes.edit, - }, + 'event_id': '\$event:example.com', + 'sender': '@bob:example.org', }, - 'event_id': '\$event:example.com', - 'sender': '@bob:example.org', - }, - ), - ); + ), + Client('testclient')); }); test('getEventById', () async { final event = await database.getEventById( diff --git a/test/matrix_database_test.dart b/test/matrix_database_test.dart index 360d6d39..4d4e8b9e 100644 --- a/test/matrix_database_test.dart +++ b/test/matrix_database_test.dart @@ -43,7 +43,8 @@ void main() { }); test('storeEventUpdate', () async { - final database = await getDatabase(null); + final client = Client('testclient'); + final database = await getDatabase(client); // store a simple update var update = EventUpdate( type: EventUpdateType.timeline, @@ -56,7 +57,7 @@ void main() { 'sender': '@blah:blubb', }, ); - await database.storeEventUpdate(update); + await database.storeEventUpdate(update, client); var event = await database.getEventById('\$event-1', room); expect(event.eventId, '\$event-1'); @@ -73,7 +74,7 @@ void main() { 'status': EventStatus.sending.intValue, }, ); - await database.storeEventUpdate(update); + await database.storeEventUpdate(update, client); event = await database.getEventById('transaction-1', room); expect(event.eventId, 'transaction-1'); update = EventUpdate( @@ -91,7 +92,7 @@ void main() { 'status': EventStatus.sent.intValue, }, ); - await database.storeEventUpdate(update); + await database.storeEventUpdate(update, client); event = await database.getEventById('transaction-1', room); expect(event, null); event = await database.getEventById('\$event-2', room); @@ -109,7 +110,7 @@ void main() { 'status': EventStatus.sending.intValue, }, ); - await database.storeEventUpdate(update); + await database.storeEventUpdate(update, client); event = await database.getEventById('\$event-3', room); expect(event.eventId, '\$event-3'); update = EventUpdate( @@ -127,7 +128,7 @@ void main() { }, }, ); - await database.storeEventUpdate(update); + await database.storeEventUpdate(update, client); event = await database.getEventById('\$event-3', room); expect(event.eventId, '\$event-3'); expect(event.status, EventStatus.sent); @@ -147,7 +148,7 @@ void main() { 'status': EventStatus.synced.intValue, }, ); - await database.storeEventUpdate(update); + await database.storeEventUpdate(update, client); event = await database.getEventById('\$event-4', room); expect(event.eventId, '\$event-4'); update = EventUpdate( @@ -165,7 +166,7 @@ void main() { }, }, ); - await database.storeEventUpdate(update); + await database.storeEventUpdate(update, client); event = await database.getEventById('\$event-4', room); expect(event.eventId, '\$event-4'); expect(event.status, EventStatus.synced);