Sliding sync tweaks (#470)

* Show the splash screen again after failing to restore a session

* Fix visible rooms cold cache not working. Construct the visibleRoomsView before calling the sliding sync builder, process invalid entries directly if the allRoomsView is not available

* Stop building custom identifiers for invalidated rooms, we still have duplicates coming from filled ones.

* Fix visibleItemRangePublisher debouncer, guard against changing the range while the view isn't "live" yet

* Add more logs
This commit is contained in:
Stefan Ceriu
2023-01-18 17:13:44 +02:00
committed by GitHub
parent 12c9e27056
commit 6615116226
4 changed files with 113 additions and 75 deletions
@@ -143,6 +143,7 @@ class AppCoordinator: AppCoordinatorProtocol {
self.restoreUserSession() self.restoreUserSession()
case (.restoringSession, .failedRestoringSession, .signedOut): case (.restoringSession, .failedRestoringSession, .signedOut):
self.showLoginErrorToast() self.showLoginErrorToast()
self.presentSplashScreen(isSoftLogout: false)
case (.restoringSession, .succeededRestoringSession, .signedIn): case (.restoringSession, .succeededRestoringSession, .signedIn):
self.hideLoadingIndicator() self.hideLoadingIndicator()
self.setupUserSession() self.setupUserSession()
@@ -390,6 +391,8 @@ class AppCoordinator: AppCoordinatorProtocol {
@objc @objc
private func applicationWillResignActive() { private func applicationWillResignActive() {
MXLog.info("Application will resign active")
guard backgroundTask == nil else { guard backgroundTask == nil else {
return return
} }
@@ -404,6 +407,8 @@ class AppCoordinator: AppCoordinatorProtocol {
@objc @objc
private func applicationDidBecomeActive() { private func applicationDidBecomeActive() {
MXLog.info("Application did become active")
backgroundTask?.stop() backgroundTask?.stop()
backgroundTask = nil backgroundTask = nil
@@ -416,6 +421,8 @@ class AppCoordinator: AppCoordinatorProtocol {
private func observeNetworkState() { private func observeNetworkState() {
let reachabilityNotificationIdentifier = "io.element.elementx.reachability.notification" let reachabilityNotificationIdentifier = "io.element.elementx.reachability.notification"
ServiceLocator.shared.networkMonitor.reachabilityPublisher.sink { reachable in ServiceLocator.shared.networkMonitor.reachabilityPublisher.sink { reachable in
MXLog.info("Reachability changed to \(reachable)")
if reachable { if reachable {
ServiceLocator.shared.userNotificationController.retractNotificationWithId(reachabilityNotificationIdentifier) ServiceLocator.shared.userNotificationController.retractNotificationWithId(reachabilityNotificationIdentifier)
} else { } else {
@@ -42,9 +42,18 @@ enum HomeScreenViewAction {
case updatedVisibleItemRange(Range<Int>) case updatedVisibleItemRange(Range<Int>)
} }
enum HomeScreenRoomListMode { enum HomeScreenRoomListMode: CustomStringConvertible {
case skeletons case skeletons
case rooms case rooms
var description: String {
switch self {
case .rooms:
return "Showing rooms"
case .skeletons:
return "Showing placeholders"
}
}
} }
struct HomeScreenViewState: BindableState { struct HomeScreenViewState: BindableState {
@@ -35,7 +35,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
// MARK: - Setup // MARK: - Setup
// swiftlint:disable:next function_body_length // swiftlint:disable:next function_body_length cyclomatic_complexity
init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) { init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) {
self.userSession = userSession self.userSession = userSession
self.attributedStringBuilder = attributedStringBuilder self.attributedStringBuilder = attributedStringBuilder
@@ -60,7 +60,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
.store(in: &cancellables) .store(in: &cancellables)
visibleItemRangePublisher visibleItemRangePublisher
.debounce(for: 0.1, scheduler: RunLoop.main) .debounce(for: 0.1, scheduler: DispatchQueue.main)
.removeDuplicates() .removeDuplicates()
.sink { range in .sink { range in
self.updateVisibleRange(range) self.updateVisibleRange(range)
@@ -92,16 +92,27 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
visibleRoomsSummaryProvider.roomListPublisher) visibleRoomsSummaryProvider.roomListPublisher)
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] state, totalCount, rooms in .sink { [weak self] state, totalCount, rooms in
guard let self = self else { return }
let isLoadingData = state != .live && (totalCount == 0 || rooms.count != totalCount) let isLoadingData = state != .live && (totalCount == 0 || rooms.count != totalCount)
let hasNoRooms = state == .live && totalCount == 0 let hasNoRooms = state == .live && totalCount == 0
var newState = self.state.roomListMode
if isLoadingData { if isLoadingData {
self?.state.roomListMode = .skeletons newState = .skeletons
} else if hasNoRooms { } else if hasNoRooms {
self?.state.roomListMode = .skeletons newState = .skeletons
} else { } else {
self?.state.roomListMode = .rooms newState = .rooms
} }
guard newState != self.state.roomListMode else {
return
}
self.state.roomListMode = newState
MXLog.info("Received visibleRoomsSummaryProvider update, setting view room list mode to \"\(self.state.roomListMode)\"")
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -197,11 +208,18 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
var rooms = [HomeScreenRoom]() var rooms = [HomeScreenRoom]()
// Try merging together results from both the visibleRoomsSummaryProvider and the allRoomsSummaryProvider
// Empty or invalidated items in the visibleRoomsSummaryProvider might have more details in the allRoomsSummaryProvider
// If items are unavailable in the allRoomsSummaryProvider (hasn't be added to SS yet / cold cache) then use what's available
for (index, summary) in visibleRoomsSummaryProvider.roomListPublisher.value.enumerated() { for (index, summary) in visibleRoomsSummaryProvider.roomListPublisher.value.enumerated() {
switch summary { switch summary {
case .empty, .invalidated: case .empty, .invalidated:
guard let allRoomsRoomSummary = allRoomsSummaryProvider?.roomListPublisher.value[safe: index] else { guard let allRoomsRoomSummary = allRoomsSummaryProvider?.roomListPublisher.value[safe: index] else {
rooms.append(HomeScreenRoom.placeholder()) if case let .invalidated(details) = summary {
rooms.append(buildRoom(with: details))
} else {
rooms.append(HomeScreenRoom.placeholder())
}
continue continue
} }
@@ -209,19 +227,17 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
case .empty: case .empty:
rooms.append(HomeScreenRoom.placeholder()) rooms.append(HomeScreenRoom.placeholder())
case .filled(let details), .invalidated(let details): case .filled(let details), .invalidated(let details):
let room = buildRoom(with: details, invalidated: true) rooms.append(buildRoom(with: details))
rooms.append(room)
} }
case .filled(let details): case .filled(let details):
let room = buildRoom(with: details, invalidated: false) rooms.append(buildRoom(with: details))
rooms.append(room)
} }
} }
state.rooms = rooms state.rooms = rooms
} }
private func buildRoom(with details: RoomSummaryDetails, invalidated: Bool) -> HomeScreenRoom { private func buildRoom(with details: RoomSummaryDetails) -> HomeScreenRoom {
let avatarImage = details.avatarURL.flatMap { userSession.mediaProvider.imageFromURL($0, avatarSize: .room(on: .home)) } let avatarImage = details.avatarURL.flatMap { userSession.mediaProvider.imageFromURL($0, avatarSize: .room(on: .home)) }
var timestamp: String? var timestamp: String?
@@ -229,8 +245,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
timestamp = lastMessageTimestamp.formatted(date: .omitted, time: .shortened) timestamp = lastMessageTimestamp.formatted(date: .omitted, time: .shortened)
} }
let identifier = invalidated ? "invalidated-" + details.id : details.id return HomeScreenRoom(id: details.id,
return HomeScreenRoom(id: identifier,
roomId: details.id, roomId: details.id,
name: details.name, name: details.name,
hasUnreads: details.unreadNotificationCount > 0, hasUnreads: details.unreadNotificationCount > 0,
@@ -241,7 +256,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
} }
private func updateVisibleRange(_ range: Range<Int>) { private func updateVisibleRange(_ range: Range<Int>) {
guard !range.isEmpty else { return } guard visibleRoomsSummaryProvider?.statePublisher.value == .live,
!range.isEmpty else { return }
guard let visibleRoomsSummaryProvider else { guard let visibleRoomsSummaryProvider else {
MXLog.error("Visible rooms summary provider unavailable") MXLog.error("Visible rooms summary provider unavailable")
@@ -31,16 +31,23 @@ private class WeakClientProxyWrapper: ClientDelegate, SlidingSyncObserver {
func didReceiveSyncUpdate() { } func didReceiveSyncUpdate() { }
func didReceiveAuthError(isSoftLogout: Bool) { func didReceiveAuthError(isSoftLogout: Bool) {
MXLog.error("Received authentication error, softlogout=\(isSoftLogout)")
clientProxy?.didReceiveAuthError(isSoftLogout: isSoftLogout) clientProxy?.didReceiveAuthError(isSoftLogout: isSoftLogout)
} }
func didUpdateRestoreToken() { func didUpdateRestoreToken() {
MXLog.info("Did update restoration token")
clientProxy?.didUpdateRestoreToken() clientProxy?.didUpdateRestoreToken()
} }
// MARK: - SlidingSyncDelegate // MARK: - SlidingSyncDelegate
func didReceiveSyncUpdate(summary: UpdateSummary) { func didReceiveSyncUpdate(summary: UpdateSummary) {
if summary.views.isEmpty, summary.rooms.isEmpty {
return
}
MXLog.info("Received sliding sync update")
clientProxy?.didReceiveSlidingSyncUpdate(summary: summary) clientProxy?.didReceiveSlidingSyncUpdate(summary: summary)
} }
} }
@@ -122,6 +129,7 @@ class ClientProxy: ClientProxyProtocol {
} }
func startSync() { func startSync() {
MXLog.info("Starting sync")
guard !client.isSoftLogout(), slidingSyncObserverToken == nil else { guard !client.isSoftLogout(), slidingSyncObserverToken == nil else {
return return
} }
@@ -130,6 +138,7 @@ class ClientProxy: ClientProxyProtocol {
} }
func stopSync() { func stopSync() {
MXLog.info("Stopping sync")
slidingSyncObserverToken?.cancel() slidingSyncObserverToken?.cancel()
slidingSyncObserverToken = nil slidingSyncObserverToken = nil
} }
@@ -238,6 +247,7 @@ class ClientProxy: ClientProxyProtocol {
// MARK: Private // MARK: Private
private func restartSync() { private func restartSync() {
MXLog.info("Restarting sync")
stopSync() stopSync()
startSync() startSync()
} }
@@ -250,7 +260,19 @@ class ClientProxy: ClientProxyProtocol {
do { do {
let slidingSyncBuilder = try client.slidingSync().homeserver(url: ServiceLocator.shared.settings.slidingSyncProxyBaseURLString) let slidingSyncBuilder = try client.slidingSync().homeserver(url: ServiceLocator.shared.settings.slidingSyncProxyBaseURLString)
// Build the visibleRoomsSlidingSyncView here so that it can take advantage of the SS builder cold cache
// We will still register the allRoomsSlidingSyncView later, and than will have no cache
let visibleRoomsView = try SlidingSyncViewBuilder()
.timelineLimit(limit: 20)
.requiredState(requiredState: slidingSyncRequiredState)
.filters(filters: slidingSyncFilters)
.name(name: "CurrentlyVisibleRooms")
.syncMode(mode: .selective)
.addRange(from: 0, to: 20)
.build()
let slidingSync = try slidingSyncBuilder let slidingSync = try slidingSyncBuilder
.addView(v: visibleRoomsView)
.withCommonExtensions() .withCommonExtensions()
.coldCache(name: "ElementX") .coldCache(name: "ElementX")
.build() .build()
@@ -259,101 +281,85 @@ class ClientProxy: ClientProxyProtocol {
self.slidingSync = slidingSync self.slidingSync = slidingSync
configureSlidingSyncViews(slidingSync: slidingSync) buildAndConfigureVisibleRoomsSlidingSyncView(slidingSync: slidingSync, visibleRoomsView: visibleRoomsView)
buildAndConfigureAllRoomsSlidingSyncView(slidingSync: slidingSync)
guard let visibleRoomsSlidingSyncView else {
MXLog.error("Visible rooms sliding sync view unavailable")
return
}
registerSlidingSyncView(visibleRoomsSlidingSyncView)
} catch { } catch {
MXLog.error("Failed building sliding sync with error: \(error)") MXLog.error("Failed building sliding sync with error: \(error)")
} }
} }
// swiftlint:disable:next function_body_length private func buildAndConfigureVisibleRoomsSlidingSyncView(slidingSync: SlidingSyncProtocol, visibleRoomsView: SlidingSyncView) {
private func configureSlidingSyncViews(slidingSync: SlidingSyncProtocol) { let visibleRoomsViewProxy = SlidingSyncViewProxy(slidingSync: slidingSync, slidingSyncView: visibleRoomsView)
guard visibleRoomsSlidingSyncView == nil,
allRoomsSlidingSyncView == nil else { visibleRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: visibleRoomsViewProxy)
visibleRoomsSlidingSyncView = visibleRoomsView
// Changes to the visibleRoomsSlidingSyncView range need to restart the connection to be applied
visibleRoomsViewProxy.visibleRangeUpdatePublisher.sink { [weak self] in
self?.restartSync()
}
.store(in: &cancellables)
// The allRoomsSlidingSyncView will be registered as soon as the visibleRoomsSlidingSyncView receives its first update
visibleRoomsViewProxyStateObservationToken = visibleRoomsViewProxy.diffPublisher.sink { [weak self] _ in
MXLog.info("Visible rooms view received first update, registering all rooms view")
self?.registerAllRoomSlidingSyncView()
self?.visibleRoomsViewProxyStateObservationToken = nil
}
}
private func buildAndConfigureAllRoomsSlidingSyncView(slidingSync: SlidingSyncProtocol) {
guard allRoomsSlidingSyncView == nil else {
fatalError("This shouldn't be called more than once") fatalError("This shouldn't be called more than once")
} }
let requiredState = [RequiredState(key: "m.room.avatar", value: ""),
RequiredState(key: "m.room.encryption", value: "")]
let filters = SlidingSyncRequestListFilters(isDm: nil,
spaces: [],
isEncrypted: nil,
isInvite: false,
isTombstoned: false,
roomTypes: [],
notRoomTypes: ["m.space"],
roomNameLike: nil,
tags: [],
notTags: [])
do { do {
let visibleRoomsView = try SlidingSyncViewBuilder()
.timelineLimit(limit: 20)
.requiredState(requiredState: requiredState)
.filters(filters: filters)
.name(name: "CurrentlyVisibleRooms")
.syncMode(mode: .selective)
.addRange(from: 0, to: 20)
.build()
let allRoomsView = try SlidingSyncViewBuilder() let allRoomsView = try SlidingSyncViewBuilder()
.noTimelineLimit() .noTimelineLimit()
.requiredState(requiredState: requiredState) .requiredState(requiredState: slidingSyncRequiredState)
.filters(filters: filters) .filters(filters: slidingSyncFilters)
.name(name: "AllRooms") .name(name: "AllRooms")
.syncMode(mode: .growingFullSync) .syncMode(mode: .growingFullSync)
.batchSize(batchSize: 100) .batchSize(batchSize: 100)
.roomLimit(limit: 500) .roomLimit(limit: 500)
.build() .build()
let visibleRoomsViewProxy = SlidingSyncViewProxy(slidingSync: slidingSync, slidingSyncView: visibleRoomsView)
let allRoomsViewProxy = SlidingSyncViewProxy(slidingSync: slidingSync, slidingSyncView: allRoomsView) let allRoomsViewProxy = SlidingSyncViewProxy(slidingSync: slidingSync, slidingSyncView: allRoomsView)
visibleRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: visibleRoomsViewProxy)
allRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: allRoomsViewProxy) allRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: allRoomsViewProxy)
visibleRoomsViewProxy.visibleRangeUpdatePublisher.sink { [weak self] in
self?.restartSync()
}
.store(in: &cancellables)
visibleRoomsViewProxyStateObservationToken = visibleRoomsViewProxy.diffPublisher.sink { [weak self] _ in
self?.registerAllRoomSlidingSyncView()
self?.visibleRoomsViewProxyStateObservationToken = nil
}
visibleRoomsSlidingSyncView = visibleRoomsView
allRoomsSlidingSyncView = allRoomsView allRoomsSlidingSyncView = allRoomsView
} catch { } catch {
MXLog.error("Failed building sliding sync views with error: \(error)") MXLog.error("Failed building the all rooms sliding sync view with error: \(error)")
} }
} }
private lazy var slidingSyncRequiredState = [RequiredState(key: "m.room.avatar", value: ""),
RequiredState(key: "m.room.encryption", value: "")]
private lazy var slidingSyncFilters = SlidingSyncRequestListFilters(isDm: nil,
spaces: [],
isEncrypted: nil,
isInvite: false,
isTombstoned: false,
roomTypes: [],
notRoomTypes: ["m.space"],
roomNameLike: nil,
tags: [],
notTags: [])
private func registerAllRoomSlidingSyncView() { private func registerAllRoomSlidingSyncView() {
guard let allRoomsSlidingSyncView else { guard let allRoomsSlidingSyncView else {
MXLog.error("All rooms sliding sync view unavailable") MXLog.error("All rooms sliding sync view unavailable")
return return
} }
registerSlidingSyncView(allRoomsSlidingSyncView) _ = slidingSync?.addView(view: allRoomsSlidingSyncView)
}
private func registerSlidingSyncView(_ view: SlidingSyncView) {
_ = slidingSync?.addView(view: view)
restartSync() restartSync()
} }
private func roomTupleForIdentifier(_ identifier: String) -> (SlidingSyncRoom?, Room?) { private func roomTupleForIdentifier(_ identifier: String) -> (SlidingSyncRoom?, Room?) {
do { do {
let slidingSyncRoom = try slidingSync?.getRoom(roomId: identifier) let slidingSyncRoom = try slidingSync?.getRoom(roomId: identifier)