Add auto-download settings for media.

This commit is contained in:
Matthew Chen 2021-01-11 20:20:41 -03:00
parent b075aa2e8e
commit 5abf846809
7 changed files with 97 additions and 30 deletions

View File

@ -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"
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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],