238 lines
9.6 KiB
Swift
238 lines
9.6 KiB
Swift
//
|
|
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import SignalServiceKit
|
|
import SignalUI
|
|
|
|
class ExperienceUpgradeManager {
|
|
|
|
private static weak var lastPresented: ExperienceUpgradeView?
|
|
|
|
// The first day is day 0, so this gives the user 1 week of megaphone
|
|
// before we display the splash.
|
|
static let splashStartDay = 7
|
|
|
|
static func presentNext(fromViewController: UIViewController) -> Bool {
|
|
let optionalNext = SSKEnvironment.shared.databaseStorageRef.read(block: { transaction in
|
|
return ExperienceUpgradeFinder.next(transaction: transaction.unwrapGrdbRead)
|
|
})
|
|
|
|
// If we already have presented this experience upgrade, do nothing.
|
|
guard
|
|
let next = optionalNext,
|
|
lastPresented?.experienceUpgrade.manifest != next.manifest
|
|
else {
|
|
if optionalNext == nil {
|
|
dismissLastPresented()
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Otherwise, dismiss any currently present experience upgrade. It's
|
|
// no longer next and may have been completed.
|
|
dismissLastPresented()
|
|
|
|
let hasMegaphone = self.hasMegaphone(forExperienceUpgrade: next)
|
|
let hasSplash = self.hasSplash(forExperienceUpgrade: next)
|
|
|
|
// If we have a megaphone and a splash, we only show the megaphone for
|
|
// 7 days after the user first viewed the megaphone. After this point
|
|
// we will display the splash. If there is only a megaphone we will
|
|
// render it for as long as the upgrade is active. We don't show the
|
|
// splash if the user currently has a selected thread, as we don't
|
|
// ever want to block access to messaging (e.g. via tapping a notification).
|
|
let didPresentView: Bool
|
|
if (hasMegaphone && !hasSplash) || (hasMegaphone && next.daysSinceFirstViewed < splashStartDay) {
|
|
let megaphone = self.megaphone(forExperienceUpgrade: next, fromViewController: fromViewController)
|
|
megaphone?.present(fromViewController: fromViewController)
|
|
lastPresented = megaphone
|
|
didPresentView = true
|
|
} else if hasSplash, !SignalApp.shared.hasSelectedThread, let splash = splash(forExperienceUpgrade: next) {
|
|
fromViewController.presentFormSheet(OWSNavigationController(rootViewController: splash), animated: true)
|
|
lastPresented = splash
|
|
didPresentView = true
|
|
} else {
|
|
Logger.info("no megaphone or splash needed for experience upgrade: \(next.id as Optional)")
|
|
didPresentView = false
|
|
}
|
|
|
|
// Track that we've successfully presented this experience upgrade once, or that it was not
|
|
// needed to be presented.
|
|
// If it was already marked as viewed, this will do nothing.
|
|
SSKEnvironment.shared.databaseStorageRef.asyncWrite { transaction in
|
|
ExperienceUpgradeFinder.markAsViewed(experienceUpgrade: next, transaction: transaction.unwrapGrdbWrite)
|
|
}
|
|
|
|
return didPresentView
|
|
}
|
|
|
|
// MARK: - Experience Specific Helpers
|
|
|
|
static func dismissSplashWithoutCompletingIfNecessary() {
|
|
guard let lastPresented = lastPresented as? SplashViewController else { return }
|
|
lastPresented.dismissWithoutCompleting(animated: false, completion: nil)
|
|
}
|
|
|
|
static func dismissPINReminderIfNecessary() {
|
|
dismissLastPresented(ifMatching: .pinReminder)
|
|
}
|
|
|
|
/// Marks the given upgrade as complete, and dismisses it if currently presented.
|
|
static func clearExperienceUpgrade(_ manifest: ExperienceUpgradeManifest, transaction: GRDBWriteTransaction) {
|
|
ExperienceUpgradeFinder.markAsComplete(experienceUpgradeManifest: manifest, transaction: transaction)
|
|
|
|
transaction.addAsyncCompletion(queue: .main) {
|
|
dismissLastPresented(ifMatching: manifest)
|
|
}
|
|
}
|
|
|
|
private static func dismissLastPresented(ifMatching manifest: ExperienceUpgradeManifest? = nil) {
|
|
guard let lastPresented = lastPresented else {
|
|
return
|
|
}
|
|
|
|
if
|
|
let manifest = manifest,
|
|
lastPresented.experienceUpgrade.manifest != manifest
|
|
{
|
|
return
|
|
}
|
|
|
|
lastPresented.dismiss(animated: false, completion: nil)
|
|
self.lastPresented = nil
|
|
}
|
|
|
|
// MARK: - Splash
|
|
|
|
private static func hasSplash(forExperienceUpgrade experienceUpgrade: ExperienceUpgrade) -> Bool {
|
|
switch experienceUpgrade.id {
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
fileprivate static func splash(forExperienceUpgrade experienceUpgrade: ExperienceUpgrade) -> SplashViewController? {
|
|
switch experienceUpgrade.id {
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MARK: - Megaphone
|
|
|
|
private static func hasMegaphone(forExperienceUpgrade experienceUpgrade: ExperienceUpgrade) -> Bool {
|
|
switch experienceUpgrade.manifest {
|
|
case
|
|
.introducingPins,
|
|
.pinReminder,
|
|
.notificationPermissionReminder,
|
|
.createUsernameReminder,
|
|
.inactiveLinkedDeviceReminder,
|
|
.contactPermissionReminder:
|
|
return true
|
|
case .remoteMegaphone:
|
|
// Remote megaphones are always presentable. We filter out any with
|
|
// unpresentable fields (e.g., unrecognized actions) before we get
|
|
// out of the `ExperienceUpgradeFinder`.
|
|
return true
|
|
case .unrecognized:
|
|
return false
|
|
}
|
|
}
|
|
|
|
private static func megaphone(forExperienceUpgrade experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) -> MegaphoneView? {
|
|
switch experienceUpgrade.manifest {
|
|
case .introducingPins:
|
|
return IntroducingPinsMegaphone(experienceUpgrade: experienceUpgrade, fromViewController: fromViewController)
|
|
case .pinReminder:
|
|
return PinReminderMegaphone(experienceUpgrade: experienceUpgrade, fromViewController: fromViewController)
|
|
case .notificationPermissionReminder:
|
|
return NotificationPermissionReminderMegaphone(experienceUpgrade: experienceUpgrade, fromViewController: fromViewController)
|
|
case .createUsernameReminder:
|
|
let usernameIsUnset: Bool = SSKEnvironment.shared.databaseStorageRef.read { tx in
|
|
return DependenciesBridge.shared.localUsernameManager
|
|
.usernameState(tx: tx.asV2Read).isExplicitlyUnset
|
|
}
|
|
|
|
guard usernameIsUnset else {
|
|
owsFailDebug("Should never try and show this megaphone if a username is set!")
|
|
return nil
|
|
}
|
|
|
|
return CreateUsernameMegaphone(
|
|
usernameSelectionCoordinator: .init(
|
|
currentUsername: nil,
|
|
context: .init(
|
|
databaseStorage: SSKEnvironment.shared.databaseStorageRef,
|
|
networkManager: SSKEnvironment.shared.networkManagerRef,
|
|
schedulers: DependenciesBridge.shared.schedulers,
|
|
storageServiceManager: SSKEnvironment.shared.storageServiceManagerRef,
|
|
usernameEducationManager: DependenciesBridge.shared.usernameEducationManager,
|
|
localUsernameManager: DependenciesBridge.shared.localUsernameManager
|
|
)
|
|
),
|
|
experienceUpgrade: experienceUpgrade,
|
|
fromViewController: fromViewController
|
|
)
|
|
case .inactiveLinkedDeviceReminder:
|
|
let inactiveLinkedDevice: InactiveLinkedDevice? = SSKEnvironment.shared.databaseStorageRef.read { tx in
|
|
return DependenciesBridge.shared.inactiveLinkedDeviceFinder
|
|
.findLeastActiveLinkedDevice(tx: tx.asV2Read)
|
|
}
|
|
|
|
guard let inactiveLinkedDevice else {
|
|
owsFailDebug("Trying to show inactive linked device megaphone, but have no device!")
|
|
return nil
|
|
}
|
|
|
|
return InactiveLinkedDeviceReminderMegaphone(
|
|
inactiveLinkedDevice: inactiveLinkedDevice,
|
|
fromViewController: fromViewController,
|
|
experienceUpgrade: experienceUpgrade
|
|
)
|
|
case .contactPermissionReminder:
|
|
return ContactPermissionReminderMegaphone(experienceUpgrade: experienceUpgrade, fromViewController: fromViewController)
|
|
case .remoteMegaphone(let megaphone):
|
|
return RemoteMegaphone(
|
|
experienceUpgrade: experienceUpgrade,
|
|
remoteMegaphoneModel: megaphone,
|
|
fromViewController: fromViewController
|
|
)
|
|
case .unrecognized:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - ExperienceUpgradeView
|
|
|
|
protocol ExperienceUpgradeView: AnyObject {
|
|
var experienceUpgrade: ExperienceUpgrade { get }
|
|
var isPresented: Bool { get }
|
|
func dismiss(animated: Bool, completion: (() -> Void)?)
|
|
}
|
|
|
|
extension ExperienceUpgradeView {
|
|
|
|
func markAsSnoozedWithSneakyTransaction() {
|
|
SSKEnvironment.shared.databaseStorageRef.write { transaction in
|
|
ExperienceUpgradeFinder.markAsSnoozed(
|
|
experienceUpgrade: self.experienceUpgrade,
|
|
transaction: transaction.unwrapGrdbWrite
|
|
)
|
|
}
|
|
}
|
|
|
|
func markAsCompleteWithSneakyTransaction() {
|
|
SSKEnvironment.shared.databaseStorageRef.write { transaction in
|
|
ExperienceUpgradeFinder.markAsComplete(
|
|
experienceUpgrade: self.experienceUpgrade,
|
|
transaction: transaction.unwrapGrdbWrite
|
|
)
|
|
}
|
|
}
|
|
}
|