Handle undownloadable attachment rendering
This commit is contained in:
parent
3c159a4ec6
commit
a26ebaa2de
@ -1648,6 +1648,7 @@
|
||||
B9A53B992D0250FC0000578B /* EditCallLinkNameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A53B982D0250FC0000578B /* EditCallLinkNameViewController.swift */; };
|
||||
B9A87A362A9D1D25009FCA13 /* EditorSticker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A87A352A9D1D25009FCA13 /* EditorSticker.swift */; };
|
||||
B9A87A382A9E34BD009FCA13 /* Hatsuishi-UPM800.otf in Resources */ = {isa = PBXBuildFile; fileRef = B9A87A372A9E34BD009FCA13 /* Hatsuishi-UPM800.otf */; };
|
||||
B9AAC9512D5A6FA900E223C0 /* CVComponentUndownloadableAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9AAC9502D5A6FA900E223C0 /* CVComponentUndownloadableAttachment.swift */; };
|
||||
B9B2AA942BC598B60060B56C /* ContactNoteSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B2AA932BC598B60060B56C /* ContactNoteSheet.swift */; };
|
||||
B9B7BC652D41C61500C26E42 /* NewLinkedDeviceNotificationMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B7BC642D41C61500C26E42 /* NewLinkedDeviceNotificationMegaphone.swift */; };
|
||||
B9B89EED2C064E760093A2FA /* notification_simple-01.caf in Resources */ = {isa = PBXBuildFile; fileRef = B9B89EEC2C064E700093A2FA /* notification_simple-01.caf */; };
|
||||
@ -5475,6 +5476,7 @@
|
||||
B9A53B982D0250FC0000578B /* EditCallLinkNameViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCallLinkNameViewController.swift; sourceTree = "<group>"; };
|
||||
B9A87A352A9D1D25009FCA13 /* EditorSticker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorSticker.swift; sourceTree = "<group>"; };
|
||||
B9A87A372A9E34BD009FCA13 /* Hatsuishi-UPM800.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Hatsuishi-UPM800.otf"; sourceTree = "<group>"; };
|
||||
B9AAC9502D5A6FA900E223C0 /* CVComponentUndownloadableAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CVComponentUndownloadableAttachment.swift; sourceTree = "<group>"; };
|
||||
B9B2AA932BC598B60060B56C /* ContactNoteSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactNoteSheet.swift; sourceTree = "<group>"; };
|
||||
B9B7BC642D41C61500C26E42 /* NewLinkedDeviceNotificationMegaphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewLinkedDeviceNotificationMegaphone.swift; sourceTree = "<group>"; };
|
||||
B9B89EEC2C064E700093A2FA /* notification_simple-01.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "notification_simple-01.caf"; sourceTree = "<group>"; };
|
||||
@ -7931,6 +7933,7 @@
|
||||
348815BB2552E67900D4F4C4 /* CVComponentSystemMessage.swift */,
|
||||
347C3856252E1E2300F3D941 /* CVComponentThreadDetails.swift */,
|
||||
347C384C252D0FDC00F3D941 /* CVComponentTypingIndicator.swift */,
|
||||
B9AAC9502D5A6FA900E223C0 /* CVComponentUndownloadableAttachment.swift */,
|
||||
347C384A252D085900F3D941 /* CVComponentUnreadIndicator.swift */,
|
||||
348815BF2553291200D4F4C4 /* CVComponentViewOnce.swift */,
|
||||
667AF9DD2B4C5824008AEE5D /* PersistableGroupUpdateItem+CVComponentSystemMessageAction.swift */,
|
||||
@ -16482,6 +16485,7 @@
|
||||
348815BC2552E67900D4F4C4 /* CVComponentSystemMessage.swift in Sources */,
|
||||
347C3857252E1E2300F3D941 /* CVComponentThreadDetails.swift in Sources */,
|
||||
347C384D252D0FDC00F3D941 /* CVComponentTypingIndicator.swift in Sources */,
|
||||
B9AAC9512D5A6FA900E223C0 /* CVComponentUndownloadableAttachment.swift in Sources */,
|
||||
347C384B252D085900F3D941 /* CVComponentUnreadIndicator.swift in Sources */,
|
||||
348815C02553291300D4F4C4 /* CVComponentViewOnce.swift in Sources */,
|
||||
3470C8802555F25200F5847C /* CVContactShareView.swift in Sources */,
|
||||
|
||||
@ -18,9 +18,6 @@ public class AudioAttachment {
|
||||
attachmentPointer: ReferencedAttachmentPointer,
|
||||
downloadState: AttachmentDownloadState
|
||||
)
|
||||
/// The attachment has no stream and cannot be downloaded because there is no cdn info.
|
||||
/// Typically happens if we restore from a free-tier backup with old media expired from transit tier.
|
||||
case undownloadable(ReferencedAttachment)
|
||||
|
||||
public static func == (lhs: AudioAttachment.State, rhs: AudioAttachment.State) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
@ -38,16 +35,9 @@ public class AudioAttachment {
|
||||
return lhsPointer.attachment.id == rhsPointer.attachment.id
|
||||
&& lhsPointer.reference.hasSameOwner(as: rhsPointer.reference)
|
||||
&& lhsState == rhsState
|
||||
case let (
|
||||
.undownloadable(lhsAttachment),
|
||||
.undownloadable(rhsAttachment)
|
||||
):
|
||||
return lhsAttachment.attachment.id == rhsAttachment.attachment.id
|
||||
&& lhsAttachment.reference.hasSameOwner(as: rhsAttachment.reference)
|
||||
case
|
||||
(.attachmentStream, _),
|
||||
(.attachmentPointer, _),
|
||||
(.undownloadable, _):
|
||||
(.attachmentPointer, _):
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -60,8 +50,6 @@ public class AudioAttachment {
|
||||
return attachmentStream.reference.sourceFilename
|
||||
case .attachmentPointer(let attachmentPointer, _):
|
||||
return attachmentPointer.reference.sourceFilename
|
||||
case .undownloadable(let attachment):
|
||||
return attachment.reference.sourceFilename
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,18 +111,6 @@ public class AudioAttachment {
|
||||
self.owningMessage = owningMessage
|
||||
}
|
||||
|
||||
public init(
|
||||
undownloadableAttachment: ReferencedAttachment,
|
||||
owningMessage: TSMessage?,
|
||||
metadata: MediaMetadata?,
|
||||
receivedAtDate: Date
|
||||
) {
|
||||
state = .undownloadable(undownloadableAttachment)
|
||||
self.isDownloading = false
|
||||
self.receivedAtDate = receivedAtDate
|
||||
self.owningMessage = owningMessage
|
||||
}
|
||||
|
||||
private static let cachedAttachmentDurations = AtomicDictionary<Int64, TimeInterval>([:], lock: .init())
|
||||
private static func cachedAudioDuration(forAttachment attachmentStream: AttachmentStream) -> TimeInterval {
|
||||
let attachmentId = attachmentStream.attachment.id
|
||||
@ -163,8 +139,6 @@ extension AudioAttachment {
|
||||
return attachmentStream.attachment
|
||||
case .attachmentPointer(let attachmentPointer, _):
|
||||
return attachmentPointer.attachment
|
||||
case .undownloadable(let attachment):
|
||||
return attachment.attachment
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,8 +148,6 @@ extension AudioAttachment {
|
||||
return attachmentStream
|
||||
case .attachmentPointer:
|
||||
return nil
|
||||
case .undownloadable:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,8 +157,6 @@ extension AudioAttachment {
|
||||
return nil
|
||||
case .attachmentPointer(let attachmentPointer, _):
|
||||
return attachmentPointer
|
||||
case .undownloadable:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,8 +166,6 @@ extension AudioAttachment {
|
||||
return audioDurationSeconds
|
||||
case .attachmentPointer:
|
||||
return 0
|
||||
case .undownloadable:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,8 +176,6 @@ extension AudioAttachment {
|
||||
return attachmentStream.reference.renderingFlag
|
||||
case .attachmentPointer(let attachmentPointer, _):
|
||||
return attachmentPointer.reference.renderingFlag
|
||||
case .undownloadable(let attachment):
|
||||
return attachment.reference.renderingFlag
|
||||
}
|
||||
}() == .voiceMessage
|
||||
}
|
||||
|
||||
@ -262,7 +262,7 @@ extension CVItemViewModelImpl {
|
||||
return false
|
||||
case .textOnlyMessage, .audio, .genericAttachment, .contactShare, .bodyMedia, .viewOnce, .stickerMessage, .quoteOnlyMessage:
|
||||
return !hasUnloadedAttachments
|
||||
case .paymentAttachment, .archivedPaymentAttachment:
|
||||
case .paymentAttachment, .archivedPaymentAttachment, .undownloadableAttachment:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ public enum CVMessageCellType: Int, CustomStringConvertible, Equatable {
|
||||
case genericAttachment
|
||||
case paymentAttachment
|
||||
case archivedPaymentAttachment
|
||||
case undownloadableAttachment
|
||||
case contactShare
|
||||
case bodyMedia
|
||||
case viewOnce
|
||||
@ -42,6 +43,7 @@ public enum CVMessageCellType: Int, CustomStringConvertible, Equatable {
|
||||
case .genericAttachment: return "genericAttachment"
|
||||
case .paymentAttachment: return "paymentAttachment"
|
||||
case .archivedPaymentAttachment: return "archivedPaymentAttachment"
|
||||
case .undownloadableAttachment: return "undownloadableAttachment"
|
||||
case .contactShare: return "contactShare"
|
||||
case .bodyMedia: return "bodyMedia"
|
||||
case .viewOnce: return "viewOnce"
|
||||
|
||||
@ -153,11 +153,6 @@ class AudioMessageView: ManualStackView {
|
||||
isDarkThemeEnabled: conversationStyle.isDarkThemeEnabled,
|
||||
mediaCache: mediaCache
|
||||
)
|
||||
case .undownloadable:
|
||||
// TODO[AttachmentRendering]: render undownloadable audio attachments.
|
||||
// Can either reuse this component or use a totally different component
|
||||
// in which case this should owsFailDebug here.
|
||||
return
|
||||
}
|
||||
|
||||
let topInnerStack = ManualStackView(name: "playerStack")
|
||||
@ -518,7 +513,7 @@ extension AudioAttachment {
|
||||
switch state {
|
||||
case .attachmentStream(let stream, _):
|
||||
return ByteCountFormatter().string(for: stream.attachmentStream.unencryptedByteCount) ?? ""
|
||||
case .attachmentPointer, .undownloadable:
|
||||
case .attachmentPointer:
|
||||
owsFailDebug("Shouldn't get here - undownloaded media not implemented")
|
||||
return ""
|
||||
}
|
||||
@ -529,7 +524,7 @@ extension AudioAttachment {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.setLocalizedDateFormatFromTemplate("Mdyy")
|
||||
return dateFormatter.string(from: receivedAtDate)
|
||||
case .attachmentPointer, .undownloadable:
|
||||
case .attachmentPointer:
|
||||
owsFailDebug("Shouldn't get here - undownloaded media not implemented")
|
||||
return ""
|
||||
}
|
||||
|
||||
@ -349,6 +349,7 @@ public enum CVComponentKey: CustomStringConvertible, CaseIterable {
|
||||
case genericAttachment
|
||||
case paymentAttachment
|
||||
case archivedPaymentAttachment
|
||||
case undownloadableAttachment
|
||||
case contactShare
|
||||
case bottomButtons
|
||||
case sendFailureBadge
|
||||
@ -395,6 +396,8 @@ public enum CVComponentKey: CustomStringConvertible, CaseIterable {
|
||||
return ".paymentAttchment"
|
||||
case .archivedPaymentAttachment:
|
||||
return ".archivedPaymentAttachment"
|
||||
case .undownloadableAttachment:
|
||||
return ".undownloadableAttachment"
|
||||
case .contactShare:
|
||||
return ".contactShare"
|
||||
case .bottomButtons:
|
||||
|
||||
@ -94,6 +94,10 @@ public protocol CVComponentDelegate: AnyObject, AudioMessageViewDelegate {
|
||||
|
||||
func didTapUndownloadableOversizeText()
|
||||
|
||||
func didTapUndownloadableAudio()
|
||||
|
||||
func didTapUndownloadableSticker()
|
||||
|
||||
func didTapBrokenVideo()
|
||||
|
||||
// MARK: - Messages
|
||||
|
||||
@ -61,6 +61,8 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent {
|
||||
|
||||
private var archivedPaymentAttachment: CVComponent?
|
||||
|
||||
private var undownloadableAttachment: CVComponent?
|
||||
|
||||
private var contactShare: CVComponent?
|
||||
|
||||
private var bottomButtons: CVComponent?
|
||||
@ -127,6 +129,8 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent {
|
||||
return self.paymentAttachment
|
||||
case .archivedPaymentAttachment:
|
||||
return self.archivedPaymentAttachment
|
||||
case .undownloadableAttachment:
|
||||
return self.undownloadableAttachment
|
||||
case .quotedReply:
|
||||
return self.quotedReply
|
||||
case .linkPreview:
|
||||
@ -168,7 +172,7 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent {
|
||||
private var isBubbleTransparent: Bool {
|
||||
if wasRemotelyDeleted {
|
||||
return false
|
||||
} else if componentState.isSticker {
|
||||
} else if componentState.shouldRenderAsSticker {
|
||||
return true
|
||||
} else if isBorderlessViewOnceMessage {
|
||||
return false
|
||||
@ -196,16 +200,45 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent {
|
||||
standaloneFooter?.tapForMoreState ?? .none
|
||||
}
|
||||
|
||||
private func footerOverlayIfItShouldShow() -> CVComponentFooter? {
|
||||
let footerShouldOverlay = (bodyText == nil && !itemViewState.shouldHideFooter && !tapForMoreState.shouldShowFooter)
|
||||
|
||||
guard footerShouldOverlay else { return nil }
|
||||
|
||||
if let footerState = itemViewState.footerState {
|
||||
return CVComponentFooter(
|
||||
itemModel: itemModel,
|
||||
footerState: footerState,
|
||||
isOverlayingMedia: false,
|
||||
isOutsideBubble: false
|
||||
)
|
||||
} else {
|
||||
owsFailDebug("Missing footerState.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func buildComponentStates() {
|
||||
|
||||
hasSendFailureBadge = componentState.sendFailureBadge != nil
|
||||
|
||||
var footerOverlay: CVComponentFooter?
|
||||
|
||||
if let senderNameState = itemViewState.senderNameState {
|
||||
self.senderName = CVComponentSenderName(itemModel: itemModel, senderNameState: senderNameState)
|
||||
}
|
||||
if let senderAvatar = componentState.senderAvatar {
|
||||
self.senderAvatar = senderAvatar
|
||||
}
|
||||
if let undownloadableAttachment = componentState.undownloadableAttachment {
|
||||
footerOverlay = self.footerOverlayIfItShouldShow()
|
||||
self.undownloadableAttachment = CVComponentUndownloadableAttachment(
|
||||
itemModel: itemModel,
|
||||
attachmentType: undownloadableAttachment,
|
||||
footerOverlay: footerOverlay
|
||||
)
|
||||
}
|
||||
if let stickerState = componentState.sticker {
|
||||
self.sticker = CVComponentSticker(itemModel: itemModel, sticker: stickerState)
|
||||
}
|
||||
@ -230,8 +263,6 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent {
|
||||
bottomButtonsState: bottomButtonsState)
|
||||
}
|
||||
|
||||
var footerOverlay: CVComponentFooter?
|
||||
|
||||
if let paymentAttachment = componentState.paymentAttachment {
|
||||
let paymentAmount: UInt64? = {
|
||||
let receipt = paymentAttachment.notification.mcReceiptData
|
||||
@ -305,18 +336,7 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent {
|
||||
}
|
||||
|
||||
if let audioAttachmentState = componentState.audioAttachment {
|
||||
let shouldFooterOverlayAudio = (bodyText == nil && !itemViewState.shouldHideFooter && !tapForMoreState.shouldShowFooter)
|
||||
if shouldFooterOverlayAudio {
|
||||
if let footerState = itemViewState.footerState {
|
||||
footerOverlay = CVComponentFooter(itemModel: itemModel,
|
||||
footerState: footerState,
|
||||
isOverlayingMedia: false,
|
||||
isOutsideBubble: false)
|
||||
} else {
|
||||
owsFailDebug("Missing footerState.")
|
||||
}
|
||||
}
|
||||
|
||||
footerOverlay = self.footerOverlayIfItShouldShow()
|
||||
self.audioAttachment = CVComponentAudioAttachment(
|
||||
itemModel: itemModel,
|
||||
audioAttachment: audioAttachmentState,
|
||||
@ -905,7 +925,7 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent {
|
||||
private static var topNestedCVComponentKeys: [CVComponentKey] { [.senderName] }
|
||||
private static var bottomFullWidthCVComponentKeys: [CVComponentKey] { [.quotedReply, .bodyMedia] }
|
||||
private static var bottomNestedShareCVComponentKeys: [CVComponentKey] { [.viewOnce, .audioAttachment, .genericAttachment, .paymentAttachment, .archivedPaymentAttachment, .contactShare, .giftBadge] }
|
||||
private static var bottomNestedTextCVComponentKeys: [CVComponentKey] { [.bodyText, .footer] }
|
||||
private static var bottomNestedTextCVComponentKeys: [CVComponentKey] { [.bodyText, .footer, .undownloadableAttachment] }
|
||||
|
||||
// The "message" contents of this component for most messages are vertically
|
||||
// stacked in four sections.
|
||||
@ -1161,6 +1181,8 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent {
|
||||
return false
|
||||
case .bodyMedia, .sticker, .quotedReply, .linkPreview, .viewOnce, .audioAttachment, .genericAttachment, .paymentAttachment, .archivedPaymentAttachment, .contactShare:
|
||||
return true
|
||||
case .undownloadableAttachment:
|
||||
return false
|
||||
case .giftBadge:
|
||||
// TODO: (GB) Confirm that Gift Badges should use large component spacing.
|
||||
return true
|
||||
@ -1961,6 +1983,7 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent {
|
||||
var audioAttachmentView: CVComponentView?
|
||||
var genericAttachmentView: CVComponentView?
|
||||
var paymentAttachmentView: CVComponentView?
|
||||
var undownloadableAttachmentView: CVComponentView?
|
||||
var archivedPaymentView: CVComponentView?
|
||||
var contactShareView: CVComponentView?
|
||||
var bottomButtonsView: CVComponentView?
|
||||
@ -1980,6 +2003,7 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent {
|
||||
audioAttachmentView,
|
||||
genericAttachmentView,
|
||||
paymentAttachmentView,
|
||||
undownloadableAttachmentView,
|
||||
archivedPaymentView,
|
||||
contactShareView,
|
||||
bottomButtonsView
|
||||
@ -2016,6 +2040,8 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent {
|
||||
return paymentAttachmentView
|
||||
case .archivedPaymentAttachment:
|
||||
return archivedPaymentView
|
||||
case .undownloadableAttachment:
|
||||
return undownloadableAttachmentView
|
||||
case .contactShare:
|
||||
return contactShareView
|
||||
case .bottomButtons:
|
||||
@ -2061,6 +2087,8 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent {
|
||||
paymentAttachmentView = subcomponentView
|
||||
case .archivedPaymentAttachment:
|
||||
archivedPaymentView = subcomponentView
|
||||
case .undownloadableAttachment:
|
||||
undownloadableAttachmentView = subcomponentView
|
||||
case .contactShare:
|
||||
contactShareView = subcomponentView
|
||||
case .bottomButtons:
|
||||
|
||||
@ -250,15 +250,12 @@ public class CVComponentState: Equatable {
|
||||
attachmentPointer: ReferencedAttachmentPointer,
|
||||
downloadState: AttachmentDownloadState
|
||||
)
|
||||
/// The attachment has no stream and cannot be downloaded because there is no cdn info.
|
||||
/// Typically happens if we restore from a free-tier backup with old media expired from transit tier.
|
||||
case undownloadable(ReferencedAttachment)
|
||||
|
||||
public var stickerMetadata: (any StickerMetadata)? {
|
||||
switch self {
|
||||
case .available(let stickerMetadata, _):
|
||||
return stickerMetadata
|
||||
case .downloading, .failedOrPending, .undownloadable:
|
||||
case .downloading, .failedOrPending:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -270,8 +267,6 @@ public class CVComponentState: Equatable {
|
||||
return nil
|
||||
case .failedOrPending:
|
||||
return nil
|
||||
case .undownloadable:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public var attachmentPointer: ReferencedAttachmentPointer? {
|
||||
@ -282,8 +277,6 @@ public class CVComponentState: Equatable {
|
||||
return attachmentPointer
|
||||
case .failedOrPending(let attachmentPointer, _):
|
||||
return attachmentPointer
|
||||
case .undownloadable:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -300,16 +293,21 @@ public class CVComponentState: Equatable {
|
||||
return lhsPointer.attachment.id == rhsPointer.attachment.id
|
||||
&& lhsPointer.reference.hasSameOwner(as: rhsPointer.reference)
|
||||
&& lhsState == rhsState
|
||||
case let (.undownloadable(lhsAttachment), .undownloadable(rhsAttachment)):
|
||||
return lhsAttachment.attachment.id == rhsAttachment.attachment.id
|
||||
&& lhsAttachment.reference.hasSameOwner(as: rhsAttachment.reference)
|
||||
case (.available, _), (.downloading, _), (.failedOrPending, _), (.undownloadable, _):
|
||||
case (.available, _), (.downloading, _), (.failedOrPending, _):
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
let sticker: Sticker?
|
||||
|
||||
/// The attachment has no stream and cannot be downloaded because there is no cdn info.
|
||||
/// Typically happens if we restore from a free-tier backup with old media expired from transit tier.
|
||||
enum UndownloadableAttachment: Equatable {
|
||||
case audio
|
||||
case sticker
|
||||
}
|
||||
let undownloadableAttachment: UndownloadableAttachment?
|
||||
|
||||
struct ContactShare: Equatable {
|
||||
let state: CVContactShareView.State
|
||||
}
|
||||
@ -452,34 +450,37 @@ public class CVComponentState: Equatable {
|
||||
|
||||
let messageHasBodyAttachments: Bool
|
||||
|
||||
fileprivate init(messageCellType: CVMessageCellType,
|
||||
senderName: SenderName?,
|
||||
senderAvatar: SenderAvatar?,
|
||||
bodyText: BodyText?,
|
||||
bodyMedia: BodyMedia?,
|
||||
genericAttachment: GenericAttachment?,
|
||||
paymentAttachment: PaymentAttachment?,
|
||||
archivedPaymentAttachment: ArchivedPaymentAttachment?,
|
||||
audioAttachment: AudioAttachment?,
|
||||
viewOnce: ViewOnce?,
|
||||
quotedReply: QuotedReply?,
|
||||
sticker: Sticker?,
|
||||
contactShare: ContactShare?,
|
||||
linkPreview: LinkPreview?,
|
||||
giftBadge: GiftBadge?,
|
||||
systemMessage: SystemMessage?,
|
||||
dateHeader: DateHeader?,
|
||||
unreadIndicator: UnreadIndicator?,
|
||||
reactions: Reactions?,
|
||||
typingIndicator: TypingIndicator?,
|
||||
threadDetails: ThreadDetails?,
|
||||
unknownThreadWarning: UnknownThreadWarning?,
|
||||
defaultDisappearingMessageTimer: DefaultDisappearingMessageTimer?,
|
||||
bottomButtons: BottomButtons?,
|
||||
failedOrPendingDownloads: FailedOrPendingDownloads?,
|
||||
sendFailureBadge: SendFailureBadge?,
|
||||
messageHasBodyAttachments: Bool,
|
||||
hasRenderableContent: Bool) {
|
||||
fileprivate init(
|
||||
messageCellType: CVMessageCellType,
|
||||
senderName: SenderName?,
|
||||
senderAvatar: SenderAvatar?,
|
||||
bodyText: BodyText?,
|
||||
bodyMedia: BodyMedia?,
|
||||
genericAttachment: GenericAttachment?,
|
||||
paymentAttachment: PaymentAttachment?,
|
||||
archivedPaymentAttachment: ArchivedPaymentAttachment?,
|
||||
audioAttachment: AudioAttachment?,
|
||||
viewOnce: ViewOnce?,
|
||||
quotedReply: QuotedReply?,
|
||||
sticker: Sticker?,
|
||||
undownloadableAttachment: UndownloadableAttachment?,
|
||||
contactShare: ContactShare?,
|
||||
linkPreview: LinkPreview?,
|
||||
giftBadge: GiftBadge?,
|
||||
systemMessage: SystemMessage?,
|
||||
dateHeader: DateHeader?,
|
||||
unreadIndicator: UnreadIndicator?,
|
||||
reactions: Reactions?,
|
||||
typingIndicator: TypingIndicator?,
|
||||
threadDetails: ThreadDetails?,
|
||||
unknownThreadWarning: UnknownThreadWarning?,
|
||||
defaultDisappearingMessageTimer: DefaultDisappearingMessageTimer?,
|
||||
bottomButtons: BottomButtons?,
|
||||
failedOrPendingDownloads: FailedOrPendingDownloads?,
|
||||
sendFailureBadge: SendFailureBadge?,
|
||||
messageHasBodyAttachments: Bool,
|
||||
hasRenderableContent: Bool
|
||||
) {
|
||||
|
||||
self.messageCellType = messageCellType
|
||||
self.senderName = senderName
|
||||
@ -493,6 +494,7 @@ public class CVComponentState: Equatable {
|
||||
self.viewOnce = viewOnce
|
||||
self.quotedReply = quotedReply
|
||||
self.sticker = sticker
|
||||
self.undownloadableAttachment = undownloadableAttachment
|
||||
self.contactShare = contactShare
|
||||
self.linkPreview = linkPreview
|
||||
self.giftBadge = giftBadge
|
||||
@ -526,6 +528,7 @@ public class CVComponentState: Equatable {
|
||||
lhs.viewOnce == rhs.viewOnce &&
|
||||
lhs.quotedReply == rhs.quotedReply &&
|
||||
lhs.sticker == rhs.sticker &&
|
||||
lhs.undownloadableAttachment == rhs.undownloadableAttachment &&
|
||||
lhs.contactShare == rhs.contactShare &&
|
||||
lhs.linkPreview == rhs.linkPreview &&
|
||||
lhs.giftBadge == rhs.giftBadge &&
|
||||
@ -555,6 +558,7 @@ public class CVComponentState: Equatable {
|
||||
typealias ViewOnce = CVComponentState.ViewOnce
|
||||
typealias QuotedReply = CVComponentState.QuotedReply
|
||||
typealias Sticker = CVComponentState.Sticker
|
||||
typealias UndownloadableAttachment = CVComponentState.UndownloadableAttachment
|
||||
typealias SystemMessage = CVComponentState.SystemMessage
|
||||
typealias ContactShare = CVComponentState.ContactShare
|
||||
typealias Reactions = CVComponentState.Reactions
|
||||
@ -588,6 +592,7 @@ public class CVComponentState: Equatable {
|
||||
var viewOnce: ViewOnce?
|
||||
var quotedReply: QuotedReply?
|
||||
var sticker: Sticker?
|
||||
var undownloadableAttachment: UndownloadableAttachment?
|
||||
var systemMessage: SystemMessage?
|
||||
var contactShare: ContactShare?
|
||||
var linkPreview: LinkPreview?
|
||||
@ -619,34 +624,37 @@ public class CVComponentState: Equatable {
|
||||
bottomButtons = BottomButtons(actions: bottomButtonsActions)
|
||||
}
|
||||
|
||||
return CVComponentState(messageCellType: messageCellType,
|
||||
senderName: senderName,
|
||||
senderAvatar: senderAvatar,
|
||||
bodyText: bodyText,
|
||||
bodyMedia: bodyMedia,
|
||||
genericAttachment: genericAttachment,
|
||||
paymentAttachment: paymentAttachment,
|
||||
archivedPaymentAttachment: archivedPaymentAttachment,
|
||||
audioAttachment: audioAttachment,
|
||||
viewOnce: viewOnce,
|
||||
quotedReply: quotedReply,
|
||||
sticker: sticker,
|
||||
contactShare: contactShare,
|
||||
linkPreview: linkPreview,
|
||||
giftBadge: giftBadge,
|
||||
systemMessage: systemMessage,
|
||||
dateHeader: dateHeader,
|
||||
unreadIndicator: unreadIndicator,
|
||||
reactions: reactions,
|
||||
typingIndicator: typingIndicator,
|
||||
threadDetails: threadDetails,
|
||||
unknownThreadWarning: unknownThreadWarning,
|
||||
defaultDisappearingMessageTimer: defaultDisappearingMessageTimer,
|
||||
bottomButtons: bottomButtons,
|
||||
failedOrPendingDownloads: failedOrPendingDownloads,
|
||||
sendFailureBadge: sendFailureBadge,
|
||||
messageHasBodyAttachments: messageHasBodyAttachments,
|
||||
hasRenderableContent: hasRenderableContent)
|
||||
return CVComponentState(
|
||||
messageCellType: messageCellType,
|
||||
senderName: senderName,
|
||||
senderAvatar: senderAvatar,
|
||||
bodyText: bodyText,
|
||||
bodyMedia: bodyMedia,
|
||||
genericAttachment: genericAttachment,
|
||||
paymentAttachment: paymentAttachment,
|
||||
archivedPaymentAttachment: archivedPaymentAttachment,
|
||||
audioAttachment: audioAttachment,
|
||||
viewOnce: viewOnce,
|
||||
quotedReply: quotedReply,
|
||||
sticker: sticker,
|
||||
undownloadableAttachment: undownloadableAttachment,
|
||||
contactShare: contactShare,
|
||||
linkPreview: linkPreview,
|
||||
giftBadge: giftBadge,
|
||||
systemMessage: systemMessage,
|
||||
dateHeader: dateHeader,
|
||||
unreadIndicator: unreadIndicator,
|
||||
reactions: reactions,
|
||||
typingIndicator: typingIndicator,
|
||||
threadDetails: threadDetails,
|
||||
unknownThreadWarning: unknownThreadWarning,
|
||||
defaultDisappearingMessageTimer: defaultDisappearingMessageTimer,
|
||||
bottomButtons: bottomButtons,
|
||||
failedOrPendingDownloads: failedOrPendingDownloads,
|
||||
sendFailureBadge: sendFailureBadge,
|
||||
messageHasBodyAttachments: messageHasBodyAttachments,
|
||||
hasRenderableContent: hasRenderableContent
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
@ -681,9 +689,12 @@ public class CVComponentState: Equatable {
|
||||
if systemMessage != nil {
|
||||
return .systemMessage
|
||||
}
|
||||
if let sticker = self.sticker {
|
||||
if self.sticker != nil {
|
||||
return .stickerMessage
|
||||
}
|
||||
if self.undownloadableAttachment != nil {
|
||||
return .undownloadableAttachment
|
||||
}
|
||||
if viewOnce != nil {
|
||||
return .viewOnce
|
||||
}
|
||||
@ -722,7 +733,7 @@ public class CVComponentState: Equatable {
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
lazy var isSticker: Bool = {
|
||||
lazy var shouldRenderAsSticker: Bool = {
|
||||
sticker != nil
|
||||
}()
|
||||
|
||||
@ -1330,10 +1341,7 @@ fileprivate extension CVComponentState.Builder {
|
||||
}
|
||||
return build()
|
||||
} else {
|
||||
// TODO[AttachmentRendering]: represent state needed to render
|
||||
// an undownloadable sticker attachment, which bears no visual
|
||||
// relationship to other sticker attachment components.
|
||||
self.sticker = .undownloadable(attachment)
|
||||
self.undownloadableAttachment = .sticker
|
||||
return build()
|
||||
}
|
||||
}
|
||||
@ -1562,15 +1570,7 @@ fileprivate extension CVComponentState.Builder {
|
||||
downloadState: attachmentPointer.attachmentPointer.downloadState(tx: transaction.asV2Read)
|
||||
)
|
||||
} else {
|
||||
// TODO[AttachmentRendering]: represent state needed to render
|
||||
// an undownloadable audio attachment, which bears no visual
|
||||
// relationship to other audio attachment components.
|
||||
self.audioAttachment = AudioAttachment(
|
||||
undownloadableAttachment: attachment,
|
||||
owningMessage: interaction as? TSMessage,
|
||||
metadata: nil,
|
||||
receivedAtDate: interaction.receivedAtDate
|
||||
)
|
||||
self.undownloadableAttachment = .audio
|
||||
}
|
||||
}
|
||||
|
||||
@ -1811,6 +1811,8 @@ public extension CVComponentState {
|
||||
case .paymentAttachment, .archivedPaymentAttachment:
|
||||
// Payments can't be forwarded.
|
||||
break
|
||||
case .undownloadableAttachment:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -105,11 +105,6 @@ public class CVComponentSticker: CVComponentBase, CVComponent {
|
||||
stackView: stackView,
|
||||
cellMeasurement: cellMeasurement
|
||||
)
|
||||
case .undownloadable(_):
|
||||
// TODO[AttachmentRendering]: render undownloadable audio attachments.
|
||||
// Can either reuse this component or use a totally different component
|
||||
// in which case this should owsFailDebug here.
|
||||
owsFailDebug("Unimplemented")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,232 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import SignalServiceKit
|
||||
import SignalUI
|
||||
|
||||
class CVComponentUndownloadableAttachment: CVComponentBase, CVComponent {
|
||||
var componentKey: CVComponentKey { .undownloadableAttachment }
|
||||
|
||||
private var icon: UIImage {
|
||||
switch attachmentType {
|
||||
case .audio:
|
||||
UIImage(named: "audio-square-slash")!
|
||||
case .sticker:
|
||||
UIImage(named: "sticker-slash")!
|
||||
}
|
||||
}
|
||||
|
||||
private var message: String {
|
||||
switch attachmentType {
|
||||
case .audio:
|
||||
OWSLocalizedString(
|
||||
"AUDIO_UNAVAILABLE_MESSAGE_LABEL",
|
||||
value: "Voice message not available",
|
||||
comment: "Message when trying to show a voice message that has expired and is unavailable for download"
|
||||
)
|
||||
case .sticker:
|
||||
OWSLocalizedString(
|
||||
"STICKER_UNAVAILABLE_MESSAGE_LABEL",
|
||||
value: "Sticker not available",
|
||||
comment: "Message when trying to show a sticker that has expired and is unavailable for download"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private let attachmentType: CVComponentState.UndownloadableAttachment
|
||||
private let footerOverlay: CVComponent?
|
||||
|
||||
init(
|
||||
itemModel: CVItemModel,
|
||||
attachmentType: CVComponentState.UndownloadableAttachment,
|
||||
footerOverlay: CVComponent?
|
||||
) {
|
||||
self.attachmentType = attachmentType
|
||||
self.footerOverlay = footerOverlay
|
||||
super.init(itemModel: itemModel)
|
||||
}
|
||||
|
||||
private static let measurementKey_stackView = "CVComponentUndownloadableAttachment.measurementKey_stackView"
|
||||
private static let measurementKey_footerSize = "CVComponentUndownloadableAttachment.measurementKey_footerSize"
|
||||
|
||||
public func buildComponentView(componentDelegate: any CVComponentDelegate) -> any CVComponentView {
|
||||
CVComponentViewUndownloadableAttachment()
|
||||
}
|
||||
|
||||
public func configureForRendering(
|
||||
componentView: any CVComponentView,
|
||||
cellMeasurement: SignalUI.CVCellMeasurement,
|
||||
componentDelegate: any CVComponentDelegate
|
||||
) {
|
||||
guard let componentView = componentView as? CVComponentViewUndownloadableAttachment else {
|
||||
owsFailDebug("Unexpected componentView.")
|
||||
componentView.reset()
|
||||
return
|
||||
}
|
||||
|
||||
let stackView = componentView.stackView
|
||||
let conversationStyle = self.conversationStyle
|
||||
|
||||
if let footerOverlay = self.footerOverlay {
|
||||
let footerView: CVComponentView
|
||||
if let footerOverlayView = componentView.footerOverlayView {
|
||||
footerView = footerOverlayView
|
||||
} else {
|
||||
let footerOverlayView = CVComponentFooter.CVComponentViewFooter()
|
||||
componentView.footerOverlayView = footerOverlayView
|
||||
footerView = footerOverlayView
|
||||
}
|
||||
footerOverlay.configureForRendering(
|
||||
componentView: footerView,
|
||||
cellMeasurement: cellMeasurement,
|
||||
componentDelegate: componentDelegate
|
||||
)
|
||||
let footerRootView = footerView.rootView
|
||||
|
||||
let footerSize = cellMeasurement.size(key: Self.measurementKey_footerSize) ?? .zero
|
||||
stackView.addSubview(footerRootView) { view in
|
||||
var footerFrame = view.bounds
|
||||
footerFrame.height = min(view.bounds.height, footerSize.height)
|
||||
footerFrame.y = view.bounds.height - footerSize.height
|
||||
footerRootView.frame = footerFrame
|
||||
}
|
||||
}
|
||||
|
||||
let label = CVTextLabel()
|
||||
label.configureForRendering(
|
||||
config: self.labelConfig(
|
||||
conversationStyle: conversationStyle,
|
||||
isIncoming: itemModel.interaction is TSIncomingMessage
|
||||
),
|
||||
spoilerAnimationManager: .init()
|
||||
)
|
||||
|
||||
stackView.configure(
|
||||
config: self.stackViewConfig,
|
||||
cellMeasurement: cellMeasurement,
|
||||
measurementKey: Self.measurementKey_stackView,
|
||||
subviews: [label.view]
|
||||
)
|
||||
}
|
||||
|
||||
public func measure(maxWidth: CGFloat, measurementBuilder: SignalUI.CVCellMeasurement.Builder) -> CGSize {
|
||||
owsAssertDebug(maxWidth > 0)
|
||||
|
||||
let config = self.labelConfig(
|
||||
conversationStyle: conversationStyle,
|
||||
isIncoming: false // Used for color. Doesn't matter for measurement
|
||||
)
|
||||
|
||||
let footerSize: CGSize
|
||||
if let footerOverlay {
|
||||
let maxFooterWidth = max(0, maxWidth - conversationStyle.textInsets.totalWidth)
|
||||
footerSize = footerOverlay.measure(
|
||||
maxWidth: maxFooterWidth,
|
||||
measurementBuilder: measurementBuilder
|
||||
)
|
||||
measurementBuilder.setSize(key: Self.measurementKey_footerSize, size: footerSize)
|
||||
} else {
|
||||
footerSize = .zero
|
||||
}
|
||||
|
||||
let info = CVText.measureBodyTextLabelInManualStackView(
|
||||
config: config,
|
||||
footerSize: footerSize,
|
||||
maxWidth: maxWidth,
|
||||
measurementBuilder: measurementBuilder
|
||||
)
|
||||
|
||||
let stackMeasurement = ManualStackView.measure(
|
||||
config: stackViewConfig,
|
||||
measurementBuilder: measurementBuilder,
|
||||
measurementKey: Self.measurementKey_stackView,
|
||||
subviewInfos: info,
|
||||
maxWidth: maxWidth
|
||||
)
|
||||
|
||||
return stackMeasurement.measuredSize
|
||||
}
|
||||
|
||||
public var stackViewConfig: CVStackViewConfig {
|
||||
CVStackViewConfig(
|
||||
axis: .vertical,
|
||||
alignment: .leading,
|
||||
spacing: 0,
|
||||
layoutMargins: .zero
|
||||
)
|
||||
}
|
||||
|
||||
private func labelConfig(
|
||||
conversationStyle: ConversationStyle,
|
||||
isIncoming: Bool
|
||||
) -> CVTextLabel.Config {
|
||||
let font = UIFont.dynamicTypeBodyClamped
|
||||
let textColor = conversationStyle.bubbleTextColor(isIncoming: isIncoming)
|
||||
|
||||
return CVTextLabel.Config(
|
||||
text: .attributedText(
|
||||
.composed(of: [
|
||||
NSAttributedString.with(
|
||||
image: self.icon,
|
||||
font: .dynamicTypeSubheadlineClamped,
|
||||
centerVerticallyRelativeTo: font
|
||||
),
|
||||
" ",
|
||||
self.message,
|
||||
SignalSymbol.LeadingCharacter.nonBreakingSpace.rawValue,
|
||||
SignalSymbol.chevronTrailing.attributedString(
|
||||
dynamicTypeBaseSize: 16,
|
||||
clamped: true,
|
||||
weight: .bold,
|
||||
leadingCharacter: .nonBreakingSpace,
|
||||
attributes: [.foregroundColor: UIColor.Signal.secondaryLabel]
|
||||
),
|
||||
]).styled(with: .font(font), .color(textColor))
|
||||
),
|
||||
displayConfig: .forUnstyledText(font: font, textColor: textColor),
|
||||
font: font,
|
||||
textColor: textColor,
|
||||
selectionStyling: [:],
|
||||
textAlignment: .natural,
|
||||
lineBreakMode: .byWordWrapping,
|
||||
items: [],
|
||||
linkifyStyle: .linkAttribute
|
||||
)
|
||||
}
|
||||
|
||||
override func handleTap(
|
||||
sender: UIGestureRecognizer,
|
||||
componentDelegate: any CVComponentDelegate,
|
||||
componentView: any CVComponentView,
|
||||
renderItem: CVRenderItem
|
||||
) -> Bool {
|
||||
switch self.attachmentType {
|
||||
case .audio:
|
||||
componentDelegate.didTapUndownloadableAudio()
|
||||
case .sticker:
|
||||
componentDelegate.didTapUndownloadableSticker()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public class CVComponentViewUndownloadableAttachment: NSObject, CVComponentView {
|
||||
fileprivate let stackView = ManualStackView(name: "CVComponentViewAudioAttachment.stackView")
|
||||
fileprivate var footerOverlayView: CVComponentView?
|
||||
|
||||
public var isDedicatedCellView: Bool = false
|
||||
|
||||
public var rootView: UIView {
|
||||
stackView
|
||||
}
|
||||
|
||||
public func setIsCellVisible(_ isCellVisible: Bool) {}
|
||||
|
||||
public func reset() {
|
||||
stackView.reset()
|
||||
footerOverlayView?.reset()
|
||||
footerOverlayView = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -253,6 +253,7 @@ extension ConversationViewController: CVComponentDelegate {
|
||||
)
|
||||
)
|
||||
actionSheet.addAction(.okay)
|
||||
actionSheet.isCancelable = true
|
||||
(conversationSplitViewController ?? self).present(actionSheet, animated: true)
|
||||
}
|
||||
|
||||
@ -268,6 +269,39 @@ extension ConversationViewController: CVComponentDelegate {
|
||||
)
|
||||
)
|
||||
actionSheet.addAction(.okay)
|
||||
actionSheet.isCancelable = true
|
||||
(conversationSplitViewController ?? self).present(actionSheet, animated: true)
|
||||
}
|
||||
|
||||
public func didTapUndownloadableAudio() {
|
||||
let actionSheet = ActionSheetController(
|
||||
title: OWSLocalizedString(
|
||||
"AUDIO_UNAVAILABLE_SHEET_TITLE",
|
||||
comment: "Title for sheet shown when tapping a voice message that has expired and is unavailable for download"
|
||||
),
|
||||
message: OWSLocalizedString(
|
||||
"AUDIO_UNAVAILABLE_SHEET_MESSAGE",
|
||||
comment: "Message for sheet shown when tapping a voice message that has expired and is unavailable for download"
|
||||
)
|
||||
)
|
||||
actionSheet.addAction(.okay)
|
||||
actionSheet.isCancelable = true
|
||||
(conversationSplitViewController ?? self).present(actionSheet, animated: true)
|
||||
}
|
||||
|
||||
public func didTapUndownloadableSticker() {
|
||||
let actionSheet = ActionSheetController(
|
||||
title: OWSLocalizedString(
|
||||
"STICKER_UNAVAILABLE_SHEET_TITLE",
|
||||
comment: "Title for sheet shown when tapping a sticker that has expired and is unavailable for download"
|
||||
),
|
||||
message: OWSLocalizedString(
|
||||
"STICKER_UNAVAILABLE_SHEET_MESSAGE",
|
||||
comment: "Message for sheet shown when tapping a sticker that has expired and is unavailable for download"
|
||||
)
|
||||
)
|
||||
actionSheet.addAction(.okay)
|
||||
actionSheet.isCancelable = true
|
||||
(conversationSplitViewController ?? self).present(actionSheet, animated: true)
|
||||
}
|
||||
|
||||
|
||||
@ -433,6 +433,7 @@ public class CVLoader: NSObject {
|
||||
rootComponent = CVComponentSystemMessage(itemModel: itemModel,
|
||||
systemMessage: defaultDisappearingMessageTimer)
|
||||
case .textOnlyMessage, .audio, .genericAttachment, .paymentAttachment, .archivedPaymentAttachment,
|
||||
.undownloadableAttachment,
|
||||
.contactShare, .bodyMedia, .viewOnce, .stickerMessage, .quoteOnlyMessage,
|
||||
.giftBadge:
|
||||
rootComponent = CVComponentMessage(itemModel: itemModel)
|
||||
|
||||
6
Signal/Symbols.xcassets/audio/Contents.json
Normal file
6
Signal/Symbols.xcassets/audio/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
16
Signal/Symbols.xcassets/audio/audio-square-slash.imageset/Contents.json
vendored
Normal file
16
Signal/Symbols.xcassets/audio/audio-square-slash.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "audio-square-slash.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true,
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
Signal/Symbols.xcassets/audio/audio-square-slash.imageset/audio-square-slash.pdf
vendored
Normal file
BIN
Signal/Symbols.xcassets/audio/audio-square-slash.imageset/audio-square-slash.pdf
vendored
Normal file
Binary file not shown.
16
Signal/Symbols.xcassets/sticker/sticker-slash.imageset/Contents.json
vendored
Normal file
16
Signal/Symbols.xcassets/sticker/sticker-slash.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "sticker-slash.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true,
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
Signal/Symbols.xcassets/sticker/sticker-slash.imageset/sticker-slash.pdf
vendored
Normal file
BIN
Signal/Symbols.xcassets/sticker/sticker-slash.imageset/sticker-slash.pdf
vendored
Normal file
Binary file not shown.
@ -361,6 +361,10 @@ extension EditHistoryTableSheetViewController: CVComponentDelegate {
|
||||
|
||||
func didTapUndownloadableOversizeText() {}
|
||||
|
||||
func didTapUndownloadableAudio() {}
|
||||
|
||||
func didTapUndownloadableSticker() {}
|
||||
|
||||
func didTapBrokenVideo() {}
|
||||
|
||||
func didTapBodyMedia(
|
||||
|
||||
@ -370,6 +370,10 @@ extension MediaGalleryFileCell: CVComponentDelegate {
|
||||
|
||||
func didTapUndownloadableOversizeText() {}
|
||||
|
||||
func didTapUndownloadableAudio() {}
|
||||
|
||||
func didTapUndownloadableSticker() {}
|
||||
|
||||
func didTapBrokenVideo() {}
|
||||
|
||||
func didTapBodyMedia(
|
||||
|
||||
@ -1066,6 +1066,10 @@ extension MessageDetailViewController: CVComponentDelegate {
|
||||
|
||||
func didTapUndownloadableOversizeText() {}
|
||||
|
||||
func didTapUndownloadableAudio() {}
|
||||
|
||||
func didTapUndownloadableSticker() {}
|
||||
|
||||
func didTapBrokenVideo() {}
|
||||
|
||||
// MARK: - Messages
|
||||
|
||||
@ -339,6 +339,10 @@ extension MockConversationView: CVComponentDelegate {
|
||||
|
||||
func didTapUndownloadableOversizeText() {}
|
||||
|
||||
func didTapUndownloadableAudio() {}
|
||||
|
||||
func didTapUndownloadableSticker() {}
|
||||
|
||||
func didTapBrokenVideo() {}
|
||||
|
||||
// MARK: - Messages
|
||||
|
||||
@ -463,6 +463,15 @@
|
||||
/* action sheet button title to enable built in speaker during a call */
|
||||
"AUDIO_ROUTE_BUILT_IN_SPEAKER" = "Speaker";
|
||||
|
||||
/* Message when trying to show a voice message that has expired and is unavailable for download */
|
||||
"AUDIO_UNAVAILABLE_MESSAGE_LABEL" = "Voice message not available";
|
||||
|
||||
/* Message for sheet shown when tapping a voice message that has expired and is unavailable for download */
|
||||
"AUDIO_UNAVAILABLE_SHEET_MESSAGE" = "This voice message was not transferred from your phone when this device was linked. Files and media older than 45 days at the time of device linking can not be synced.";
|
||||
|
||||
/* Title for sheet shown when tapping a voice message that has expired and is unavailable for download */
|
||||
"AUDIO_UNAVAILABLE_SHEET_TITLE" = "Voice Message Not Available";
|
||||
|
||||
/* Text prompting the user to choose a color when editing their avatar */
|
||||
"AVATAR_EDIT_VIEW_CHOOSE_A_COLOR" = "Choose a Color";
|
||||
|
||||
@ -7690,6 +7699,15 @@
|
||||
/* Preview text shown in notifications and conversation list for sticker messages. */
|
||||
"STICKER_MESSAGE_PREVIEW" = "Sticker Message";
|
||||
|
||||
/* Message when trying to show a sticker that has expired and is unavailable for download */
|
||||
"STICKER_UNAVAILABLE_MESSAGE_LABEL" = "Sticker not available";
|
||||
|
||||
/* Message for sheet shown when tapping a sticker that has expired and is unavailable for download */
|
||||
"STICKER_UNAVAILABLE_SHEET_MESSAGE" = "This sticker was not transferred from your phone when this device was linked. Media and files older than 45 days at the time of device linking can not be synced.";
|
||||
|
||||
/* Title for sheet shown when tapping a sticker that has expired and is unavailable for download */
|
||||
"STICKER_UNAVAILABLE_SHEET_TITLE" = "Sticker Not Available";
|
||||
|
||||
/* Label for the 'install sticker pack' button. */
|
||||
"STICKERS_INSTALL_BUTTON" = "Install";
|
||||
|
||||
|
||||
@ -352,6 +352,57 @@ public class CVText {
|
||||
|
||||
return measurement
|
||||
}
|
||||
|
||||
public static func measureBodyTextLabelInManualStackView(
|
||||
config: CVTextLabel.Config,
|
||||
footerSize: CGSize,
|
||||
maxWidth: CGFloat,
|
||||
measurementBuilder: CVCellMeasurement.Builder
|
||||
) -> [ManualStackSubviewInfo] {
|
||||
let footerWidthWithSpacing = footerSize.width + 6
|
||||
let maxTextWidthForAdjacentFooter = maxWidth - footerWidthWithSpacing
|
||||
|
||||
let measurementWithSpaceForAdjacentFooter = CVText.measureBodyTextLabel(
|
||||
config: config,
|
||||
maxWidth: maxTextWidthForAdjacentFooter
|
||||
)
|
||||
|
||||
let canFitOnOneLineWithAdjacentFooter =
|
||||
if let lastLineRect = measurementWithSpaceForAdjacentFooter.lastLineRect {
|
||||
lastLineRect.height == measurementWithSpaceForAdjacentFooter.size.height
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
let info: [ManualStackSubviewInfo]
|
||||
if canFitOnOneLineWithAdjacentFooter {
|
||||
let textSize = measurementWithSpaceForAdjacentFooter.size
|
||||
info = [CGSize(width: textSize.width + footerWidthWithSpacing, height: textSize.height).ceil.asManualSubviewInfo]
|
||||
} else {
|
||||
let measurementForFullWidth = CVText.measureBodyTextLabel(
|
||||
config: config,
|
||||
maxWidth: maxWidth
|
||||
)
|
||||
let textInfo = measurementForFullWidth.size.ceil.asManualSubviewInfo
|
||||
|
||||
let footerShouldOverlapWithLastLine = measurementForFullWidth
|
||||
.lastLineRect.map { lastLineRect in
|
||||
lastLineRect.width <= maxTextWidthForAdjacentFooter
|
||||
} ?? false
|
||||
|
||||
if footerShouldOverlapWithLastLine {
|
||||
info = [textInfo]
|
||||
} else {
|
||||
let footerSpacerInfo = CGSize(
|
||||
width: footerSize.width,
|
||||
height: footerSize.height + 3
|
||||
).ceil.asManualSubviewInfo
|
||||
info = [textInfo, footerSpacerInfo]
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
Loading…
Reference in New Issue
Block a user