diff --git a/lib/src/client.dart b/lib/src/client.dart index 4ba8ce9b..12004caa 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1071,19 +1071,19 @@ class Client extends MatrixApi { [])); archivedRoom.prev_batch = update.timeline?.prevBatch; - update.state?.forEach((event) { - archivedRoom.setState(Event.fromMatrixEvent( - event, - archivedRoom, - )); - }); - update.timeline?.events?.forEach((event) { - archivedRoom.setState(Event.fromMatrixEvent( - event, - archivedRoom, - )); - }); + final stateEvents = roomUpdate.state; + if (stateEvents != null) { + await _handleRoomEvents(archivedRoom, stateEvents, EventUpdateType.state, + store: false); + } + + final timelineEvents = roomUpdate.timeline?.events; + if (timelineEvents != null) { + await _handleRoomEvents(archivedRoom, timelineEvents.reversed.toList(), + EventUpdateType.timeline, + store: false); + } for (var i = 0; i < timeline.events.length; i++) { // Try to decrypt encrypted events but don't update the database. @@ -2463,6 +2463,10 @@ class Client extends MatrixApi { final importantOrRoomLoaded = eventUpdate.type == EventUpdateType.inviteState || !room.partial || + // make sure we do overwrite events we have already loaded. + room.states[stateEvent.type] + ?.containsKey(stateEvent.stateKey ?? '') == + true || importantStateEvents.contains(stateEvent.type); if ((noMessageOrNoEdit || editingLastEvent || consecutiveEdit) && importantOrRoomLoaded) { diff --git a/lib/src/event.dart b/lib/src/event.dart index b5549d1d..650f3d93 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -210,10 +210,8 @@ class Event extends MatrixEvent { type: jsonPayload['type'], eventId: jsonPayload['event_id'] ?? '', senderId: jsonPayload['sender'], - originServerTs: jsonPayload['origin_server_ts'] != null - ? DateTime.fromMillisecondsSinceEpoch( - jsonPayload['origin_server_ts']) - : DateTime.now(), + originServerTs: DateTime.fromMillisecondsSinceEpoch( + jsonPayload['origin_server_ts'] ?? 0), unsigned: unsigned, room: room, originalSource: originalSource.isEmpty diff --git a/lib/src/room.dart b/lib/src/room.dart index f330b72a..cf4fbf1f 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -196,15 +196,6 @@ class Room { return; } - // Do not set old events as state events - final prevEvent = getState(state.type, stateKey); - if (prevEvent != null && - prevEvent.eventId != state.eventId && - prevEvent.originServerTs.millisecondsSinceEpoch > - state.originServerTs.millisecondsSinceEpoch) { - return; - } - (states[state.type] ??= {})[stateKey] = state; client.onRoomState.add(state); @@ -1574,8 +1565,6 @@ class Room { return []; } - bool _requestedParticipants = false; - /// Request the full list of participants from the server. The local list /// from the store is not complete if the client uses lazy loading. /// List `membershipFilter` defines with what membership do you want the @@ -1591,17 +1580,20 @@ class Room { ], bool suppressWarning = false, bool cache = true]) async { - if (!participantListComplete && partial) { + if (!participantListComplete || partial) { // we aren't fully loaded, maybe the users are in the database + // We always need to check the database in the partial case, since state + // events won't get written to memory in this case and someone new could + // have joined, while someone else left, which might lead to the same + // count in the completeness check. final users = await client.database?.getUsers(this) ?? []; for (final user in users) { setState(user); } } - // Do not request users from the server if we have already done it - // in this session or have a complete list locally. - if (_requestedParticipants || participantListComplete) { + // Do not request users from the server if we have already have a complete list locally. + if (participantListComplete) { return getParticipants(membershipFilter); } @@ -1626,7 +1618,6 @@ class Room { } } - _requestedParticipants = cache; users.removeWhere((u) => !membershipFilter.contains(u.membership)); return users; } @@ -1634,10 +1625,14 @@ class Room { /// Checks if the local participant list of joined and invited users is complete. bool get participantListComplete { final knownParticipants = getParticipants(); - knownParticipants.removeWhere( - (u) => ![Membership.join, Membership.invite].contains(u.membership)); - return knownParticipants.length == - (summary.mJoinedMemberCount ?? 0) + (summary.mInvitedMemberCount ?? 0); + final joinedCount = + knownParticipants.where((u) => u.membership == Membership.join).length; + final invitedCount = knownParticipants + .where((u) => u.membership == Membership.invite) + .length; + + return (summary.mJoinedMemberCount ?? 0) == joinedCount && + (summary.mInvitedMemberCount ?? 0) == invitedCount; } @Deprecated( diff --git a/test/client_test.dart b/test/client_test.dart index 6244e431..8f41d652 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -653,6 +653,32 @@ void main() { 'mxc://example.org/SEsfnsuifSDFSSEF'); expect(aliceProfile.displayName, 'Alice Margatroid'); }); + test('joinAfterInviteMembership', () async { + final client = await getClient(); + await client.abortSync(); + client.rooms.clear(); + await client.database?.clearCache(); + + await client.handleSync(SyncUpdate.fromJson(jsonDecode( + '{"next_batch":"s198510_227245_8_1404_23586_11_51065_267416_0_2639","rooms":{"invite":{"!bWEUQDujMKwjxkCXYr:tim-alpha.staging.famedly.de":{"invite_state":{"events":[{"type":"m.room.create","content":{"type":"de.gematik.tim.roomtype.default.v1","room_version":"10","creator":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":""},{"type":"m.room.encryption","content":{"algorithm":"m.megolm.v1.aes-sha2"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":""},{"type":"m.room.join_rules","content":{"join_rule":"invite"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":""},{"type":"m.room.member","content":{"membership":"join","displayname":"Tóboggen, Veronika Freifrau"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"},{"type":"m.room.member","content":{"is_direct":true,"membership":"invite","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de"}]}}}},"presence":{"events":[{"type":"m.presence","content":{"presence":"online","last_active_ago":5948,"currently_active":true},"sender":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de"}]},"device_one_time_keys_count":{"signed_curve25519":66},"device_unused_fallback_key_types":["signed_curve25519"],"org.matrix.msc2732.device_unused_fallback_key_types":["signed_curve25519"]}'))); + await client.handleSync(SyncUpdate.fromJson(jsonDecode( + '{"next_batch":"s198511_227245_8_1404_23588_11_51066_267416_0_2639","account_data":{"events":[{"type":"m.direct","content":{"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de":["!bWEUQDujMKwjxkCXYr:tim-alpha.staging.famedly.de"]}}]},"device_one_time_keys_count":{"signed_curve25519":65},"device_unused_fallback_key_types":["signed_curve25519"],"org.matrix.msc2732.device_unused_fallback_key_types":["signed_curve25519"]}'))); + await client.handleSync(SyncUpdate.fromJson(jsonDecode( + '{"next_batch":"s198512_227245_8_1404_23588_11_51066_267416_0_2639","rooms":{"join":{"!bWEUQDujMKwjxkCXYr:tim-alpha.staging.famedly.de":{"summary":{"m.heroes":["@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"],"m.joined_member_count":2,"m.invited_member_count":0},"state":{"events":[{"type":"m.room.create","content":{"type":"de.gematik.tim.roomtype.default.v1","room_version":"10","creator":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$qSgXGXjly6p5Kwbdb_PMBC_EF7nzHDbM23mvJFVeoiE","origin_server_ts":1709565579735,"unsigned":{"age":2255}}]},"timeline":{"events":[{"type":"m.room.member","content":{"membership":"join","displayname":"Tóboggen, Veronika Freifrau"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","event_id":"\$rQzxxTrSd9Y0koxIGlkalPAV_lwu94jLOA-8PSunY24","origin_server_ts":1709565579871,"unsigned":{"age":2119,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.power_levels","content":{"users":{"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de":100,"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de":100},"users_default":0,"events":{"m.room.name":50,"m.room.power_levels":100,"m.room.history_visibility":100,"m.room.canonical_alias":50,"m.room.avatar":50,"m.room.tombstone":100,"m.room.server_acl":100,"m.room.encryption":100},"events_default":0,"state_default":50,"ban":50,"kick":50,"redact":50,"invite":0,"historical":100},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$d6sgGs8PmkAbC3Iw3CkPT1QSub2zFTTvytegOxkPYPs","origin_server_ts":1709565579966,"unsigned":{"age":2024,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.join_rules","content":{"join_rule":"invite"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$EnA2Podch5181X4G1ZX34zaFGS_V4ZCZzLkBEfS_qyg","origin_server_ts":1709565579979,"unsigned":{"age":2011,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.history_visibility","content":{"history_visibility":"shared"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$6tNNo6ZkpZZrHrn8ZjXhMqI0CNv-VNNBw4R0h3_O-Tc","origin_server_ts":1709565579979,"unsigned":{"age":2011,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.guest_access","content":{"guest_access":"can_join"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$ViuL_LpN1sY9oYcGwycNjtp6FcGj__smUg8mzj3oa2o","origin_server_ts":1709565579980,"unsigned":{"age":2010,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.encryption","content":{"algorithm":"m.megolm.v1.aes-sha2"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$_e0az7OP7D78QU7DItiRAtlHlZmA07B5wenR93x5V1E","origin_server_ts":1709565579981,"unsigned":{"age":2009,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.member","content":{"is_direct":true,"membership":"invite","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","event_id":"\$4sZ3CF67SUh0n5WG0ZKS47Epj9B_d842RJjnrQmUKQo","origin_server_ts":1709565580185,"unsigned":{"age":1805,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.notsoencrypted","content":{"algorithm":"m.megolm.v1.aes-sha2","ciphertext":"AwgAEpACEZw8Ymg99Yfl7VsXRIdczlQ3+YSJ6te3o6ka/XXP0h4ZsgR2bu1Q8puQ77fOpwX5dPnrrCi5SQg9Zv5/u+0QbFV4FKE/k03Vxao/tiswb6wST14x9kYkwViOrZe7fzg7VF9tCi8U88TqxGsPDDOVjO+WNxG8I9ldP1zvPsxYzVSyGPhaB5E+q6llwlXcQ56wvpf7Ke7gX4Ly2Dlxa8Bmy7aUSCBoWAt/xFRdzCOsE9qI8oxzuvk4RF0H/7bY+4DkGTsP1rIYgA7Q0JueIFb47Yu6pK26BCKo1yPAR8qvpe8vGBICm4slMbKaJN4RqBHtR0zc12E5DXud91o3mArqTksv1NEbI1F4XgDREl76WBw8a7MafDSuun09JuWpGxzPHvLVOUVny6tTJPRutsZLkmnTeMTiXnsPexUiY7UTYlzOMeeoUSTDuJXJz6CM+gSc52CiKoHK/gE","device_id":"TNLOYXJFXM","sender_key":"e9W0gpUcSEKOQ8P/xIdroHUpP7yG4EjQfueiAngESRk","session_id":"hhZ8TBs9Xp0dmuvC6XpDBYsAKnTqb8WiBhZMzHcbBXI"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","event_id":"\$KKIZX8cuB3S3uzS7CDtRlTkcaJRW73e2HW2NuW6OTEg","origin_server_ts":1709565580991,"unsigned":{"age":999,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.member","content":{"membership":"join","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"sender":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","state_key":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","event_id":"\$UNSLEyhC_93oQlt0tWoai4CCd3LH2GJJexM0WN2wxCA","origin_server_ts":1709565581813,"unsigned":{"replaces_state":"\$4sZ3CF67SUh0n5WG0ZKS47Epj9B_d842RJjnrQmUKQo","prev_content":{"is_direct":true,"membership":"invite","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"prev_sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","age":177,"com.famedly.famedlysdk.message_sending_status":2}},{"type":"m.room.member","content":{"membership":"join","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"sender":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","state_key":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","event_id":"\$UNSLEyhC_93oQlt0tWoai4CCd3LH2GJJexM0WN2wxCA","origin_server_ts":1709565581813,"unsigned":{"replaces_state":"\$4sZ3CF67SUh0n5WG0ZKS47Epj9B_d842RJjnrQmUKQo","prev_content":{"is_direct":true,"membership":"invite","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"prev_sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","age":177,"com.famedly.famedlysdk.message_sending_status":2}}],"limited":true,"prev_batch":"s198503_227245_8_1404_23588_11_51066_267416_0_2639"},"ephemeral":{"events":[]},"account_data":{"events":[]},"unread_notifications":{"highlight_count":0,"notification_count":0}}}},"presence":{"events":[{"type":"m.presence","content":{"presence":"online","last_active_ago":843,"currently_active":true},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"}]},"device_lists":{"changed":["@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"],"left":[]},"device_one_time_keys_count":{"signed_curve25519":65},"device_unused_fallback_key_types":["signed_curve25519"],"org.matrix.msc2732.device_unused_fallback_key_types":["signed_curve25519"]}'))); + //await client.handleSync(SyncUpdate.fromJson(jsonDecode(''))); + final room = client + .getRoomById('!bWEUQDujMKwjxkCXYr:tim-alpha.staging.famedly.de')!; + await room.postLoad(); + final participants = await room.requestParticipants(); + + expect( + participants.where((u) => u.membership == Membership.join).length, 2); + + await client.abortSync(); + client.rooms.clear(); + await client.database?.clearCache(); + await client.dispose(closeDatabase: true); + }); test('ownProfile', () async { final client = await getClient(); await client.abortSync(); diff --git a/test/room_test.dart b/test/room_test.dart index a1a2efe8..2cf313e7 100644 --- a/test/room_test.dart +++ b/test/room_test.dart @@ -70,6 +70,7 @@ void main() { ), }, ); + room.setState(Event( room: room, eventId: '143273582443PhrSn:example.org', @@ -80,6 +81,46 @@ void main() { content: {'join_rule': 'public'}, stateKey: '', )); + room.setState(Event( + room: room, + eventId: '143273582443PhrSnY:example.org', + originServerTs: DateTime.fromMillisecondsSinceEpoch(1432735824653), + senderId: matrix.userID!, + type: 'm.room.member', + unsigned: {'age': 1234}, + content: {'membership': 'join', 'displayname': 'YOU'}, + stateKey: matrix.userID!, + )); + room.setState(Event( + room: room, + eventId: '143273582443PhrSnA:example.org', + originServerTs: DateTime.fromMillisecondsSinceEpoch(1432735824653), + senderId: '@alice:matrix.org', + type: 'm.room.member', + unsigned: {'age': 1234}, + content: {'membership': 'join', 'displayname': 'Alice Margatroid'}, + stateKey: '@alice:matrix.org', + )); + room.setState(Event( + room: room, + eventId: '143273582443PhrSnB:example.org', + originServerTs: DateTime.fromMillisecondsSinceEpoch(1432735824653), + senderId: '@bob:example.com', + type: 'm.room.member', + unsigned: {'age': 1234}, + content: {'membership': 'invite', 'displayname': 'Bob'}, + stateKey: '@bob:example.com', + )); + room.setState(Event( + room: room, + eventId: '143273582443PhrSnC:example.org', + originServerTs: DateTime.fromMillisecondsSinceEpoch(1432735824653), + senderId: '@charley:example.org', + type: 'm.room.member', + unsigned: {'age': 1234}, + content: {'membership': 'invite', 'displayname': 'Charley'}, + stateKey: '@charley:example.org', + )); final heroUsers = await room.loadHeroUsers(); expect(heroUsers.length, 3); @@ -89,9 +130,10 @@ void main() { expect(room.notificationCount, notificationCount); expect(room.highlightCount, highlightCount); expect(room.summary.mJoinedMemberCount, notificationCount); - expect(room.summary.mInvitedMemberCount, notificationCount); + expect(room.summary.mInvitedMemberCount, 2); expect(room.summary.mHeroes, heroes); - expect(room.getLocalizedDisplayname(), 'Group with Alice, Bob, Charley'); + expect(room.getLocalizedDisplayname(), + 'Group with Alice Margatroid, Bob, Charley'); expect( room.getState('m.room.join_rules')?.content['join_rule'], 'public'); expect(room.roomAccountData['com.test.foo']?.content['foo'], 'bar'); @@ -434,12 +476,12 @@ void main() { test('requestParticipants', () async { final participants = await room.requestParticipants(); - expect(participants.length, 1); - final user = participants[0]; - expect(user.id, '@alice:example.org'); + expect(participants.length, 4); + final user = participants.singleWhere((u) => u.id == '@alice:matrix.org'); + expect(user.id, '@alice:matrix.org'); expect(user.displayName, 'Alice Margatroid'); expect(user.membership, Membership.join); - expect(user.avatarUrl.toString(), 'mxc://example.org/SEsfnsuifSDFSSEF'); + //expect(user.avatarUrl.toString(), 'mxc://example.org/SEsfnsuifSDFSSEF'); expect(user.room.id, '!localpart:server.abc'); }); @@ -1405,10 +1447,13 @@ void main() { test('getMention', () async { expect(room.getMention('@invalid'), null); - expect(room.getMention('@[Alice Margatroid]'), '@alice:example.org'); - expect(room.getMention('@[Alice Margatroid]#1754'), '@alice:example.org'); + expect(room.getMention('@[Alice Margatroid]'), '@alice:matrix.org'); + expect(room.getMention('@[Alice Margatroid]#1667'), '@alice:matrix.org'); }); test('inviteLink', () async { + // ensure we don't rerequest members + room.summary.mJoinedMemberCount = 4; + var matrixToLink = await room.matrixToInviteLink(); expect(matrixToLink.toString(), 'https://matrix.to/#/%23testalias%3Aexample.com'); @@ -1424,7 +1469,7 @@ void main() { ); matrixToLink = await room.matrixToInviteLink(); expect(matrixToLink.toString(), - 'https://matrix.to/#/!localpart%3Aserver.abc?via=example.org&via=example.com&via=test.abc'); + 'https://matrix.to/#/!localpart%3Aserver.abc?via=example.com&via=test.abc&via=example.org'); }); test('callMemberStateIsExpired', () {