mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Add developer option to hide media in the timeline. (#3366)
This commit is contained in:
@@ -6,12 +6,17 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Compound
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
/// Used to configure animations
|
||||
enum LoadableImageMediaType {
|
||||
/// An avatar (can be displayed anywhere within the app).
|
||||
case avatar
|
||||
/// An image displayed in the timeline.
|
||||
case timelineItem
|
||||
/// Any other media (can be displayed anywhere within the app).
|
||||
case generic
|
||||
}
|
||||
|
||||
@@ -79,6 +84,8 @@ struct LoadableImage<TransformerView: View, PlaceholderView: View>: View {
|
||||
}
|
||||
|
||||
private struct LoadableImageContent<TransformerView: View, PlaceholderView: View>: View, ImageDataProvider {
|
||||
@Environment(\.shouldAutomaticallyLoadImages) private var loadAutomatically
|
||||
|
||||
private let mediaSource: MediaSourceProxy
|
||||
private let mediaType: LoadableImageMediaType
|
||||
private let blurhash: String?
|
||||
@@ -86,6 +93,7 @@ private struct LoadableImageContent<TransformerView: View, PlaceholderView: View
|
||||
private let placeholder: () -> PlaceholderView
|
||||
|
||||
@StateObject private var contentLoader: ContentLoader
|
||||
@State private var loadManually = false
|
||||
|
||||
init(mediaSource: MediaSourceProxy,
|
||||
mediaType: LoadableImageMediaType,
|
||||
@@ -104,36 +112,40 @@ private struct LoadableImageContent<TransformerView: View, PlaceholderView: View
|
||||
_contentLoader = StateObject(wrappedValue: ContentLoader(mediaSource: mediaSource, size: size, mediaProvider: mediaProvider))
|
||||
}
|
||||
|
||||
var shouldRender: Bool {
|
||||
loadAutomatically || loadManually
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// Tried putting this in the body's .task but it randomly
|
||||
// decides to not execute the request
|
||||
let _ = Task {
|
||||
guard contentLoader.content == nil else {
|
||||
ZStack {
|
||||
switch (contentLoader.content, shouldRender) {
|
||||
case (.image(let image), true):
|
||||
transformer(
|
||||
AnyView(Image(uiImage: image).resizable())
|
||||
)
|
||||
case (.gifData, true):
|
||||
transformer(AnyView(KFAnimatedImage(source: .provider(self))))
|
||||
case (.none, _), (_, false):
|
||||
if let blurHashView {
|
||||
if shouldRender {
|
||||
transformer(blurHashView)
|
||||
} else {
|
||||
blurHashView
|
||||
}
|
||||
} else {
|
||||
placeholder().overlay { placeholderOverlay }
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(mediaType == .avatar ? .noAnimation : .elementDefault, value: contentLoader.content)
|
||||
.animation(.elementDefault, value: loadManually)
|
||||
.task(id: mediaSource.url.absoluteString + "\(shouldRender)") {
|
||||
guard shouldRender, contentLoader.content == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
await contentLoader.load()
|
||||
}
|
||||
|
||||
ZStack {
|
||||
switch contentLoader.content {
|
||||
case .image(let image):
|
||||
transformer(
|
||||
AnyView(Image(uiImage: image).resizable())
|
||||
)
|
||||
case .gifData:
|
||||
transformer(AnyView(KFAnimatedImage(source: .provider(self))))
|
||||
case .none:
|
||||
if let blurhash,
|
||||
// Build a small blurhash image so that it's fast
|
||||
let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) {
|
||||
transformer(AnyView(Image(uiImage: image).resizable()))
|
||||
} else {
|
||||
placeholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(mediaType == .avatar ? .noAnimation : .elementDefault, value: contentLoader.content)
|
||||
.onDisappear {
|
||||
guard contentLoader.content == nil else {
|
||||
return
|
||||
@@ -143,6 +155,66 @@ private struct LoadableImageContent<TransformerView: View, PlaceholderView: View
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Returns `AnyView` as this is what `transformer` expects.
|
||||
var blurHashView: AnyView? {
|
||||
if let blurhash,
|
||||
// Build a small blurhash image so that it's fast
|
||||
let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) {
|
||||
return AnyView(Image(uiImage: image).resizable().overlay { blurHashOverlay })
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Overlays
|
||||
|
||||
@ViewBuilder
|
||||
var placeholderOverlay: some View {
|
||||
switch mediaType {
|
||||
case .avatar, .generic:
|
||||
EmptyView()
|
||||
case .timelineItem:
|
||||
if shouldRender {
|
||||
ProgressView(L10n.commonLoading)
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
loadManuallyButton
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var blurHashOverlay: some View {
|
||||
if !shouldRender {
|
||||
loadManuallyButton
|
||||
}
|
||||
}
|
||||
|
||||
var loadManuallyButton: some View {
|
||||
ZStack {
|
||||
Color.black.opacity(0.6)
|
||||
.contentShape(.rect)
|
||||
.onTapGesture { /* Empty gesture to block the itemTapped action */ }
|
||||
|
||||
// Don't use a real Button as it sometimes triggers simultaneously with the long press gesture.
|
||||
Text(L10n.actionShow)
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.foregroundStyle(.compound.textOnSolidPrimary)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 4)
|
||||
.overlay {
|
||||
Capsule()
|
||||
.stroke(lineWidth: 1)
|
||||
.foregroundStyle(.compound.borderInteractiveSecondary)
|
||||
}
|
||||
.contentShape(.capsule)
|
||||
.onTapGesture {
|
||||
loadManually = true
|
||||
}
|
||||
.environment(\.colorScheme, .light)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ImageDataProvider
|
||||
|
||||
var cacheKey: String {
|
||||
@@ -222,3 +294,120 @@ private class ContentLoader: ObservableObject {
|
||||
mediaSource.mimeType == "image/gif"
|
||||
}
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
/// Whether or not images should be loaded inside `LoadableImage` without a user interaction.
|
||||
@Entry var shouldAutomaticallyLoadImages = true
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct LoadableImage_Previews: PreviewProvider, TestablePreview {
|
||||
static let mediaProvider = makeMediaProvider()
|
||||
static let loadingMediaProvider = makeMediaProvider(isLoading: true)
|
||||
|
||||
static var previews: some View {
|
||||
LazyVGrid(columns: [.init(.adaptive(minimum: 110, maximum: 110))], spacing: 24) {
|
||||
LoadableImage(url: "mxc://wherever/1234",
|
||||
mediaType: .timelineItem,
|
||||
mediaProvider: mediaProvider,
|
||||
placeholder: placeholder)
|
||||
.layout(title: "Loaded")
|
||||
|
||||
LoadableImage(url: "mxc://wherever/2345",
|
||||
mediaType: .timelineItem,
|
||||
blurhash: "KpE4oyayR5|GbHb];3j@of",
|
||||
mediaProvider: mediaProvider,
|
||||
placeholder: placeholder)
|
||||
.layout(title: "Hidden (blurhash)", hideTimelineMedia: true)
|
||||
|
||||
LoadableImage(url: "mxc://wherever/3456",
|
||||
mediaType: .timelineItem,
|
||||
mediaProvider: mediaProvider,
|
||||
placeholder: placeholder)
|
||||
.layout(title: "Hidden (placeholder)", hideTimelineMedia: true)
|
||||
|
||||
LoadableImage(url: "mxc://wherever/4567",
|
||||
mediaType: .timelineItem,
|
||||
blurhash: "KbLM^j]q$jT|EfR-3rtjXk",
|
||||
mediaProvider: loadingMediaProvider,
|
||||
placeholder: placeholder)
|
||||
.layout(title: "Loading (blurhash)")
|
||||
|
||||
LoadableImage(url: "mxc://wherever/5678",
|
||||
mediaType: .timelineItem,
|
||||
mediaProvider: loadingMediaProvider,
|
||||
placeholder: placeholder)
|
||||
.layout(title: "Loading (placeholder)")
|
||||
|
||||
LoadableImage(url: "mxc://wherever/6789",
|
||||
mediaType: .avatar,
|
||||
mediaProvider: loadingMediaProvider,
|
||||
placeholder: placeholder)
|
||||
.layout(title: "Loading (avatar)")
|
||||
|
||||
LoadableImage(url: "mxc://wherever/345",
|
||||
mediaType: .timelineItem,
|
||||
blurhash: "KbLM^j]q$jT|EfR-3rtjXk",
|
||||
mediaProvider: mediaProvider,
|
||||
transformer: transformer,
|
||||
placeholder: placeholder)
|
||||
.layout(title: "Loaded (transformer)")
|
||||
|
||||
LoadableImage(url: "mxc://wherever/345",
|
||||
mediaType: .timelineItem,
|
||||
blurhash: "KbLM^j]q$jT|EfR-3rtjXk",
|
||||
mediaProvider: loadingMediaProvider,
|
||||
transformer: transformer,
|
||||
placeholder: placeholder)
|
||||
.layout(title: "Loading (transformer)")
|
||||
|
||||
LoadableImage(url: "mxc://wherever/234",
|
||||
mediaType: .timelineItem,
|
||||
blurhash: "KbLM^j]q$jT|EfR-3rtjXk",
|
||||
mediaProvider: mediaProvider,
|
||||
transformer: transformer,
|
||||
placeholder: placeholder)
|
||||
.layout(title: "Hidden (transformer)", hideTimelineMedia: true)
|
||||
}
|
||||
}
|
||||
|
||||
static func placeholder() -> some View { Color.compound._bgBubbleIncoming }
|
||||
static func transformer(_ view: AnyView) -> some View {
|
||||
view.overlay {
|
||||
Image(systemSymbol: .playCircleFill)
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(.compound.iconAccentPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
static func makeMediaProvider(isLoading: Bool = false) -> MediaProviderProtocol {
|
||||
let mediaProvider = MediaProviderMock(configuration: .init())
|
||||
|
||||
if isLoading {
|
||||
mediaProvider.imageFromSourceSizeClosure = { _, _ in nil }
|
||||
mediaProvider.loadFileFromSourceBodyClosure = { _, _ in .failure(.failedRetrievingFile) }
|
||||
mediaProvider.loadImageDataFromSourceClosure = { _ in .failure(.failedRetrievingImage) }
|
||||
mediaProvider.loadImageFromSourceSizeClosure = { _, _ in .failure(.failedRetrievingImage) }
|
||||
mediaProvider.loadThumbnailForSourceSourceSizeClosure = { _, _ in .failure(.failedRetrievingThumbnail) }
|
||||
mediaProvider.loadImageRetryingOnReconnectionSizeClosure = { _, _ in
|
||||
Task { throw MediaProviderError.failedRetrievingImage }
|
||||
}
|
||||
}
|
||||
return mediaProvider
|
||||
}
|
||||
}
|
||||
|
||||
private extension View {
|
||||
func layout(title: String, hideTimelineMedia: Bool = false) -> some View {
|
||||
aspectRatio(contentMode: .fit)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
.overlay(alignment: .bottom) {
|
||||
Text(title)
|
||||
.font(.caption2)
|
||||
.offset(y: 16)
|
||||
.padding(.horizontal, -5)
|
||||
}
|
||||
.environment(\.shouldAutomaticallyLoadImages, !hideTimelineMedia)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user