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

228 lines
8.3 KiB
Swift

//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import LibSignalClient
public import SignalServiceKit
/// Manages state for banners that might be hidden.
private class BannerHiding {
private static func hiddenStateKey(forThreadId threadId: String) -> String {
"hiddenState_\(threadId)"
}
/// Encapsulates state for a hidden banner.
private struct HiddenState: Codable {
private enum CodingKeys: String, CodingKey {
case lastHiddenDate
case numberOfTimesHidden
}
/// The last time this banner was hidden.
let lastHiddenDate: Date
/// How many times this banner has been hidden.
let numberOfTimesHidden: UInt
}
let bannerHidingStore: KeyValueStore
private let hideDuration: TimeInterval
private let hideForeverAfterNumberOfHides: UInt?
/// - Parameter identifier: an identifier for the banner whose hides we are tracking
/// - Parameter hideDuration: how long to hide the banner for, after a hide is recorded
/// - Parameter hideForeverAfterNumberOfHides: after this many manual hides, the banner will be hidden forever. If `nil`, the banner is never hidden forever.
init(
identifier: String,
hideDuration: TimeInterval,
hideForeverAfterNumberOfHides: UInt? = nil
) {
bannerHidingStore = KeyValueStore(collection: identifier)
self.hideDuration = hideDuration
self.hideForeverAfterNumberOfHides = hideForeverAfterNumberOfHides
}
func isHidden(threadUniqueId threadId: String, transaction: SDSAnyReadTransaction) -> Bool {
guard let hiddenState = getHiddenState(forThreadId: threadId, transaction: transaction) else {
// We've never hidden this banner before, so no reason to hide it now.
return false
}
if
let hideForeverAfterNumberOfHides = hideForeverAfterNumberOfHides,
hiddenState.numberOfTimesHidden >= hideForeverAfterNumberOfHides
{
// This banner was hidden too many times, and is now hidden forever.
return true
}
let timeIntervalSinceLastHidden = Date().timeIntervalSince(hiddenState.lastHiddenDate)
if timeIntervalSinceLastHidden < hideDuration {
// It has not been sufficiently long since we last hid this banner.
return true
}
return false
}
func hide(threadUniqueId threadId: String, transaction: SDSAnyWriteTransaction) {
let stateToWrite: HiddenState
if let existingHiddenState = getHiddenState(forThreadId: threadId, transaction: transaction) {
stateToWrite = HiddenState(
lastHiddenDate: Date(),
numberOfTimesHidden: existingHiddenState.numberOfTimesHidden + 1
)
} else {
stateToWrite = HiddenState(lastHiddenDate: Date(), numberOfTimesHidden: 1)
}
do {
try bannerHidingStore.setCodable(
stateToWrite,
key: Self.hiddenStateKey(forThreadId: threadId),
transaction: transaction.asV2Write
)
} catch let error {
owsFailDebug("Caught error while encoding banner hiding state: \(error)!")
}
}
private func getHiddenState(forThreadId threadId: String, transaction: SDSAnyReadTransaction) -> HiddenState? {
do {
return try bannerHidingStore.getCodableValue(
forKey: Self.hiddenStateKey(forThreadId: threadId),
transaction: transaction.asV2Read
)
} catch let error {
owsFailDebug("Caught error while getting banner hiding state: \(error)!")
return nil
}
}
}
/// Manages state for the "pending member requests" banner.
private class PendingMemberRequestsBannerHiding: BannerHiding {
private struct RequestingMembersState: Codable {
let requestingMemberAcis: Set<AciUuid>
enum CodingKeys: String, CodingKey {
case requestingMemberAcis = "requestingMemberUuids"
}
}
private static func requestingMembersStateKey(forThreadId threadId: String) -> String {
"requestingMembersState_\(threadId)"
}
func isHidden(
currentRequestingMemberAcis: [Aci],
threadUniqueId threadId: String,
transaction: SDSAnyReadTransaction
) -> Bool {
guard isHidden(threadUniqueId: threadId, transaction: transaction) else {
return false
}
// We may want to show the banner, even if it is hidden, if we have
// pending member requests we didn't know about last time we snoozed.
let persistedMemberRequestAcis: [Aci] = getRequestingMembersState(
forThreadId: threadId,
transaction: transaction
)?.requestingMemberAcis.map({ $0.wrappedValue }) ?? []
return Set(currentRequestingMemberAcis).subtracting(persistedMemberRequestAcis).isEmpty
}
func hide(
currentPendingMemberRequestAcis: [Aci],
threadUniqueId threadId: String,
transaction: SDSAnyWriteTransaction
) {
super.hide(threadUniqueId: threadId, transaction: transaction)
do {
let newPendingMemberRequestState = RequestingMembersState(
requestingMemberAcis: Set(currentPendingMemberRequestAcis.map { $0.codableUuid })
)
try bannerHidingStore.setCodable(
newPendingMemberRequestState,
key: Self.requestingMembersStateKey(forThreadId: threadId),
transaction: transaction.asV2Write
)
} catch let error {
owsFailDebug("Caught error while encoding banner hiding state: \(error)!")
}
}
private func getRequestingMembersState(
forThreadId threadId: String,
transaction: SDSAnyReadTransaction
) -> RequestingMembersState? {
do {
return try bannerHidingStore.getCodableValue(
forKey: Self.requestingMembersStateKey(forThreadId: threadId),
transaction: transaction.asV2Read
)
} catch let error {
owsFailDebug("Caught error while getting banner hiding state: \(error)!")
return nil
}
}
}
public extension CVViewState {
/// This banner will snooze for 1 week after each hiding, and is
/// responsive to changes in pending member request state.
private static let isPendingMemberRequestsBannerHiding = PendingMemberRequestsBannerHiding(
identifier: "BannerHiding_pendingMemberRequests",
hideDuration: kWeekInterval
)
/// This banner will snooze for only 1 hour after each hiding, since this
/// is a potential safety concern (and only appears in message requests).
private static let isMessageRequestNameCollisionBannerHiding = BannerHiding(
identifier: "BannerHiding_messageRequestNameCollision",
hideDuration: kHourInterval
)
func shouldShowPendingMemberRequestsBanner(
currentPendingMembers: some Sequence<SignalServiceAddress>,
transaction: SDSAnyReadTransaction
) -> Bool {
let currentPendingMemberAcis = currentPendingMembers.compactMap { $0.serviceId as? Aci }
return !Self.isPendingMemberRequestsBannerHiding.isHidden(
currentRequestingMemberAcis: currentPendingMemberAcis,
threadUniqueId: threadUniqueId,
transaction: transaction
)
}
func hidePendingMemberRequestsBanner(
currentPendingMembers: some Sequence<SignalServiceAddress>,
transaction: SDSAnyWriteTransaction
) {
let currentPendingMemberAcis = currentPendingMembers.compactMap { $0.serviceId as? Aci }
Self.isPendingMemberRequestsBannerHiding.hide(
currentPendingMemberRequestAcis: currentPendingMemberAcis,
threadUniqueId: threadUniqueId,
transaction: transaction
)
}
func shouldShowMessageRequestNameCollisionBanner(transaction: SDSAnyReadTransaction) -> Bool {
!Self.isMessageRequestNameCollisionBannerHiding.isHidden(threadUniqueId: threadUniqueId, transaction: transaction)
}
func hideMessageRequestNameCollisionBanner(transaction: SDSAnyWriteTransaction) {
Self.isMessageRequestNameCollisionBannerHiding.hide(threadUniqueId: threadUniqueId, transaction: transaction)
}
}