Implemented room list loading skeletons and placeholder rooms (#206)

This commit is contained in:
Stefan Ceriu
2022-09-23 13:21:54 +03:00
committed by GitHub
parent bdc83dac27
commit 867cce7b99
16 changed files with 283 additions and 180 deletions
@@ -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")
]
}
@@ -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()