Rework the presentation of the media browser quick look view to use SwiftUI. (#3619)

* Embed the media preview quick look inside a full screen cover

With a zoom transition on iOS 18.

* Use a the representable coordinator properly.

* Fix a bug with the toolbar appearance.

* Format

* Try prevent the zoom transition being upside down.

* Fix the snapshot test configuration.
This commit is contained in:
Doug
2024-12-16 15:27:50 +00:00
committed by GitHub
parent 45a630dd85
commit 3a82b88859
23 changed files with 593 additions and 473 deletions
@@ -9,10 +9,9 @@ import Compound
import SwiftUI
struct TimelineMediaPreviewDetailsView: View {
let item: TimelineMediaPreviewItem
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem }
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
@@ -21,9 +20,13 @@ struct TimelineMediaPreviewDetailsView: View {
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.presentationDetents([.medium])
.presentationDragIndicator(.visible)
.padding(.top, 19) // For the drag indicator
.sheet(isPresented: $context.isPresentingRedactConfirmation) {
TimelineMediaPreviewRedactConfirmationView(context: context)
.presentationBackground(.compound.bgCanvasDefault)
.preferredColorScheme(.dark)
.sheet(item: $context.redactConfirmationItem) { item in
TimelineMediaPreviewRedactConfirmationView(item: item, context: context)
}
}
@@ -31,20 +34,20 @@ struct TimelineMediaPreviewDetailsView: View {
VStack(alignment: .leading, spacing: 24) {
DetailsRow(title: L10n.screenMediaDetailsUploadedBy) {
HStack(spacing: 8) {
LoadableAvatarImage(url: currentItem.sender.avatarURL,
name: currentItem.sender.displayName,
contentID: currentItem.sender.id,
LoadableAvatarImage(url: item.sender.avatarURL,
name: item.sender.displayName,
contentID: item.sender.id,
avatarSize: .user(on: .mediaPreviewDetails),
mediaProvider: context.mediaProvider)
VStack(alignment: .leading, spacing: 0) {
if let displayName = currentItem.sender.displayName {
if let displayName = item.sender.displayName {
Text(displayName)
.font(.compound.bodyMDSemibold)
.foregroundStyle(.compound.decorativeColor(for: currentItem.sender.id).text)
.foregroundStyle(.compound.decorativeColor(for: item.sender.id).text)
}
Text(currentItem.sender.id)
Text(item.sender.id)
.font(.compound.bodySM)
.foregroundStyle(.compound.textSecondary)
}
@@ -52,21 +55,21 @@ struct TimelineMediaPreviewDetailsView: View {
}
DetailsRow(title: L10n.screenMediaDetailsUploadedOn) {
Text(currentItem.timestamp.formatted(date: .abbreviated, time: .shortened))
Text(item.timestamp.formatted(date: .abbreviated, time: .shortened))
.font(.compound.bodyMD)
.foregroundStyle(.compound.textPrimary)
}
DetailsRow(title: L10n.screenMediaDetailsFilename) {
Text(currentItem.filename ?? "")
Text(item.filename ?? "")
.font(.compound.bodyMD)
.foregroundStyle(.compound.textPrimary)
}
if let contentType = currentItem.contentType {
if let contentType = item.contentType {
DetailsRow(title: L10n.screenMediaDetailsFileFormat) {
Group {
if let fileSize = currentItem.fileSize {
if let fileSize = item.fileSize {
Text(contentType) + Text(" – ") + Text(UInt(fileSize).formatted(.byteCount(style: .file)))
} else {
Text(contentType)
@@ -93,7 +96,7 @@ struct TimelineMediaPreviewDetailsView: View {
ForEach(actions.actions, id: \.self) { action in
Button(role: action.isDestructive ? .destructive : nil) {
context.send(viewAction: .menuAction(action))
context.send(viewAction: .menuAction(action, item: item))
} label: {
action.label
}
@@ -107,7 +110,7 @@ struct TimelineMediaPreviewDetailsView: View {
ForEach(actions.secondaryActions, id: \.self) { action in
Button(role: action.isDestructive ? .destructive : nil) {
context.send(viewAction: .menuAction(action))
context.send(viewAction: .menuAction(action, item: item))
} label: {
action.label
}
@@ -139,19 +142,24 @@ struct TimelineMediaPreviewDetailsView: View {
import UniformTypeIdentifiers
struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePreview {
@Namespace private static var previewNamespace
static let viewModel = makeViewModel(contentType: .jpeg, isOutgoing: true)
static let unknownTypeViewModel = makeViewModel()
static let presentedOnRoomViewModel = makeViewModel(isPresentedOnRoomScreen: true)
static var previews: some View {
TimelineMediaPreviewDetailsView(context: viewModel.context)
TimelineMediaPreviewDetailsView(item: viewModel.state.currentItem,
context: viewModel.context)
.previewDisplayName("Image")
.snapshotPreferences(delay: 0.1)
TimelineMediaPreviewDetailsView(context: unknownTypeViewModel.context)
TimelineMediaPreviewDetailsView(item: unknownTypeViewModel.state.currentItem,
context: unknownTypeViewModel.context)
.previewDisplayName("Unknown type")
.snapshotPreferences(delay: 0.1)
TimelineMediaPreviewDetailsView(context: presentedOnRoomViewModel.context)
TimelineMediaPreviewDetailsView(item: presentedOnRoomViewModel.state.currentItem,
context: presentedOnRoomViewModel.context)
.previewDisplayName("Incoming on Room")
.snapshotPreferences(delay: 0.1)
}
@@ -172,8 +180,9 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie
contentType: contentType))
let timelineKind = TimelineKind.media(isPresentedOnRoomScreen ? .roomScreen : .mediaFilesScreen)
return TimelineMediaPreviewViewModel(initialItem: item,
timelineViewModel: TimelineViewModel.mock(timelineKind: timelineKind),
return TimelineMediaPreviewViewModel(context: .init(item: item,
viewModel: TimelineViewModel.mock(timelineKind: timelineKind),
namespace: previewNamespace),
mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: UserIndicatorControllerMock())
}
@@ -11,10 +11,9 @@ import SwiftUI
struct TimelineMediaPreviewRedactConfirmationView: View {
@Environment(\.dismiss) private var dismiss
let item: TimelineMediaPreviewItem
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem }
var body: some View {
ScrollView {
VStack(spacing: 0) {
@@ -54,13 +53,13 @@ struct TimelineMediaPreviewRedactConfirmationView: View {
@ViewBuilder
private var preview: some View {
HStack(spacing: 12) {
if let mediaSource = currentItem.thumbnailMediaSource {
if let mediaSource = item.thumbnailMediaSource {
Color.clear
.scaledFrame(size: 40)
.background {
LoadableImage(mediaSource: mediaSource,
mediaType: .timelineItem(uniqueID: currentItem.id.uniqueID.id),
blurhash: currentItem.blurhash,
mediaType: .timelineItem(uniqueID: item.id.uniqueID.id),
blurhash: item.blurhash,
mediaProvider: context.mediaProvider) {
Color.compound.bgSubtleSecondary
}
@@ -70,13 +69,13 @@ struct TimelineMediaPreviewRedactConfirmationView: View {
}
VStack(alignment: .leading, spacing: 4) {
Text(currentItem.filename ?? "")
Text(item.filename ?? "")
.font(.compound.bodyMD)
.foregroundStyle(.compound.textPrimary)
if let contentType = currentItem.contentType {
if let contentType = item.contentType {
Group {
if let fileSize = currentItem.fileSize {
if let fileSize = item.fileSize {
Text(contentType) + Text(" – ") + Text(UInt(fileSize).formatted(.byteCount(style: .file)))
} else {
Text(contentType)
@@ -95,7 +94,7 @@ struct TimelineMediaPreviewRedactConfirmationView: View {
private var buttons: some View {
VStack(spacing: 16) {
Button(L10n.actionRemove, role: .destructive) {
context.send(viewAction: .redactConfirmation)
context.send(viewAction: .redactConfirmation(item: item))
}
.buttonStyle(.compound(.primary))
@@ -117,10 +116,11 @@ struct TimelineMediaPreviewRedactConfirmationView: View {
import UniformTypeIdentifiers
struct TimelineMediaPreviewRedactConfirmationView_Previews: PreviewProvider, TestablePreview {
@Namespace private static var previewNamespace
static let viewModel = makeViewModel(contentType: .jpeg)
static var previews: some View {
TimelineMediaPreviewRedactConfirmationView(context: viewModel.context)
TimelineMediaPreviewRedactConfirmationView(item: viewModel.state.currentItem, context: viewModel.context)
}
static func makeViewModel(contentType: UTType? = nil) -> TimelineMediaPreviewViewModel {
@@ -138,8 +138,9 @@ struct TimelineMediaPreviewRedactConfirmationView_Previews: PreviewProvider, Tes
thumbnailInfo: .mockThumbnail,
contentType: contentType))
return TimelineMediaPreviewViewModel(initialItem: item,
timelineViewModel: TimelineViewModel.mock,
return TimelineMediaPreviewViewModel(context: .init(item: item,
viewModel: TimelineViewModel.mock,
namespace: previewNamespace),
mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: UserIndicatorControllerMock())
}