diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVComponent.swift b/Signal/src/ViewControllers/ConversationView/CV/CVComponent.swift index 625a95fd3b..64492233a9 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVComponent.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVComponent.swift @@ -525,7 +525,7 @@ public enum CVComponentKey: CustomStringConvertible, CaseIterable { case unreadIndicator case typingIndicator case threadDetails - case failedDownloads + case failedOrPendingDownloads public var description: String { switch self { @@ -567,8 +567,8 @@ public enum CVComponentKey: CustomStringConvertible, CaseIterable { return ".typingIndicator" case .threadDetails: return ".threadDetails" - case .failedDownloads: - return ".failedDownloads" + case .failedOrPendingDownloads: + return ".failedOrPendingDownloads" case .sendFailureBadge: return ".sendFailureBadge" } diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVComponentState.swift b/Signal/src/ViewControllers/ConversationView/CV/CVComponentState.swift index c044abb8fe..013008b51d 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVComponentState.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVComponentState.swift @@ -216,10 +216,10 @@ public class CVComponentState: Equatable { } let bottomButtons: BottomButtons? - struct FailedDownloads: Equatable { + struct FailedOrPendingDownloads: Equatable { let attachmentPointers: [TSAttachmentPointer] } - let failedDownloads: FailedDownloads? + let failedOrPendingDownloads: FailedOrPendingDownloads? struct SendFailureBadge: Equatable { } @@ -244,7 +244,7 @@ public class CVComponentState: Equatable { typingIndicator: TypingIndicator?, threadDetails: ThreadDetails?, bottomButtons: BottomButtons?, - failedDownloads: FailedDownloads?, + failedOrPendingDownloads: FailedOrPendingDownloads?, sendFailureBadge: SendFailureBadge?) { self.messageCellType = messageCellType @@ -266,7 +266,7 @@ public class CVComponentState: Equatable { self.typingIndicator = typingIndicator self.threadDetails = threadDetails self.bottomButtons = bottomButtons - self.failedDownloads = failedDownloads + self.failedOrPendingDownloads = failedOrPendingDownloads self.sendFailureBadge = sendFailureBadge } @@ -292,7 +292,7 @@ public class CVComponentState: Equatable { lhs.typingIndicator == rhs.typingIndicator && lhs.threadDetails == rhs.threadDetails && lhs.bottomButtons == rhs.bottomButtons && - lhs.failedDownloads == rhs.failedDownloads && + lhs.failedOrPendingDownloads == rhs.failedOrPendingDownloads && lhs.sendFailureBadge == rhs.sendFailureBadge) } @@ -315,7 +315,7 @@ public class CVComponentState: Equatable { typealias UnreadIndicator = CVComponentState.UnreadIndicator typealias TypingIndicator = CVComponentState.TypingIndicator typealias ThreadDetails = CVComponentState.ThreadDetails - typealias FailedDownloads = CVComponentState.FailedDownloads + typealias FailedOrPendingDownloads = CVComponentState.FailedOrPendingDownloads typealias BottomButtons = CVComponentState.BottomButtons typealias SendFailureBadge = CVComponentState.SendFailureBadge @@ -339,7 +339,7 @@ public class CVComponentState: Equatable { var typingIndicator: TypingIndicator? var threadDetails: ThreadDetails? var reactions: Reactions? - var failedDownloads: FailedDownloads? + var failedOrPendingDownloads: FailedOrPendingDownloads? var sendFailureBadge: SendFailureBadge? var bottomButtonsActions = [CVMessageAction]() @@ -374,7 +374,7 @@ public class CVComponentState: Equatable { typingIndicator: typingIndicator, threadDetails: threadDetails, bottomButtons: bottomButtons, - failedDownloads: failedDownloads, + failedOrPendingDownloads: failedOrPendingDownloads, sendFailureBadge: sendFailureBadge) } @@ -501,8 +501,8 @@ public class CVComponentState: Equatable { if bottomButtons != nil { result.insert(.bottomButtons) } - if failedDownloads != nil { - result.insert(.failedDownloads) + if failedOrPendingDownloads != nil { + result.insert(.failedOrPendingDownloads) } if sendFailureBadge != nil { result.insert(.sendFailureBadge) @@ -610,7 +610,7 @@ fileprivate extension CVComponentState.Builder { self.senderAvatar = tryToBuildSenderAvatar() - self.failedDownloads = tryToBuildFailedDownloads() + self.failedOrPendingDownloads = tryToBuildFailedOrPendingDownloads() switch interaction.interactionType() { case .threadDetails: @@ -668,15 +668,15 @@ fileprivate extension CVComponentState.Builder { return SenderAvatar(senderAvatar: avatar) } - private func tryToBuildFailedDownloads() -> FailedDownloads? { + private func tryToBuildFailedOrPendingDownloads() -> FailedOrPendingDownloads? { guard let message = interaction as? TSMessage else { return nil } - let failedAttachmentPointers = message.failedAttachments(transaction: transaction) - guard !failedAttachmentPointers.isEmpty else { + let attachmentPointers = message.failedOrPendingAttachments(transaction: transaction) + guard !attachmentPointers.isEmpty else { return nil } - return FailedDownloads(attachmentPointers: failedAttachmentPointers) + return FailedOrPendingDownloads(attachmentPointers: attachmentPointers) } mutating func populateAndBuild(message: TSMessage) throws -> CVComponentState { diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentBodyMedia.swift b/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentBodyMedia.swift index 61352be9c6..85ee8b2cc1 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentBodyMedia.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentBodyMedia.swift @@ -18,6 +18,10 @@ public class CVComponentBodyMedia: CVComponentBase, CVComponent { bodyMedia.mediaAlbumHasPendingAttachment } + var hasDownloadButton: Bool { + mediaAlbumHasFailedAttachment || mediaAlbumHasPendingAttachment + } + private let footerOverlay: CVComponent? init(itemModel: CVItemModel, bodyMedia: CVComponentState.BodyMedia, footerOverlay: CVComponent?) { @@ -115,6 +119,18 @@ public class CVComponentBodyMedia: CVComponentBase, CVComponent { let accessibilityDescription = NSLocalizedString("ACCESSIBILITY_LABEL_MEDIA", comment: "Accessibility label for media.") albumView.accessibilityLabel = accessibilityLabel(description: accessibilityDescription) + + if hasDownloadButton { + let iconView = UIImageView.withTemplateImageName("arrow-down-24", + tintColor: UIColor.ows_white) + iconView.autoSetDimensions(to: CGSize.square(16)) + let downloadButton = OWSLayerView.circleView(size: 44) + downloadButton.backgroundColor = UIColor.ows_black + downloadButton.addSubview(iconView) + iconView.autoCenterInSuperview() + componentView.rootView.addSubview(downloadButton) + downloadButton.autoCenterInSuperview() + } } public func bubbleViewPartner(componentView: CVComponentView) -> OWSBubbleViewPartner? { @@ -174,7 +190,7 @@ public class CVComponentBodyMedia: CVComponentBase, CVComponent { owsFailDebug("Invalid interaction.") return false } - if mediaAlbumHasPendingAttachment { + if hasDownloadButton { componentDelegate.cvc_didTapFailedOrPendingDownloads(message) return true } diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentMessage.swift b/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentMessage.swift index 44a66484f5..d37301625c 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentMessage.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentMessage.swift @@ -140,7 +140,7 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent { // We don't render sender avatars with a subcomponent. case .senderAvatar: return nil - case .systemMessage, .dateHeader, .unreadIndicator, .typingIndicator, .threadDetails, .failedDownloads, .sendFailureBadge: + case .systemMessage, .dateHeader, .unreadIndicator, .typingIndicator, .threadDetails, .failedOrPendingDownloads, .sendFailureBadge: return nil } } @@ -887,7 +887,7 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent { } if let message = interaction as? TSMessage, - nil != componentState.failedDownloads { + nil != componentState.failedOrPendingDownloads { Logger.verbose("Retrying failed downloads.") componentDelegate.cvc_didTapFailedOrPendingDownloads(message) return true @@ -1116,7 +1116,7 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent { case .senderAvatar: owsFailDebug("Invalid component key: \(key)") return nil - case .systemMessage, .dateHeader, .unreadIndicator, .typingIndicator, .threadDetails, .failedDownloads, .sendFailureBadge: + case .systemMessage, .dateHeader, .unreadIndicator, .typingIndicator, .threadDetails, .failedOrPendingDownloads, .sendFailureBadge: owsFailDebug("Invalid component key: \(key)") return nil } @@ -1154,7 +1154,7 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent { // We don't render sender avatars with a subcomponent. case .senderAvatar: owsAssertDebug(subcomponentView == nil) - case .systemMessage, .dateHeader, .unreadIndicator, .typingIndicator, .threadDetails, .failedDownloads, .sendFailureBadge: + case .systemMessage, .dateHeader, .unreadIndicator, .typingIndicator, .threadDetails, .failedOrPendingDownloads, .sendFailureBadge: owsAssertDebug(subcomponentView == nil) } } diff --git a/SignalMessaging/Views/OWSLayerView.swift b/SignalMessaging/Views/OWSLayerView.swift index d056ff4675..13c117ec28 100644 --- a/SignalMessaging/Views/OWSLayerView.swift +++ b/SignalMessaging/Views/OWSLayerView.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. // import Foundation @@ -31,6 +31,16 @@ open class OWSLayerView: UIView { super.init(coder: aDecoder) } + public static func circleView(size: CGFloat? = nil) -> OWSLayerView { + let result = OWSLayerView(frame: .zero) { view in + view.layer.cornerRadius = min(view.width, view.height) * 0.5 + } + if let size = size { + result.autoSetDimensions(to: CGSize.square(size)) + } + return result + } + public override var bounds: CGRect { didSet { if oldValue != bounds { diff --git a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.swift b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.swift index f2c01b4294..225535bc6c 100644 --- a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.swift +++ b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.swift @@ -517,6 +517,14 @@ public extension OWSAttachmentDownloads { } } + #if TESTABLE_BUILD + struct DevFlags { + static let forceFailure = false + static let forceBlockedByPendingMessageRequest = false + static let forceBlockedByAutoDownloadSettingsSettings = false + } + #endif + private func enqueueJobs(forAttachmentReferences attachmentReferences: [AttachmentReference], message: TSMessage?, downloadBehavior: AttachmentDownloadBehavior, @@ -549,6 +557,13 @@ public extension OWSAttachmentDownloads { } func isDownloadBlockedByPendingMessageRequest(_ attachmentPointer: TSAttachmentPointer) -> Bool { + + #if TESTABLE_BUILD + if DevFlags.forceBlockedByPendingMessageRequest { + return true + } + #endif + guard attachmentPointer.isVisualMedia, hasPendingMessageRequest, let message = message, @@ -561,6 +576,13 @@ public extension OWSAttachmentDownloads { func isDownloadBlockedByAutoDownloadSettingsSettings(_ attachmentPointer: TSAttachmentPointer, category: AttachmentCategory) -> Bool { + + #if TESTABLE_BUILD + if DevFlags.forceBlockedByAutoDownloadSettingsSettings { + return true + } + #endif + guard !downloadBehavior.bypassPendingManualDownload else { return false } @@ -596,6 +618,19 @@ public extension OWSAttachmentDownloads { attachmentStreams.append(attachmentStream) } case .attachmentPointer(let attachmentPointer, let category): + + #if TESTABLE_BUILD + if DevFlags.forceFailure { + Logger.info("Skipping media download for thread with pending message request.") + Self.databaseStorage.write { transaction in + attachmentPointer.updateAttachmentPointerState(from: .enqueued, + to: .failed, + transaction: transaction) + } + continue + } + #endif + if isDownloadBlockedByPendingMessageRequest(attachmentPointer) { Logger.info("Skipping media download for thread with pending message request.") Self.databaseStorage.write { transaction in diff --git a/SignalServiceKit/src/Messages/Interactions/TSMessage.swift b/SignalServiceKit/src/Messages/Interactions/TSMessage.swift index 387adab293..b2bedfdc79 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSMessage.swift +++ b/SignalServiceKit/src/Messages/Interactions/TSMessage.swift @@ -16,20 +16,26 @@ public extension TSMessage { func failedAttachments(transaction: SDSAnyReadTransaction) -> [TSAttachmentPointer] { let attachments: [TSAttachment] = allAttachments(with: transaction.unwrapGrdbRead) - let states = Set([TSAttachmentPointerState.failed]) - return Self.onlyAttachmentPointers(attachments: attachments, withStateIn: states) + let states: [TSAttachmentPointerState] = [.failed] + return Self.onlyAttachmentPointers(attachments: attachments, withStateIn: Set(states)) + } + + func failedOrPendingAttachments(transaction: SDSAnyReadTransaction) -> [TSAttachmentPointer] { + let attachments: [TSAttachment] = allAttachments(with: transaction.unwrapGrdbRead) + let states: [TSAttachmentPointerState] = [.failed, .pendingMessageRequest, .pendingManualDownload] + return Self.onlyAttachmentPointers(attachments: attachments, withStateIn: Set(states)) } func failedBodyAttachments(transaction: SDSAnyReadTransaction) -> [TSAttachmentPointer] { let attachments: [TSAttachment] = bodyAttachments(with: transaction.unwrapGrdbRead) - let states = Set([TSAttachmentPointerState.failed]) - return Self.onlyAttachmentPointers(attachments: attachments, withStateIn: states) + let states: [TSAttachmentPointerState] = [.failed] + return Self.onlyAttachmentPointers(attachments: attachments, withStateIn: Set(states)) } func pendingBodyAttachments(transaction: SDSAnyReadTransaction) -> [TSAttachmentPointer] { let attachments: [TSAttachment] = bodyAttachments(with: transaction.unwrapGrdbRead) - let states = Set([TSAttachmentPointerState.pendingMessageRequest, TSAttachmentPointerState.pendingManualDownload ]) - return Self.onlyAttachmentPointers(attachments: attachments, withStateIn: states) + let states: [TSAttachmentPointerState] = [.pendingMessageRequest, .pendingManualDownload] + return Self.onlyAttachmentPointers(attachments: attachments, withStateIn: Set(states)) } private static func onlyAttachmentPointers(attachments: [TSAttachment],