mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +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:
@@ -49,7 +49,7 @@ enum RoomScreenViewAction {
|
||||
struct RoomScreenViewState: BindableState {
|
||||
var roomId: String
|
||||
var roomTitle = ""
|
||||
var roomAvatar: UIImage?
|
||||
var roomAvatarURL: URL?
|
||||
var items: [RoomTimelineViewProvider] = []
|
||||
var canBackPaginate = true
|
||||
var isBackPaginating = false
|
||||
|
||||
@@ -28,7 +28,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
|
||||
private let timelineController: RoomTimelineControllerProtocol
|
||||
private let timelineViewFactory: RoomTimelineViewFactoryProtocol
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@@ -39,12 +38,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
roomAvatarUrl: URL? = nil) {
|
||||
self.timelineController = timelineController
|
||||
self.timelineViewFactory = timelineViewFactory
|
||||
self.mediaProvider = mediaProvider
|
||||
|
||||
super.init(initialViewState: RoomScreenViewState(roomId: timelineController.roomID,
|
||||
roomTitle: roomName ?? "Unknown room 💥",
|
||||
roomAvatar: nil,
|
||||
bindings: .init(composerText: "", composerFocused: false)))
|
||||
roomAvatarURL: roomAvatarUrl,
|
||||
bindings: .init(composerText: "", composerFocused: false)),
|
||||
imageProvider: mediaProvider)
|
||||
|
||||
timelineController.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
@@ -76,15 +75,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
state.contextMenuBuilder = buildContextMenuForItemId(_:)
|
||||
|
||||
buildTimelineViews()
|
||||
|
||||
if let roomAvatarUrl {
|
||||
Task {
|
||||
if case let .success(avatar) = await mediaProvider.loadImageFromURL(roomAvatarUrl,
|
||||
avatarSize: .room(on: .timeline)) {
|
||||
state.roomAvatar = avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
@@ -38,24 +38,12 @@ struct RoomHeaderView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private var roomAvatar: some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
roomAvatarImage
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.frame(width: AvatarSize.room(on: .timeline).value, height: AvatarSize.room(on: .timeline).value)
|
||||
}
|
||||
|
||||
@ViewBuilder private var roomAvatarImage: some View {
|
||||
if let avatar = context.viewState.roomAvatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.accessibilityIdentifier("roomAvatarImage")
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: context.viewState.roomTitle,
|
||||
contentId: context.viewState.roomId)
|
||||
.accessibilityIdentifier("roomAvatarPlaceholderImage")
|
||||
}
|
||||
LoadableAvatarImage(url: context.viewState.roomAvatarURL,
|
||||
name: context.viewState.roomTitle,
|
||||
contentID: context.viewState.roomId,
|
||||
avatarSize: .room(on: .timeline),
|
||||
imageProvider: context.imageProvider)
|
||||
.accessibilityIdentifier("roomAvatarImage")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,33 +18,29 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct ImageRoomTimelineView: View {
|
||||
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
||||
let timelineItem: ImageRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
if let image = timelineItem.image {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
} else if let blurhash = timelineItem.blurhash,
|
||||
// Build a small blurhash image so that it's fast
|
||||
let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
} else {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.foregroundColor(.element.systemGray6)
|
||||
.opacity(0.3)
|
||||
|
||||
ProgressView(ElementL10n.loading)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
LoadableImage(mediaSource: timelineItem.source,
|
||||
blurhash: timelineItem.blurhash,
|
||||
imageProvider: context.imageProvider) {
|
||||
placeholder
|
||||
}
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
}
|
||||
}
|
||||
|
||||
var placeholder: some View {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.foregroundColor(.element.systemGray6)
|
||||
.opacity(0.3)
|
||||
|
||||
ProgressView(ElementL10n.loading)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.animation(.elementDefault, value: timelineItem.image)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +59,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "Bob"),
|
||||
source: nil,
|
||||
image: UIImage(systemName: "photo")))
|
||||
source: nil))
|
||||
|
||||
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some other image",
|
||||
@@ -73,8 +68,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "Bob"),
|
||||
source: nil,
|
||||
image: nil))
|
||||
source: nil))
|
||||
|
||||
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Blurhashed image",
|
||||
@@ -84,7 +78,6 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
isEditable: false,
|
||||
sender: .init(id: "Bob"),
|
||||
source: nil,
|
||||
image: nil,
|
||||
aspectRatio: 0.7,
|
||||
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW"))
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PlaceholderAvatarImage: View {
|
||||
private let textForImage: String
|
||||
private let contentId: String?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
bgColor
|
||||
Text(textForImage)
|
||||
.padding(4)
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 200).weight(.semibold))
|
||||
.minimumScaleFactor(0.001)
|
||||
}
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
}
|
||||
|
||||
init(text: String, contentId: String? = nil) {
|
||||
textForImage = text.first?.uppercased() ?? ""
|
||||
self.contentId = contentId
|
||||
}
|
||||
|
||||
private var bgColor: Color {
|
||||
guard let contentId else {
|
||||
return .element.accent
|
||||
}
|
||||
|
||||
return .element.avatarBackground(for: contentId)
|
||||
}
|
||||
}
|
||||
|
||||
struct PlaceholderAvatarImage_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PlaceholderAvatarImage(text: "X", contentId: "@userid:matrix.org")
|
||||
.clipShape(Circle())
|
||||
.frame(width: 150, height: 100)
|
||||
}
|
||||
}
|
||||
@@ -18,35 +18,31 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct StickerRoomTimelineView: View {
|
||||
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
||||
let timelineItem: StickerRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
if let image = timelineItem.image {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
} else if let blurhash = timelineItem.blurhash,
|
||||
// Build a small blurhash image so that it's fast
|
||||
let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
} else {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.foregroundColor(.element.systemGray6)
|
||||
.opacity(0.3)
|
||||
|
||||
ProgressView(ElementL10n.loading)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
LoadableImage(url: timelineItem.imageURL,
|
||||
blurhash: timelineItem.blurhash,
|
||||
imageProvider: context.imageProvider) {
|
||||
placeholder
|
||||
}
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
}
|
||||
.animation(.elementDefault, value: timelineItem.image)
|
||||
.accessibilityLabel(timelineItem.text)
|
||||
}
|
||||
|
||||
private var placeholder: some View {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.foregroundColor(.element.systemGray6)
|
||||
.opacity(0.3)
|
||||
|
||||
ProgressView(ElementL10n.loading)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StickerRoomTimelineView_Previews: PreviewProvider {
|
||||
@@ -64,8 +60,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "Bob"),
|
||||
imageURL: nil,
|
||||
image: UIImage(systemName: "photo")))
|
||||
imageURL: URL.picturesDirectory))
|
||||
|
||||
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some other image",
|
||||
@@ -74,8 +69,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "Bob"),
|
||||
imageURL: nil,
|
||||
image: nil))
|
||||
imageURL: URL.picturesDirectory))
|
||||
|
||||
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Blurhashed image",
|
||||
@@ -84,8 +78,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "Bob"),
|
||||
imageURL: nil,
|
||||
image: nil,
|
||||
imageURL: URL.picturesDirectory,
|
||||
aspectRatio: 0.7,
|
||||
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW"))
|
||||
}
|
||||
|
||||
@@ -18,42 +18,39 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct VideoRoomTimelineView: View {
|
||||
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
||||
let timelineItem: VideoRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
if let image = timelineItem.image {
|
||||
thumbnail(with: image)
|
||||
} else if let blurhash = timelineItem.blurhash,
|
||||
// Build a small blurhash image so that it's fast
|
||||
let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) {
|
||||
thumbnail(with: image)
|
||||
} else {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.foregroundColor(.element.systemGray6)
|
||||
.opacity(0.3)
|
||||
|
||||
ProgressView(ElementL10n.loading)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
LoadableImage(mediaSource: timelineItem.thumbnailSource,
|
||||
blurhash: timelineItem.blurhash,
|
||||
imageProvider: context.imageProvider) { imageView in
|
||||
imageView
|
||||
.overlay { playIcon }
|
||||
} placeholder: {
|
||||
placeholder
|
||||
}
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
}
|
||||
.animation(.elementDefault, value: timelineItem.image)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func thumbnail(with image: UIImage) -> some View {
|
||||
|
||||
var playIcon: some View {
|
||||
Image(systemName: "play.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.background(.ultraThinMaterial, in: Circle())
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
var placeholder: some View {
|
||||
ZStack {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
Image(systemName: "play.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.background(.ultraThinMaterial, in: Circle())
|
||||
.foregroundColor(.white)
|
||||
Rectangle()
|
||||
.foregroundColor(.element.systemGray6)
|
||||
.opacity(0.3)
|
||||
|
||||
ProgressView(ElementL10n.loading)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,8 +72,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
||||
sender: .init(id: "Bob"),
|
||||
duration: 21,
|
||||
source: nil,
|
||||
thumbnailSource: nil,
|
||||
image: UIImage(systemName: "photo")))
|
||||
thumbnailSource: nil))
|
||||
|
||||
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some other video",
|
||||
@@ -87,8 +83,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
||||
sender: .init(id: "Bob"),
|
||||
duration: 22,
|
||||
source: nil,
|
||||
thumbnailSource: nil,
|
||||
image: nil))
|
||||
thumbnailSource: nil))
|
||||
|
||||
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Blurhashed video",
|
||||
@@ -100,7 +95,6 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
||||
duration: 23,
|
||||
source: nil,
|
||||
thumbnailSource: nil,
|
||||
image: nil,
|
||||
aspectRatio: 0.7,
|
||||
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW"))
|
||||
}
|
||||
|
||||
@@ -18,29 +18,19 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineSenderAvatarView: View {
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
|
||||
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
||||
@ScaledMetric private var avatarSize = AvatarSize.user(on: .timeline).value
|
||||
|
||||
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .center) {
|
||||
if let avatar = timelineItem.sender.avatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.overlay(Circle().stroke(Color.element.accent))
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: timelineItem.sender.displayName ?? timelineItem.sender.id,
|
||||
contentId: timelineItem.sender.id)
|
||||
}
|
||||
}
|
||||
.clipShape(Circle())
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color.element.background, lineWidth: 3)
|
||||
)
|
||||
|
||||
.animation(.elementDefault, value: timelineItem.sender.avatar)
|
||||
LoadableAvatarImage(url: timelineItem.sender.avatarURL,
|
||||
name: timelineItem.sender.displayName,
|
||||
contentID: timelineItem.sender.id,
|
||||
avatarSize: .user(on: .timeline),
|
||||
imageProvider: context.imageProvider)
|
||||
.overlay(
|
||||
Circle().stroke(Color.element.background, lineWidth: 3)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user