mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Implemented room list loading skeletons and placeholder rooms (#206)
This commit is contained in:
@@ -334,7 +334,8 @@ class AppCoordinator: Coordinator {
|
||||
default:
|
||||
break
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func deobserveUserSessionChanges() {
|
||||
|
||||
@@ -38,6 +38,11 @@ enum HomeScreenViewAction {
|
||||
case skipSessionVerification
|
||||
}
|
||||
|
||||
enum HomeScreenRoomListMode {
|
||||
case skeletons
|
||||
case rooms
|
||||
}
|
||||
|
||||
struct HomeScreenViewState: BindableState {
|
||||
var userID: String
|
||||
var userDisplayName: String?
|
||||
@@ -47,11 +52,13 @@ struct HomeScreenViewState: BindableState {
|
||||
|
||||
var rooms: [HomeScreenRoom] = []
|
||||
|
||||
var isLoadingRooms: Bool {
|
||||
rooms.isEmpty
|
||||
}
|
||||
var roomListMode: HomeScreenRoomListMode = .rooms
|
||||
|
||||
var visibleRooms: [HomeScreenRoom] {
|
||||
if roomListMode == .skeletons {
|
||||
return placeholderRooms
|
||||
}
|
||||
|
||||
if bindings.searchQuery.isEmpty {
|
||||
return rooms
|
||||
}
|
||||
@@ -60,6 +67,12 @@ struct HomeScreenViewState: BindableState {
|
||||
}
|
||||
|
||||
var bindings = HomeScreenViewStateBindings()
|
||||
|
||||
var placeholderRooms: [HomeScreenRoom] {
|
||||
(1...10).map { index in
|
||||
HomeScreenRoom.placeholder(id: "\(index)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeScreenViewStateBindings {
|
||||
@@ -78,12 +91,16 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
var lastMessage: AttributedString?
|
||||
|
||||
var avatar: UIImage?
|
||||
}
|
||||
|
||||
extension MutableCollection where Element == HomeScreenRoom {
|
||||
mutating func updateEach(_ update: (inout Element) -> Void) {
|
||||
for index in indices {
|
||||
update(&self[index])
|
||||
}
|
||||
|
||||
var isPlaceholder = false
|
||||
|
||||
static func placeholder(id: String) -> HomeScreenRoom {
|
||||
HomeScreenRoom(id: id,
|
||||
name: "Placeholder room name",
|
||||
hasUnreads: false,
|
||||
timestamp: "Now",
|
||||
lastMessage: AttributedString("Last message"),
|
||||
avatar: UIImage(systemName: "photo"),
|
||||
isPlaceholder: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) {
|
||||
self.userSession = userSession
|
||||
roomSummaryProvider = userSession.clientProxy.roomSummaryProvider
|
||||
@@ -46,18 +47,36 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
default:
|
||||
break
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
roomSummaryProvider.callbacks
|
||||
Publishers.CombineLatest3(roomSummaryProvider.statePublisher,
|
||||
roomSummaryProvider.countPublisher,
|
||||
roomSummaryProvider.roomListPublisher)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
switch callback {
|
||||
case .updatedRoomSummaries:
|
||||
Task {
|
||||
await self?.updateRooms()
|
||||
.sink { [weak self] state, totalCount, rooms in
|
||||
if state != .live {
|
||||
if totalCount == 0 || rooms.count != totalCount {
|
||||
self?.state.roomListMode = .skeletons
|
||||
} else {
|
||||
self?.state.roomListMode = .rooms
|
||||
}
|
||||
} else if totalCount == 0 {
|
||||
#warning("Empty state but it never happens because SS never goes into live for empty accounts")
|
||||
} else {
|
||||
self?.state.roomListMode = .rooms
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
roomSummaryProvider.roomListPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
Task {
|
||||
await self?.updateRooms()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
Task {
|
||||
if case let .success(userAvatarURLString) = await userSession.clientProxy.loadUserAvatarURLString() {
|
||||
@@ -65,15 +84,17 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
state.userAvatar = avatar
|
||||
}
|
||||
}
|
||||
|
||||
await updateRooms()
|
||||
}
|
||||
|
||||
|
||||
Task {
|
||||
if case let .success(userDisplayName) = await userSession.clientProxy.loadUserDisplayName() {
|
||||
state.userDisplayName = userDisplayName
|
||||
}
|
||||
}
|
||||
|
||||
Task {
|
||||
await updateRooms()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -96,23 +117,22 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
// MARK: - Private
|
||||
|
||||
private func loadDataForRoomIdentifier(_ identifier: String) {
|
||||
guard let summary = roomSummaryProvider.roomSummaries.first(where: { $0.id == identifier }),
|
||||
var room = state.rooms.first(where: { $0.id == identifier }) else {
|
||||
guard let summary = roomSummaryProvider.roomListPublisher.value.first(where: { $0.asFilled?.id == identifier })?.asFilled,
|
||||
let homeRoomIndex = state.rooms.firstIndex(where: { $0.id == identifier }) else {
|
||||
return
|
||||
}
|
||||
|
||||
if room.avatar != nil {
|
||||
var details = state.rooms[homeRoomIndex]
|
||||
|
||||
guard details.avatar == nil,
|
||||
let avatarURLString = summary.avatarURLString else {
|
||||
return
|
||||
}
|
||||
|
||||
if let avatarURLString = summary.avatarURLString {
|
||||
Task {
|
||||
if case let .success(image) = await userSession.mediaProvider.loadImageFromURLString(avatarURLString, avatarSize: .room(on: .home)) {
|
||||
if let index = state.rooms.firstIndex(of: room) {
|
||||
room.avatar = image
|
||||
state.rooms[index] = room
|
||||
}
|
||||
}
|
||||
Task {
|
||||
if case let .success(image) = await userSession.mediaProvider.loadImageFromURLString(avatarURLString, avatarSize: .room(on: .home)) {
|
||||
details.avatar = image
|
||||
state.rooms[homeRoomIndex] = details
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,20 +141,25 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
state.rooms = await Task.detached {
|
||||
var rooms = [HomeScreenRoom]()
|
||||
|
||||
for summary in self.roomSummaryProvider.roomSummaries {
|
||||
let avatarImage = await self.userSession.mediaProvider.imageFromURLString(summary.avatarURLString, avatarSize: .room(on: .home))
|
||||
|
||||
var timestamp: String?
|
||||
if let lastMessageTimestamp = summary.lastMessageTimestamp {
|
||||
timestamp = lastMessageTimestamp.formatted(date: .omitted, time: .shortened)
|
||||
for summary in self.roomSummaryProvider.roomListPublisher.value {
|
||||
switch summary {
|
||||
case .empty(let id):
|
||||
rooms.append(HomeScreenRoom.placeholder(id: id))
|
||||
case .filled(let summary):
|
||||
let avatarImage = await self.userSession.mediaProvider.imageFromURLString(summary.avatarURLString, avatarSize: .room(on: .home))
|
||||
|
||||
var timestamp: String?
|
||||
if let lastMessageTimestamp = summary.lastMessageTimestamp {
|
||||
timestamp = lastMessageTimestamp.formatted(date: .omitted, time: .shortened)
|
||||
}
|
||||
|
||||
rooms.append(HomeScreenRoom(id: summary.id,
|
||||
name: summary.name,
|
||||
hasUnreads: summary.unreadNotificationCount > 0,
|
||||
timestamp: timestamp,
|
||||
lastMessage: summary.lastMessage,
|
||||
avatar: avatarImage))
|
||||
}
|
||||
|
||||
rooms.append(HomeScreenRoom(id: summary.id,
|
||||
name: summary.name,
|
||||
hasUnreads: summary.unreadNotificationCount > 0,
|
||||
timestamp: timestamp,
|
||||
lastMessage: summary.lastMessage,
|
||||
avatar: avatarImage))
|
||||
}
|
||||
|
||||
return rooms
|
||||
|
||||
@@ -23,32 +23,28 @@ struct HomeScreen: View {
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0.0) {
|
||||
if context.viewState.isLoadingRooms {
|
||||
VStack {
|
||||
Text(ElementL10n.loading)
|
||||
ProgressView()
|
||||
}
|
||||
} else {
|
||||
ScrollView {
|
||||
if context.viewState.showSessionVerificationBanner {
|
||||
sessionVerificationBanner
|
||||
}
|
||||
|
||||
LazyVStack {
|
||||
ForEach(context.viewState.visibleRooms) { room in
|
||||
HomeScreenRoomCell(room: room, context: context)
|
||||
}
|
||||
}
|
||||
.searchable(text: $context.searchQuery)
|
||||
.animation(.default, value: context.viewState.visibleRooms)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
ScrollView {
|
||||
if context.viewState.showSessionVerificationBanner {
|
||||
sessionVerificationBanner
|
||||
}
|
||||
|
||||
Spacer()
|
||||
LazyVStack {
|
||||
ForEach(context.viewState.visibleRooms) { room in
|
||||
if room.isPlaceholder {
|
||||
HomeScreenRoomCell(room: room, context: context)
|
||||
.redacted(reason: .placeholder)
|
||||
.disabled(true)
|
||||
} else {
|
||||
HomeScreenRoomCell(room: room, context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(.default, value: context.viewState.visibleRooms)
|
||||
.padding(.horizontal)
|
||||
.searchable(text: $context.searchQuery)
|
||||
}
|
||||
.transition(.slide)
|
||||
.disabled(context.viewState.roomListMode == .skeletons)
|
||||
.animation(.elementDefault, value: context.viewState.roomListMode)
|
||||
.animation(.elementDefault, value: context.viewState.showSessionVerificationBanner)
|
||||
.ignoresSafeArea(.all, edges: .bottom)
|
||||
.navigationTitle(ElementL10n.allChats)
|
||||
@@ -169,14 +165,19 @@ struct HomeScreen: View {
|
||||
|
||||
struct HomeScreen_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
body.preferredColorScheme(.light)
|
||||
body(.loading).preferredColorScheme(.light)
|
||||
.tint(.element.accent)
|
||||
body.preferredColorScheme(.dark)
|
||||
body(.loading).preferredColorScheme(.dark)
|
||||
.tint(.element.accent)
|
||||
body(.loaded).preferredColorScheme(.light)
|
||||
.tint(.element.accent)
|
||||
body(.loaded).preferredColorScheme(.dark)
|
||||
.tint(.element.accent)
|
||||
}
|
||||
|
||||
static var body: some View {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "John Doe"),
|
||||
static func body(_ state: MockRoomSummaryProviderState) -> some View {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "John Doe",
|
||||
roomSummaryProvider: MockRoomSummaryProvider(state: state)),
|
||||
mediaProvider: MockMediaProvider())
|
||||
|
||||
let viewModel = HomeScreenViewModel(userSession: userSession,
|
||||
|
||||
@@ -108,14 +108,27 @@ struct HomeScreenRoomCell_Previews: PreviewProvider {
|
||||
}
|
||||
|
||||
static var body: some View {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "John Doe"),
|
||||
let summaryProvider = MockRoomSummaryProvider(state: .loaded)
|
||||
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "John Doe", roomSummaryProvider: summaryProvider),
|
||||
mediaProvider: MockMediaProvider())
|
||||
|
||||
let viewModel = HomeScreenViewModel(userSession: userSession,
|
||||
attributedStringBuilder: AttributedStringBuilder())
|
||||
|
||||
let rooms: [HomeScreenRoom] = summaryProvider.roomListPublisher.value.compactMap { summary in
|
||||
guard let summary = summary.asFilled else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return HomeScreenRoom(id: summary.id,
|
||||
name: summary.name,
|
||||
hasUnreads: summary.unreadNotificationCount > 0,
|
||||
timestamp: Date.now.formatted(date: .omitted, time: .shortened))
|
||||
}
|
||||
|
||||
return VStack {
|
||||
ForEach(viewModel.context.viewState.rooms) { room in
|
||||
ForEach(rooms) { room in
|
||||
HomeScreenRoomCell(room: room, context: viewModel.context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
|
||||
self.state.items[viewIndex] = timelineViewFactory.buildTimelineViewFor(timelineItem: timelineItem)
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
state.contextMenuBuilder = buildContexMenuForItemId(_:)
|
||||
|
||||
|
||||
@@ -63,7 +63,8 @@ class SessionVerificationViewModel: SessionVerificationViewModelType, SessionVer
|
||||
case .failed:
|
||||
self.stateMachine.processEvent(.didFail)
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
@@ -242,13 +242,13 @@ struct Settings_Previews: PreviewProvider {
|
||||
body.preferredColorScheme(.light)
|
||||
body.preferredColorScheme(.dark)
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
static var body: some View {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@userid:example.com"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
let viewModel = SettingsViewModel(withUserSession: userSession)
|
||||
|
||||
|
||||
NavigationView {
|
||||
SettingsScreen(context: viewModel.context)
|
||||
.previewInterfaceOrientation(.portrait)
|
||||
|
||||
@@ -26,7 +26,7 @@ struct MockClientProxy: ClientProxyProtocol {
|
||||
let homeserver = ""
|
||||
let restoreToken: String? = nil
|
||||
|
||||
let roomSummaryProvider: RoomSummaryProviderProtocol = MockRoomSummaryProvider()
|
||||
var roomSummaryProvider: RoomSummaryProviderProtocol = MockRoomSummaryProvider()
|
||||
|
||||
func startSync() { }
|
||||
|
||||
|
||||
@@ -17,33 +17,58 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
enum MockRoomSummaryProviderState {
|
||||
case loading
|
||||
case loaded
|
||||
}
|
||||
|
||||
class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
var callbacks = PassthroughSubject<RoomSummaryProviderCallback, Never>()
|
||||
var stateUpdatePublisher = CurrentValueSubject<RoomSummaryProviderState, Never>(.cold)
|
||||
var countUpdatePublisher = CurrentValueSubject<UInt, Never>(0)
|
||||
|
||||
var roomSummaries: [RoomSummary] = [
|
||||
RoomSummary(id: "1", name: "First room",
|
||||
isDirect: true,
|
||||
avatarURLString: nil,
|
||||
lastMessage: AttributedString("Prosciutto beef ribs pancetta filet mignon kevin hamburger, chuck ham venison picanha. Beef ribs chislic turkey biltong tenderloin."),
|
||||
lastMessageTimestamp: .now,
|
||||
unreadNotificationCount: 4),
|
||||
RoomSummary(id: "2",
|
||||
name: "Second room",
|
||||
isDirect: true,
|
||||
avatarURLString: nil,
|
||||
lastMessage: nil,
|
||||
lastMessageTimestamp: nil,
|
||||
unreadNotificationCount: 1),
|
||||
RoomSummary(id: "3",
|
||||
name: "Third room",
|
||||
isDirect: true,
|
||||
avatarURLString: nil,
|
||||
lastMessage: try? AttributedString(markdown: "**@mock:client.com**: T-bone beef ribs bacon"),
|
||||
lastMessageTimestamp: .now,
|
||||
unreadNotificationCount: 0)
|
||||
]
|
||||
let roomListPublisher: CurrentValueSubject<[RoomSummary], Never>
|
||||
let statePublisher: CurrentValueSubject<RoomSummaryProviderState, Never>
|
||||
let countPublisher: CurrentValueSubject<UInt, Never>
|
||||
|
||||
func updateRoomsWithIdentifiers(_ identifiers: [String]) { }
|
||||
|
||||
convenience init() {
|
||||
self.init(state: .loading)
|
||||
}
|
||||
|
||||
init(state: MockRoomSummaryProviderState) {
|
||||
switch state {
|
||||
case .loading:
|
||||
roomListPublisher = .init([])
|
||||
statePublisher = .init(.cold)
|
||||
countPublisher = .init(0)
|
||||
case .loaded:
|
||||
roomListPublisher = .init(Self.rooms)
|
||||
statePublisher = .init(.live)
|
||||
countPublisher = .init(UInt(Self.rooms.count))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
static let rooms: [RoomSummary] = [
|
||||
.filled(details: RoomSummaryDetails(id: "1", name: "First room",
|
||||
isDirect: true,
|
||||
avatarURLString: nil,
|
||||
lastMessage: AttributedString("Prosciutto beef ribs pancetta filet mignon kevin hamburger, chuck ham venison picanha. Beef ribs chislic turkey biltong tenderloin."),
|
||||
lastMessageTimestamp: .now,
|
||||
unreadNotificationCount: 4)),
|
||||
.filled(details: RoomSummaryDetails(id: "2",
|
||||
name: "Second room",
|
||||
isDirect: true,
|
||||
avatarURLString: "mockImageURLString",
|
||||
lastMessage: nil,
|
||||
lastMessageTimestamp: nil,
|
||||
unreadNotificationCount: 1)),
|
||||
.filled(details: RoomSummaryDetails(id: "3",
|
||||
name: "Third room",
|
||||
isDirect: true,
|
||||
avatarURLString: nil,
|
||||
lastMessage: try? AttributedString(markdown: "**@mock:client.com**: T-bone beef ribs bacon"),
|
||||
lastMessageTimestamp: .now,
|
||||
unreadNotificationCount: 0)),
|
||||
.empty(id: "3")
|
||||
]
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct RoomSummary {
|
||||
struct RoomSummaryDetails {
|
||||
let id: String
|
||||
let name: String
|
||||
let isDirect: Bool
|
||||
@@ -60,22 +60,24 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
|
||||
private var listUpdateObserverToken: StoppableSpawn?
|
||||
private var stateUpdateObserverToken: StoppableSpawn?
|
||||
private var countUpdateObserverToken: StoppableSpawn?
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
let callbacks = PassthroughSubject<RoomSummaryProviderCallback, Never>()
|
||||
let stateUpdatePublisher = CurrentValueSubject<RoomSummaryProviderState, Never>(.cold)
|
||||
let countUpdatePublisher = CurrentValueSubject<UInt, Never>(0)
|
||||
|
||||
private(set) var roomSummaries: [RoomSummary] = [] {
|
||||
let roomListPublisher = CurrentValueSubject<[RoomSummary], Never>([])
|
||||
let statePublisher = CurrentValueSubject<RoomSummaryProviderState, Never>(.cold)
|
||||
let countPublisher = CurrentValueSubject<UInt, Never>(0)
|
||||
|
||||
private var rooms: [RoomSummary] = [] {
|
||||
didSet {
|
||||
callbacks.send(.updatedRoomSummaries)
|
||||
roomListPublisher.send(rooms)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
listUpdateObserverToken?.cancel()
|
||||
stateUpdateObserverToken?.cancel()
|
||||
countUpdateObserverToken?.cancel()
|
||||
}
|
||||
|
||||
init(slidingSyncController: SlidingSyncProtocol, slidingSyncView: SlidingSyncViewProtocol, roomMessageFactory: RoomMessageFactoryProtocol) {
|
||||
@@ -87,39 +89,40 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
|
||||
weakProvider.stateUpdatePublisher
|
||||
.map(RoomSummaryProviderState.init)
|
||||
.subscribe(stateUpdatePublisher)
|
||||
.subscribe(statePublisher)
|
||||
.store(in: &cancellables)
|
||||
|
||||
weakProvider.countUpdatePublisher
|
||||
.subscribe(countUpdatePublisher)
|
||||
.subscribe(countPublisher)
|
||||
.store(in: &cancellables)
|
||||
|
||||
weakProvider.roomListDiffPublisher
|
||||
.collect(.byTime(DispatchQueue.global(qos: .background), 0.5))
|
||||
.sink { self.updateRoomsWithDiffs($0) }
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
listUpdateObserverToken = slidingSyncView.observeRoomList(observer: weakProvider)
|
||||
stateUpdateObserverToken = slidingSyncView.observeState(observer: weakProvider)
|
||||
countUpdateObserverToken = slidingSyncView.observeRoomsCount(observer: weakProvider)
|
||||
}
|
||||
|
||||
func updateRoomsWithIdentifiers(_ identifiers: [String]) {
|
||||
Task.detached {
|
||||
guard self.stateUpdatePublisher.value == .live else {
|
||||
guard self.statePublisher.value == .live else {
|
||||
return
|
||||
}
|
||||
|
||||
var changes = [CollectionDifference<RoomSummary>.Change]()
|
||||
for identifier in identifiers {
|
||||
guard let oldSummary = self.roomSummaries.first(where: { $0.id == identifier }),
|
||||
let index = self.roomSummaries.firstIndex(where: { $0.id == identifier }) else {
|
||||
guard let index = self.rooms.firstIndex(where: { $0.id == identifier }) else {
|
||||
continue
|
||||
}
|
||||
|
||||
let newSummary = self.buildRoomSummaryForIdentifier(identifier)
|
||||
let oldRoom = self.rooms[index]
|
||||
let newRoom = self.buildRoomSummaryForIdentifier(identifier)
|
||||
|
||||
changes.append(.remove(offset: index, element: oldSummary, associatedWith: nil))
|
||||
changes.append(.insert(offset: index, element: newSummary, associatedWith: nil))
|
||||
changes.append(.remove(offset: index, element: oldRoom, associatedWith: nil))
|
||||
changes.append(.insert(offset: index, element: newRoom, associatedWith: nil))
|
||||
}
|
||||
|
||||
guard let diff = CollectionDifference(changes) else {
|
||||
@@ -127,31 +130,25 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
guard let newSummaries = self.roomSummaries.applying(diff) else {
|
||||
guard let newSummaries = self.rooms.applying(diff) else {
|
||||
MXLog.error("Failed applying diff: \(diff)")
|
||||
return
|
||||
}
|
||||
|
||||
self.roomSummaries = newSummaries
|
||||
self.rooms = newSummaries
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
fileprivate func updateRoomsWithDiffs(_ diffs: [SlidingSyncViewRoomsListDiff]) {
|
||||
roomSummaries = diffs
|
||||
rooms = diffs
|
||||
.compactMap(buildDiff)
|
||||
.reduce(roomSummaries) { $0.applying($1) ?? $0 }
|
||||
.reduce(rooms) { $0.applying($1) ?? $0 }
|
||||
}
|
||||
|
||||
private func buildEmptyRoomSummary(forIdentifier identifier: String = UUID().uuidString) -> RoomSummary {
|
||||
RoomSummary(id: identifier,
|
||||
name: "",
|
||||
isDirect: false,
|
||||
avatarURLString: nil,
|
||||
lastMessage: nil,
|
||||
lastMessageTimestamp: nil,
|
||||
unreadNotificationCount: 0)
|
||||
.empty(id: identifier)
|
||||
}
|
||||
|
||||
private func buildRoomSummaryForIdentifier(_ identifier: String) -> RoomSummary {
|
||||
@@ -170,14 +167,14 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
}
|
||||
lastMessageTimestamp = lastMessage.originServerTs
|
||||
}
|
||||
|
||||
return RoomSummary(id: room.roomId(),
|
||||
name: room.name() ?? room.roomId(),
|
||||
isDirect: room.isDm() ?? false,
|
||||
avatarURLString: room.fullRoom()?.avatarUrl(),
|
||||
lastMessage: attributedLastMessage,
|
||||
lastMessageTimestamp: lastMessageTimestamp,
|
||||
unreadNotificationCount: UInt(room.unreadNotifications().notificationCount()))
|
||||
|
||||
return .filled(details: RoomSummaryDetails(id: room.roomId(),
|
||||
name: room.name() ?? room.roomId(),
|
||||
isDirect: room.isDm() ?? false,
|
||||
avatarURLString: room.fullRoom()?.avatarUrl(),
|
||||
lastMessage: attributedLastMessage,
|
||||
lastMessageTimestamp: lastMessageTimestamp,
|
||||
unreadNotificationCount: UInt(room.unreadNotifications().notificationCount())))
|
||||
}
|
||||
|
||||
private func buildSummaryForRoomListEntry(_ entry: RoomListEntry) -> RoomSummary {
|
||||
@@ -202,7 +199,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
switch diff {
|
||||
case .push(value: let value):
|
||||
let summary = buildSummaryForRoomListEntry(value)
|
||||
changes.append(.insert(offset: Int(roomSummaries.count), element: summary, associatedWith: nil))
|
||||
changes.append(.insert(offset: Int(rooms.count), element: summary, associatedWith: nil))
|
||||
case .updateAt(let index, let value):
|
||||
let summary = buildSummaryForRoomListEntry(value)
|
||||
changes.append(.remove(offset: Int(index), element: summary, associatedWith: nil))
|
||||
@@ -211,14 +208,14 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
let summary = buildSummaryForRoomListEntry(value)
|
||||
changes.append(.insert(offset: Int(index), element: summary, associatedWith: nil))
|
||||
case .move(let oldIndex, let newIndex):
|
||||
let summary = roomSummaries[Int(oldIndex)]
|
||||
let summary = rooms[Int(oldIndex)]
|
||||
changes.append(.remove(offset: Int(oldIndex), element: summary, associatedWith: nil))
|
||||
changes.append(.insert(offset: Int(newIndex), element: summary, associatedWith: nil))
|
||||
case .removeAt(let index):
|
||||
let summary = roomSummaries[Int(index)]
|
||||
let summary = rooms[Int(index)]
|
||||
changes.append(.remove(offset: Int(index), element: summary, associatedWith: nil))
|
||||
case .replace(let values):
|
||||
for (index, summary) in roomSummaries.enumerated() {
|
||||
for (index, summary) in rooms.enumerated() {
|
||||
changes.append(.remove(offset: index, element: summary, associatedWith: nil))
|
||||
}
|
||||
|
||||
@@ -253,12 +250,14 @@ extension SlidingSyncViewRoomsListDiff {
|
||||
extension RoomSummaryProviderState {
|
||||
init(slidingSyncState: SlidingSyncState) {
|
||||
switch slidingSyncState {
|
||||
case .cold:
|
||||
self = .cold
|
||||
case .preload:
|
||||
self = .preload
|
||||
case .catchingUp:
|
||||
self = .live
|
||||
self = .catchingUp
|
||||
case .live:
|
||||
self = .live
|
||||
default:
|
||||
self = .cold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,26 +17,44 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
enum RoomSummaryProviderCallback {
|
||||
case updatedRoomSummaries
|
||||
}
|
||||
|
||||
enum RoomSummaryProviderState {
|
||||
case cold
|
||||
case preload
|
||||
case catchingUp
|
||||
case live
|
||||
}
|
||||
|
||||
enum RoomSummary: Identifiable {
|
||||
case empty(id: String)
|
||||
case filled(details: RoomSummaryDetails)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .empty(let id):
|
||||
return id
|
||||
case .filled(let summary):
|
||||
return summary.id
|
||||
}
|
||||
}
|
||||
|
||||
var asFilled: RoomSummaryDetails? {
|
||||
guard case let .filled(details) = self else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return details
|
||||
}
|
||||
}
|
||||
|
||||
protocol RoomSummaryProviderProtocol {
|
||||
var callbacks: PassthroughSubject<RoomSummaryProviderCallback, Never> { get }
|
||||
/// Publishes the currently available room summaries
|
||||
var roomListPublisher: CurrentValueSubject<[RoomSummary], Never> { get }
|
||||
|
||||
/// Publishes the current state the summary provider is finding itself in
|
||||
var stateUpdatePublisher: CurrentValueSubject<RoomSummaryProviderState, Never> { get }
|
||||
var statePublisher: CurrentValueSubject<RoomSummaryProviderState, Never> { get }
|
||||
|
||||
/// Publishes the total number of rooms
|
||||
var countUpdatePublisher: CurrentValueSubject<UInt, Never> { get }
|
||||
|
||||
/// The current list of rooms currently provided by the sliding sync view
|
||||
var roomSummaries: [RoomSummary] { get }
|
||||
var countPublisher: CurrentValueSubject<UInt, Never> { get }
|
||||
|
||||
/// Invoked by the sliding sync controller whenever certain rooms have updated
|
||||
/// without necessarily changing their position in the list
|
||||
|
||||
@@ -73,7 +73,8 @@ class UserSession: UserSessionProtocol {
|
||||
default:
|
||||
break
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
case .failure(let error):
|
||||
MXLog.info("Failed getting session verification controller with error: \(error). Will retry on the next sync update.")
|
||||
|
||||
@@ -60,7 +60,8 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
case .updatedMessages:
|
||||
self.updateTimelineItems()
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
updateTimelineItems()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user