668 lines
26 KiB
Swift
668 lines
26 KiB
Swift
//
|
|
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
public import SignalServiceKit
|
|
public import SignalUI
|
|
|
|
public class CVComponentFooter: CVComponentBase, CVComponent {
|
|
|
|
public var componentKey: CVComponentKey { .footer }
|
|
|
|
struct StatusIndicator: Equatable {
|
|
let imageName: String
|
|
let imageSize: CGSize
|
|
let isAnimated: Bool
|
|
}
|
|
|
|
struct State: Equatable {
|
|
let timestampText: String
|
|
let statusIndicator: StatusIndicator?
|
|
let accessibilityLabel: String?
|
|
let hasTapForMore: Bool
|
|
let displayEditedLabel: Bool
|
|
|
|
struct Expiration: Equatable {
|
|
let expirationTimestamp: UInt64
|
|
let expiresInSeconds: UInt32
|
|
}
|
|
let expiration: Expiration?
|
|
|
|
}
|
|
private let footerState: State
|
|
|
|
public var timestampText: String {
|
|
footerState.timestampText
|
|
}
|
|
private var statusIndicator: StatusIndicator? {
|
|
footerState.statusIndicator
|
|
}
|
|
public var hasTapForMore: Bool {
|
|
footerState.hasTapForMore
|
|
}
|
|
public var displayEditedLabel: Bool {
|
|
footerState.displayEditedLabel
|
|
}
|
|
private var expiration: State.Expiration? {
|
|
footerState.expiration
|
|
}
|
|
|
|
let isOverlayingMedia: Bool
|
|
private let isOutsideBubble: Bool
|
|
|
|
init(itemModel: CVItemModel,
|
|
footerState: State,
|
|
isOverlayingMedia: Bool,
|
|
isOutsideBubble: Bool) {
|
|
|
|
self.footerState = footerState
|
|
self.isOverlayingMedia = isOverlayingMedia
|
|
self.isOutsideBubble = isOutsideBubble
|
|
|
|
super.init(itemModel: itemModel)
|
|
}
|
|
|
|
public func buildComponentView(componentDelegate: CVComponentDelegate) -> CVComponentView {
|
|
CVComponentViewFooter()
|
|
}
|
|
|
|
public override func updateScrollingContent(componentView: CVComponentView) {
|
|
super.updateScrollingContent(componentView: componentView)
|
|
|
|
guard let componentView = componentView as? CVComponentViewFooter else {
|
|
owsFailDebug("Unexpected componentView.")
|
|
return
|
|
}
|
|
componentView.chatColorView.updateAppearance()
|
|
}
|
|
|
|
public static let textViewVSpacing: CGFloat = 2
|
|
public static let bodyMediaQuotedReplyVSpacing: CGFloat = 6
|
|
public static let quotedReplyTopMargin: CGFloat = 6
|
|
|
|
public func configureForRendering(componentView componentViewParam: CVComponentView,
|
|
cellMeasurement: CVCellMeasurement,
|
|
componentDelegate: CVComponentDelegate) {
|
|
guard let componentView = componentViewParam as? CVComponentViewFooter else {
|
|
owsFailDebug("Unexpected componentView.")
|
|
componentViewParam.reset()
|
|
return
|
|
}
|
|
|
|
let outerStack = componentView.outerStack
|
|
let innerStack = componentView.innerStack
|
|
innerStack.reset()
|
|
outerStack.reset()
|
|
|
|
var outerViews = [UIView]()
|
|
var innerViews = [UIView]()
|
|
|
|
if isBorderless && conversationStyle.hasWallpaper {
|
|
let chatColorView = componentView.chatColorView
|
|
chatColorView.configure(value: conversationStyle.bubbleChatColor(isIncoming: isIncoming),
|
|
referenceView: componentDelegate.view,
|
|
hasPillRounding: true)
|
|
innerStack.addSubviewToFillSuperviewEdges(chatColorView)
|
|
}
|
|
|
|
if let tapForMoreLabelConfig = self.tapForMoreLabelConfig {
|
|
let tapForMoreLabel = componentView.tapForMoreLabel
|
|
tapForMoreLabelConfig.applyForRendering(label: tapForMoreLabel)
|
|
outerViews.append(tapForMoreLabel)
|
|
}
|
|
|
|
// We always use a stretching spacer.
|
|
outerViews.append(UIView.hStretchingSpacer())
|
|
outerViews.append(innerStack)
|
|
|
|
let timestampLabel = componentView.timestampLabel
|
|
let textColor: UIColor
|
|
if wasRemotelyDeleted && !conversationStyle.hasWallpaper {
|
|
owsAssertDebug(!isOverlayingMedia)
|
|
textColor = Theme.primaryTextColor
|
|
} else if isOverlayingMedia {
|
|
textColor = .ows_white
|
|
} else if isOutsideBubble && !conversationStyle.hasWallpaper {
|
|
textColor = Theme.secondaryTextAndIconColor
|
|
} else {
|
|
textColor = conversationStyle.bubbleSecondaryTextColor(isIncoming: isIncoming)
|
|
}
|
|
|
|
if displayEditedLabel {
|
|
let editedLabel = componentView.editedLabel
|
|
editedLabelConfig(textColor: textColor).applyForRendering(label: editedLabel)
|
|
innerViews.append(editedLabel)
|
|
}
|
|
|
|
timestampLabelConfig(textColor: textColor).applyForRendering(label: timestampLabel)
|
|
innerViews.append(timestampLabel)
|
|
|
|
if let expiration = expiration {
|
|
let messageTimerView = componentView.messageTimerView
|
|
messageTimerView.configure(
|
|
expirationTimestampMs: expiration.expirationTimestamp,
|
|
disappearingMessageInterval: expiration.expiresInSeconds,
|
|
tintColor: textColor
|
|
)
|
|
innerViews.append(messageTimerView)
|
|
}
|
|
|
|
if isRepresentingSmsMessageRestoredFromBackup {
|
|
let smsLockIconView = componentView.smsLockIconView
|
|
smsLockIconView.configure(tintColor: textColor)
|
|
innerViews.append(smsLockIconView)
|
|
}
|
|
|
|
if let statusIndicator = self.statusIndicator {
|
|
if let icon = UIImage(named: statusIndicator.imageName) {
|
|
let statusIndicatorImageView = componentView.statusIndicatorImageView
|
|
owsAssertDebug(icon.size == statusIndicator.imageSize)
|
|
statusIndicatorImageView.image = icon.withRenderingMode(.alwaysTemplate)
|
|
statusIndicatorImageView.tintColor = textColor
|
|
innerViews.append(statusIndicatorImageView)
|
|
|
|
if statusIndicator.isAnimated {
|
|
componentView.animateSpinningIcon()
|
|
}
|
|
} else {
|
|
owsFailDebug("Missing statusIndicatorImage.")
|
|
}
|
|
}
|
|
|
|
innerStack.configure(config: innerStackConfig,
|
|
cellMeasurement: cellMeasurement,
|
|
measurementKey: Self.measurementKey_innerStack,
|
|
subviews: innerViews)
|
|
outerStack.configure(config: outerStackConfig,
|
|
cellMeasurement: cellMeasurement,
|
|
measurementKey: Self.measurementKey_outerStack,
|
|
subviews: outerViews)
|
|
}
|
|
|
|
static func outgoingMessageStatus(interaction: TSInteraction, hasBodyAttachments: Bool) -> MessageReceiptStatus? {
|
|
guard let outgoingMessage = interaction as? TSOutgoingMessage else {
|
|
return nil
|
|
}
|
|
return MessageRecipientStatusUtils.recipientStatus(outgoingMessage: outgoingMessage, hasBodyAttachments: hasBodyAttachments)
|
|
}
|
|
|
|
public static func timestampText(
|
|
forInteraction interaction: TSInteraction,
|
|
shouldUseLongFormat: Bool,
|
|
hasBodyAttachments: Bool
|
|
) -> String {
|
|
|
|
let status = Self.outgoingMessageStatus(interaction: interaction, hasBodyAttachments: hasBodyAttachments)
|
|
let isPendingOutgoingMessage = status == .pending
|
|
let isFailedOutgoingMessage = status == .failed
|
|
let wasSentToAnyRecipient: Bool = {
|
|
guard let outgoingMessage = interaction as? TSOutgoingMessage else {
|
|
return false
|
|
}
|
|
return outgoingMessage.wasSentToAnyRecipient
|
|
}()
|
|
|
|
if isPendingOutgoingMessage {
|
|
return OWSLocalizedString("MESSAGE_STATUS_PENDING",
|
|
comment: "Label indicating that a message send was paused.")
|
|
} else if isFailedOutgoingMessage {
|
|
if wasSentToAnyRecipient {
|
|
return OWSLocalizedString("MESSAGE_STATUS_PARTIALLY_SENT",
|
|
comment: "Label indicating that a message was only sent to some recipients.")
|
|
} else {
|
|
return OWSLocalizedString("MESSAGE_STATUS_SEND_FAILED",
|
|
comment: "Label indicating that a message failed to send.")
|
|
}
|
|
} else {
|
|
return DateUtil.formatMessageTimestampForCVC(interaction.timestamp,
|
|
shouldUseLongFormat: shouldUseLongFormat)
|
|
}
|
|
}
|
|
|
|
static func buildPaymentState(
|
|
interaction: TSInteraction,
|
|
paymentNotification: TSPaymentNotification?,
|
|
hasTapForMore: Bool,
|
|
transaction: SDSAnyReadTransaction
|
|
) -> State {
|
|
|
|
guard
|
|
let receiptData = paymentNotification?.mcReceiptData,
|
|
let paymentModel = PaymentFinder.paymentModels(
|
|
forMcReceiptData: receiptData,
|
|
transaction: transaction).first
|
|
else {
|
|
let hasBodyAttachments = (interaction as? TSMessage)?.hasBodyAttachments(transaction: transaction) ?? false
|
|
let timestampText = Self.timestampText(
|
|
forInteraction: interaction,
|
|
shouldUseLongFormat: false,
|
|
hasBodyAttachments: hasBodyAttachments
|
|
)
|
|
return State(
|
|
timestampText: timestampText,
|
|
statusIndicator: nil,
|
|
accessibilityLabel: nil,
|
|
hasTapForMore: hasTapForMore,
|
|
displayEditedLabel: false,
|
|
expiration: nil
|
|
)
|
|
}
|
|
|
|
let timestampText = Self.paymentMessageTimestampText(
|
|
forInteraction: interaction,
|
|
paymentState: paymentModel.paymentState,
|
|
shouldUseLongFormat: false
|
|
)
|
|
|
|
var statusIndicator: StatusIndicator?
|
|
var accessibilityLabel: String?
|
|
if let outgoingMessage = interaction as? TSOutgoingMessage {
|
|
|
|
let messageStatus = MessageRecipientStatusUtils.recipientStatus(
|
|
outgoingMessage: outgoingMessage,
|
|
paymentModel: paymentModel
|
|
)
|
|
accessibilityLabel = MessageRecipientStatusUtils.receiptMessage(
|
|
outgoingMessage: outgoingMessage,
|
|
paymentModel: paymentModel
|
|
)
|
|
|
|
switch messageStatus {
|
|
case .uploading, .sending:
|
|
statusIndicator = StatusIndicator(
|
|
imageName: "message_status_sending",
|
|
imageSize: .square(12),
|
|
isAnimated: true
|
|
)
|
|
case .pending:
|
|
statusIndicator = StatusIndicator(
|
|
imageName: "message_status_sending",
|
|
imageSize: .square(12),
|
|
isAnimated: false
|
|
)
|
|
case .sent, .skipped:
|
|
statusIndicator = StatusIndicator(
|
|
imageName: "message_status_sent",
|
|
imageSize: .square(12),
|
|
isAnimated: false
|
|
)
|
|
case .delivered:
|
|
statusIndicator = StatusIndicator(
|
|
imageName: "message_status_delivered",
|
|
imageSize: .init(width: 18, height: 12),
|
|
isAnimated: false
|
|
)
|
|
case .read, .viewed:
|
|
statusIndicator = StatusIndicator(
|
|
imageName: "message_status_read",
|
|
imageSize: .init(width: 18, height: 12),
|
|
isAnimated: false
|
|
)
|
|
case .failed:
|
|
// No status indicator icon.
|
|
break
|
|
}
|
|
|
|
if outgoingMessage.wasRemotelyDeleted {
|
|
statusIndicator = nil
|
|
}
|
|
}
|
|
|
|
var expiration: State.Expiration?
|
|
if let message = interaction as? TSMessage,
|
|
message.hasPerConversationExpiration {
|
|
expiration = State.Expiration(
|
|
expirationTimestamp: message.expiresAt,
|
|
expiresInSeconds: message.expiresInSeconds
|
|
)
|
|
}
|
|
|
|
return State(
|
|
timestampText: timestampText,
|
|
statusIndicator: statusIndicator,
|
|
accessibilityLabel: accessibilityLabel,
|
|
hasTapForMore: hasTapForMore,
|
|
displayEditedLabel: false,
|
|
expiration: expiration
|
|
)
|
|
}
|
|
|
|
public static func paymentMessageTimestampText(
|
|
forInteraction interaction: TSInteraction,
|
|
paymentState: TSPaymentState,
|
|
shouldUseLongFormat: Bool
|
|
) -> String {
|
|
|
|
switch paymentState.messageReceiptStatus {
|
|
case .pending:
|
|
return OWSLocalizedString(
|
|
"MESSAGE_STATUS_PENDING",
|
|
comment: "Label indicating that a message send was paused."
|
|
)
|
|
case .failed:
|
|
return OWSLocalizedString(
|
|
"MESSAGE_STATUS_SEND_FAILED",
|
|
comment: "Label indicating that a message failed to send."
|
|
)
|
|
default:
|
|
return DateUtil.formatMessageTimestampForCVC(
|
|
interaction.timestamp,
|
|
shouldUseLongFormat: shouldUseLongFormat
|
|
)
|
|
}
|
|
}
|
|
|
|
static func buildState(interaction: TSInteraction, hasTapForMore: Bool, transaction: SDSAnyReadTransaction) -> State {
|
|
|
|
let hasBodyAttachments = (interaction as? TSMessage)?.hasBodyAttachments(transaction: transaction) ?? false
|
|
let timestampText = Self.timestampText(
|
|
forInteraction: interaction,
|
|
shouldUseLongFormat: false,
|
|
hasBodyAttachments: hasBodyAttachments
|
|
)
|
|
|
|
var statusIndicator: StatusIndicator?
|
|
var accessibilityLabel: String?
|
|
if let outgoingMessage = interaction as? TSOutgoingMessage {
|
|
let (messageStatus, label) = MessageRecipientStatusUtils.receiptStatusAndMessage(
|
|
outgoingMessage: outgoingMessage,
|
|
transaction: transaction
|
|
)
|
|
accessibilityLabel = label
|
|
|
|
switch messageStatus {
|
|
case .uploading, .sending:
|
|
statusIndicator = StatusIndicator(imageName: "message_status_sending",
|
|
imageSize: .square(12),
|
|
isAnimated: true)
|
|
case .pending:
|
|
statusIndicator = StatusIndicator(imageName: "message_status_sending",
|
|
imageSize: .square(12),
|
|
isAnimated: false)
|
|
case .sent, .skipped:
|
|
statusIndicator = StatusIndicator(imageName: "message_status_sent",
|
|
imageSize: .square(12),
|
|
isAnimated: false)
|
|
case .delivered:
|
|
statusIndicator = StatusIndicator(imageName: "message_status_delivered",
|
|
imageSize: .init(width: 18, height: 12),
|
|
isAnimated: false)
|
|
case .read, .viewed:
|
|
statusIndicator = StatusIndicator(imageName: "message_status_read",
|
|
imageSize: .init(width: 18, height: 12),
|
|
isAnimated: false)
|
|
case .failed:
|
|
// No status indicator icon.
|
|
break
|
|
}
|
|
|
|
if outgoingMessage.wasRemotelyDeleted {
|
|
statusIndicator = nil
|
|
}
|
|
}
|
|
|
|
var expiration: State.Expiration?
|
|
var displayEditedLabel: Bool = false
|
|
if let message = interaction as? TSMessage {
|
|
if message.hasPerConversationExpiration {
|
|
expiration = State.Expiration(
|
|
expirationTimestamp: message.expiresAt,
|
|
expiresInSeconds: message.expiresInSeconds
|
|
)
|
|
}
|
|
|
|
if !message.wasRemotelyDeleted {
|
|
switch message.editState {
|
|
case .latestRevisionRead, .latestRevisionUnread:
|
|
displayEditedLabel = true
|
|
case .none, .pastRevision:
|
|
displayEditedLabel = false
|
|
}
|
|
}
|
|
}
|
|
|
|
return State(
|
|
timestampText: timestampText,
|
|
statusIndicator: statusIndicator,
|
|
accessibilityLabel: accessibilityLabel,
|
|
hasTapForMore: hasTapForMore,
|
|
displayEditedLabel: displayEditedLabel,
|
|
expiration: expiration
|
|
)
|
|
}
|
|
|
|
private func editedLabelConfig(textColor: UIColor) -> CVLabelConfig {
|
|
let text = OWSLocalizedString(
|
|
"MESSAGE_STATUS_EDITED",
|
|
comment: "status meesage for edited messages"
|
|
)
|
|
|
|
return CVLabelConfig.unstyledText(
|
|
text,
|
|
font: .dynamicTypeCaption1,
|
|
textColor: textColor
|
|
)
|
|
}
|
|
|
|
private func timestampLabelConfig(textColor: UIColor) -> CVLabelConfig {
|
|
return CVLabelConfig.unstyledText(
|
|
timestampText,
|
|
font: .dynamicTypeCaption1,
|
|
textColor: textColor
|
|
)
|
|
}
|
|
|
|
private var tapForMoreLabelConfig: CVLabelConfig? {
|
|
guard hasTapForMore, !wasRemotelyDeleted else {
|
|
return nil
|
|
}
|
|
guard let message = interaction as? TSMessage else {
|
|
owsFailDebug("Invalid interaction.")
|
|
return nil
|
|
}
|
|
let text = OWSLocalizedString("CONVERSATION_VIEW_OVERSIZE_TEXT_TAP_FOR_MORE",
|
|
comment: "Indicator on truncated text messages that they can be tapped to see the entire text message.")
|
|
return CVLabelConfig.unstyledText(
|
|
text,
|
|
font: UIFont.dynamicTypeSubheadlineClamped.semibold(),
|
|
textColor: conversationStyle.bubbleReadMoreTextColor(message: message),
|
|
textAlignment: .trailing
|
|
)
|
|
}
|
|
|
|
private let tapForMoreHeightFactor: CGFloat = 1.25
|
|
|
|
private var outerStackConfig: CVStackViewConfig {
|
|
CVStackViewConfig(axis: .horizontal,
|
|
alignment: .bottom,
|
|
spacing: CVComponentFooter.hSpacing,
|
|
layoutMargins: .zero)
|
|
}
|
|
|
|
private var innerStackConfig: CVStackViewConfig {
|
|
let layoutMargins = isBorderless ? UIEdgeInsets(hMargin: 12, vMargin: 3) : .zero
|
|
return CVStackViewConfig(axis: .horizontal,
|
|
alignment: .center,
|
|
spacing: CVComponentFooter.hSpacing,
|
|
layoutMargins: layoutMargins)
|
|
}
|
|
|
|
private static let measurementKey_outerStack = "CVComponentFooter.measurementKey_outerStack"
|
|
private static let measurementKey_innerStack = "CVComponentFooter.measurementKey_innerStack"
|
|
|
|
// Extract the overall measurement for this component.
|
|
public static func footerMeasurement(measurementBuilder: CVCellMeasurement.Builder) -> CVCellMeasurement.Measurement? {
|
|
measurementBuilder.getMeasurement(key: measurementKey_outerStack)
|
|
}
|
|
|
|
public func measure(maxWidth: CGFloat, measurementBuilder: CVCellMeasurement.Builder) -> CGSize {
|
|
owsAssertDebug(maxWidth > 0)
|
|
|
|
var outerSubviewInfos = [ManualStackSubviewInfo]()
|
|
var innerSubviewInfos = [ManualStackSubviewInfo]()
|
|
|
|
if let tapForMoreLabelConfig = self.tapForMoreLabelConfig {
|
|
var tapForMoreSize = CVText.measureLabel(config: tapForMoreLabelConfig,
|
|
maxWidth: maxWidth)
|
|
tapForMoreSize.height *= tapForMoreHeightFactor
|
|
outerSubviewInfos.append(tapForMoreSize.asManualSubviewInfo(hasFixedWidth: true))
|
|
}
|
|
|
|
// We always use a stretching spacer.
|
|
outerSubviewInfos.append(ManualStackSubviewInfo.empty)
|
|
|
|
if displayEditedLabel {
|
|
let editedLabelConfig = self.editedLabelConfig(textColor: .black)
|
|
let editedLabelSize = CVText.measureLabel(config: editedLabelConfig, maxWidth: maxWidth)
|
|
innerSubviewInfos.append(editedLabelSize.asManualSubviewInfo(hasFixedWidth: true))
|
|
}
|
|
|
|
// The color doesn't matter for measurement.
|
|
let timestampLabelConfig = self.timestampLabelConfig(textColor: UIColor.black)
|
|
let timestampLabelSize = CVText.measureLabel(config: timestampLabelConfig,
|
|
maxWidth: maxWidth)
|
|
innerSubviewInfos.append(timestampLabelSize.asManualSubviewInfo(hasFixedWidth: true))
|
|
|
|
if hasPerConversationExpiration,
|
|
interaction is TSMessage {
|
|
let timerSize = MessageTimerView.measureSize
|
|
innerSubviewInfos.append(timerSize.asManualSubviewInfo(hasFixedWidth: true))
|
|
}
|
|
|
|
if isRepresentingSmsMessageRestoredFromBackup {
|
|
let lockIconSize = SmsLockIconView.size
|
|
innerSubviewInfos.append(lockIconSize.asManualSubviewInfo(hasFixedWidth: true))
|
|
}
|
|
|
|
if let statusIndicator = self.statusIndicator {
|
|
let statusSize = statusIndicator.imageSize
|
|
innerSubviewInfos.append(statusSize.asManualSubviewInfo(hasFixedWidth: true))
|
|
}
|
|
|
|
let innerStackMeasurement = ManualStackView.measure(config: innerStackConfig,
|
|
measurementBuilder: measurementBuilder,
|
|
measurementKey: Self.measurementKey_innerStack,
|
|
subviewInfos: innerSubviewInfos)
|
|
outerSubviewInfos.append(innerStackMeasurement.measuredSize.asManualSubviewInfo(hasFixedWidth: true))
|
|
let outerStackMeasurement = ManualStackView.measure(config: outerStackConfig,
|
|
measurementBuilder: measurementBuilder,
|
|
measurementKey: Self.measurementKey_outerStack,
|
|
subviewInfos: outerSubviewInfos,
|
|
maxWidth: maxWidth)
|
|
return outerStackMeasurement.measuredSize
|
|
}
|
|
|
|
private static let hSpacing: CGFloat = 4
|
|
|
|
// MARK: - Events
|
|
|
|
public override func handleTap(sender: UIGestureRecognizer,
|
|
componentDelegate: CVComponentDelegate,
|
|
componentView: CVComponentView,
|
|
renderItem: CVRenderItem) -> Bool {
|
|
|
|
guard let componentView = componentView as? CVComponentViewFooter else {
|
|
owsFailDebug("Unexpected componentView.")
|
|
return false
|
|
}
|
|
|
|
if hasTapForMore {
|
|
let readMoreLabel = componentView.tapForMoreLabel
|
|
let location = sender.location(in: readMoreLabel)
|
|
if readMoreLabel.bounds.contains(location) {
|
|
let itemViewModel = CVItemViewModelImpl(renderItem: renderItem)
|
|
componentDelegate.didTapTruncatedTextMessage(itemViewModel)
|
|
return true
|
|
}
|
|
}
|
|
if displayEditedLabel {
|
|
let editedLabel = componentView.editedLabel
|
|
let location = sender.location(in: editedLabel)
|
|
if editedLabel.bounds.contains(location) {
|
|
let itemViewModel = CVItemViewModelImpl(renderItem: renderItem)
|
|
componentDelegate.didTapShowEditHistory(itemViewModel)
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
// Used for rendering some portion of an Conversation View item.
|
|
// It could be the entire item or some part thereof.
|
|
public class CVComponentViewFooter: NSObject, CVComponentView {
|
|
|
|
fileprivate let outerStack = ManualStackView(name: "footer.outerStack")
|
|
fileprivate let innerStack = ManualStackViewWithLayer(name: "footer.innerStack")
|
|
fileprivate let tapForMoreLabel = CVLabel()
|
|
fileprivate let editedLabel = CVLabel()
|
|
fileprivate let timestampLabel = CVLabel()
|
|
fileprivate let statusIndicatorImageView = CVImageView()
|
|
fileprivate let messageTimerView = MessageTimerView()
|
|
fileprivate let smsLockIconView = SmsLockIconView()
|
|
fileprivate let chatColorView = CVColorOrGradientView()
|
|
|
|
public var isDedicatedCellView = false
|
|
|
|
public var rootView: UIView {
|
|
outerStack
|
|
}
|
|
|
|
override init() {
|
|
timestampLabel.textAlignment = .trailing
|
|
}
|
|
|
|
public func setIsCellVisible(_ isCellVisible: Bool) {}
|
|
|
|
public func reset() {
|
|
outerStack.reset()
|
|
innerStack.reset()
|
|
innerStack.backgroundColor = nil
|
|
|
|
tapForMoreLabel.text = nil
|
|
editedLabel.text = nil
|
|
timestampLabel.text = nil
|
|
statusIndicatorImageView.image = nil
|
|
|
|
statusIndicatorImageView.layer.removeAllAnimations()
|
|
|
|
messageTimerView.prepareForReuse()
|
|
messageTimerView.removeFromSuperview()
|
|
|
|
smsLockIconView.removeFromSuperview()
|
|
|
|
chatColorView.reset()
|
|
chatColorView.removeFromSuperview()
|
|
}
|
|
|
|
fileprivate func animateSpinningIcon() {
|
|
let animation = CABasicAnimation.init(keyPath: "transform.rotation.z")
|
|
animation.toValue = CGFloat.pi * 2
|
|
animation.duration = kSecondInterval * 1
|
|
animation.isCumulative = true
|
|
animation.repeatCount = .greatestFiniteMagnitude
|
|
statusIndicatorImageView.layer.add(animation, forKey: "animation")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private extension CVComponentFooter {
|
|
/// Is this footer representing an SMS message we restored from a Backup?
|
|
///
|
|
/// If so, we want to add some UI to indicate such, matching the UI for
|
|
/// these on Android, where they originated.
|
|
var isRepresentingSmsMessageRestoredFromBackup: Bool {
|
|
if
|
|
let message = interaction as? TSMessage,
|
|
message.isSmsMessageRestoredFromBackup
|
|
{
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|