180 lines
6.3 KiB
Swift
180 lines
6.3 KiB
Swift
//
|
|
// Copyright 2022 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
import SignalServiceKit
|
|
public import SignalUI
|
|
|
|
public class CVComponentGiftBadge: CVComponentBase, CVComponent {
|
|
|
|
public var componentKey: CVComponentKey { .giftBadge }
|
|
|
|
private let giftBadge: CVComponentState.GiftBadge
|
|
|
|
private let viewState: ViewState
|
|
|
|
// Component state objects are derived from TSInteractions, and they're
|
|
// only updated when the underlying interaction changes. The "N days
|
|
// remaining" label depends on the current time, so we need to use
|
|
// CVItemViewState, which is refreshed even when the underlying interaction
|
|
// hasn't changed. This is similar to how the time in the footer works.
|
|
struct ViewState: Equatable {
|
|
let timeRemainingText: String
|
|
let cachedBadgeValue: CachedBadge.Value?
|
|
}
|
|
|
|
static func buildViewState(_ giftBadge: CVComponentState.GiftBadge) -> ViewState {
|
|
ViewState(
|
|
timeRemainingText: GiftBadgeView.timeRemainingText(for: giftBadge.expirationDate),
|
|
cachedBadgeValue: giftBadge.cachedBadge.cachedValue
|
|
)
|
|
}
|
|
|
|
private var state: GiftBadgeView.State {
|
|
let stateBadge: GiftBadgeView.State.Badge
|
|
switch self.viewState.cachedBadgeValue {
|
|
case .none:
|
|
let cachedBadge = self.giftBadge.cachedBadge
|
|
stateBadge = .notLoaded({ cachedBadge.fetchIfNeeded().asVoid() })
|
|
case .some(.notFound):
|
|
stateBadge = .notFound
|
|
case .some(.profileBadge(let profileBadge)):
|
|
stateBadge = .loaded(profileBadge)
|
|
}
|
|
return GiftBadgeView.State(
|
|
badge: stateBadge,
|
|
messageUniqueId: self.giftBadge.messageUniqueId,
|
|
timeRemainingText: self.viewState.timeRemainingText,
|
|
otherUserShortName: self.giftBadge.otherUserShortName,
|
|
redemptionState: self.giftBadge.redemptionState,
|
|
isIncoming: self.isIncoming,
|
|
conversationStyle: self.conversationStyle
|
|
)
|
|
}
|
|
|
|
init(itemModel: CVItemModel, giftBadge: CVComponentState.GiftBadge, viewState: ViewState) {
|
|
self.giftBadge = giftBadge
|
|
self.viewState = viewState
|
|
super.init(itemModel: itemModel)
|
|
}
|
|
|
|
public func buildComponentView(componentDelegate: CVComponentDelegate) -> CVComponentView {
|
|
CVComponentViewGiftBadge(componentDelegate: componentDelegate)
|
|
}
|
|
|
|
public func configureForRendering(
|
|
componentView: CVComponentView,
|
|
cellMeasurement: CVCellMeasurement,
|
|
componentDelegate: CVComponentDelegate
|
|
) {
|
|
guard let componentView = componentView as? CVComponentViewGiftBadge else {
|
|
owsFailDebug("unexpected componentView")
|
|
componentView.reset()
|
|
return
|
|
}
|
|
|
|
componentView.messageUniqueId = self.giftBadge.messageUniqueId
|
|
componentView.giftBadgeView.configureForRendering(
|
|
state: self.state,
|
|
cellMeasurement: cellMeasurement,
|
|
componentDelegate: componentDelegate
|
|
)
|
|
}
|
|
|
|
public func measure(maxWidth: CGFloat, measurementBuilder: CVCellMeasurement.Builder) -> CGSize {
|
|
return GiftBadgeView.measurement(for: self.state, maxWidth: maxWidth, measurementBuilder: measurementBuilder)
|
|
}
|
|
|
|
public func configureGiftWrapIfNeeded(componentView: CVComponentView) -> (ManualLayoutView, OWSBubbleViewPartner)? {
|
|
guard let componentView = componentView as? CVComponentViewGiftBadge else {
|
|
owsFailDebug("unexpected componentView")
|
|
return nil
|
|
}
|
|
guard let giftWrap = componentView.giftBadgeView.giftWrap else {
|
|
return nil
|
|
}
|
|
return (giftWrap.rootView, giftWrap.bubbleViewPartner)
|
|
}
|
|
|
|
public override func handleTap(
|
|
sender: UIGestureRecognizer,
|
|
componentDelegate: CVComponentDelegate,
|
|
componentView: CVComponentView,
|
|
renderItem: CVRenderItem
|
|
) -> Bool {
|
|
|
|
guard let componentView = componentView as? CVComponentViewGiftBadge else {
|
|
owsFailDebug("unexpected componentView")
|
|
return false
|
|
}
|
|
let itemViewModel = CVItemViewModelImpl(renderItem: renderItem)
|
|
let giftBadgeView = componentView.giftBadgeView
|
|
|
|
if giftBadgeView.giftWrap != nil {
|
|
componentDelegate.willUnwrapGift(itemViewModel)
|
|
giftBadgeView.animateUnwrap()
|
|
return true
|
|
}
|
|
|
|
let profileBadge: ProfileBadge
|
|
switch self.viewState.cachedBadgeValue {
|
|
case .some(.profileBadge(let value)):
|
|
profileBadge = value
|
|
default:
|
|
// If there's not a badge, it's still showing the loading indicator.
|
|
return false
|
|
}
|
|
|
|
let buttonView = giftBadgeView.buttonStack
|
|
guard buttonView.bounds.contains(sender.location(in: buttonView)) else {
|
|
return false
|
|
}
|
|
|
|
let giftBadge = self.giftBadge
|
|
componentDelegate.didTapGiftBadge(
|
|
itemViewModel,
|
|
profileBadge: profileBadge,
|
|
isExpired: giftBadge.expirationDate.isBeforeNow,
|
|
isRedeemed: giftBadge.redemptionState == .redeemed
|
|
)
|
|
return true
|
|
}
|
|
|
|
public class CVComponentViewGiftBadge: NSObject, CVComponentView {
|
|
fileprivate let giftBadgeView = GiftBadgeView(name: "GiftBadgeView")
|
|
|
|
fileprivate var messageUniqueId: String?
|
|
|
|
private weak var componentDelegate: CVComponentDelegate?
|
|
|
|
public var isDedicatedCellView = false
|
|
|
|
public var rootView: UIView { giftBadgeView }
|
|
|
|
init(componentDelegate: CVComponentDelegate) {
|
|
self.componentDelegate = componentDelegate
|
|
}
|
|
|
|
public func setIsCellVisible(_ isCellVisible: Bool) {
|
|
guard
|
|
isCellVisible,
|
|
let giftWrap = self.giftBadgeView.giftWrap,
|
|
let componentDelegate = self.componentDelegate,
|
|
let messageUniqueId = self.messageUniqueId,
|
|
componentDelegate.willShakeGift(messageUniqueId)
|
|
else {
|
|
return
|
|
}
|
|
|
|
_ = componentDelegate.beginCellAnimation(maximumDuration: GiftWrap.shakeAnimationDuration)
|
|
giftWrap.animateShake()
|
|
}
|
|
|
|
public func reset() {
|
|
self.messageUniqueId = nil
|
|
giftBadgeView.reset()
|
|
}
|
|
}
|
|
}
|