mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Media loading flow changes (#483)
* Use an imageProvider directly from the view in the home screen * Add support for media request coalescing * Rename MediaProxy to MediaLoader * Add new image loading mechanism to the room details screen avatar. * Use the `SettingsScreen` prefix for all settings screen related components * Add new image loading mechanism to the room header * Add new image loading mechanism to the room member details screen * Introduce a LoadableImage SwiftUI view that will automatically handle image loading * Adopt the new LoadableImage where possible * Fix LoadableImage not using/storing loaded images properly * Simplify media loader enqueueing * Made LodableImage load content after mediaSource updates. Adopt it on the home and settings screens * Introduce a LoadableAvatarImage component and reuse it throughout the app * Small logging tweaks, made some LoadableImage properties private * Fix redacted skeletons avatar background color * Fix placeholder avatars changing when backgrounding the app * PR comments. - Trim the @ sign off of mxid placeholders. - Only expose AvatarSize on the avatar image, use CGSize elsewhere. Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
@@ -34,7 +34,6 @@ enum HomeScreenViewUserMenuAction {
|
||||
}
|
||||
|
||||
enum HomeScreenViewAction {
|
||||
case loadRoomData(roomIdentifier: String)
|
||||
case selectRoom(roomIdentifier: String)
|
||||
case userMenu(action: HomeScreenViewUserMenuAction)
|
||||
case verifySession
|
||||
@@ -59,7 +58,7 @@ enum HomeScreenRoomListMode: CustomStringConvertible {
|
||||
struct HomeScreenViewState: BindableState {
|
||||
var userID: String
|
||||
var userDisplayName: String?
|
||||
var userAvatar: UIImage?
|
||||
var userAvatarURL: URL?
|
||||
|
||||
var showSessionVerificationBanner = false
|
||||
|
||||
@@ -96,7 +95,6 @@ struct HomeScreenViewStateBindings {
|
||||
|
||||
struct HomeScreenRoom: Identifiable, Equatable {
|
||||
private static let placeholderLastMessage = AttributedString("Last message")
|
||||
private static let placeholderAvatar = UIImage(systemName: "photo")
|
||||
|
||||
/// 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
|
||||
@@ -115,8 +113,6 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
|
||||
var avatarURL: URL?
|
||||
|
||||
var avatar: UIImage?
|
||||
|
||||
var isPlaceholder = false
|
||||
|
||||
static func placeholder() -> HomeScreenRoom {
|
||||
@@ -126,7 +122,6 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
hasUnreads: false,
|
||||
timestamp: "Now",
|
||||
lastMessage: Self.placeholderLastMessage,
|
||||
avatar: Self.placeholderAvatar,
|
||||
isPlaceholder: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
// swiftlint:disable:next function_body_length cyclomatic_complexity
|
||||
// swiftlint:disable:next function_body_length
|
||||
init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) {
|
||||
self.userSession = userSession
|
||||
self.attributedStringBuilder = attributedStringBuilder
|
||||
@@ -45,7 +45,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
visibleRoomsSummaryProvider = userSession.clientProxy.visibleRoomsSummaryProvider
|
||||
allRoomsSummaryProvider = userSession.clientProxy.allRoomsSummaryProvider
|
||||
|
||||
super.init(initialViewState: HomeScreenViewState(userID: userSession.userID))
|
||||
super.init(initialViewState: HomeScreenViewState(userID: userSession.userID),
|
||||
imageProvider: userSession.mediaProvider)
|
||||
|
||||
userSession.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
@@ -71,9 +72,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
|
||||
Task {
|
||||
if case let .success(url) = await userSession.clientProxy.loadUserAvatarURL() {
|
||||
if case let .success(avatar) = await userSession.mediaProvider.loadImageFromURL(url, avatarSize: .user(on: .home)) {
|
||||
state.userAvatar = avatar
|
||||
}
|
||||
state.userAvatarURL = url
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,13 +138,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
override func process(viewAction: HomeScreenViewAction) async {
|
||||
switch viewAction {
|
||||
case .loadRoomData(let roomIdentifier):
|
||||
if state.roomListMode != .skeletons {
|
||||
loadDataForRoomIdentifier(roomIdentifier)
|
||||
}
|
||||
case .selectRoom(let roomIdentifier):
|
||||
callback?(.presentRoom(roomIdentifier: roomIdentifier))
|
||||
case .userMenu(let action):
|
||||
@@ -178,27 +172,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func loadDataForRoomIdentifier(_ identifier: String) {
|
||||
guard let room = roomsForIdentifiers[identifier],
|
||||
room.avatar == nil,
|
||||
let avatarURL = room.avatarURL else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
if case let .success(image) = await userSession.mediaProvider.loadImageFromURL(avatarURL, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This method will update all view state rooms by merging the data from both summary providers
|
||||
/// If a room is empty in the visible room summary provider it will try to get it from the allRooms one
|
||||
/// This ensures that we show as many room details as possible without loading up timelines
|
||||
@@ -256,10 +230,6 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
room.hasUnreads = details.unreadNotificationCount > 0
|
||||
room.lastMessage = details.lastMessage
|
||||
|
||||
if let avatarURL = details.avatarURL {
|
||||
room.avatar = userSession.mediaProvider.imageFromURL(avatarURL, avatarSize: .room(on: .home))
|
||||
}
|
||||
|
||||
if let lastMessageTimestamp = details.lastMessageTimestamp {
|
||||
room.timestamp = lastMessageTimestamp.formattedMinimal()
|
||||
}
|
||||
|
||||
@@ -102,9 +102,12 @@ struct HomeScreen: View {
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
userAvatarImageView
|
||||
.animation(.elementDefault, value: context.viewState.userAvatar)
|
||||
.transition(.opacity)
|
||||
LoadableAvatarImage(url: context.viewState.userAvatarURL,
|
||||
name: context.viewState.userDisplayName,
|
||||
contentID: context.viewState.userID,
|
||||
avatarSize: .user(on: .home),
|
||||
imageProvider: context.imageProvider)
|
||||
.accessibilityIdentifier("userAvatarImage")
|
||||
}
|
||||
.alert(ElementL10n.actionSignOut,
|
||||
isPresented: $showingLogoutConfirmation) {
|
||||
@@ -116,26 +119,6 @@ struct HomeScreen: View {
|
||||
}
|
||||
.accessibilityLabel(ElementL10n.a11yAllChatsUserAvatarMenu)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var userAvatarImageView: some View {
|
||||
userAvatarImage
|
||||
.frame(width: AvatarSize.user(on: .home).value, height: AvatarSize.user(on: .home).value, alignment: .center)
|
||||
.clipShape(Circle())
|
||||
.accessibilityIdentifier("userAvatarImage")
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var userAvatarImage: some View {
|
||||
if let avatar = context.viewState.userAvatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: context.viewState.userDisplayName ?? context.viewState.userID,
|
||||
contentId: context.viewState.userID)
|
||||
}
|
||||
}
|
||||
|
||||
private var sessionVerificationBanner: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
import SwiftUI
|
||||
|
||||
struct HomeScreenRoomCell: View {
|
||||
@ScaledMetric private var avatarSize = AvatarSize.room(on: .home).value
|
||||
|
||||
let room: HomeScreenRoom
|
||||
let context: HomeScreenViewModel.Context
|
||||
|
||||
@@ -38,11 +36,6 @@ struct HomeScreenRoomCell: View {
|
||||
}
|
||||
.frame(minHeight: 64.0)
|
||||
.accessibilityElement(children: .combine)
|
||||
.task {
|
||||
if let roomId = room.roomId {
|
||||
context.send(viewAction: .loadRoomData(roomIdentifier: roomId))
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(HomeScreenRoomCellButtonStyle())
|
||||
.accessibilityIdentifier("roomName:\(room.name)")
|
||||
@@ -50,19 +43,12 @@ struct HomeScreenRoomCell: View {
|
||||
|
||||
@ViewBuilder
|
||||
var avatar: some View {
|
||||
if let avatar = room.avatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.clipShape(Circle())
|
||||
.accessibilityHidden(true)
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: room.name, contentId: room.roomId)
|
||||
.clipShape(Circle())
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
LoadableAvatarImage(url: room.avatarURL,
|
||||
name: room.name,
|
||||
contentID: room.roomId,
|
||||
avatarSize: .room(on: .home),
|
||||
imageProvider: context.imageProvider)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
Reference in New Issue
Block a user