mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
Room list fixes (#431)
* Fixes #396 - Avoid duplicates in room list, cleanup logic throughout * Re-enabled the allRoomsView with a hard 500 room limit and reduced the diffing collection time. * Fixes #385, #411 - Filter out spaces, invites and tombstoned rooms from the room list * Various tweaks following code review
This commit is contained in:
@@ -70,8 +70,8 @@ struct HomeScreenViewState: BindableState {
|
||||
var bindings = HomeScreenViewStateBindings()
|
||||
|
||||
var placeholderRooms: [HomeScreenRoom] {
|
||||
(1...10).map { index in
|
||||
HomeScreenRoom.placeholder(id: "\(index)")
|
||||
(1...10).map { _ in
|
||||
HomeScreenRoom.placeholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,8 +83,13 @@ struct HomeScreenViewStateBindings {
|
||||
}
|
||||
|
||||
struct HomeScreenRoom: Identifiable, Equatable {
|
||||
/// The list item identifier can be a real room identifier, a custom one for invalidated entries
|
||||
/// or a completely unique one for empty items and skeletons
|
||||
let id: String
|
||||
|
||||
/// The real room identifier this item points to
|
||||
let roomId: String?
|
||||
|
||||
let name: String
|
||||
|
||||
let hasUnreads: Bool
|
||||
@@ -93,12 +98,15 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
|
||||
var lastMessage: AttributedString?
|
||||
|
||||
var avatarURLString: String?
|
||||
|
||||
var avatar: UIImage?
|
||||
|
||||
var isPlaceholder = false
|
||||
|
||||
static func placeholder(id: String) -> HomeScreenRoom {
|
||||
HomeScreenRoom(id: id,
|
||||
static func placeholder() -> HomeScreenRoom {
|
||||
HomeScreenRoom(id: UUID().uuidString,
|
||||
roomId: nil,
|
||||
name: "Placeholder room name",
|
||||
hasUnreads: false,
|
||||
timestamp: "Now",
|
||||
|
||||
@@ -24,7 +24,6 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
private let visibleRoomsSummaryProvider: RoomSummaryProviderProtocol?
|
||||
private let allRoomsSummaryProvider: RoomSummaryProviderProtocol?
|
||||
private let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||
private var roomsForIdentifiers = [String: HomeScreenRoom]()
|
||||
|
||||
var callback: ((HomeScreenViewModelAction) -> Void)?
|
||||
|
||||
@@ -139,28 +138,21 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
// MARK: - Private
|
||||
|
||||
private func loadDataForRoomIdentifier(_ identifier: String) {
|
||||
guard let visibleRoomsSummaryProvider else {
|
||||
MXLog.error("Room summary provider unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
guard let roomSummary = visibleRoomsSummaryProvider.roomListPublisher.value.first(where: { $0.asFilled?.id == identifier })?.asFilled,
|
||||
let roomIndex = state.rooms.firstIndex(where: { $0.id == identifier }) else {
|
||||
return
|
||||
}
|
||||
|
||||
var room = state.rooms[roomIndex]
|
||||
|
||||
guard room.avatar == nil,
|
||||
let avatarURLString = roomSummary.avatarURLString else {
|
||||
guard let room = state.rooms.first(where: { $0.roomId == identifier }),
|
||||
room.avatar == nil,
|
||||
let avatarURLString = room.avatarURLString else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
if case let .success(image) = await userSession.mediaProvider.loadImageFromURLString(avatarURLString, avatarSize: .room(on: .home)) {
|
||||
guard let roomIndex = state.rooms.firstIndex(where: { $0.roomId == identifier }) else {
|
||||
return
|
||||
}
|
||||
|
||||
var room = state.rooms[roomIndex]
|
||||
room.avatar = image
|
||||
state.rooms[roomIndex] = room
|
||||
roomsForIdentifiers[roomSummary.id] = room
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,46 +167,51 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
}
|
||||
|
||||
var rooms = [HomeScreenRoom]()
|
||||
var newRoomsForIdentifiers = [String: HomeScreenRoom]()
|
||||
|
||||
for (index, summary) in visibleRoomsSummaryProvider.roomListPublisher.value.enumerated() {
|
||||
switch summary {
|
||||
case .empty(let id):
|
||||
guard case let .filled(summary) = allRoomsSummaryProvider?.roomListPublisher.value[safe: index] else {
|
||||
rooms.append(HomeScreenRoom.placeholder(id: id))
|
||||
case .empty:
|
||||
guard let allRoomsRoomSummary = allRoomsSummaryProvider?.roomListPublisher.value[safe: index] else {
|
||||
rooms.append(HomeScreenRoom.placeholder())
|
||||
continue
|
||||
}
|
||||
|
||||
let room = buildRoomForSummary(summary)
|
||||
|
||||
switch allRoomsRoomSummary {
|
||||
case .empty:
|
||||
rooms.append(HomeScreenRoom.placeholder())
|
||||
case .filled(let details), .invalidated(let details):
|
||||
let room = buildRoom(with: details, invalidated: true)
|
||||
rooms.append(room)
|
||||
}
|
||||
case .filled(let details):
|
||||
let room = buildRoom(with: details, invalidated: false)
|
||||
rooms.append(room)
|
||||
newRoomsForIdentifiers[summary.id] = room
|
||||
case .filled(let summary):
|
||||
let room = buildRoomForSummary(summary)
|
||||
case .invalidated(let details):
|
||||
let room = buildRoom(with: details, invalidated: true)
|
||||
rooms.append(room)
|
||||
newRoomsForIdentifiers[summary.id] = room
|
||||
}
|
||||
}
|
||||
|
||||
state.rooms = rooms
|
||||
roomsForIdentifiers = newRoomsForIdentifiers
|
||||
}
|
||||
|
||||
private func buildRoomForSummary(_ summary: RoomSummaryDetails) -> HomeScreenRoom {
|
||||
let oldRoom = roomsForIdentifiers[summary.id]
|
||||
|
||||
let avatarImage = userSession.mediaProvider.imageFromURLString(summary.avatarURLString, avatarSize: .room(on: .home))
|
||||
private func buildRoom(with details: RoomSummaryDetails, invalidated: Bool) -> HomeScreenRoom {
|
||||
let avatarImage = userSession.mediaProvider.imageFromURLString(details.avatarURLString, avatarSize: .room(on: .home))
|
||||
|
||||
var timestamp: String?
|
||||
if let lastMessageTimestamp = summary.lastMessageTimestamp {
|
||||
if let lastMessageTimestamp = details.lastMessageTimestamp {
|
||||
timestamp = lastMessageTimestamp.formatted(date: .omitted, time: .shortened)
|
||||
}
|
||||
|
||||
return HomeScreenRoom(id: summary.id,
|
||||
name: summary.name,
|
||||
hasUnreads: summary.unreadNotificationCount > 0,
|
||||
timestamp: timestamp ?? oldRoom?.timestamp,
|
||||
lastMessage: summary.lastMessage ?? oldRoom?.lastMessage,
|
||||
avatar: avatarImage ?? oldRoom?.avatar)
|
||||
let identifier = invalidated ? "invalidated-" + details.id : details.id
|
||||
return HomeScreenRoom(id: identifier,
|
||||
roomId: details.id,
|
||||
name: details.name,
|
||||
hasUnreads: details.unreadNotificationCount > 0,
|
||||
timestamp: timestamp,
|
||||
lastMessage: details.lastMessage,
|
||||
avatarURLString: details.avatarURLString,
|
||||
avatar: avatarImage)
|
||||
}
|
||||
|
||||
private func updateVisibleRange(visibleItemIdentifiers items: Set<String>) {
|
||||
|
||||
@@ -24,7 +24,9 @@ struct HomeScreenRoomCell: View {
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
context.send(viewAction: .selectRoom(roomIdentifier: room.id))
|
||||
if let roomId = room.roomId {
|
||||
context.send(viewAction: .selectRoom(roomIdentifier: roomId))
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 16.0) {
|
||||
if let avatar = room.avatar {
|
||||
@@ -35,7 +37,7 @@ struct HomeScreenRoomCell: View {
|
||||
.clipShape(Circle())
|
||||
.accessibilityHidden(true)
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: room.name, contentId: room.id)
|
||||
PlaceholderAvatarImage(text: room.name, contentId: room.roomId)
|
||||
.clipShape(Circle())
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.accessibilityHidden(true)
|
||||
@@ -86,7 +88,9 @@ struct HomeScreenRoomCell: View {
|
||||
.frame(minHeight: 64.0)
|
||||
.accessibilityElement(children: .combine)
|
||||
.task {
|
||||
context.send(viewAction: .loadRoomData(roomIdentifier: room.id))
|
||||
if let roomId = room.roomId {
|
||||
context.send(viewAction: .loadRoomData(roomIdentifier: roomId))
|
||||
}
|
||||
}
|
||||
}
|
||||
.accessibilityIdentifier("roomName:\(room.name)")
|
||||
@@ -113,27 +117,29 @@ struct HomeScreenRoomCell_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
body.tint(.element.accent)
|
||||
}
|
||||
|
||||
|
||||
static var body: some View {
|
||||
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 {
|
||||
let rooms: [HomeScreenRoom] = summaryProvider.roomListPublisher.value.compactMap { summary -> HomeScreenRoom? in
|
||||
switch summary {
|
||||
case .empty:
|
||||
return nil
|
||||
case .filled(let details), .invalidated(let details):
|
||||
return HomeScreenRoom(id: UUID().uuidString,
|
||||
roomId: details.id,
|
||||
name: details.name,
|
||||
hasUnreads: details.unreadNotificationCount > 0,
|
||||
timestamp: Date.now.formatted(date: .omitted, time: .shortened))
|
||||
}
|
||||
|
||||
return HomeScreenRoom(id: summary.id,
|
||||
name: summary.name,
|
||||
hasUnreads: summary.unreadNotificationCount > 0,
|
||||
timestamp: Date.now.formatted(date: .omitted, time: .shortened))
|
||||
}
|
||||
|
||||
|
||||
return VStack {
|
||||
ForEach(rooms) { room in
|
||||
HomeScreenRoomCell(room: room, context: viewModel.context)
|
||||
|
||||
Reference in New Issue
Block a user