diff --git a/lib/src/client.dart b/lib/src/client.dart index 4aafc6cb..def88969 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1666,29 +1666,29 @@ class Client extends MatrixApi { progress: ++handledRooms / rooms.length, )); final id = entry.key; - final room = entry.value; + final syncRoomUpdate = entry.value; - await database?.storeRoomUpdate(id, room, this); - _updateRoomsByRoomUpdate(id, room); + await database?.storeRoomUpdate(id, syncRoomUpdate, this); + final room = _updateRoomsByRoomUpdate(id, syncRoomUpdate); /// Handle now all room events and save them in the database - if (room is JoinedRoomUpdate) { - final state = room.state; + if (syncRoomUpdate is JoinedRoomUpdate) { + final state = syncRoomUpdate.state; if (state != null && state.isNotEmpty) { // TODO: This method seems to be comperatively slow for some updates await _handleRoomEvents( - id, - state.map((i) => i.toJson()).toList(), + room, + state, EventUpdateType.state, ); } - final timelineEvents = room.timeline?.events; + final timelineEvents = syncRoomUpdate.timeline?.events; if (timelineEvents != null && timelineEvents.isNotEmpty) { await _handleRoomEvents( - id, - timelineEvents.map((i) => i.toJson()).toList(), + room, + timelineEvents, direction != null ? (direction == Direction.b ? EventUpdateType.history @@ -1696,73 +1696,72 @@ class Client extends MatrixApi { : EventUpdateType.timeline); } - final ephemeral = room.ephemeral; + final ephemeral = syncRoomUpdate.ephemeral; if (ephemeral != null && ephemeral.isNotEmpty) { // TODO: This method seems to be comperatively slow for some updates await _handleEphemerals( - id, ephemeral.map((i) => i.toJson()).toList()); + room, + ephemeral, + ); } - final accountData = room.accountData; + final accountData = syncRoomUpdate.accountData; if (accountData != null && accountData.isNotEmpty) { await _handleRoomEvents( - id, - accountData.map((i) => i.toJson()).toList(), - EventUpdateType.accountData); + room, + accountData, + EventUpdateType.accountData, + ); } } - if (room is LeftRoomUpdate) { - final timelineEvents = room.timeline?.events; + if (syncRoomUpdate is LeftRoomUpdate) { + final timelineEvents = syncRoomUpdate.timeline?.events; if (timelineEvents != null && timelineEvents.isNotEmpty) { await _handleRoomEvents( - id, - timelineEvents.map((i) => i.toJson()).toList(), + room, + timelineEvents, EventUpdateType.timeline, ); } - final accountData = room.accountData; + final accountData = syncRoomUpdate.accountData; if (accountData != null && accountData.isNotEmpty) { await _handleRoomEvents( - id, - accountData.map((i) => i.toJson()).toList(), - EventUpdateType.accountData); + room, + accountData, + EventUpdateType.accountData, + ); } - final state = room.state; + final state = syncRoomUpdate.state; if (state != null && state.isNotEmpty) { - await _handleRoomEvents( - id, state.map((i) => i.toJson()).toList(), EventUpdateType.state); + await _handleRoomEvents(room, state, EventUpdateType.state); } } - if (room is InvitedRoomUpdate) { - final state = room.inviteState; + if (syncRoomUpdate is InvitedRoomUpdate) { + final state = syncRoomUpdate.inviteState; if (state != null && state.isNotEmpty) { - await _handleRoomEvents(id, state.map((i) => i.toJson()).toList(), - EventUpdateType.inviteState); + await _handleRoomEvents(room, state, EventUpdateType.inviteState); } } } } - Future _handleEphemerals(String id, List events) async { + Future _handleEphemerals(Room room, List events) async { for (final event in events) { - await _handleEvent(event, id, EventUpdateType.ephemeral); + await _handleRoomEvents(room, [event], EventUpdateType.ephemeral); // Receipt events are deltas between two states. We will create a // fake room account data event for this and store the difference // there. - if (event['type'] == 'm.receipt') { - var room = getRoomById(id); - room ??= Room(id: id, client: this); - + if (event.type == 'm.receipt') { final receiptStateContent = room.roomAccountData['m.receipt']?.content ?? {}; - for (final eventEntry in event['content'].entries) { - final String eventID = eventEntry.key; - if (event['content'][eventID]['m.read'] != null) { + for (final eventEntry in event.content.entries) { + final eventID = eventEntry.key; + if (event.content[eventID]['m.read'] != null) { final Map userTimestampMap = - event['content'][eventID]['m.read']; + event.content[eventID]['m.read']; for (final userTimestampMapEntry in userTimestampMap.entries) { final mxid = userTimestampMapEntry.key; @@ -1783,48 +1782,48 @@ class Client extends MatrixApi { } } } - event['content'] = receiptStateContent; - await _handleEvent(event, id, EventUpdateType.accountData); + event.content = receiptStateContent; + await _handleRoomEvents(room, [event], EventUpdateType.accountData); } } } Future _handleRoomEvents( - String chat_id, List events, EventUpdateType type) async { - for (final event in events) { - await _handleEvent(event, chat_id, type); - } - } + Room room, + List events, + EventUpdateType type, + ) async { + // Calling events can be omitted if they are outdated from the same sync. So + // we collect them first before we handle them. + final callEvents = {}; - Future _handleEvent( - Map event, String roomID, EventUpdateType type) async { - if (event['type'] is String && event['content'] is Map) { + for (final event in events) { // The client must ignore any new m.room.encryption event to prevent // man-in-the-middle attacks! - final room = getRoomById(roomID); - if (room == null || - (event['type'] == EventTypes.Encryption && - room.encrypted && - event['content']['algorithm'] != - room.getState(EventTypes.Encryption)?.content['algorithm'])) { + if ((event.type == EventTypes.Encryption && + room.encrypted && + event.content['algorithm'] != + room.getState(EventTypes.Encryption)?.content['algorithm'])) { return; } - var update = EventUpdate(roomID: roomID, type: type, content: event); - if (event['type'] == EventTypes.Encrypted && encryptionEnabled) { + var update = + EventUpdate(roomID: room.id, type: type, content: event.toJson()); + if (event.type == EventTypes.Encrypted && encryptionEnabled) { update = await update.decrypt(room); } - if (event['type'] == EventTypes.Message && + if (event.type == EventTypes.Message && !room.isDirectChat && database != null && - room.getState(EventTypes.RoomMember, event['sender']) == null) { + event is MatrixEvent && + room.getState(EventTypes.RoomMember, event.senderId) == null) { // In order to correctly render room list previews we need to fetch the member from the database - final user = await database?.getUser(event['sender'], room); + final user = await database?.getUser(event.senderId, room); if (user != null) { room.setState(user); } } - _updateRoomsByEventUpdate(update); + _updateRoomsByEventUpdate(room, update); if (type != EventUpdateType.ephemeral) { await database?.storeEventUpdate(update, this); } @@ -1833,47 +1832,78 @@ class Client extends MatrixApi { } onEvent.add(update); - final rawUnencryptedEvent = update.content; - if (prevBatch != null && type == EventUpdateType.timeline) { - if (rawUnencryptedEvent['type'] == EventTypes.CallInvite) { - onCallInvite.add(Event.fromJson(rawUnencryptedEvent, room)); - } else if (rawUnencryptedEvent['type'] == EventTypes.CallHangup) { - onCallHangup.add(Event.fromJson(rawUnencryptedEvent, room)); - } else if (rawUnencryptedEvent['type'] == EventTypes.CallAnswer) { - onCallAnswer.add(Event.fromJson(rawUnencryptedEvent, room)); - } else if (rawUnencryptedEvent['type'] == EventTypes.CallCandidates) { - onCallCandidates.add(Event.fromJson(rawUnencryptedEvent, room)); - } else if (rawUnencryptedEvent['type'] == EventTypes.CallSelectAnswer) { - onCallSelectAnswer.add(Event.fromJson(rawUnencryptedEvent, room)); - } else if (rawUnencryptedEvent['type'] == EventTypes.CallReject) { - onCallReject.add(Event.fromJson(rawUnencryptedEvent, room)); - } else if (rawUnencryptedEvent['type'] == EventTypes.CallNegotiate) { - onCallNegotiate.add(Event.fromJson(rawUnencryptedEvent, room)); - } else if (rawUnencryptedEvent['type'] == EventTypes.CallReplaces) { - onCallReplaces.add(Event.fromJson(rawUnencryptedEvent, room)); - } else if (rawUnencryptedEvent['type'] == - EventTypes.CallAssertedIdentity || - rawUnencryptedEvent['type'] == - EventTypes.CallAssertedIdentityPrefix) { - onAssertedIdentityReceived - .add(Event.fromJson(rawUnencryptedEvent, room)); - } else if (rawUnencryptedEvent['type'] == - EventTypes.CallSDPStreamMetadataChanged || - rawUnencryptedEvent['type'] == - EventTypes.CallSDPStreamMetadataChangedPrefix) { - onSDPStreamMetadataChangedReceived - .add(Event.fromJson(rawUnencryptedEvent, room)); - // TODO(duan): Only used (org.matrix.msc3401.call) during the current test, - // need to add GroupCallPrefix in matrix_api_lite - } else if (rawUnencryptedEvent['type'] == EventTypes.GroupCallPrefix) { - onGroupCallRequest.add(Event.fromJson(rawUnencryptedEvent, room)); + if (update.content.tryGet('type')?.startsWith('m.call.') ?? + false) { + final callEvent = Event.fromJson(update.content, room); + final callId = callEvent.content.tryGet('call_id'); + callEvents.add(callEvent); + + // Call Invites should be omitted for a call that is already answered, + // has ended, is rejectd or replaced. + const callEndedEventTypes = { + EventTypes.CallAnswer, + EventTypes.CallHangup, + EventTypes.CallReject, + EventTypes.CallReplaces, + }; + const ommitWhenCallEndedTypes = { + EventTypes.CallInvite, + EventTypes.CallCandidates, + EventTypes.CallNegotiate, + EventTypes.CallSDPStreamMetadataChanged, + EventTypes.CallSDPStreamMetadataChangedPrefix, + }; + + if (callEndedEventTypes.contains(callEvent.type)) { + callEvents.removeWhere((event) { + if (ommitWhenCallEndedTypes.contains(event.type) && + event.content.tryGet('call_id') == callId) { + Logs().v( + 'Ommit "${event.type}" event for an already terminated call'); + return true; + } + return false; + }); + } } } } + + callEvents.forEach(_callStreamByCallEvent); } - void _updateRoomsByRoomUpdate(String roomId, SyncRoomUpdate chatUpdate) { + void _callStreamByCallEvent(Event event) { + if (event.type == EventTypes.CallInvite) { + onCallInvite.add(event); + } else if (event.type == EventTypes.CallHangup) { + onCallHangup.add(event); + } else if (event.type == EventTypes.CallAnswer) { + onCallAnswer.add(event); + } else if (event.type == EventTypes.CallCandidates) { + onCallCandidates.add(event); + } else if (event.type == EventTypes.CallSelectAnswer) { + onCallSelectAnswer.add(event); + } else if (event.type == EventTypes.CallReject) { + onCallReject.add(event); + } else if (event.type == EventTypes.CallNegotiate) { + onCallNegotiate.add(event); + } else if (event.type == EventTypes.CallReplaces) { + onCallReplaces.add(event); + } else if (event.type == EventTypes.CallAssertedIdentity || + event.type == EventTypes.CallAssertedIdentityPrefix) { + onAssertedIdentityReceived.add(event); + } else if (event.type == EventTypes.CallSDPStreamMetadataChanged || + event.type == EventTypes.CallSDPStreamMetadataChangedPrefix) { + onSDPStreamMetadataChangedReceived.add(event); + // TODO(duan): Only used (org.matrix.msc3401.call) during the current test, + // need to add GroupCallPrefix in matrix_api_lite + } else if (event.type == EventTypes.GroupCallPrefix) { + onGroupCallRequest.add(event); + } + } + + Room _updateRoomsByRoomUpdate(String roomId, SyncRoomUpdate chatUpdate) { // Update the chat list item. // Search the room in the rooms final roomIndex = rooms.indexWhere((r) => r.id == roomId); @@ -1884,24 +1914,27 @@ class Client extends MatrixApi { ? Membership.invite : Membership.join; + final room = found + ? rooms[roomIndex] + : (chatUpdate is JoinedRoomUpdate + ? Room( + id: roomId, + membership: membership, + prev_batch: chatUpdate.timeline?.prevBatch, + highlightCount: + chatUpdate.unreadNotifications?.highlightCount ?? 0, + notificationCount: + chatUpdate.unreadNotifications?.notificationCount ?? 0, + summary: chatUpdate.summary, + client: this, + ) + : Room(id: roomId, membership: membership, client: this)); + // Does the chat already exist in the list rooms? if (!found && membership != Membership.leave) { final position = membership == Membership.invite ? 0 : rooms.length; // Add the new chat to the list - final newRoom = chatUpdate is JoinedRoomUpdate - ? Room( - id: roomId, - membership: membership, - prev_batch: chatUpdate.timeline?.prevBatch, - highlightCount: - chatUpdate.unreadNotifications?.highlightCount ?? 0, - notificationCount: - chatUpdate.unreadNotifications?.notificationCount ?? 0, - summary: chatUpdate.summary, - client: this, - ) - : Room(id: roomId, membership: membership, client: this); - rooms.insert(position, newRoom); + rooms.insert(position, room); } // If the membership is "leave" then remove the item and stop here else if (found && membership == Membership.leave) { @@ -1940,14 +1973,12 @@ class Client extends MatrixApi { runInRoot(rooms[roomIndex].requestHistory); } } + return room; } - void _updateRoomsByEventUpdate(EventUpdate eventUpdate) { + void _updateRoomsByEventUpdate(Room room, EventUpdate eventUpdate) { if (eventUpdate.type == EventUpdateType.history) return; - final room = getRoomById(eventUpdate.roomID); - if (room == null) return; - switch (eventUpdate.type) { case EventUpdateType.timeline: case EventUpdateType.state: diff --git a/test/client_test.dart b/test/client_test.dart index ec33a508..e61a9777 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -239,7 +239,7 @@ void main() { final eventUpdateList = await eventUpdateListFuture; - expect(eventUpdateList.length, 14); + expect(eventUpdateList.length, 18); expect(eventUpdateList[0].content['type'], 'm.room.member'); expect(eventUpdateList[0].roomID, '!726s6s6q:example.com');