diff --git a/Podfile.lock b/Podfile.lock index 673d4ecc0..979f760d9 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -39,9 +39,9 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatrixSDK (0.27.8): - - MatrixSDK/Core (= 0.27.8) - - MatrixSDK/Core (0.27.8): + - MatrixSDK (0.27.9): + - MatrixSDK/Core (= 0.27.9) + - MatrixSDK/Core (0.27.9): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) @@ -49,7 +49,7 @@ PODS: - OLMKit (~> 3.2.5) - Realm (= 10.27.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.27.8): + - MatrixSDK/JingleCallStack (0.27.9): - JitsiMeetSDKLite (= 8.1.2-lite) - MatrixSDK/Core - MatrixSDKCrypto (0.4.2) @@ -187,7 +187,7 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: 4c5a8572a481340ab233451ad36c1322d371fae5 + MatrixSDK: 246fd1d3620afcbf8cb76794e9343ebf3cbf881b MatrixSDKCrypto: 736069ee0a5ec12852ab3498bf2242acecc443fc OLMKit: da115f16582e47626616874e20f7bb92222c7a51 ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d @@ -209,4 +209,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: b622ffadc1a0fe5442787bd9023ca3d110384814 -COCOAPODS: 1.15.2 +COCOAPODS: 1.14.3 diff --git a/Riot/Categories/MXRoomSummary.swift b/Riot/Categories/MXRoomSummary.swift new file mode 100644 index 000000000..b48f1a70f --- /dev/null +++ b/Riot/Categories/MXRoomSummary.swift @@ -0,0 +1,58 @@ +// +// Copyright 2024 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +extension Notification.Name { + static let roomSummaryDidRemoveExpiredDataFromStore = Notification.Name(MXRoomSummary.roomSummaryDidRemoveExpiredDataFromStore) +} + +extension MXRoomSummary { + @objc static let roomSummaryDidRemoveExpiredDataFromStore = "roomSummaryDidRemoveExpiredDataFromStore" + @objc static let roomRetentionStateEventType = "m.room.retention" + + private enum Constants { + static let roomRetentionInDaysKey = "roomRetentionInDays" + } + /// Get the room messages retention period in days + func roomRetentionPeriodInDays() -> uint { + if let period = self.others[Constants.roomRetentionInDaysKey] as? uint { + return period + } else { + return 365 + } + } + + /// Get the timestamp below which the received messages must be removed from the store, and the display + @objc func minimumTimestamp() -> UInt64 { + let periodInMs = Tools.durationInMs(fromDays: self.roomRetentionPeriodInDays()) + let currentTs = (UInt64)(Date().timeIntervalSince1970 * 1000) + return (currentTs - periodInMs) + } + + /// Remove the expired messages from the store. + /// If some data are removed, this operation posts the notification: roomSummaryDidRemoveExpiredDataFromStore. + /// This operation does not commit the potential change. We let the caller trigger the commit when this is the more suitable. + /// + /// Provide a boolean telling whether some data have been removed. + @objc func removeExpiredRoomContentsFromStore() -> Bool { + let ret = self.mxSession.store.removeAllMessagesSent(before: self.minimumTimestamp(), inRoom: roomId) + if ret { + NotificationCenter.default.post(name: .roomSummaryDidRemoveExpiredDataFromStore, object: self) + } + return ret + } +} diff --git a/Riot/Categories/MXSession.swift b/Riot/Categories/MXSession.swift index 10df98141..4d7256b62 100644 --- a/Riot/Categories/MXSession.swift +++ b/Riot/Categories/MXSession.swift @@ -25,4 +25,16 @@ extension MXSession { matrixItemId: userId, displayName: user?.displayname) } + + /// Clean the storage of a session by removing the expired contents. + @objc func removeExpiredMessages() { + var hasStoreChanged = false + for room in self.rooms { + hasStoreChanged = hasStoreChanged || room.summary.removeExpiredRoomContentsFromStore() + } + + if hasStoreChanged { + self.store.commit?() + } + } } diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 79d263cef..c5030714d 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1826,6 +1826,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [self registerNewRequestNotificationForSession:mxSession]; [self.pushNotificationService checkPushKitPushersInSession:mxSession]; + + // Clean the storage by removing expired data + [mxSession removeExpiredMessages]; } else if (mxSession.state == MXSessionStateRunning) { diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 16e05c423..55ef184f1 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -33,6 +33,9 @@ const CGFloat kTypingCellHeight = 24; { // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. id kThemeServiceDidChangeThemeNotificationObserver; + + // The listener to the room retention changes. + id retentionListener; } // Observe key verification request changes @@ -167,6 +170,31 @@ const CGFloat kTypingCellHeight = 24; self.eventFormatter.eventTypesFilterForMessages = [MXKAppSettings standardAppSettings].eventsFilterForMessages; } +- (void)setDelegate:(id)delegate +{ + [self unregisterRoomSummaryDidRemoveExpiredDataFromStoreNotifications]; + [self removeRoomRetentionEventListener]; + + if (delegate && self.isLive) + { + if (self.room) + { + // Remove the potential expired messages from the store + if ([self.room.summary removeExpiredRoomContentsFromStore]) + { + [self.mxSession.store commit]; + } + [self addRoomRetentionEventListener]; + } + + // Observe room history flush (expired content data) + [self registerRoomSummaryDidRemoveExpiredDataFromStoreNotifications]; + [self roomSummaryDidRemoveExpiredDataFromStore]; + } + + [super setDelegate:delegate]; +} + - (void)destroy { if (kThemeServiceDidChangeThemeNotificationObserver) @@ -197,6 +225,9 @@ const CGFloat kTypingCellHeight = 24; [self.mxSession.aggregations.beaconAggregations removeListener:self.beaconInfoSummaryDeletionListener]; } + [self unregisterRoomSummaryDidRemoveExpiredDataFromStoreNotifications]; + [self removeRoomRetentionEventListener]; + [super destroy]; } @@ -1242,4 +1273,79 @@ const CGFloat kTypingCellHeight = 24; } } +#pragma mark - roomSummaryDidRemoveExpiredDataFromStore notifications + +- (void)registerRoomSummaryDidRemoveExpiredDataFromStoreNotifications +{ + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(roomSummaryDidRemoveExpiredDataFromStore:) name:MXRoomSummary.roomSummaryDidRemoveExpiredDataFromStore object:nil]; +} + +- (void)unregisterRoomSummaryDidRemoveExpiredDataFromStoreNotifications +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:MXRoomSummary.roomSummaryDidRemoveExpiredDataFromStore object:nil]; +} + +- (void)roomSummaryDidRemoveExpiredDataFromStore:(NSNotification*)notification +{ + MXRoomSummary *roomSummary = notification.object; + if (self.mxSession == roomSummary.mxSession && [self.roomId isEqualToString:roomSummary.roomId]) + { + [self roomSummaryDidRemoveExpiredDataFromStore]; + } +} + +- (void)roomSummaryDidRemoveExpiredDataFromStore +{ + // Check whether the first cell data refers to an expired event (this may be a state event + MXEvent *firstMessageEvent; + for (id cellData in bubbles) + { + for (MXEvent *event in cellData.events) + { + if (!event.isState) { + firstMessageEvent = event; + break; + } + } + + if (firstMessageEvent) + { + break; + } + } + + if (firstMessageEvent && firstMessageEvent.originServerTs < self.room.summary.minimumTimestamp) + { + [self reload]; + } +} + +#pragma mark - room retention event listener + +- (void)addRoomRetentionEventListener +{ + // Register a listener to handle the room retention in live timelines + retentionListener = [self.timeline listenToEventsOfTypes:@[MXRoomSummary.roomRetentionStateEventType] onEvent:^(MXEvent *redactionEvent, MXTimelineDirection direction, MXRoomState *roomState) { + + // Consider only live events + if (direction == MXTimelineDirectionForwards) + { + // Remove the potential expired messages from the store + if ([self.room.summary removeExpiredRoomContentsFromStore]) + { + [self.mxSession.store commit]; + } + } + }]; +} + +- (void)removeRoomRetentionEventListener +{ + if (retentionListener) + { + [self.timeline removeListener:retentionListener]; + retentionListener = nil; + } +} + @end diff --git a/Riot/Utils/Tools.h b/Riot/Utils/Tools.h index 57eacbcdb..8d54e06b1 100644 --- a/Riot/Utils/Tools.h +++ b/Riot/Utils/Tools.h @@ -48,4 +48,17 @@ */ + (NSURL*)fixURLWithSeveralHashKeys:(NSURL*)url; +#pragma mark - Time utilities + +/** + * Convert a number of days to a duration in ms. + */ ++ (uint64_t)durationInMsFromDays:(uint)days; + +/** + * Convert a duration in ms to a number of days. + */ ++ (uint)numberOfDaysFromDurationInMs:(uint64_t)duration; + + @end diff --git a/Riot/Utils/Tools.m b/Riot/Utils/Tools.m index 128fe3694..5866273ac 100644 --- a/Riot/Utils/Tools.m +++ b/Riot/Utils/Tools.m @@ -117,4 +117,16 @@ return fixedURL; } +#pragma mark - Time utilities + ++ (uint64_t)durationInMsFromDays:(uint)days +{ + return days * (uint64_t)(86400000); +} + ++ (uint)numberOfDaysFromDurationInMs:(uint64_t)duration +{ + return (uint)(duration / 86400000); +} + @end diff --git a/matrix-ios-sdk b/matrix-ios-sdk index 3052a396b..fcfbb6718 160000 --- a/matrix-ios-sdk +++ b/matrix-ios-sdk @@ -1 +1 @@ -Subproject commit 3052a396b88095cbcfe1b4f488127f69e3280cda +Subproject commit fcfbb6718224e9819105b93fa9e86a9db1cf04a6