Update MediaGallery to tolerate undownloaded attachments

This commit is contained in:
Pete Walters 2026-05-11 17:15:04 -05:00 committed by GitHub
parent 2abb4f2508
commit 5e0bfaadbf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 195 additions and 156 deletions

View File

@ -519,20 +519,14 @@ extension AudioAttachment {
case .attachmentStream(let stream, _):
return ByteCountFormatter().string(for: stream.attachmentStream.unencryptedByteCount) ?? ""
case .attachmentPointer:
owsFailDebug("Shouldn't get here - undownloaded media not implemented")
// TODO: [Media Gallery]: Source byte information for undownloaded attachment
return ""
}
}
var dateString: String {
switch state {
case .attachmentStream:
let dateFormatter = DateFormatter()
dateFormatter.setLocalizedDateFormatFromTemplate("Mdyy")
return dateFormatter.string(from: receivedAtDate)
case .attachmentPointer:
owsFailDebug("Shouldn't get here - undownloaded media not implemented")
return ""
}
let dateFormatter = DateFormatter()
dateFormatter.setLocalizedDateFormatFromTemplate("Mdyy")
return dateFormatter.string(from: receivedAtDate)
}
}

View File

@ -437,21 +437,25 @@ class CVComponentBodyMedia: CVComponentBase, CVComponent {
componentDelegate.didCancelDownload(message, attachmentId: pointer.attachment.id)
return true
}
case .stream(let stream, isUploading: _, imageMetadata: _):
case .stream(let referencedAttachmentStream, isUploading: _, imageMetadata: _):
let itemViewModel = CVItemViewModelImpl(renderItem: renderItem)
if let item = items.first(where: { $0.attachment.attachment.attachment.id == stream.attachment.id }), item.isBroken {
if let item = items.first(where: { $0.attachment.attachment.attachment.id == referencedAttachmentStream.attachment.id }), item.isBroken {
componentDelegate.didTapBrokenVideo()
return true
}
componentDelegate.didTapBodyMedia(
itemViewModel: itemViewModel,
attachmentStream: stream,
attachment: referencedAttachmentStream,
imageView: mediaView,
)
return true
case .backupThumbnail:
// Download the fullsize attachment
componentDelegate.didTapSkippedDownloads(message)
case .backupThumbnail(let thumbnail):
let itemViewModel = CVItemViewModelImpl(renderItem: renderItem)
componentDelegate.didTapBodyMedia(
itemViewModel: itemViewModel,
attachment: thumbnail,
imageView: mediaView,
)
return true
case .undownloadable:
componentDelegate.didTapUndownloadableMedia()

View File

@ -134,7 +134,7 @@ public protocol CVComponentDelegate: AnyObject, AudioMessageViewDelegate, CVPoll
func didTapBodyMedia(
itemViewModel: CVItemViewModelImpl,
attachmentStream: ReferencedAttachmentStream,
attachment: ReferencedAttachment,
imageView: UIView,
)

View File

@ -483,7 +483,7 @@ extension ConversationViewController: CVComponentDelegate {
public func didTapBodyMedia(
itemViewModel: CVItemViewModelImpl,
attachmentStream: ReferencedAttachmentStream,
attachment: ReferencedAttachment,
imageView: UIView,
) {
AssertIsOnMainThread()
@ -492,7 +492,7 @@ extension ConversationViewController: CVComponentDelegate {
guard
let pageVC = MediaPageViewController(
initialMediaAttachment: attachmentStream,
initialMediaAttachment: attachment,
thread: self.thread,
spoilerState: self.viewState.spoilerState,
)

View File

@ -333,7 +333,7 @@ extension ConversationViewController: MediaPresentationContextProvider {
return nil
}
guard let mediaView = messageCell.albumItemView(forAttachment: galleryItem.attachmentStream) else {
guard let mediaView = messageCell.albumItemView(forAttachment: galleryItem.referencedAttachment) else {
owsFailDebug("itemView was unexpectedly nil")
return nil
}

View File

@ -404,7 +404,7 @@ extension EditHistoryTableSheetViewController: CVComponentDelegate {
func didTapBodyMedia(
itemViewModel: CVItemViewModelImpl,
attachmentStream: ReferencedAttachmentStream,
attachment: ReferencedAttachment,
imageView: UIView,
) {}

View File

@ -14,16 +14,9 @@ class AudioCell: MediaTileListModeCell {
private var audioItem: MediaGalleryCellItemAudio? {
didSet {
guard let audioItem else {
if audioItem == nil {
audioAttachment = nil
return
}
audioAttachment = AudioAttachment(
attachmentStream: audioItem.attachmentStream,
owningMessage: audioItem.message,
metadata: audioItem.metadata,
receivedAtDate: audioItem.receivedAtDate,
)
}
}
@ -45,7 +38,7 @@ class AudioCell: MediaTileListModeCell {
let currentContentSizeCategory = UITraitCollection.current.preferredContentSizeCategory
let displaysTopLabel = AudioAllMediaPresenter.hasAttachmentLabel(
attachment: audioItem.attachmentStream.attachment,
attachment: audioItem.referencedAttachment.attachment,
isVoiceMessage: audioItem.isVoiceMessage,
)
@ -60,8 +53,9 @@ class AudioCell: MediaTileListModeCell {
}
guard
let stream = audioItem.referencedAttachment.asReferencedStream,
let audioAttachment = AudioAttachment(
attachmentStream: audioItem.attachmentStream,
attachmentStream: stream,
owningMessage: audioItem.message,
metadata: audioItem.metadata,
receivedAtDate: audioItem.receivedAtDate,
@ -266,7 +260,7 @@ class AudioCell: MediaTileListModeCell {
let cvAudioPlayer = AppEnvironment.shared.cvAudioPlayerRef
cvAudioPlayer.setPlaybackProgress(
progress: scrubbedTime,
forAttachment: audioItem.attachmentStream.attachment,
forAttachment: audioItem.referencedAttachment.attachment,
)
case .possible, .failed, .cancelled:
audioMessageView.clearOverrideProgress(animated: false)
@ -283,6 +277,7 @@ class AudioCell: MediaTileListModeCell {
guard let itemModel, let audioMessageView, let audioItem, let audioAttachment else {
return
}
guard case .attachmentStream = audioAttachment.state else { return }
if audioMessageView.handleTap(sender: sender, itemModel: itemModel) {
return
}
@ -317,7 +312,6 @@ class AudioCell: MediaTileListModeCell {
owsFailDebug("Unexpected item type")
return
}
self.audioItem = audioItem
self.spoilerState = spoilerState
if let audioMessageView {
@ -325,7 +319,24 @@ class AudioCell: MediaTileListModeCell {
self.audioMessageView = nil
}
self.audioItem = audioItem
SSKEnvironment.shared.databaseStorageRef.read { transaction in
if let referencedStream = audioItem.referencedAttachment.asReferencedStream {
audioAttachment = AudioAttachment(
attachmentStream: referencedStream,
owningMessage: audioItem.message,
metadata: audioItem.metadata,
receivedAtDate: audioItem.receivedAtDate,
)
} else if let pointer = audioItem.referencedAttachment.asReferencedAnyPointer {
audioAttachment = AudioAttachment(
attachmentPointer: pointer,
owningMessage: audioItem.message,
metadata: audioItem.metadata,
receivedAtDate: audioItem.receivedAtDate,
downloadState: pointer.attachmentPointer.downloadState(tx: transaction),
)
}
createAudioMessageView(transaction: transaction)
}
}

View File

@ -12,7 +12,7 @@ class MediaGalleryFileCell: MediaTileListModeCell {
static let reuseIdentifier = "MediaGalleryFileCell"
private var attachment: ReferencedAttachmentStream?
private var referencedAttachment: ReferencedAttachment?
private var receivedAtDate: Date?
private var owningMessage: TSMessage?
private var mediaMetadata: MediaMetadata?
@ -20,13 +20,13 @@ class MediaGalleryFileCell: MediaTileListModeCell {
private var fileItem: MediaGalleryCellItemOtherFile? {
didSet {
guard let fileItem else {
attachment = nil
referencedAttachment = nil
receivedAtDate = nil
owningMessage = nil
mediaMetadata = nil
return
}
attachment = fileItem.attachmentStream
referencedAttachment = fileItem.referencedAttachment
receivedAtDate = fileItem.receivedAtDate
owningMessage = fileItem.message
mediaMetadata = fileItem.metadata
@ -58,7 +58,7 @@ class MediaGalleryFileCell: MediaTileListModeCell {
return cellHeight
}
guard let attachment = item.attachmentStream else {
guard let attachment = item.referencedAttachment?.asReferencedStream else {
return defaultCellHeight
}
let genericAttachment = CVComponentState.GenericAttachment(
@ -86,6 +86,7 @@ class MediaGalleryFileCell: MediaTileListModeCell {
private var itemModel: CVItemModel?
private var genericAttachmentView: CVComponentView?
private var attachmentType: CVAttachment?
private let genericAttachmentContainerView: UIView = {
let view = UIView.container()
@ -157,11 +158,21 @@ class MediaGalleryFileCell: MediaTileListModeCell {
itemViewState: itemViewState.build(),
coreState: coreState,
)
let genericAttachment = CVComponentState.GenericAttachment(attachment: .stream(
fileItem.attachmentStream,
isUploading: false,
imageMetadata: nil,
))
self.attachmentType = {
if let stream = fileItem.referencedAttachment.asReferencedStream {
return .stream(stream, isUploading: false, imageMetadata: nil)
} else if let backupPointer = fileItem.referencedAttachment.asReferencedBackupThumbnail {
return .backupThumbnail(backupPointer)
} else if let pointer = fileItem.referencedAttachment.asReferencedAnyPointer {
let downloadState = pointer.attachmentPointer.downloadState(tx: transaction)
return .pointer(pointer, downloadState: downloadState)
}
return nil
}()
guard let attachmentType else { return }
let genericAttachment = CVComponentState.GenericAttachment(attachment: attachmentType)
let component = CVComponentGenericAttachment(
itemModel: itemModel,
genericAttachment: genericAttachment,
@ -222,16 +233,15 @@ class MediaGalleryFileCell: MediaTileListModeCell {
@objc
private func handleTapGesture(_ sender: UITapGestureRecognizer) {
guard let fileItem, let itemModel else {
guard
let itemModel,
let attachmentType
else {
return
}
let genericAttachment = CVComponentGenericAttachment(
itemModel: itemModel,
genericAttachment: .init(attachment: .stream(
fileItem.attachmentStream,
isUploading: false,
imageMetadata: nil,
)),
genericAttachment: .init(attachment: attachmentType),
)
if
PKAddPassesViewController.canAddPasses(),
@ -419,7 +429,7 @@ extension MediaGalleryFileCell: CVComponentDelegate {
func didTapBodyMedia(
itemViewModel: CVItemViewModelImpl,
attachmentStream: ReferencedAttachmentStream,
attachment: ReferencedAttachment,
imageView: UIView,
) {}

View File

@ -40,10 +40,10 @@ struct MediaGalleryItem: Equatable, Hashable, MediaGallerySectionItem {
let message: TSMessage
let sender: Sender?
let attachmentStream: ReferencedAttachmentStream
let referencedAttachment: ReferencedAttachment
let receivedAtDate: Date
var renderingFlag: AttachmentReference.RenderingFlag { attachmentStream.reference.renderingFlag }
var renderingFlag: AttachmentReference.RenderingFlag { referencedAttachment.reference.renderingFlag }
let galleryDate: GalleryDate
let captionForDisplay: MediaCaptionView.Content?
@ -54,7 +54,7 @@ struct MediaGalleryItem: Equatable, Hashable, MediaGallerySectionItem {
init(
message: TSMessage,
sender: Sender?,
attachmentStream: ReferencedAttachmentStream,
referencedAttachment: ReferencedAttachment,
albumIndex: Int,
numItemsInAlbum: Int,
spoilerState: SpoilerRenderState,
@ -62,13 +62,13 @@ struct MediaGalleryItem: Equatable, Hashable, MediaGallerySectionItem {
) {
self.message = message
self.sender = sender
self.attachmentStream = attachmentStream
self.referencedAttachment = referencedAttachment
self.receivedAtDate = message.receivedAtDate
self.galleryDate = GalleryDate(message: message)
self.albumIndex = albumIndex
self.numItemsInAlbum = numItemsInAlbum
self.orderingKey = MediaGalleryItemOrderingKey(messageSortKey: message.sortId, attachmentSortKey: albumIndex)
if let captionText = attachmentStream.reference.legacyMessageCaption?.filterForDisplay {
if let captionText = referencedAttachment.reference.legacyMessageCaption?.filterForDisplay {
self.captionForDisplay = .attachmentStreamCaption(captionText)
} else if let body = message.body {
let hydratedMessageBody = MessageBody(
@ -83,11 +83,15 @@ struct MediaGalleryItem: Equatable, Hashable, MediaGallerySectionItem {
}
}
private var mimeType: String { attachmentStream.attachmentStream.mimeType }
private var mimeType: String { referencedAttachment.attachment.mimeType }
var isVideo: Bool {
switch attachmentStream.attachmentStream.contentType {
switch referencedAttachment.attachment.contentType {
case .video:
// For now, if the video isn't a stream, don't treat it as a video.
if referencedAttachment.asReferencedStream == nil {
return false
}
return renderingFlag != .shouldLoop
case .file, .image, .audio:
return false
@ -95,7 +99,7 @@ struct MediaGalleryItem: Equatable, Hashable, MediaGallerySectionItem {
}
var isAnimated: Bool {
switch attachmentStream.attachmentStream.contentType {
switch referencedAttachment.attachment.contentType {
case .video:
return renderingFlag == .shouldLoop
case .file, .image, .audio:
@ -104,7 +108,7 @@ struct MediaGalleryItem: Equatable, Hashable, MediaGallerySectionItem {
}
var isImage: Bool {
switch attachmentStream.attachmentStream.contentType {
switch referencedAttachment.attachment.contentType {
case .image:
return true
case .file, .video, .audio:
@ -112,33 +116,33 @@ struct MediaGalleryItem: Equatable, Hashable, MediaGallerySectionItem {
}
}
var attachmentId: AttachmentReferenceId { attachmentStream.reference.referenceId }
var attachmentId: AttachmentReferenceId { referencedAttachment.reference.referenceId }
typealias AsyncThumbnailBlock = @MainActor (UIImage) -> Void
func thumbnailImage(completion: @escaping AsyncThumbnailBlock) {
Task { [attachmentStream] in
if let image = await attachmentStream.attachmentStream.thumbnailImage(quality: .small) {
Task { [referencedAttachment] in
if let image = await referencedAttachment.getThumbnailImage(quality: .small) {
await completion(image)
}
}
}
func thumbnailImageSync() -> UIImage? {
return attachmentStream.attachmentStream.thumbnailImageSync(quality: .small)
referencedAttachment.getThumbnailImageSync(quality: .small)
}
// MARK: Equatable
static func ==(lhs: MediaGalleryItem, rhs: MediaGalleryItem) -> Bool {
return lhs.attachmentStream.attachmentStream.id == rhs.attachmentStream.attachmentStream.id
&& lhs.attachmentStream.reference.hasSameOwner(as: rhs.attachmentStream.reference)
return lhs.referencedAttachment.attachment.id == rhs.referencedAttachment.attachment.id
&& lhs.referencedAttachment.reference.hasSameOwner(as: rhs.referencedAttachment.reference)
}
// MARK: Hashable
func hash(into hasher: inout Hasher) {
hasher.combine(attachmentStream.attachmentStream.id)
let attachmentReference = attachmentStream.reference
hasher.combine(referencedAttachment.attachment.id)
let attachmentReference = referencedAttachment.reference
hasher.combine(attachmentReference.owner.id)
}
@ -484,10 +488,6 @@ class MediaGallery {
spoilerState: SpoilerRenderState,
transaction: DBReadTransaction,
) -> MediaGalleryItem? {
guard let attachmentStream = attachment.attachment.asStream() else {
owsFailDebug("gallery doesn't yet support showing undownloaded attachments")
return nil
}
let message: TSMessage
switch attachment.reference.owner {
@ -560,7 +560,7 @@ class MediaGallery {
return MediaGalleryItem(
message: message,
sender: sender,
attachmentStream: .init(reference: attachment.reference, attachmentStream: attachmentStream),
referencedAttachment: .init(reference: attachment.reference, attachment: attachment.attachment),
albumIndex: Int(albumIndex),
numItemsInAlbum: itemsInAlbum.count,
spoilerState: spoilerState,
@ -674,7 +674,7 @@ class MediaGallery {
shouldLoadAlbumRemainder: Bool,
) {
guard let path = indexPath(for: item) else {
owsFailDebug("showing detail view for an item that hasn't been loaded: \(item.attachmentStream)")
owsFailDebug("showing detail view for an item that hasn't been loaded: \(item.referencedAttachment)")
return
}
@ -702,7 +702,7 @@ class MediaGallery {
guard
let itemId = mediaGalleryFinder.galleryItemId(
of: focusedItem.attachmentStream,
of: focusedItem.referencedAttachment,
in: focusedItem.galleryDate.interval,
excluding: deletedAttachmentIds,
tx: transaction,
@ -839,13 +839,13 @@ class MediaGallery {
return
}
Logger.info("with items: \(items.map { ($0.attachmentStream, $0.message.timestamp) })")
Logger.info("with items: \(items.map { ($0.referencedAttachment, $0.message.timestamp) })")
deletedGalleryItems.formUnion(items)
delegates.forEach { $0.mediaGallery(self, willDelete: items, initiatedBy: initiatedBy) }
deletedAttachmentIds.formUnion(items.lazy.map {
$0.attachmentStream.reference.referenceId
$0.referencedAttachment.reference.referenceId
})
Task {
@ -858,7 +858,7 @@ class MediaGallery {
for item in items {
let message = item.message
let referencedAttachment: ReferencedAttachment = item.attachmentStream
let referencedAttachment: ReferencedAttachment = item.referencedAttachment
attachmentStore.removeReference(
reference: referencedAttachment.reference,

View File

@ -22,14 +22,14 @@ enum MediaGalleryCellItem {
case audio(MediaGalleryCellItemAudio)
case otherFile(MediaGalleryCellItemOtherFile)
var attachmentStream: ReferencedAttachmentStream? {
var referencedAttachment: ReferencedAttachment? {
switch self {
case .photoVideo(let item):
return item.galleryItem.attachmentStream
return item.galleryItem.referencedAttachment
case .audio(let audioItem):
return audioItem.attachmentStream
return audioItem.referencedAttachment
case .otherFile(let fileItem):
return fileItem.attachmentStream
return fileItem.referencedAttachment
}
}
}
@ -40,9 +40,9 @@ extension MediaGalleryCellItem: Equatable {
case let (.photoVideo(lvalue), .photoVideo(rvalue)):
return lvalue === rvalue
case let (.audio(lvalue), .audio(rvalue)):
return lvalue.attachmentStream.reference.attachmentRowId == rvalue.attachmentStream.reference.attachmentRowId
return lvalue.referencedAttachment.reference.attachmentRowId == rvalue.referencedAttachment.reference.attachmentRowId
case let (.otherFile(lvalue), .otherFile(rvalue)):
return lvalue.attachmentStream.reference.attachmentRowId == rvalue.attachmentStream.reference.attachmentRowId
return lvalue.referencedAttachment.reference.attachmentRowId == rvalue.referencedAttachment.reference.attachmentRowId
case (.photoVideo, _), (.audio, _), (.otherFile, _):
return false
}
@ -53,7 +53,7 @@ struct MediaGalleryCellItemAudio {
var message: TSMessage
var interaction: TSInteraction
var thread: TSThread
var attachmentStream: ReferencedAttachmentStream
var referencedAttachment: ReferencedAttachment
var receivedAtDate: Date
var isVoiceMessage: Bool
var mediaCache: CVMediaCache
@ -78,13 +78,13 @@ struct MediaGalleryCellItemOtherFile {
var message: TSMessage
var interaction: TSInteraction
var thread: TSThread
var attachmentStream: ReferencedAttachmentStream
var referencedAttachment: ReferencedAttachment
var receivedAtDate: Date
var mediaCache: CVMediaCache
var metadata: MediaMetadata
var size: UInt {
UInt(attachmentStream.attachmentStream.unencryptedByteCount)
var size: UInt64 {
referencedAttachment.unencryptedByteCount() ?? 0
}
var localizedString: String {
@ -105,7 +105,7 @@ class MediaGalleryCellItemPhotoVideo: PhotoGridItem {
var type: PhotoGridItemType {
if galleryItem.isVideo {
return .video(
duration: galleryItem.attachmentStream.attachmentStream.cachedVideoDuration,
duration: galleryItem.referencedAttachment.asReferencedStream?.attachmentStream.cachedVideoDuration,
)
} else if galleryItem.isAnimated {
return .animated
@ -128,7 +128,7 @@ extension MediaGalleryItem {
return MediaMetadata(
sender: sender?.name ?? "",
abbreviatedSender: sender?.abbreviatedName ?? "",
byteSize: Int(attachmentStream.attachmentStream.unencryptedByteCount),
byteSize: Int(clamping: referencedAttachment.unencryptedByteCount() ?? 0),
creationDate: receivedAtDate,
)
}

View File

@ -33,7 +33,7 @@ class MediaItemViewController: OWSViewController, VideoPlaybackStatusProvider {
super.init()
image = attachmentStream.thumbnailImageSync(quality: .large)
image = galleryItem.referencedAttachment.getThumbnailImageSync(quality: .large)
}
deinit {
@ -116,37 +116,43 @@ class MediaItemViewController: OWSViewController, VideoPlaybackStatusProvider {
guard mediaView == nil else { return }
let view: UIView
if attachmentStream.contentType.isVideo, galleryItem.renderingFlag == .shouldLoop {
if let loopingVideoPlayerView = buildLoopingVideoPlayerView() {
let referencedAttachmentStream = galleryItem.referencedAttachment.asReferencedStream
if
let referencedAttachmentStream,
referencedAttachmentStream.attachmentStream.contentType.isVideo,
galleryItem.renderingFlag == .shouldLoop
{
if let loopingVideoPlayerView = buildLoopingVideoPlayerView(attachmentStream: referencedAttachmentStream.attachmentStream) {
loopingVideoPlayerView.delegate = self
view = loopingVideoPlayerView
} else {
view = buildPlaceholderView()
}
} else if
let imageMetadata = attachmentStream.imageMetadata(),
let referencedAttachmentStream,
let imageMetadata = referencedAttachmentStream.attachmentStream.imageMetadata(),
imageMetadata.isAnimated
{
if let animatedGif = try? attachmentStream.decryptedSDAnimatedImage() {
if let animatedGif = try? referencedAttachmentStream.attachmentStream.decryptedSDAnimatedImage() {
view = SDAnimatedImageView(image: animatedGif)
} else {
view = buildPlaceholderView()
}
} else if image == nil {
// Still loading thumbnail.
view = buildPlaceholderView()
} else if isVideo {
if attachmentStream.contentType.isVideo, let videoPlayerView = buildVideoPlayerView() {
videoPlayerView.delegate = self
videoPlayerView.videoPlayer?.delegate = self
} else if
let referencedAttachmentStream,
isVideo, // TODO: Separate isVideo from isVideoPlayable
let videoPlayerView = buildVideoPlayerView(referencedAttachmentStream: referencedAttachmentStream)
{
videoPlayerView.delegate = self
videoPlayerView.videoPlayer?.delegate = self
view = videoPlayerView
} else {
view = buildPlaceholderView()
}
} else {
view = videoPlayerView
} else if let image {
// Present the static image using standard UIImageView
view = UIImageView(image: image)
} else {
// Still loading thumbnail.
view = buildPlaceholderView()
}
mediaView = view
@ -158,7 +164,7 @@ class MediaItemViewController: OWSViewController, VideoPlaybackStatusProvider {
return view
}
private func buildLoopingVideoPlayerView() -> LoopingVideoView? {
private func buildLoopingVideoPlayerView(attachmentStream: AttachmentStream) -> LoopingVideoView? {
guard let loopingVideo = LoopingVideo(attachmentStream) else {
owsFailBeta("Invalid looping video")
return nil
@ -168,8 +174,8 @@ class MediaItemViewController: OWSViewController, VideoPlaybackStatusProvider {
return videoView
}
private func buildVideoPlayerView() -> VideoPlayerView? {
guard let videoPlayer = try? VideoPlayer(attachment: galleryItem.attachmentStream) else {
private func buildVideoPlayerView(referencedAttachmentStream: ReferencedAttachmentStream) -> VideoPlayerView? {
guard let videoPlayer = try? VideoPlayer(attachment: referencedAttachmentStream) else {
owsFailBeta("Invalid attachment")
return nil
}
@ -224,7 +230,7 @@ class MediaItemViewController: OWSViewController, VideoPlaybackStatusProvider {
}
let timestamp = Date().ows_millisecondsSince1970
let attachmentId = galleryItem.attachmentStream.attachment.id
let attachmentId = galleryItem.referencedAttachment.attachment.id
Task {
await DependenciesBridge.shared.db.awaitableWrite { tx in
DependenciesBridge.shared.attachmentStore.markViewedFullscreen(
@ -246,8 +252,6 @@ class MediaItemViewController: OWSViewController, VideoPlaybackStatusProvider {
private var image: UIImage?
private var attachmentStream: AttachmentStream { galleryItem.attachmentStream.attachmentStream }
// MARK: - Video Playback
var shouldAutoPlayVideo: Bool = false

View File

@ -547,8 +547,13 @@ class MediaPageViewController: UIPageViewController {
private func shareCurrentMedia(fromNavigationBar: Bool) {
guard let currentViewController else { return }
guard let stream = currentViewController.galleryItem.referencedAttachment.asReferencedStream else {
// TODO: [MediaGallery]: Handle undownloaded media
owsFailDebug("Cannot share undownloaded media")
return
}
guard
let attachmentStream = (try? [currentViewController.galleryItem.attachmentStream].asShareableAttachments())?.first
let attachmentStream = (try? [stream].asShareableAttachments())?.first
else {
return
}
@ -560,9 +565,13 @@ class MediaPageViewController: UIPageViewController {
private func saveCurrentMediaToPhotos() {
guard let mediaItem = currentItem else { return }
guard let stream = mediaItem.referencedAttachment.asReferencedStream else {
// TODO: [MediaGallery]: Handle undownloaded media
owsFailDebug("Cannot share undownloaded media")
return
}
AttachmentSaving.saveToPhotoLibrary(
referencedAttachmentStreams: [mediaItem.attachmentStream],
referencedAttachmentStreams: [stream],
)
}
@ -835,7 +844,7 @@ extension MediaPageViewController: MediaGalleryDelegate {
}
func didReloadAllSectionsInMediaGallery(_ mediaGallery: MediaGallery) {
let attachment = currentItem.attachmentStream
let attachment = currentItem.referencedAttachment
guard let reloadedItem = mediaGallery.ensureLoadedForDetailView(focusedAttachment: attachment) else {
// Assume the item was deleted.
dismissSelf(animated: true)

View File

@ -613,7 +613,7 @@ class MediaTileViewController: UICollectionViewController, MediaGalleryDelegate,
guard
let pageVC = MediaPageViewController(
initialMediaAttachment: galleryItem.attachmentStream,
initialMediaAttachment: galleryItem.referencedAttachment,
mediaGallery: mediaGallery,
spoilerState: spoilerState,
)
@ -804,7 +804,7 @@ class MediaTileViewController: UICollectionViewController, MediaGalleryDelegate,
message: galleryItem.message,
interaction: galleryItem.message,
thread: thread,
attachmentStream: galleryItem.attachmentStream,
referencedAttachment: galleryItem.referencedAttachment,
receivedAtDate: galleryItem.receivedAtDate,
isVoiceMessage: galleryItem.renderingFlag == .voiceMessage,
mediaCache: mediaCache,
@ -815,7 +815,7 @@ class MediaTileViewController: UICollectionViewController, MediaGalleryDelegate,
message: galleryItem.message,
interaction: galleryItem.message,
thread: thread,
attachmentStream: galleryItem.attachmentStream,
referencedAttachment: galleryItem.referencedAttachment,
receivedAtDate: galleryItem.receivedAtDate,
mediaCache: mediaCache,
metadata: galleryItem.mediaMetadata!,
@ -1521,7 +1521,7 @@ extension MediaTileViewController: MediaGalleryPrimaryViewController {
}
let totalSize = items.reduce(UInt64(0), { result, item in
result + UInt64(safeCast: item.attachmentStream.attachmentStream.unencryptedByteCount)
result + (item.referencedAttachment.unencryptedByteCount() ?? 0)
})
return (items.count, totalSize)
}
@ -1737,8 +1737,12 @@ extension MediaTileViewController: MediaGalleryPrimaryViewController {
return
}
let attachments = indexPaths.compactMap {
self.galleryItem(at: $0)?.attachmentStream
let attachments: [ReferencedAttachmentStream] = indexPaths.compactMap {
self.galleryItem(at: $0)?.referencedAttachment.asReferencedStream
}
if attachments.count < indexPaths.count {
// TODO: [MediaGallery] Better handling of undownloaded attachmets
owsFailDebug("Attempting to share undownloaded attachments")
}
let items: [ShareableAttachment] = (try? attachments.asShareableAttachments()) ?? []
guard items.count == indexPaths.count else {

View File

@ -12,7 +12,7 @@ enum Media {
var image: UIImage? {
switch self {
case let .gallery(item):
return try? item.attachmentStream.attachmentStream.decryptedImage()
return item.referencedAttachment.getBestAvailableLocalImage()
case let .image(image):
return image
}

View File

@ -173,11 +173,10 @@ class VideoPlaybackControlView: UIView {
weak var delegate: VideoPlaybackControlViewDelegate?
func updateWithMediaItem(_ mediaItem: MediaGalleryItem) {
let attachmentStream = mediaItem.attachmentStream.attachmentStream
switch attachmentStream.contentType {
switch mediaItem.referencedAttachment.attachment.contentType {
case .video:
if
let videoDuration = attachmentStream.cachedVideoDuration,
let videoDuration = mediaItem.referencedAttachment.asReferencedStream?.attachmentStream.cachedVideoDuration,
videoDuration > 30
{
showRewindAndFastForward = true

View File

@ -685,7 +685,7 @@ extension MemberLabelViewController: CVComponentDelegate {
func didTapBodyMedia(
itemViewModel: CVItemViewModelImpl,
attachmentStream: ReferencedAttachmentStream,
attachment: ReferencedAttachment,
imageView: UIView,
) {}

View File

@ -820,7 +820,7 @@ extension MessageDetailViewController: MediaPresentationContextProvider {
return nil
}
guard let mediaView = cellView.albumItemView(forAttachment: galleryItem.attachmentStream) else {
guard let mediaView = cellView.albumItemView(forAttachment: galleryItem.referencedAttachment) else {
owsFailDebug("itemView was unexpectedly nil")
return nil
}
@ -1160,7 +1160,7 @@ extension MessageDetailViewController: CVComponentDelegate {
func didTapBodyMedia(
itemViewModel: CVItemViewModelImpl,
attachmentStream: ReferencedAttachmentStream,
attachment: ReferencedAttachment,
imageView: UIView,
) {
guard let thread else {
@ -1169,7 +1169,7 @@ extension MessageDetailViewController: CVComponentDelegate {
}
guard
let mediaPageVC = MediaPageViewController(
initialMediaAttachment: attachmentStream,
initialMediaAttachment: attachment,
thread: thread,
spoilerState: self.spoilerState,
showingSingleMessage: true,

View File

@ -530,7 +530,7 @@ extension PinnedMessagesDetailsViewController: CVComponentDelegate {
func didTapBodyMedia(
itemViewModel: CVItemViewModelImpl,
attachmentStream: ReferencedAttachmentStream,
attachment: ReferencedAttachment,
imageView: UIView,
) {}

View File

@ -275,9 +275,9 @@ extension ConversationSettingsViewController {
let availableWidth = self.view.width - ((Self.cellHInnerMargin * 2) + self.cellOuterInsets.totalWidth + self.view.safeAreaInsets.totalWidth)
let imageWidth = (availableWidth - totalSpacerSize) / CGFloat(self.maximumRecentMedia)
for (attachmentStream, imageView) in self.recentMedia.orderedValues {
for (referencedAttachment, imageView) in self.recentMedia.orderedValues {
let button = OWSButton { [weak self] in
self?.showMediaPageView(for: attachmentStream)
self?.showMediaPageView(for: referencedAttachment)
}
stackView.addArrangedSubview(button)
button.autoSetDimensions(to: CGSize(square: imageWidth))

View File

@ -860,10 +860,10 @@ class ConversationSettingsViewController: OWSTableViewController2, BadgeCollecti
navigationController?.pushViewController(tileVC, animated: true)
}
func showMediaPageView(for attachmentStream: ReferencedAttachmentStream) {
func showMediaPageView(for referencedAttachment: ReferencedAttachment) {
guard
let vc = MediaPageViewController(
initialMediaAttachment: attachmentStream,
initialMediaAttachment: referencedAttachment,
thread: thread,
spoilerState: spoilerState,
)
@ -879,7 +879,7 @@ class ConversationSettingsViewController: OWSTableViewController2, BadgeCollecti
let maximumRecentMedia = 4
private(set) var recentMedia = OrderedDictionary<
AttachmentReferenceId,
(attachment: ReferencedAttachmentStream, imageView: UIImageView),
(attachment: ReferencedAttachment, imageView: UIImageView),
>() {
didSet { AssertIsOnMainThread() }
}
@ -894,10 +894,6 @@ class ConversationSettingsViewController: OWSTableViewController2, BadgeCollecti
mediaGalleryFinder.recentMediaAttachments(limit: maximumRecentMedia, tx: transaction)
}
recentMedia = recentAttachments.reduce(into: OrderedDictionary(), { result, attachment in
guard let attachmentStream = attachment.asReferencedStream else {
return owsFailDebug("Unexpected type of attachment")
}
let imageView = UIImageView()
imageView.clipsToBounds = true
if #available(iOS 26, *) {
@ -907,14 +903,11 @@ class ConversationSettingsViewController: OWSTableViewController2, BadgeCollecti
imageView.layer.cornerRadius = 4
}
imageView.contentMode = .scaleAspectFill
Task {
imageView.image = await attachmentStream.attachmentStream.thumbnailImage(quality: .small)
}
imageView.image = attachment.getThumbnailImageSync(quality: .small)
result.append(
key: attachmentStream.reference.referenceId,
value: (attachmentStream, imageView),
key: attachment.reference.referenceId,
value: (attachment, imageView),
)
})
shouldRefreshAttachmentsOnReappear = false
@ -1093,7 +1086,7 @@ extension ConversationSettingsViewController: MediaPresentationContextProvider {
let mediaViewShape: MediaViewShape
switch item {
case .gallery(let galleryItem):
guard let imageView = recentMedia[galleryItem.attachmentStream.reference.referenceId]?.imageView else { return nil }
guard let imageView = recentMedia[galleryItem.referencedAttachment.reference.referenceId]?.imageView else { return nil }
mediaView = imageView
mediaViewShape = .rectangle(imageView.layer.cornerRadius)
case .image:

View File

@ -422,7 +422,7 @@ extension MockConversationView: CVComponentDelegate {
func didTapBodyMedia(
itemViewModel: CVItemViewModelImpl,
attachmentStream: ReferencedAttachmentStream,
attachment: ReferencedAttachment,
imageView: UIView,
) {}

View File

@ -7,7 +7,7 @@ import Foundation
/// Just a simple structure holding an attachment and a reference to it,
/// since that's something we need to do very often.
public class ReferencedAttachment {
public class ReferencedAttachment: CustomDebugStringConvertible {
public let reference: AttachmentReference
public let attachment: Attachment
@ -54,26 +54,32 @@ public class ReferencedAttachment {
public func getThumbnailImage(quality: AttachmentThumbnailQuality) async -> UIImage? {
if let stream = asReferencedStream?.attachmentStream {
return await stream.thumbnailImage(quality: quality)
} else if let backupThumbnail = AttachmentBackupThumbnail(attachment: attachment) {
return backupThumbnail.image
}
return nil
return getPlaceholderImage()
}
public func getThumbnailImageSync(quality: AttachmentThumbnailQuality) -> UIImage? {
if let stream = asReferencedStream?.attachmentStream {
return stream.thumbnailImageSync(quality: quality)
} else if let backupThumbnail = AttachmentBackupThumbnail(attachment: attachment) {
return backupThumbnail.image
}
return nil
return getPlaceholderImage()
}
public func getBestAvailableLocalImage() -> UIImage? {
if let stream = asReferencedStream?.attachmentStream {
return try? stream.decryptedImage()
} else if let backupThumbnail = AttachmentBackupThumbnail(attachment: attachment) {
}
return getPlaceholderImage()
}
private func getPlaceholderImage() -> UIImage? {
if let backupThumbnail = AttachmentBackupThumbnail(attachment: attachment) {
return backupThumbnail.image
} else if
let blurHash = attachment.blurHash?.nilIfEmpty,
let blurHashImage = BlurHash.image(for: blurHash)
{
return blurHashImage
}
return nil
}
@ -86,6 +92,11 @@ public class ReferencedAttachment {
}
return nil
}
public var debugDescription: String {
let isStream = self as? ReferencedAttachmentStream != nil
return "ReferencedAttachment(reference: \(reference.owner.id), attachment: \(attachment.id), downloaded: \(isStream))"
}
}
public class ReferencedAttachmentStream: ReferencedAttachment {

View File

@ -241,7 +241,7 @@ public struct MediaGalleryAttachmentFinder {
// Disregards filter.
public func galleryItemId(
of attachment: ReferencedAttachmentStream,
of attachment: ReferencedAttachment,
in interval: DateInterval,
excluding deletedAttachmentIds: Set<AttachmentReferenceId>,
tx: DBReadTransaction,