mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +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:
@@ -92,7 +92,7 @@ extension JoinedRoomProxyMock {
|
||||
}
|
||||
canUserInviteUserIDReturnValue = .success(configuration.canUserInvite)
|
||||
canUserRedactOtherUserIDReturnValue = .success(false)
|
||||
canUserRedactOwnUserIDReturnValue = .success(false)
|
||||
canUserRedactOwnUserIDReturnValue = .success(true)
|
||||
canUserKickUserIDClosure = { [weak self] userID in
|
||||
.success(self?.membersPublisher.value.first { $0.userID == userID }?.role ?? .user != .user)
|
||||
}
|
||||
|
||||
@@ -132,13 +132,14 @@ class TimelineMediaPreviewController: QLPreviewController, QLPreviewControllerDa
|
||||
|
||||
private struct HeaderView: View {
|
||||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
||||
private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem }
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Text(context.viewState.currentItem?.sender.displayName ?? context.viewState.currentItem?.sender.id ?? L10n.commonLoading)
|
||||
Text(currentItem.sender.displayName ?? currentItem.sender.id)
|
||||
.font(.compound.bodySMSemibold)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
Text(context.viewState.currentItem?.timestamp.formatted(date: .abbreviated, time: .omitted) ?? "")
|
||||
Text(currentItem.timestamp.formatted(date: .abbreviated, time: .omitted))
|
||||
.font(.compound.bodyXS)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.textCase(.uppercase)
|
||||
@@ -148,9 +149,10 @@ private struct HeaderView: View {
|
||||
|
||||
private struct CaptionView: View {
|
||||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
||||
private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem }
|
||||
|
||||
var body: some View {
|
||||
if let caption = context.viewState.currentItem?.caption {
|
||||
if let caption = currentItem.caption {
|
||||
Text(caption)
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
|
||||
@@ -14,12 +14,19 @@ enum TimelineMediaPreviewViewModelAction {
|
||||
|
||||
struct TimelineMediaPreviewViewState: BindableState {
|
||||
var previewItems: [TimelineMediaPreviewItem]
|
||||
var currentItem: TimelineMediaPreviewItem?
|
||||
var currentItem: TimelineMediaPreviewItem
|
||||
var currentItemActions: TimelineItemMenuActions?
|
||||
|
||||
var bindings = TimelineMediaPreviewViewStateBindings()
|
||||
}
|
||||
|
||||
struct TimelineMediaPreviewViewStateBindings {
|
||||
var isPresentingRedactConfirmation = false
|
||||
}
|
||||
|
||||
/// Wraps a media file and title to be previewed with QuickLook.
|
||||
class TimelineMediaPreviewItem: NSObject, QLPreviewItem {
|
||||
private let timelineItem: EventBasedMessageTimelineItemProtocol
|
||||
let timelineItem: EventBasedMessageTimelineItemProtocol
|
||||
var fileHandle: MediaFileHandleProxy?
|
||||
|
||||
init(timelineItem: EventBasedMessageTimelineItemProtocol) {
|
||||
@@ -159,6 +166,6 @@ class TimelineMediaPreviewItem: NSObject, QLPreviewItem {
|
||||
}
|
||||
|
||||
enum TimelineMediaPreviewViewAction {
|
||||
case viewInTimeline
|
||||
case redact
|
||||
case menuAction(TimelineItemMenuAction)
|
||||
case redactConfirmation
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import Foundation
|
||||
typealias TimelineMediaPreviewViewModelType = StateStoreViewModel<TimelineMediaPreviewViewState, TimelineMediaPreviewViewAction>
|
||||
|
||||
class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
private let timelineViewModel: TimelineViewModelProtocol
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
@@ -19,26 +20,51 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(previewItems: [EventBasedMessageTimelineItemProtocol], mediaProvider: MediaProviderProtocol, userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
init(initialItem: EventBasedMessageTimelineItemProtocol,
|
||||
timelineViewModel: TimelineViewModelProtocol,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.timelineViewModel = timelineViewModel
|
||||
self.mediaProvider = mediaProvider
|
||||
|
||||
// We might not want to inject this, instead creating a new instance with a custom position and colour scheme 🤔
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
super.init(initialViewState: TimelineMediaPreviewViewState(previewItems: previewItems.map(TimelineMediaPreviewItem.init)), mediaProvider: mediaProvider)
|
||||
let currentItem = TimelineMediaPreviewItem(timelineItem: initialItem)
|
||||
|
||||
super.init(initialViewState: TimelineMediaPreviewViewState(previewItems: [currentItem],
|
||||
currentItem: currentItem),
|
||||
mediaProvider: mediaProvider)
|
||||
|
||||
rebuildCurrentItemActions()
|
||||
|
||||
timelineViewModel.context.$viewState.map(\.canCurrentUserRedactSelf)
|
||||
.merge(with: timelineViewModel.context.$viewState.map(\.canCurrentUserRedactOthers))
|
||||
.sink { [weak self] _ in
|
||||
self?.rebuildCurrentItemActions()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
override func process(viewAction: TimelineMediaPreviewViewAction) {
|
||||
switch viewAction {
|
||||
case .viewInTimeline:
|
||||
actionsSubject.send(.viewInTimeline)
|
||||
case .redact:
|
||||
case .menuAction(let action):
|
||||
switch action {
|
||||
case .viewInRoomTimeline:
|
||||
actionsSubject.send(.viewInTimeline)
|
||||
case .redact:
|
||||
state.bindings.isPresentingRedactConfirmation = true
|
||||
default:
|
||||
MXLog.error("Received unexpected action: \(action)")
|
||||
}
|
||||
case .redactConfirmation:
|
||||
break // Do it here??
|
||||
}
|
||||
}
|
||||
|
||||
func updateCurrentItem(_ previewItem: TimelineMediaPreviewItem) async {
|
||||
state.currentItem = previewItem
|
||||
rebuildCurrentItemActions()
|
||||
|
||||
if previewItem.fileHandle == nil, let source = previewItem.mediaSource {
|
||||
showDownloadingIndicator(itemID: previewItem.id)
|
||||
@@ -50,11 +76,26 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
actionsSubject.send(.loadedMediaFile)
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed loading media: \(error)")
|
||||
#warning("Show the error!")
|
||||
showDownloadErrorIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func rebuildCurrentItemActions() {
|
||||
let timelineContext = timelineViewModel.context
|
||||
let provider = TimelineItemMenuActionProvider(timelineItem: state.currentItem.timelineItem,
|
||||
canCurrentUserRedactSelf: timelineContext.viewState.canCurrentUserRedactSelf,
|
||||
canCurrentUserRedactOthers: timelineContext.viewState.canCurrentUserRedactOthers,
|
||||
canCurrentUserPin: timelineContext.viewState.canCurrentUserPin,
|
||||
pinnedEventIDs: timelineContext.viewState.pinnedEventIDs,
|
||||
isDM: timelineContext.viewState.isEncryptedOneToOneRoom,
|
||||
isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled,
|
||||
isCreateMediaCaptionsEnabled: timelineContext.viewState.isCreateMediaCaptionsEnabled,
|
||||
timelineKind: timelineContext.viewState.timelineKind,
|
||||
emojiProvider: timelineContext.viewState.emojiProvider)
|
||||
state.currentItemActions = provider.makeActions()
|
||||
}
|
||||
|
||||
private func showDownloadingIndicator(itemID: TimelineItemIdentifier) {
|
||||
let indicatorID = makeDownloadIndicatorID(itemID: itemID)
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: indicatorID,
|
||||
@@ -69,7 +110,16 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
userIndicatorController.retractIndicatorWithId(indicatorID)
|
||||
}
|
||||
|
||||
private func showDownloadErrorIndicator() {
|
||||
// FIXME: Add the correct string and indicator type??
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: downloadErrorIndicatorID,
|
||||
type: .modal,
|
||||
title: L10n.errorUnknown,
|
||||
iconName: "exclamationmark.circle.fill"))
|
||||
}
|
||||
|
||||
private var downloadErrorIndicatorID: String { "\(Self.self)-DownloadError" }
|
||||
private func makeDownloadIndicatorID(itemID: TimelineItemIdentifier) -> String {
|
||||
"\(TimelineMediaPreviewViewModel.self)-Download-\(itemID.uniqueID.id)"
|
||||
"\(Self.self)-Download-\(itemID.uniqueID.id)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,21 +134,22 @@ struct TimelineMediaQuickLook_Previews: PreviewProvider {
|
||||
}
|
||||
|
||||
static func makeViewModel() -> TimelineMediaPreviewViewModel {
|
||||
let previewItem = FileRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: false,
|
||||
sender: .init(id: "", displayName: "Sally Sanderson"),
|
||||
content: .init(filename: "Important document.pdf",
|
||||
caption: "A caption goes right here.",
|
||||
source: try? .init(url: .mockMXCFile, mimeType: nil),
|
||||
fileSize: 3 * 1024 * 1024,
|
||||
thumbnailSource: nil,
|
||||
contentType: .pdf))
|
||||
let item = FileRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: false,
|
||||
sender: .init(id: "", displayName: "Sally Sanderson"),
|
||||
content: .init(filename: "Important document.pdf",
|
||||
caption: "A caption goes right here.",
|
||||
source: try? .init(url: .mockMXCFile, mimeType: nil),
|
||||
fileSize: 3 * 1024 * 1024,
|
||||
thumbnailSource: nil,
|
||||
contentType: .pdf))
|
||||
|
||||
return TimelineMediaPreviewViewModel(previewItems: [previewItem],
|
||||
return TimelineMediaPreviewViewModel(initialItem: item,
|
||||
timelineViewModel: TimelineViewModel.mock,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
|
||||
+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())
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -144,7 +144,8 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
return
|
||||
}
|
||||
|
||||
let viewModel = TimelineMediaPreviewViewModel(previewItems: [item],
|
||||
let viewModel = TimelineMediaPreviewViewModel(initialItem: item,
|
||||
timelineViewModel: activeTimelineViewModel,
|
||||
mediaProvider: mediaProvider,
|
||||
userIndicatorController: userIndicatorController)
|
||||
state.bindings.mediaPreviewViewModel = viewModel
|
||||
|
||||
+1
-1
@@ -164,7 +164,7 @@ extension View {
|
||||
|
||||
struct MediaEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let timelineViewModel: TimelineViewModel = {
|
||||
let timelineController = MockRoomTimelineController(timelineKind: .media)
|
||||
let timelineController = MockRoomTimelineController(timelineKind: .media(.mediaFilesScreen))
|
||||
return TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")),
|
||||
timelineController: timelineController,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ struct PinnedEventsTimelineScreen: View {
|
||||
isDM: timelineContext.viewState.isEncryptedOneToOneRoom,
|
||||
isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled,
|
||||
isCreateMediaCaptionsEnabled: timelineContext.viewState.isCreateMediaCaptionsEnabled,
|
||||
isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline,
|
||||
timelineKind: timelineContext.viewState.timelineKind,
|
||||
emojiProvider: timelineContext.viewState.emojiProvider)
|
||||
.makeActions()
|
||||
if let actions {
|
||||
|
||||
@@ -76,7 +76,7 @@ struct RoomScreen: View {
|
||||
isDM: timelineContext.viewState.isEncryptedOneToOneRoom,
|
||||
isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled,
|
||||
isCreateMediaCaptionsEnabled: timelineContext.viewState.isCreateMediaCaptionsEnabled,
|
||||
isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline,
|
||||
timelineKind: timelineContext.viewState.timelineKind,
|
||||
emojiProvider: timelineContext.viewState.emojiProvider)
|
||||
.makeActions()
|
||||
if let actions {
|
||||
|
||||
@@ -85,7 +85,7 @@ enum TimelineComposerAction {
|
||||
}
|
||||
|
||||
struct TimelineViewState: BindableState {
|
||||
let isPinnedEventsTimeline: Bool
|
||||
let timelineKind: TimelineKind
|
||||
var roomID: String
|
||||
var members: [String: RoomMemberState] = [:]
|
||||
var typingMembers: [String] = []
|
||||
|
||||
@@ -316,7 +316,7 @@ class TimelineTableViewController: UIViewController {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, TimelineUniqueId>()
|
||||
|
||||
// We don't want to display the typing notification in this timeline
|
||||
if !coordinator.context.viewState.isPinnedEventsTimeline {
|
||||
if coordinator.context.viewState.timelineKind != .pinned {
|
||||
snapshot.appendSections([.typingIndicator])
|
||||
snapshot.appendItems([TimelineUniqueId(id: TimelineTypingIndicatorCell.reuseIdentifier)])
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
appSettings: appSettings,
|
||||
analyticsService: analyticsService)
|
||||
|
||||
super.init(initialViewState: TimelineViewState(isPinnedEventsTimeline: timelineController.timelineKind == .pinned,
|
||||
super.init(initialViewState: TimelineViewState(timelineKind: timelineController.timelineKind,
|
||||
roomID: roomProxy.id,
|
||||
isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom,
|
||||
timelineState: TimelineState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }),
|
||||
@@ -690,13 +690,13 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
} else {
|
||||
for (index, item) in itemGroup.enumerated() {
|
||||
if index == 0 {
|
||||
timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.isPinnedEventsTimeline ? .single : .first),
|
||||
timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.timelineKind == .pinned ? .single : .first),
|
||||
forKey: item.id.uniqueID)
|
||||
} else if index == itemGroup.count - 1 {
|
||||
timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.isPinnedEventsTimeline ? .single : .last),
|
||||
timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.timelineKind == .pinned ? .single : .last),
|
||||
forKey: item.id.uniqueID)
|
||||
} else {
|
||||
timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.isPinnedEventsTimeline ? .single : .middle),
|
||||
timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.timelineKind == .pinned ? .single : .middle),
|
||||
forKey: item.id.uniqueID)
|
||||
}
|
||||
}
|
||||
@@ -868,29 +868,21 @@ private extension RoomInfoProxy {
|
||||
// MARK: - Mocks
|
||||
|
||||
extension TimelineViewModel {
|
||||
static let mock = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")),
|
||||
focussedEventID: nil,
|
||||
timelineController: MockRoomTimelineController(),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
|
||||
static let mock = mock(timelineKind: .live)
|
||||
|
||||
static let pinnedEventsTimelineMock = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")),
|
||||
focussedEventID: nil,
|
||||
timelineController: MockRoomTimelineController(timelineKind: .pinned),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
|
||||
static func mock(timelineKind: TimelineKind = .live) -> TimelineViewModel {
|
||||
TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")),
|
||||
focussedEventID: nil,
|
||||
timelineController: MockRoomTimelineController(timelineKind: timelineKind),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
|
||||
}
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
|
||||
@@ -345,7 +345,7 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview {
|
||||
isDM: true,
|
||||
isViewSourceEnabled: true,
|
||||
isCreateMediaCaptionsEnabled: true,
|
||||
isPinnedEventsTimeline: false,
|
||||
timelineKind: .live,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
|
||||
guard let actions = provider.makeActions() else { return nil }
|
||||
|
||||
|
||||
@@ -82,9 +82,9 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
||||
var switchToDefaultComposer: Bool {
|
||||
switch self {
|
||||
case .reply, .edit, .addCaption, .editCaption, .editPoll:
|
||||
return false
|
||||
false
|
||||
default:
|
||||
return true
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +92,9 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
||||
var canAppearInFailedEcho: Bool {
|
||||
switch self {
|
||||
case .copy, .edit, .redact, .viewSource, .editPoll:
|
||||
return true
|
||||
true
|
||||
default:
|
||||
return false
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +102,9 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
||||
var canAppearInRedacted: Bool {
|
||||
switch self {
|
||||
case .viewSource, .unpin, .viewInRoomTimeline:
|
||||
return true
|
||||
true
|
||||
default:
|
||||
return false
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,18 +112,27 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
||||
var isDestructive: Bool {
|
||||
switch self {
|
||||
case .redact, .report, .removeCaption:
|
||||
return true
|
||||
true
|
||||
default:
|
||||
return false
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
var canAppearInPinnedEventsTimeline: Bool {
|
||||
switch self {
|
||||
case .viewInRoomTimeline, .pin, .unpin, .forward:
|
||||
return true
|
||||
true
|
||||
default:
|
||||
return false
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
var canAppearInMediaDetails: Bool {
|
||||
switch self {
|
||||
case .viewInRoomTimeline, .redact:
|
||||
true
|
||||
default:
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
-7
@@ -17,7 +17,7 @@ struct TimelineItemMenuActionProvider {
|
||||
let isDM: Bool
|
||||
let isViewSourceEnabled: Bool
|
||||
let isCreateMediaCaptionsEnabled: Bool
|
||||
let isPinnedEventsTimeline: Bool
|
||||
let timelineKind: TimelineKind
|
||||
let emojiProvider: EmojiProviderProtocol
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
@@ -38,6 +38,10 @@ struct TimelineItemMenuActionProvider {
|
||||
|
||||
var actions: [TimelineItemMenuAction] = []
|
||||
var secondaryActions: [TimelineItemMenuAction] = []
|
||||
|
||||
if timelineKind == .pinned || timelineKind == .media(.mediaFilesScreen) {
|
||||
actions.append(.viewInRoomTimeline)
|
||||
}
|
||||
|
||||
if item.canBeRepliedTo {
|
||||
if let messageItem = item as? EventBasedMessageTimelineItemProtocol {
|
||||
@@ -99,10 +103,15 @@ struct TimelineItemMenuActionProvider {
|
||||
secondaryActions.append(.redact)
|
||||
}
|
||||
|
||||
if isPinnedEventsTimeline {
|
||||
actions.insert(.viewInRoomTimeline, at: 0)
|
||||
switch timelineKind {
|
||||
case .pinned:
|
||||
actions = actions.filter(\.canAppearInPinnedEventsTimeline)
|
||||
secondaryActions = secondaryActions.filter(\.canAppearInPinnedEventsTimeline)
|
||||
case .media:
|
||||
actions = actions.filter(\.canAppearInMediaDetails)
|
||||
secondaryActions = secondaryActions.filter(\.canAppearInMediaDetails)
|
||||
case .live, .detached:
|
||||
break // viewInRoomTimeline is the only non-room item and was added conditionally.
|
||||
}
|
||||
|
||||
if item.hasFailedToSend {
|
||||
@@ -114,11 +123,10 @@ struct TimelineItemMenuActionProvider {
|
||||
actions = actions.filter(\.canAppearInRedacted)
|
||||
secondaryActions = secondaryActions.filter(\.canAppearInRedacted)
|
||||
}
|
||||
|
||||
let isReactable = timelineKind == .live || timelineKind == .detached ? item.isReactable : false
|
||||
|
||||
return .init(isReactable: isPinnedEventsTimeline ? false : item.isReactable,
|
||||
actions: actions,
|
||||
secondaryActions: secondaryActions,
|
||||
emojiProvider: emojiProvider)
|
||||
return .init(isReactable: isReactable, actions: actions, secondaryActions: secondaryActions, emojiProvider: emojiProvider)
|
||||
}
|
||||
|
||||
private func makeEncryptedItemActions(_ encryptedItem: EncryptedRoomTimelineItem) -> TimelineItemMenuActions? {
|
||||
|
||||
@@ -20,7 +20,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
private var isEncryptedOneToOneRoom: Bool { context.viewState.isEncryptedOneToOneRoom }
|
||||
private var isFocussed: Bool { focussedEventID != nil && timelineItem.id.eventID == focussedEventID }
|
||||
private var isPinned: Bool {
|
||||
guard !context.viewState.isPinnedEventsTimeline,
|
||||
guard context.viewState.timelineKind != .pinned,
|
||||
let eventID = timelineItem.id.eventID else {
|
||||
return false
|
||||
}
|
||||
@@ -110,7 +110,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
}
|
||||
|
||||
// Do not display reactions in the pinned events timeline
|
||||
if !context.viewState.isPinnedEventsTimeline,
|
||||
if context.viewState.timelineKind != .pinned,
|
||||
!timelineItem.properties.reactions.isEmpty {
|
||||
TimelineReactionsView(context: context,
|
||||
itemID: timelineItem.id,
|
||||
@@ -150,7 +150,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
isDM: context.viewState.isEncryptedOneToOneRoom,
|
||||
isViewSourceEnabled: context.viewState.isViewSourceEnabled,
|
||||
isCreateMediaCaptionsEnabled: context.viewState.isCreateMediaCaptionsEnabled,
|
||||
isPinnedEventsTimeline: context.viewState.isPinnedEventsTimeline,
|
||||
timelineKind: context.viewState.timelineKind,
|
||||
emojiProvider: context.viewState.emojiProvider)
|
||||
TimelineItemMacContextMenu(item: timelineItem, actionProvider: provider) { action in
|
||||
context.send(viewAction: .handleTimelineItemMenuAction(itemID: timelineItem.id, action: action))
|
||||
|
||||
@@ -23,7 +23,7 @@ struct TimelineItemStatusView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var mainContent: some View {
|
||||
if context.viewState.isPinnedEventsTimeline {
|
||||
if context.viewState.timelineKind == .pinned {
|
||||
// Do not display any status when is a pinned events timeline
|
||||
EmptyView()
|
||||
} else if context.viewState.showReadReceipts, !timelineItem.properties.orderedReadReceipts.isEmpty {
|
||||
|
||||
@@ -12,7 +12,7 @@ struct PollRoomTimelineView: View {
|
||||
@EnvironmentObject private var context: TimelineViewModel.Context
|
||||
|
||||
private var state: PollViewState {
|
||||
if context.viewState.isPinnedEventsTimeline {
|
||||
if context.viewState.timelineKind == .pinned {
|
||||
return .preview
|
||||
} else {
|
||||
return .full(isEditable: timelineItem.isEditable)
|
||||
@@ -51,7 +51,7 @@ struct PollRoomTimelineView: View {
|
||||
|
||||
struct PollRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = TimelineViewModel.mock
|
||||
static let pinnedEventsTimelineViewModel = TimelineViewModel.pinnedEventsTimelineMock
|
||||
static let pinnedEventsTimelineViewModel = TimelineViewModel.mock(timelineKind: .pinned)
|
||||
|
||||
static var previews: some View {
|
||||
PollRoomTimelineView(timelineItem: .mock(poll: .disclosed(), isOutgoing: false))
|
||||
|
||||
@@ -171,7 +171,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
||||
let timeline = try await TimelineProxy(timeline: room.messageFilteredTimeline(internalIdPrefix: nil,
|
||||
allowedMessageTypes: allowedMessageTypes,
|
||||
dateDividerMode: .monthly),
|
||||
kind: .media)
|
||||
kind: .media(.mediaFilesScreen))
|
||||
await timeline.subscribeForUpdates()
|
||||
|
||||
return .success(timeline)
|
||||
|
||||
@@ -9,11 +9,13 @@ import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
enum TimelineKind {
|
||||
enum TimelineKind: Equatable {
|
||||
case live
|
||||
case detached
|
||||
case pinned
|
||||
case media
|
||||
|
||||
enum MediaPresentation { case roomScreen, mediaFilesScreen }
|
||||
case media(MediaPresentation)
|
||||
}
|
||||
|
||||
enum TimelineProxyError: Error {
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6f6b9b7eca06b7b1afcd0616f0e79047478581e0fb526dfeae6b8439114b0226
|
||||
size 117138
|
||||
oid sha256:a30bdebdc1c816e6ca11cc4259749ec3e7f873e89c38e5cc64b23b5b7dcf7ebb
|
||||
size 129684
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8f6ff841733941f55b065477a234c318de67cc068870ea560d8650093556e385
|
||||
size 106118
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a7196cc10b241f834aecd1002c58f4ccb2cb11781e4981b8cb6a6b9470a889cb
|
||||
size 107373
|
||||
oid sha256:a6045b90ca6ad9b362f26505caeda788504d98461a4bdc6003603f79ae25cc45
|
||||
size 112315
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:49a257b2f3211b9fdfca3c28370c070215fd430df8bbde86627f248422350aa2
|
||||
size 125751
|
||||
oid sha256:90f32840b85ff12b6de880e8ab1890f4442b873fac71083093324fc02e9700d3
|
||||
size 128067
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7a1466df7ac398bb7f47bad54aeb8f6a3c6fb7ed29bcfa839feb4c39969afe19
|
||||
size 106123
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:95120569dffed5d1fe5be2022dfa5b90c31bb81c67faf7610274142d57e2aee2
|
||||
size 114855
|
||||
oid sha256:3ac674ce8b1e9f8c508ed259d9dc50a4b58f2304d9a3e2d9903121676502091d
|
||||
size 111333
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1104e19b712f200d87642dc62242940a4c568d1fb83e03d47136706db313e950
|
||||
size 70169
|
||||
oid sha256:339faa15c493a6e36f0c567b89eb6d036010c5637a0be03f15f7915306612320
|
||||
size 82212
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:444149b32adb1da10c83c3f6a19da8bff11b26be4cda4a88e90d5ed50ac61b3d
|
||||
size 59813
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:22c63f9b069c435c5f809d97b0b8c21755b4deb233bae72754fe168e65fef78f
|
||||
size 62044
|
||||
oid sha256:0cd944ccb9183bfa3c39e7a866b38835baa6c158338c74e05f816f99268d892e
|
||||
size 66083
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:27efc1726973b2bab72413648b4f4c26037185fb147dc28b9901733804c9c78a
|
||||
size 77471
|
||||
oid sha256:04241b16f28aa03f42de4e365eadf80fca0637494e891eb5c81a337d7693f476
|
||||
size 80107
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ce1be4e6bf0c06b2a1d0046d3c8eed0830292880365a10b048f02bdd15688246
|
||||
size 59774
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d268bbbaf1996132ff6ed23ba92afcec1b5e6307aec34b17e40edb549754c996
|
||||
size 68547
|
||||
oid sha256:54f7a21d3defe67d94c06aa226b503fcc30de8717043e5ca13242c76a4272bc4
|
||||
size 64878
|
||||
|
||||
@@ -21,9 +21,9 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
// Given a fresh view model.
|
||||
setupViewModel()
|
||||
XCTAssertFalse(mediaProvider.loadFileFromSourceFilenameCalled)
|
||||
XCTAssertNil(context.viewState.currentItem)
|
||||
XCTAssertEqual(context.viewState.currentItem, context.viewState.previewItems[0])
|
||||
|
||||
// When setting the current item.
|
||||
// When the preview controller sets the current item.
|
||||
await viewModel.updateCurrentItem(context.viewState.previewItems[0])
|
||||
|
||||
// Then the view model should load the item and update its view state.
|
||||
@@ -34,22 +34,21 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupViewModel() {
|
||||
let previewItems = [
|
||||
ImageRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: false,
|
||||
sender: .init(id: "", displayName: "Sally Sanderson"),
|
||||
content: .init(filename: "Amazing image.jpeg",
|
||||
caption: "A caption goes right here.",
|
||||
imageInfo: .mockImage,
|
||||
thumbnailInfo: .mockThumbnail))
|
||||
]
|
||||
let item = ImageRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: false,
|
||||
sender: .init(id: "", displayName: "Sally Sanderson"),
|
||||
content: .init(filename: "Amazing image.jpeg",
|
||||
caption: "A caption goes right here.",
|
||||
imageInfo: .mockImage,
|
||||
thumbnailInfo: .mockThumbnail))
|
||||
|
||||
mediaProvider = MediaProviderMock(configuration: .init())
|
||||
viewModel = TimelineMediaPreviewViewModel(previewItems: previewItems,
|
||||
viewModel = TimelineMediaPreviewViewModel(initialItem: item,
|
||||
timelineViewModel: TimelineViewModel.mock,
|
||||
mediaProvider: mediaProvider,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user