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
@@ -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)
}
}