TM-SGNL-iOS/Signal/ConversationView/CVViewState.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

529 lines
16 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
public import SignalServiceKit
public import SignalUI
public protocol CVViewStateDelegate: AnyObject {
func viewStateUIModeDidChange(oldValue: ConversationUIMode)
}
// MARK: -
// This can be a simple place to hang CVC's mutable view state.
//
// These properties should only be accessed on the main thread.
public class CVViewState: NSObject {
public weak var delegate: CVViewStateDelegate?
public let threadUniqueId: String
public var conversationStyle: ConversationStyle
public var inputToolbar: ConversationInputToolbar?
public let headerView = ConversationHeaderView()
public let inputAccessoryPlaceholder = InputAccessoryViewPlaceholder()
public var bottomBar = UIView.container()
public var bottomBarBottomConstraint: NSLayoutConstraint?
public var requestView: UIView?
public var bannerView: UIView?
public var groupNameCollisionFinder: GroupMembershipNameCollisionFinder?
public var isDismissingInteractively = false
public var isViewCompletelyAppeared = false
public var isViewVisible = false
public var shouldAnimateKeyboardChanges = false
public var isInPreviewPlatter = false
public let viewCreationDate = Date()
public var hasAppliedFirstLoad = false
public var isUserScrolling = false
public var scrollingAnimationCompletionTimer: Timer?
public var hasScrollingAnimation: Bool {
AssertIsOnMainThread()
return scrollingAnimationCompletionTimer != nil
}
public var scrollActionForSizeTransition: CVScrollAction?
public var scrollActionForUpdate: CVScrollAction?
public var lastKnownDistanceFromBottom: CGFloat?
public var lastSearchedText: String?
public var activeCellAnimations = Set<UUID>()
public func beginCellAnimation(identifier: UUID) {
activeCellAnimations.insert(identifier)
}
public func endCellAnimation(identifier: UUID) {
activeCellAnimations.remove(identifier)
}
var bottomViewType: CVCBottomViewType = .none
public var uiMode: ConversationUIMode = .normal {
didSet {
AssertIsOnMainThread()
let didChange = uiMode != oldValue
if didChange {
selectionState.reset()
delegate?.viewStateUIModeDidChange(oldValue: oldValue)
}
}
}
enum SelectionAnimationState { case idle, willAnimate, animating }
var selectionAnimationState: SelectionAnimationState = .idle
public let selectionState = CVSelectionState()
public let textExpansion = CVTextExpansion()
public let spoilerState = SpoilerRenderState()
public let messageSwipeActionState = CVMessageSwipeActionState()
public var isDarkThemeEnabled: Bool = Theme.isDarkThemeEnabled
public var sendMessageController: SendMessageController?
public let mediaCache = CVMediaCache()
let contactShareViewHelper = ContactShareViewHelper()
public var userHasScrolled = false
public var groupCallTooltip: GroupCallTooltip?
public var groupCallTooltipTailReferenceView: UIView?
public var didAlreadyShowGroupCallTooltipEnoughTimes: Bool
public var hasIncrementedGroupCallTooltipShownCount = false
public var groupCallBarButtonItem: UIBarButtonItem?
public var lastMessageSentDate: Date?
public let scrollDownButton = ConversationScrollButton(iconName: "chevron-down-20")
public var isHidingScrollDownButton = false
public let scrollToNextMentionButton = ConversationScrollButton(iconName: "at-display")
public var isHidingScrollToNextMentionButton = false
public var scrollUpdateTimer: Timer?
public var isWaitingForDeceleration = false
public var actionOnOpen: ConversationViewAction = .none
public var readTimer: Timer?
public var reloadTimer: Timer?
public var lastSortIdMarkedRead: UInt64 = 0
public var isMarkingAsRead = false
// MARK: - Gestures
public let collectionViewTapGestureRecognizer = SingleOrDoubleTapGestureRecognizer()
public let collectionViewLongPressGestureRecognizer = UILongPressGestureRecognizer()
public let collectionViewContextMenuGestureRecognizer = UILongPressGestureRecognizer()
public var collectionViewContextMenuSecondaryClickRecognizer: UITapGestureRecognizer?
public let collectionViewPanGestureRecognizer = UIPanGestureRecognizer()
public var collectionViewActiveContextMenuInteraction: ChatHistoryContextMenuInteraction?
public var longPressHandler: CVLongPressHandler?
public var panHandler: CVPanHandler?
// MARK: -
var initialScrollState: CVInitialScrollState?
public var presentationStatus: CVPresentationStatus = .notYetPresented
public let backgroundContainer = CVBackgroundContainer()
public var wallpaperViewBuilder: WallpaperViewBuilder?
var chatColor: ColorOrGradientSetting
weak var reactionsDetailSheet: ReactionsDetailSheet?
public var lastKeyboardAnimationDate: Date?
// MARK: - Voice Messages
var inProgressVoiceMessage: VoiceMessageInProgressDraft?
// MARK: - Gift Badges
var shakenGiftMessageIds = Set<String>()
var unwrappedGiftMessageIds = Set<String>()
// MARK: -
public init(
threadUniqueId: String,
conversationStyle: ConversationStyle,
didAlreadyShowGroupCallTooltipEnoughTimes: Bool,
chatColor: ColorOrGradientSetting,
wallpaperViewBuilder: WallpaperViewBuilder?
) {
self.threadUniqueId = threadUniqueId
self.conversationStyle = conversationStyle
self.didAlreadyShowGroupCallTooltipEnoughTimes = didAlreadyShowGroupCallTooltipEnoughTimes
self.chatColor = chatColor
self.wallpaperViewBuilder = wallpaperViewBuilder
}
}
// MARK: -
extension ConversationViewController {
var threadViewModel: ThreadViewModel { renderState.threadViewModel }
var conversationViewModel: ConversationViewModel { renderState.conversationViewModel }
var thread: TSThread { threadViewModel.threadRecord }
var disappearingMessagesConfiguration: OWSDisappearingMessagesConfiguration { threadViewModel.disappearingMessagesConfiguration }
var conversationStyle: ConversationStyle {
get { viewState.conversationStyle }
set { viewState.conversationStyle = newValue }
}
var headerView: ConversationHeaderView { viewState.headerView }
var inputToolbar: ConversationInputToolbar? {
get { viewState.inputToolbar }
set { viewState.inputToolbar = newValue }
}
var inputAccessoryPlaceholder: InputAccessoryViewPlaceholder {
viewState.inputAccessoryPlaceholder
}
var bottomBar: UIView {
viewState.bottomBar
}
var bottomBarBottomConstraint: NSLayoutConstraint? {
get { viewState.bottomBarBottomConstraint }
set { viewState.bottomBarBottomConstraint = newValue }
}
var requestView: UIView? {
get { viewState.requestView }
set { viewState.requestView = newValue }
}
var bannerView: UIView? {
get { viewState.bannerView }
set { viewState.bannerView = newValue }
}
var isDismissingInteractively: Bool {
get { viewState.isDismissingInteractively }
set { viewState.isDismissingInteractively = newValue }
}
var isViewCompletelyAppeared: Bool {
get { viewState.isViewCompletelyAppeared }
set { viewState.isViewCompletelyAppeared = newValue }
}
var shouldAnimateKeyboardChanges: Bool {
get { viewState.shouldAnimateKeyboardChanges }
set { viewState.shouldAnimateKeyboardChanges = newValue }
}
var isUserScrolling: Bool {
get { viewState.isUserScrolling }
set { viewState.isUserScrolling = newValue }
}
var scrollingAnimationCompletionTimer: Timer? {
get { viewState.scrollingAnimationCompletionTimer }
set { viewState.scrollingAnimationCompletionTimer = newValue }
}
var hasScrollingAnimation: Bool { viewState.hasScrollingAnimation }
var uiMode: ConversationUIMode {
get { viewState.uiMode }
set {
let oldValue = viewState.uiMode
guard oldValue != newValue else {
return
}
viewState.uiMode = newValue
uiModeDidChange(oldValue: oldValue)
}
}
var isShowingSelectionUI: Bool { viewState.uiMode.hasSelectionUI }
var lastSearchedText: String? {
get { viewState.lastSearchedText }
set { viewState.lastSearchedText = newValue }
}
var isDarkThemeEnabled: Bool {
get { viewState.isDarkThemeEnabled }
set { viewState.isDarkThemeEnabled = newValue }
}
var isMeasuringKeyboardHeight: Bool { inputToolbar?.isMeasuringKeyboardHeight ?? false }
var isSwitchingKeyboard: Bool {
// See comment in `ConversationInputToolbar.isSwitchingKeyboard`.
guard #available(iOS 17, *) else { return false }
return inputToolbar?.isSwitchingKeyboard ?? false
}
var mediaCache: CVMediaCache { viewState.mediaCache }
var groupCallBarButtonItem: UIBarButtonItem? {
get { viewState.groupCallBarButtonItem }
set { viewState.groupCallBarButtonItem = newValue }
}
var lastMessageSentDate: Date? {
get { viewState.lastMessageSentDate }
set { viewState.lastMessageSentDate = newValue }
}
var actionOnOpen: ConversationViewAction {
get { viewState.actionOnOpen }
set { viewState.actionOnOpen = newValue }
}
// MARK: - Gestures
var collectionViewTapGestureRecognizer: SingleOrDoubleTapGestureRecognizer {
viewState.collectionViewTapGestureRecognizer
}
var collectionViewLongPressGestureRecognizer: UILongPressGestureRecognizer {
viewState.collectionViewLongPressGestureRecognizer
}
var collectionViewContextMenuGestureRecognizer: UILongPressGestureRecognizer {
viewState.collectionViewContextMenuGestureRecognizer
}
var collectionViewContextMenuSecondaryClickRecognizer: UITapGestureRecognizer? {
get { viewState.collectionViewContextMenuSecondaryClickRecognizer }
set { viewState.collectionViewContextMenuSecondaryClickRecognizer = newValue }
}
var collectionViewPanGestureRecognizer: UIPanGestureRecognizer {
viewState.collectionViewPanGestureRecognizer
}
var collectionViewActiveContextMenuInteraction: ChatHistoryContextMenuInteraction? {
get { viewState.collectionViewActiveContextMenuInteraction }
set { viewState.collectionViewActiveContextMenuInteraction = newValue }
}
var backgroundContainer: CVBackgroundContainer { viewState.backgroundContainer }
internal var reactionsDetailSheet: ReactionsDetailSheet? {
get { viewState.reactionsDetailSheet }
set { viewState.reactionsDetailSheet = newValue }
}
var contactShareViewHelper: ContactShareViewHelper { viewState.contactShareViewHelper }
}
// MARK: -
extension CVViewState {
var asCoreState: CVCoreState {
CVCoreState(conversationStyle: conversationStyle, mediaCache: mediaCache)
}
}
// MARK: -
// Accessors for the non-@objc properties.
extension ConversationViewController {
var longPressHandler: CVLongPressHandler? {
get { viewState.longPressHandler }
set { viewState.longPressHandler = newValue }
}
var panHandler: CVPanHandler? {
get { viewState.panHandler }
set { viewState.panHandler = newValue }
}
public var selectionState: CVSelectionState { viewState.selectionState }
func isTextExpanded(interactionId: String) -> Bool {
viewState.textExpansion.isTextExpanded(interactionId: interactionId)
}
func setTextExpanded(interactionId: String) {
viewState.textExpansion.setTextExpanded(interactionId: interactionId)
}
var initialScrollState: CVInitialScrollState? {
get { viewState.initialScrollState }
set { viewState.initialScrollState = newValue }
}
var lastKnownDistanceFromBottom: CGFloat? {
get { viewState.lastKnownDistanceFromBottom }
set { viewState.lastKnownDistanceFromBottom = newValue }
}
var sendMessageController: SendMessageController? {
get { viewState.sendMessageController }
set { viewState.sendMessageController = newValue }
}
}
// MARK: -
// This struct facilitates passing around a few key
// pieces of CVC state during async loads.
struct CVCoreState {
let conversationStyle: ConversationStyle
let mediaCache: CVMediaCache
}
// MARK: -
public class CVTextExpansion {
private var expandedTextInteractionsIds = Set<String>()
init(expandedTextInteractionsIds: Set<String>? = nil) {
if let expandedTextInteractionsIds = expandedTextInteractionsIds {
self.expandedTextInteractionsIds = expandedTextInteractionsIds
}
}
public func isTextExpanded(interactionId: String) -> Bool {
expandedTextInteractionsIds.contains(interactionId)
}
public func setTextExpanded(interactionId: String) {
expandedTextInteractionsIds.insert(interactionId)
}
func copy() -> CVTextExpansion {
CVTextExpansion(expandedTextInteractionsIds: expandedTextInteractionsIds)
}
// // TODO: collapseCutoffDate
// let collapseCutoffDate = Date()
}
// MARK: -
public class CVMessageSwipeActionState {
public struct Progress {
let xOffset: CGFloat
}
public typealias ProgressMap = [String: Progress]
private var progressMap = ProgressMap()
init(progressMap: ProgressMap? = nil) {
if let progressMap = progressMap {
self.progressMap = progressMap
}
}
public func getProgress(interactionId: String) -> Progress? {
progressMap[interactionId]
}
public func setProgress(interactionId: String, progress: Progress) {
progressMap[interactionId] = progress
}
public func resetProgress(interactionId: String) {
progressMap[interactionId] = nil
}
func copy() -> CVMessageSwipeActionState {
CVMessageSwipeActionState(progressMap: progressMap)
}
}
// MARK: -
// Describes the initial scroll state when we present CVC.
//
// Initial scroll state only applies until the first time
// CVC.viewDidAppear() is called.
struct CVInitialScrollState {
let focusMessageId: String?
}
// MARK: -
// Records whether or not the conversation view
// has ever reached these milestones of its lifecycle.
public enum CVPresentationStatus: UInt, CustomStringConvertible {
case notYetPresented = 0
case firstViewWillAppearHasBegun
case firstViewWillAppearHasCompleted
case firstViewDidAppearHasBegun
case firstViewDidAppearHasCompleted
public var description: String {
switch self {
case .notYetPresented:
return ".notYetPresented"
case .firstViewWillAppearHasBegun:
return ".firstViewWillAppearHasBegun"
case .firstViewWillAppearHasCompleted:
return ".firstViewWillAppearHasCompleted"
case .firstViewDidAppearHasBegun:
return ".firstViewDidAppearHasBegun"
case .firstViewDidAppearHasCompleted:
return ".firstViewDidAppearHasCompleted"
}
}
}
// MARK: -
public extension ConversationViewController {
var presentationStatus: CVPresentationStatus { viewState.presentationStatus }
private func updatePresentationStatus(_ value: CVPresentationStatus) {
AssertIsOnMainThread()
if viewState.presentationStatus.rawValue < value.rawValue {
viewState.presentationStatus = value
}
}
func viewWillAppearDidBegin() {
updatePresentationStatus(.firstViewWillAppearHasBegun)
}
func viewWillAppearDidComplete() {
updatePresentationStatus(.firstViewWillAppearHasCompleted)
}
func viewDidAppearDidBegin() {
updatePresentationStatus(.firstViewDidAppearHasBegun)
}
func viewDidAppearDidComplete() {
updatePresentationStatus(.firstViewDidAppearHasCompleted)
}
var hasViewWillAppearEverBegun: Bool {
viewState.presentationStatus.rawValue >= CVPresentationStatus.firstViewWillAppearHasBegun.rawValue
}
var hasViewDidAppearEverBegun: Bool {
viewState.presentationStatus.rawValue >= CVPresentationStatus.firstViewDidAppearHasBegun.rawValue
}
var hasViewDidAppearEverCompleted: Bool {
viewState.presentationStatus.rawValue >= CVPresentationStatus.firstViewDidAppearHasCompleted.rawValue
}
var viewHasEverAppeared: Bool {
hasViewDidAppearEverCompleted
}
}