diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index adb8b1175..b231eeabe 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -143,6 +143,7 @@ class AppCoordinator: AppCoordinatorProtocol { self.restoreUserSession() case (.restoringSession, .failedRestoringSession, .signedOut): self.showLoginErrorToast() + self.presentSplashScreen(isSoftLogout: false) case (.restoringSession, .succeededRestoringSession, .signedIn): self.hideLoadingIndicator() self.setupUserSession() @@ -390,6 +391,8 @@ class AppCoordinator: AppCoordinatorProtocol { @objc private func applicationWillResignActive() { + MXLog.info("Application will resign active") + guard backgroundTask == nil else { return } @@ -404,6 +407,8 @@ class AppCoordinator: AppCoordinatorProtocol { @objc private func applicationDidBecomeActive() { + MXLog.info("Application did become active") + backgroundTask?.stop() backgroundTask = nil @@ -416,6 +421,8 @@ class AppCoordinator: AppCoordinatorProtocol { private func observeNetworkState() { let reachabilityNotificationIdentifier = "io.element.elementx.reachability.notification" ServiceLocator.shared.networkMonitor.reachabilityPublisher.sink { reachable in + MXLog.info("Reachability changed to \(reachable)") + if reachable { ServiceLocator.shared.userNotificationController.retractNotificationWithId(reachabilityNotificationIdentifier) } else { diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index 992024e4f..e6d985abe 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -42,9 +42,18 @@ enum HomeScreenViewAction { case updatedVisibleItemRange(Range) } -enum HomeScreenRoomListMode { +enum HomeScreenRoomListMode: CustomStringConvertible { case skeletons case rooms + + var description: String { + switch self { + case .rooms: + return "Showing rooms" + case .skeletons: + return "Showing placeholders" + } + } } struct HomeScreenViewState: BindableState { diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 5bf961215..70930815f 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -35,7 +35,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol // MARK: - Setup - // swiftlint:disable:next function_body_length + // swiftlint:disable:next function_body_length cyclomatic_complexity init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) { self.userSession = userSession self.attributedStringBuilder = attributedStringBuilder @@ -60,7 +60,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol .store(in: &cancellables) visibleItemRangePublisher - .debounce(for: 0.1, scheduler: RunLoop.main) + .debounce(for: 0.1, scheduler: DispatchQueue.main) .removeDuplicates() .sink { range in self.updateVisibleRange(range) @@ -92,16 +92,27 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol visibleRoomsSummaryProvider.roomListPublisher) .receive(on: DispatchQueue.main) .sink { [weak self] state, totalCount, rooms in + guard let self = self else { return } + let isLoadingData = state != .live && (totalCount == 0 || rooms.count != totalCount) let hasNoRooms = state == .live && totalCount == 0 + var newState = self.state.roomListMode if isLoadingData { - self?.state.roomListMode = .skeletons + newState = .skeletons } else if hasNoRooms { - self?.state.roomListMode = .skeletons + newState = .skeletons } 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) @@ -197,11 +208,18 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol 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() { switch summary { case .empty, .invalidated: 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 } @@ -209,19 +227,17 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol case .empty: rooms.append(HomeScreenRoom.placeholder()) case .filled(let details), .invalidated(let details): - let room = buildRoom(with: details, invalidated: true) - rooms.append(room) + rooms.append(buildRoom(with: details)) } case .filled(let details): - let room = buildRoom(with: details, invalidated: false) - rooms.append(room) + rooms.append(buildRoom(with: details)) } } 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)) } var timestamp: String? @@ -229,8 +245,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol timestamp = lastMessageTimestamp.formatted(date: .omitted, time: .shortened) } - let identifier = invalidated ? "invalidated-" + details.id : details.id - return HomeScreenRoom(id: identifier, + return HomeScreenRoom(id: details.id, roomId: details.id, name: details.name, hasUnreads: details.unreadNotificationCount > 0, @@ -241,7 +256,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol } private func updateVisibleRange(_ range: Range) { - guard !range.isEmpty else { return } + guard visibleRoomsSummaryProvider?.statePublisher.value == .live, + !range.isEmpty else { return } guard let visibleRoomsSummaryProvider else { MXLog.error("Visible rooms summary provider unavailable") diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index acc3bc4b1..6c2ce4807 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -31,16 +31,23 @@ private class WeakClientProxyWrapper: ClientDelegate, SlidingSyncObserver { func didReceiveSyncUpdate() { } func didReceiveAuthError(isSoftLogout: Bool) { + MXLog.error("Received authentication error, softlogout=\(isSoftLogout)") clientProxy?.didReceiveAuthError(isSoftLogout: isSoftLogout) } func didUpdateRestoreToken() { + MXLog.info("Did update restoration token") clientProxy?.didUpdateRestoreToken() } // MARK: - SlidingSyncDelegate func didReceiveSyncUpdate(summary: UpdateSummary) { + if summary.views.isEmpty, summary.rooms.isEmpty { + return + } + + MXLog.info("Received sliding sync update") clientProxy?.didReceiveSlidingSyncUpdate(summary: summary) } } @@ -122,6 +129,7 @@ class ClientProxy: ClientProxyProtocol { } func startSync() { + MXLog.info("Starting sync") guard !client.isSoftLogout(), slidingSyncObserverToken == nil else { return } @@ -130,6 +138,7 @@ class ClientProxy: ClientProxyProtocol { } func stopSync() { + MXLog.info("Stopping sync") slidingSyncObserverToken?.cancel() slidingSyncObserverToken = nil } @@ -238,6 +247,7 @@ class ClientProxy: ClientProxyProtocol { // MARK: Private private func restartSync() { + MXLog.info("Restarting sync") stopSync() startSync() } @@ -250,7 +260,19 @@ class ClientProxy: ClientProxyProtocol { do { 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 + .addView(v: visibleRoomsView) .withCommonExtensions() .coldCache(name: "ElementX") .build() @@ -259,101 +281,85 @@ class ClientProxy: ClientProxyProtocol { self.slidingSync = slidingSync - configureSlidingSyncViews(slidingSync: slidingSync) - - guard let visibleRoomsSlidingSyncView else { - MXLog.error("Visible rooms sliding sync view unavailable") - return - } - - registerSlidingSyncView(visibleRoomsSlidingSyncView) - + buildAndConfigureVisibleRoomsSlidingSyncView(slidingSync: slidingSync, visibleRoomsView: visibleRoomsView) + buildAndConfigureAllRoomsSlidingSyncView(slidingSync: slidingSync) } catch { MXLog.error("Failed building sliding sync with error: \(error)") } } - // swiftlint:disable:next function_body_length - private func configureSlidingSyncViews(slidingSync: SlidingSyncProtocol) { - guard visibleRoomsSlidingSyncView == nil, - allRoomsSlidingSyncView == nil else { + private func buildAndConfigureVisibleRoomsSlidingSyncView(slidingSync: SlidingSyncProtocol, visibleRoomsView: SlidingSyncView) { + let visibleRoomsViewProxy = SlidingSyncViewProxy(slidingSync: slidingSync, slidingSyncView: visibleRoomsView) + + 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") } - 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 { - 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() .noTimelineLimit() - .requiredState(requiredState: requiredState) - .filters(filters: filters) + .requiredState(requiredState: slidingSyncRequiredState) + .filters(filters: slidingSyncFilters) .name(name: "AllRooms") .syncMode(mode: .growingFullSync) .batchSize(batchSize: 100) .roomLimit(limit: 500) .build() - let visibleRoomsViewProxy = SlidingSyncViewProxy(slidingSync: slidingSync, slidingSyncView: visibleRoomsView) - let allRoomsViewProxy = SlidingSyncViewProxy(slidingSync: slidingSync, slidingSyncView: allRoomsView) - visibleRoomsSummaryProvider = RoomSummaryProvider(slidingSyncViewProxy: visibleRoomsViewProxy) - 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 } 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() { guard let allRoomsSlidingSyncView else { MXLog.error("All rooms sliding sync view unavailable") return } - registerSlidingSyncView(allRoomsSlidingSyncView) - } - - private func registerSlidingSyncView(_ view: SlidingSyncView) { - _ = slidingSync?.addView(view: view) + _ = slidingSync?.addView(view: allRoomsSlidingSyncView) restartSync() } - + private func roomTupleForIdentifier(_ identifier: String) -> (SlidingSyncRoom?, Room?) { do { let slidingSyncRoom = try slidingSync?.getRoom(roomId: identifier)