mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Configure the media preview screen based on the event and presentation. (#3604)
* Configure media previews based on the presenting screen. * Switch on TimelineKind instead of having an isPinnedEventsTimeline Bool.
This commit is contained in:
+79
-66
@@ -11,7 +11,7 @@ import SwiftUI
|
||||
struct TimelineMediaPreviewDetailsView: View {
|
||||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
||||
|
||||
@State private var isPresentingRedactConfirmation = false
|
||||
private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem }
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
@@ -19,10 +19,10 @@ struct TimelineMediaPreviewDetailsView: View {
|
||||
details
|
||||
actions
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding(.top, 19) // For the drag indicator
|
||||
.sheet(isPresented: $isPresentingRedactConfirmation) {
|
||||
.sheet(isPresented: $context.isPresentingRedactConfirmation) {
|
||||
TimelineMediaPreviewRedactConfirmationView(context: context)
|
||||
}
|
||||
}
|
||||
@@ -31,48 +31,42 @@ struct TimelineMediaPreviewDetailsView: View {
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
DetailsRow(title: L10n.screenMediaDetailsUploadedBy) {
|
||||
HStack(spacing: 8) {
|
||||
if let sender = context.viewState.currentItem?.sender {
|
||||
LoadableAvatarImage(url: sender.avatarURL,
|
||||
name: sender.displayName,
|
||||
contentID: sender.id,
|
||||
avatarSize: .user(on: .mediaPreviewDetails),
|
||||
mediaProvider: context.mediaProvider)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let displayName = sender.displayName {
|
||||
Text(displayName)
|
||||
.font(.compound.bodyMDSemibold)
|
||||
.foregroundStyle(.compound.decorativeColor(for: sender.id).text)
|
||||
}
|
||||
|
||||
Text(sender.id)
|
||||
.font(.compound.bodySM)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
LoadableAvatarImage(url: currentItem.sender.avatarURL,
|
||||
name: currentItem.sender.displayName,
|
||||
contentID: currentItem.sender.id,
|
||||
avatarSize: .user(on: .mediaPreviewDetails),
|
||||
mediaProvider: context.mediaProvider)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let displayName = currentItem.sender.displayName {
|
||||
Text(displayName)
|
||||
.font(.compound.bodyMDSemibold)
|
||||
.foregroundStyle(.compound.decorativeColor(for: currentItem.sender.id).text)
|
||||
}
|
||||
} else {
|
||||
Text(L10n.commonLoading)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
|
||||
Text(currentItem.sender.id)
|
||||
.font(.compound.bodySM)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DetailsRow(title: L10n.screenMediaDetailsUploadedOn) {
|
||||
Text(context.viewState.currentItem?.timestamp.formatted(date: .abbreviated, time: .shortened) ?? "")
|
||||
Text(currentItem.timestamp.formatted(date: .abbreviated, time: .shortened))
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
}
|
||||
|
||||
DetailsRow(title: L10n.screenMediaDetailsFilename) {
|
||||
Text(context.viewState.currentItem?.filename ?? "")
|
||||
Text(currentItem.filename ?? "")
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
}
|
||||
|
||||
if let contentType = context.viewState.currentItem?.contentType {
|
||||
if let contentType = currentItem.contentType {
|
||||
DetailsRow(title: L10n.screenMediaDetailsFileFormat) {
|
||||
Group {
|
||||
if let fileSize = context.viewState.currentItem?.fileSize {
|
||||
if let fileSize = currentItem.fileSize {
|
||||
Text(contentType) + Text(" – ") + Text(UInt(fileSize).formatted(.byteCount(style: .file)))
|
||||
} else {
|
||||
Text(contentType)
|
||||
@@ -88,23 +82,38 @@ struct TimelineMediaPreviewDetailsView: View {
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var actions: some View {
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
.background(Color.compound.bgSubtlePrimary)
|
||||
|
||||
Button { context.send(viewAction: .viewInTimeline) } label: {
|
||||
Label(L10n.actionViewInTimeline, icon: \.visibilityOn)
|
||||
if let actions = context.viewState.currentItemActions {
|
||||
VStack(spacing: 0) {
|
||||
if !actions.actions.isEmpty {
|
||||
Divider()
|
||||
.background(Color.compound.bgSubtlePrimary)
|
||||
}
|
||||
|
||||
ForEach(actions.actions, id: \.self) { action in
|
||||
Button(role: action.isDestructive ? .destructive : nil) {
|
||||
context.send(viewAction: .menuAction(action))
|
||||
} label: {
|
||||
action.label
|
||||
}
|
||||
.buttonStyle(.menuSheet)
|
||||
}
|
||||
|
||||
if !actions.secondaryActions.isEmpty {
|
||||
Divider()
|
||||
.background(Color.compound.bgSubtlePrimary)
|
||||
}
|
||||
|
||||
ForEach(actions.secondaryActions, id: \.self) { action in
|
||||
Button(role: action.isDestructive ? .destructive : nil) {
|
||||
context.send(viewAction: .menuAction(action))
|
||||
} label: {
|
||||
action.label
|
||||
}
|
||||
.buttonStyle(.menuSheet)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.menuSheet)
|
||||
|
||||
Divider()
|
||||
.background(Color.compound.bgSubtlePrimary)
|
||||
|
||||
Button(role: .destructive) { isPresentingRedactConfirmation = true } label: {
|
||||
Label(L10n.actionRemove, icon: \.delete)
|
||||
}
|
||||
.buttonStyle(.menuSheet)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,38 +139,42 @@ struct TimelineMediaPreviewDetailsView: View {
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = makeViewModel(contentType: .jpeg)
|
||||
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)
|
||||
.previewDisplayName("Image")
|
||||
.snapshotPreferences(delay: 0.1)
|
||||
TimelineMediaPreviewDetailsView(context: unknownTypeViewModel.context)
|
||||
.previewDisplayName("Unknown type")
|
||||
.snapshotPreferences(delay: 0.1)
|
||||
|
||||
TimelineMediaPreviewDetailsView(context: presentedOnRoomViewModel.context)
|
||||
.previewDisplayName("Incoming on Room")
|
||||
.snapshotPreferences(delay: 0.1)
|
||||
}
|
||||
|
||||
static func makeViewModel(contentType: UTType? = nil) -> TimelineMediaPreviewViewModel {
|
||||
let previewItems = [
|
||||
ImageRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
isEditable: true,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: false,
|
||||
sender: .init(id: "@alice:matrix.org",
|
||||
displayName: "Alice",
|
||||
avatarURL: .mockMXCUserAvatar),
|
||||
content: .init(filename: "Amazing Image.jpeg",
|
||||
imageInfo: .mockImage,
|
||||
thumbnailInfo: .mockThumbnail,
|
||||
contentType: contentType))
|
||||
]
|
||||
static func makeViewModel(contentType: UTType? = nil, isOutgoing: Bool = false, isPresentedOnRoomScreen: Bool = false) -> TimelineMediaPreviewViewModel {
|
||||
let item = ImageRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: true,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: false,
|
||||
sender: .init(id: "@alice:matrix.org",
|
||||
displayName: "Alice",
|
||||
avatarURL: .mockMXCUserAvatar),
|
||||
content: .init(filename: "Amazing Image.jpeg",
|
||||
imageInfo: .mockImage,
|
||||
thumbnailInfo: .mockThumbnail,
|
||||
contentType: contentType))
|
||||
|
||||
let viewModel = TimelineMediaPreviewViewModel(previewItems: previewItems,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
viewModel.state.currentItem = viewModel.state.previewItems.first
|
||||
|
||||
return viewModel
|
||||
let timelineKind = TimelineKind.media(isPresentedOnRoomScreen ? .roomScreen : .mediaFilesScreen)
|
||||
return TimelineMediaPreviewViewModel(initialItem: item,
|
||||
timelineViewModel: TimelineViewModel.mock(timelineKind: timelineKind),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
}
|
||||
|
||||
+52
-56
@@ -13,6 +13,8 @@ struct TimelineMediaPreviewRedactConfirmationView: View {
|
||||
|
||||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
||||
|
||||
private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem }
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
@@ -51,51 +53,49 @@ struct TimelineMediaPreviewRedactConfirmationView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var preview: some View {
|
||||
if let currentItem = context.viewState.currentItem {
|
||||
HStack(spacing: 12) {
|
||||
if let mediaSource = currentItem.thumbnailMediaSource {
|
||||
Color.clear
|
||||
.scaledFrame(size: 40)
|
||||
.background {
|
||||
LoadableImage(mediaSource: mediaSource,
|
||||
mediaType: .timelineItem(uniqueID: currentItem.id.uniqueID.id),
|
||||
blurhash: currentItem.blurhash,
|
||||
mediaProvider: context.mediaProvider) {
|
||||
Color.compound.bgSubtleSecondary
|
||||
}
|
||||
.aspectRatio(contentMode: .fill)
|
||||
HStack(spacing: 12) {
|
||||
if let mediaSource = currentItem.thumbnailMediaSource {
|
||||
Color.clear
|
||||
.scaledFrame(size: 40)
|
||||
.background {
|
||||
LoadableImage(mediaSource: mediaSource,
|
||||
mediaType: .timelineItem(uniqueID: currentItem.id.uniqueID.id),
|
||||
blurhash: currentItem.blurhash,
|
||||
mediaProvider: context.mediaProvider) {
|
||||
Color.compound.bgSubtleSecondary
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(currentItem.filename ?? "")
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
|
||||
if let contentType = currentItem.contentType {
|
||||
Group {
|
||||
if let fileSize = currentItem.fileSize {
|
||||
Text(contentType) + Text(" – ") + Text(UInt(fileSize).formatted(.byteCount(style: .file)))
|
||||
} else {
|
||||
Text(contentType)
|
||||
}
|
||||
}
|
||||
.font(.compound.bodySM)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
.aspectRatio(contentMode: .fill)
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(currentItem.filename ?? "")
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
|
||||
if let contentType = currentItem.contentType {
|
||||
Group {
|
||||
if let fileSize = currentItem.fileSize {
|
||||
Text(contentType) + Text(" – ") + Text(UInt(fileSize).formatted(.byteCount(style: .file)))
|
||||
} else {
|
||||
Text(contentType)
|
||||
}
|
||||
}
|
||||
.font(.compound.bodySM)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
|
||||
private var buttons: some View {
|
||||
VStack(spacing: 16) {
|
||||
Button(L10n.actionRemove, role: .destructive) {
|
||||
context.send(viewAction: .redact)
|
||||
context.send(viewAction: .redactConfirmation)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
|
||||
@@ -124,27 +124,23 @@ struct TimelineMediaPreviewRedactConfirmationView_Previews: PreviewProvider, Tes
|
||||
}
|
||||
|
||||
static func makeViewModel(contentType: UTType? = nil) -> TimelineMediaPreviewViewModel {
|
||||
let previewItems = [
|
||||
ImageRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
isEditable: true,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: false,
|
||||
sender: .init(id: "@alice:matrix.org",
|
||||
displayName: "Alice",
|
||||
avatarURL: .mockMXCUserAvatar),
|
||||
content: .init(filename: "Amazing Image.jpeg",
|
||||
imageInfo: .mockImage,
|
||||
thumbnailInfo: .mockThumbnail,
|
||||
contentType: contentType))
|
||||
]
|
||||
let item = ImageRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
isEditable: true,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: false,
|
||||
sender: .init(id: "@alice:matrix.org",
|
||||
displayName: "Alice",
|
||||
avatarURL: .mockMXCUserAvatar),
|
||||
content: .init(filename: "Amazing Image.jpeg",
|
||||
imageInfo: .mockImage,
|
||||
thumbnailInfo: .mockThumbnail,
|
||||
contentType: contentType))
|
||||
|
||||
let viewModel = TimelineMediaPreviewViewModel(previewItems: previewItems,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
viewModel.state.currentItem = viewModel.state.previewItems.first
|
||||
|
||||
return viewModel
|
||||
return TimelineMediaPreviewViewModel(initialItem: item,
|
||||
timelineViewModel: TimelineViewModel.mock,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user