Split Megaphone, MegaphoneView

This commit is contained in:
Sasha Weiss 2026-06-01 12:56:00 -07:00 committed by GitHub
parent 8b1379149c
commit 28e9247793
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 148 additions and 97 deletions

View File

@ -1675,7 +1675,7 @@
88A357B923639384009D6B9A /* MemberActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A357B823639384009D6B9A /* MemberActionSheet.swift */; };
88A4CC10246CE2760082211F /* TransferProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A4CC0F246CE2760082211F /* TransferProgressView.swift */; };
88A505F423DA16E10005C012 /* ExperienceUpgradeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A505F323DA16E10005C012 /* ExperienceUpgradeManager.swift */; };
88A505FA23DBA1360005C012 /* IntroducingPINs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A505F923DBA1360005C012 /* IntroducingPINs.swift */; };
88A505FA23DBA1360005C012 /* IntroducingPINsMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A505F923DBA1360005C012 /* IntroducingPINsMegaphone.swift */; };
88A941992409A391000E9700 /* LottieToggleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A941982409A391000E9700 /* LottieToggleButton.swift */; };
88A9729222FA5D4B004B4FBF /* AttachmentFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A9729122FA5D4B004B4FBF /* AttachmentFormatPickerView.swift */; };
88A9729422FB4D02004B4FBF /* LocationPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A9729322FB4D02004B4FBF /* LocationPicker.swift */; };
@ -5922,7 +5922,7 @@
88A4717228664DE3001A3065 /* BaseMemberViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMemberViewController.swift; sourceTree = "<group>"; };
88A4CC0F246CE2760082211F /* TransferProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferProgressView.swift; sourceTree = "<group>"; };
88A505F323DA16E10005C012 /* ExperienceUpgradeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperienceUpgradeManager.swift; sourceTree = "<group>"; };
88A505F923DBA1360005C012 /* IntroducingPINs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroducingPINs.swift; sourceTree = "<group>"; };
88A505F923DBA1360005C012 /* IntroducingPINsMegaphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroducingPINsMegaphone.swift; sourceTree = "<group>"; };
88A695BC232C18DF002F7B9B /* AudioWaveformProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioWaveformProgressView.swift; sourceTree = "<group>"; };
88A941982409A391000E9700 /* LottieToggleButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieToggleButton.swift; sourceTree = "<group>"; };
88A9729122FA5D4B004B4FBF /* AttachmentFormatPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentFormatPickerView.swift; sourceTree = "<group>"; };
@ -11438,7 +11438,7 @@
D9C2D77F299EC11400D79715 /* CreateUsernameMegaphone.swift */,
D9C0AE682BD82DBC00FCB05E /* InactiveLinkedDeviceReminderMegaphone.swift */,
04BBBE8F2E259A5F00E914B1 /* InactivePrimaryDeviceReminderMegaphone.swift */,
88A505F923DBA1360005C012 /* IntroducingPINs.swift */,
88A505F923DBA1360005C012 /* IntroducingPINsMegaphone.swift */,
8837F74023DA0B0F00772A32 /* MegaphoneView.swift */,
B9B7BC642D41C61500C26E42 /* NewLinkedDeviceNotificationMegaphone.swift */,
8806EF18248DBD7200E764C7 /* NotificationPermissionReminderMegaphone.swift */,
@ -18433,7 +18433,7 @@
66D31F972E5E685300A1C82D /* InternalListMediaViewController.swift in Sources */,
8862A55925F090C5005D65DB /* InternalSettingsViewController.swift in Sources */,
663883572D4C0360008EA898 /* InternalSQLClientViewController.swift in Sources */,
88A505FA23DBA1360005C012 /* IntroducingPINs.swift in Sources */,
88A505FA23DBA1360005C012 /* IntroducingPINsMegaphone.swift in Sources */,
32AC5CE7255B51E900829BD8 /* JoinGroupCallPill.swift in Sources */,
45C845AD291466C0005F6EA5 /* JournalingOrderedDictionary.swift in Sources */,
5045F44229E0DB7100058E5F /* LaunchJobs.swift in Sources */,

View File

@ -12,7 +12,9 @@ class ExperienceUpgradeManager {
static let lastExperienceUpgradeDismissDate = "lastExperienceUpgradeDismissDate"
}
private weak static var lastPresented: MegaphoneView?
// The Megaphone is retained for the lifetime of the MegaphoneView.
private weak static var lastPresentedMegaphone: Megaphone?
private weak static var lastPresentedMegaphoneView: MegaphoneView?
private static let backupSettingsStore = BackupSettingsStore()
private static var db: DB { DependenciesBridge.shared.db }
@ -152,8 +154,8 @@ class ExperienceUpgradeManager {
}
if
let lastPresented,
lastPresented.experienceUpgrade.manifest == nextExperienceUpgrade.manifest
let lastPresentedMegaphone,
lastPresentedMegaphone.experienceUpgrade.manifest == nextExperienceUpgrade.manifest
{
return
}
@ -170,8 +172,12 @@ class ExperienceUpgradeManager {
fromViewController: fromViewController,
)
{
megaphone.present(fromViewController: fromViewController)
lastPresented = megaphone
let megaphoneView = megaphone.buildView()
ObjectRetainer.retainObject(megaphone, forLifetimeOf: megaphoneView)
megaphoneView.present(fromViewController: fromViewController)
lastPresentedMegaphone = megaphone
lastPresentedMegaphoneView = megaphoneView
db.write { tx in
experienceUpgradeStore.markAsViewed(
@ -219,7 +225,7 @@ class ExperienceUpgradeManager {
/// - Returns
/// Whether or not we dismissed a megaphone.
private static func dismissLastPresented(now: Date) -> Bool {
guard let lastPresented else {
guard lastPresentedMegaphone != nil, let lastPresentedMegaphoneView else {
return false
}
@ -231,8 +237,9 @@ class ExperienceUpgradeManager {
)
}
lastPresented.dismiss(animated: false, completion: nil)
self.lastPresented = nil
lastPresentedMegaphoneView.dismiss(animated: false, completion: nil)
self.lastPresentedMegaphone = nil
self.lastPresentedMegaphoneView = nil
return true
}
@ -263,7 +270,7 @@ class ExperienceUpgradeManager {
}
}
private static func megaphone(forExperienceUpgrade experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) -> MegaphoneView? {
private static func megaphone(forExperienceUpgrade experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) -> Megaphone? {
let db = DependenciesBridge.shared.db
let deviceStore = DependenciesBridge.shared.deviceStore
let localUsernameManager = DependenciesBridge.shared.localUsernameManager

View File

@ -7,7 +7,7 @@ import Foundation
import SignalServiceKit
import UIKit
class BackupEnablementMegaphone: MegaphoneView {
class BackupEnablementMegaphone: Megaphone {
init(
experienceUpgrade: ExperienceUpgrade,
fromViewController: UIViewController,
@ -33,7 +33,7 @@ class BackupEnablementMegaphone: MegaphoneView {
comment: "Snooze text for Backup enablement reminder megaphone",
)
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak self] in
let primaryButton = Button(title: primaryButtonTitle) { [weak self] in
SignalApp.shared.showAppSettings(mode: .backups())
self?.markAsSnoozedWithSneakyTransaction()
}

View File

@ -7,7 +7,7 @@ import Foundation
import SignalServiceKit
import UIKit
class BackupsEnabledNotificationMegaphone: MegaphoneView {
class BackupsEnabledNotificationMegaphone: Megaphone {
private let db: DB
private let backupSettingsStore: BackupSettingsStore
init(
@ -40,12 +40,12 @@ class BackupsEnabledNotificationMegaphone: MegaphoneView {
"BACKUPS_VIEW_SETTINGS_BUTTON",
comment: "Action text for backups enabled megaphone taking user to backup settings",
)
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak self] in
let primaryButton = Button(title: primaryButtonTitle) { [weak self] in
SignalApp.shared.showAppSettings(mode: .backups())
self?.stopShowing()
}
let secondaryButton = MegaphoneView.Button(title: CommonStrings.okButton) { [weak self] in
let secondaryButton = Button(title: CommonStrings.okButton) { [weak self] in
self?.stopShowing()
}

View File

@ -6,7 +6,7 @@
import SignalServiceKit
import SignalUI
class ContactPermissionReminderMegaphone: MegaphoneView {
class ContactPermissionReminderMegaphone: Megaphone {
init(experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) {
super.init(experienceUpgrade: experienceUpgrade)
@ -25,7 +25,7 @@ class ContactPermissionReminderMegaphone: MegaphoneView {
comment: "Action text for contact permission reminder megaphone",
)
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) {
let primaryButton = Button(title: primaryButtonTitle) {
let actionSheetController = ActionSheetController()
actionSheetController.isCancelable = true

View File

@ -7,7 +7,7 @@ import Foundation
import SignalServiceKit
import UIKit
class CreateUsernameMegaphone: MegaphoneView {
class CreateUsernameMegaphone: Megaphone {
private let usernameSelectionCoordinator: UsernameSelectionCoordinator
init(

View File

@ -6,7 +6,7 @@
import SignalServiceKit
import UIKit
final class InactiveLinkedDeviceReminderMegaphone: MegaphoneView {
final class InactiveLinkedDeviceReminderMegaphone: Megaphone {
private let inactiveLinkedDevice: InactiveLinkedDevice
/// The number of days until the linked device represented by this megaphone

View File

@ -6,7 +6,7 @@
import SafariServices
import SignalServiceKit
final class InactivePrimaryDeviceReminderMegaphone: MegaphoneView {
final class InactivePrimaryDeviceReminderMegaphone: Megaphone {
init(
fromViewController: UIViewController,
experienceUpgrade: ExperienceUpgrade,

View File

@ -7,7 +7,7 @@ import SafariServices
import SignalServiceKit
import SignalUI
class IntroducingPinsMegaphone: MegaphoneView {
class IntroducingPinsMegaphone: Megaphone {
init(experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) {
super.init(experienceUpgrade: experienceUpgrade)
@ -17,7 +17,7 @@ class IntroducingPinsMegaphone: MegaphoneView {
let primaryButtonTitle = OWSLocalizedString("PINS_MEGAPHONE_ACTION", comment: "Action text for PIN megaphone when user doesn't have a PIN")
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak self] in
let primaryButton = Button(title: primaryButtonTitle) { [weak self] in
let viewController = PinSetupViewController(
mode: .creating,
showCancelButton: true,
@ -27,20 +27,20 @@ class IntroducingPinsMegaphone: MegaphoneView {
markAsCompleteWithSneakyTransaction()
presentToast(
text: OWSLocalizedString(
"PINS_MEGAPHONE_TOAST",
comment: "Toast indicating that a PIN has been created.",
),
fromViewController: fromViewController,
)
fromViewController.presentToast(text: OWSLocalizedString(
"PINS_MEGAPHONE_TOAST",
comment: "Toast indicating that a PIN has been created.",
))
}
},
)
fromViewController.present(OWSNavigationController(rootViewController: viewController), animated: true)
}
let secondaryButton = snoozeButton(fromViewController: fromViewController)
let secondaryButton = snoozeButton(
fromViewController: fromViewController,
snoozeTitle: MegaphoneStrings.remindMeLater,
)
buttons = [primaryButton, secondaryButton]
}

View File

@ -7,7 +7,7 @@ import Lottie
import SignalServiceKit
import SignalUI
class MegaphoneView: UIView {
class Megaphone {
struct Button {
let title: String
let action: () -> Void
@ -20,11 +20,94 @@ class MegaphoneView: UIView {
var bodyText: String?
var buttons: [Button] = []
init(experienceUpgrade: ExperienceUpgrade) {
self.experienceUpgrade = experienceUpgrade
}
func buildView() -> MegaphoneView {
guard let titleText, let bodyText else {
owsFail("Megaphone missing title or body text!")
}
guard (1...2).contains(buttons.count) else {
owsFail("Megaphone must have 1 or 2 buttons!")
}
return MegaphoneView(
image: image,
imageContentMode: imageContentMode,
titleText: titleText,
bodyText: bodyText,
buttons: buttons,
)
}
func snoozeButton(
fromViewController: UIViewController,
snoozeTitle: String,
) -> Button {
return Button(title: snoozeTitle) { [weak self, weak fromViewController] in
guard let self, let fromViewController else { return }
markAsSnoozedWithSneakyTransaction()
fromViewController.presentToast(text: MegaphoneStrings.weWillRemindYouLater)
}
}
// MARK: -
func markAsSnoozedWithSneakyTransaction() {
let db = DependenciesBridge.shared.db
let experienceUpgradeStore = ExperienceUpgradeStore()
db.write { tx in
experienceUpgradeStore.markAsSnoozed(
experienceUpgrade: experienceUpgrade,
tx: tx,
)
}
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
}
func markAsCompleteWithSneakyTransaction() {
let db = DependenciesBridge.shared.db
let experienceUpgradeStore = ExperienceUpgradeStore()
db.write { tx in
experienceUpgradeStore.markAsComplete(
experienceUpgrade: experienceUpgrade,
tx: tx,
)
}
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
}
}
// MARK: -
class MegaphoneView: UIView {
private let image: UIImage?
private let imageContentMode: UIView.ContentMode
private let titleText: String
private let bodyText: String
private let buttons: [Megaphone.Button]
private let darkThemeBackgroundOverlay = UIView()
private let stackView = UIStackView()
init(experienceUpgrade: ExperienceUpgrade) {
self.experienceUpgrade = experienceUpgrade
init(
image: UIImage?,
imageContentMode: UIView.ContentMode,
titleText: String,
bodyText: String,
buttons: [Megaphone.Button],
) {
self.image = image
self.imageContentMode = imageContentMode
self.titleText = titleText
self.bodyText = bodyText
self.buttons = buttons
super.init(frame: .zero)
@ -60,10 +143,6 @@ class MegaphoneView: UIView {
guard !hasPresented else { return owsFailDebug("can only present once") }
guard titleText != nil, bodyText != nil, !buttons.isEmpty else {
owsFail("Megaphone missing required properties!")
}
let labelStack = createLabelStack()
let topStackSubviews: [UIView]
@ -153,7 +232,10 @@ class MegaphoneView: UIView {
return container
}
private func createButtonView(_ button: Button, font: UIFont = .regularFont(ofSize: 15)) -> OWSFlatButton {
private func createButtonView(
_ button: Megaphone.Button,
font: UIFont = .regularFont(ofSize: 15),
) -> OWSFlatButton {
let buttonView = OWSFlatButton()
buttonView.setTitle(title: button.title, font: font, titleColor: Theme.darkThemePrimaryColor)
@ -170,13 +252,16 @@ class MegaphoneView: UIView {
switch buttons.count {
case 1:
buttonsStack.addArrangedSubview(createButtonView(buttons[0]))
buttonsStack.addArrangedSubview(createButtonView(
buttons[0],
font: .regularFont(ofSize: 15),
))
case 2:
var previousButton: UIView?
for button in buttons {
let buttonView = createButtonView(
button,
font: previousButton == nil ? UIFont.semiboldFont(ofSize: 15) : .regularFont(ofSize: 15),
font: previousButton == nil ? .semiboldFont(ofSize: 15) : .regularFont(ofSize: 15),
)
buttonsStack.insertArrangedSubview(buttonView, at: 0)
@ -199,43 +284,4 @@ class MegaphoneView: UIView {
return buttonsStack
}
func snoozeButton(fromViewController: UIViewController, snoozeTitle: String = MegaphoneStrings.remindMeLater) -> Button {
return Button(title: snoozeTitle) { [weak self, weak fromViewController] in
guard let self, let fromViewController else { return }
markAsSnoozedWithSneakyTransaction()
presentToast(text: MegaphoneStrings.weWillRemindYouLater, fromViewController: fromViewController)
}
}
// MARK: -
func markAsSnoozedWithSneakyTransaction() {
let db = DependenciesBridge.shared.db
let experienceUpgradeStore = ExperienceUpgradeStore()
db.write { tx in
experienceUpgradeStore.markAsSnoozed(
experienceUpgrade: experienceUpgrade,
tx: tx,
)
}
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
}
func markAsCompleteWithSneakyTransaction() {
let db = DependenciesBridge.shared.db
let experienceUpgradeStore = ExperienceUpgradeStore()
db.write { tx in
experienceUpgradeStore.markAsComplete(
experienceUpgrade: experienceUpgrade,
tx: tx,
)
}
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
}
}

View File

@ -5,7 +5,7 @@
import SignalServiceKit
final class NewLinkedDeviceNotificationMegaphone: MegaphoneView {
final class NewLinkedDeviceNotificationMegaphone: Megaphone {
private let db: DB
private let deviceStore: OWSDeviceStore

View File

@ -6,7 +6,7 @@
import SignalServiceKit
import SignalUI
class NotificationPermissionReminderMegaphone: MegaphoneView {
class NotificationPermissionReminderMegaphone: Megaphone {
init(experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) {
super.init(experienceUpgrade: experienceUpgrade)
@ -25,7 +25,7 @@ class NotificationPermissionReminderMegaphone: MegaphoneView {
comment: "Action text for notification permission reminder megaphone",
)
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) {
let primaryButton = Button(title: primaryButtonTitle) {
let actionSheetController = ActionSheetController()
actionSheetController.isCancelable = true

View File

@ -7,7 +7,7 @@ import Foundation
import SignalServiceKit
import UIKit
class PinReminderMegaphone: MegaphoneView {
class PinReminderMegaphone: Megaphone {
init(experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) {
super.init(experienceUpgrade: experienceUpgrade)
@ -17,7 +17,7 @@ class PinReminderMegaphone: MegaphoneView {
let primaryButtonTitle = OWSLocalizedString("PIN_REMINDER_MEGAPHONE_ACTION", comment: "Action text for PIN reminder megaphone")
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak fromViewController] in
let primaryButton = Button(title: primaryButtonTitle) { [weak fromViewController] in
guard let fromViewController else { return }
let vc = PinReminderViewController { [weak self] pinReminderViewController, result in
@ -104,6 +104,6 @@ class PinReminderMegaphone: MegaphoneView {
toastText = MegaphoneStrings.weWillRemindYouLater
}
presentToast(text: toastText, fromViewController: fromViewController)
fromViewController.presentToast(text: toastText)
}
}

View File

@ -7,7 +7,7 @@ import Foundation
import SignalServiceKit
import UIKit
class RecoveryKeyReminderMegaphone: MegaphoneView {
class RecoveryKeyReminderMegaphone: Megaphone {
init(
experienceUpgrade: ExperienceUpgrade,
fromViewController: UIViewController,
@ -33,7 +33,7 @@ class RecoveryKeyReminderMegaphone: MegaphoneView {
comment: "Snooze text for Recovery Key reminder megaphone",
)
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) {
let primaryButton = Button(title: primaryButtonTitle) {
let accountKeyStore = DependenciesBridge.shared.accountKeyStore
let backupSettingsStore = BackupSettingsStore()
let db = DependenciesBridge.shared.db
@ -45,9 +45,7 @@ class RecoveryKeyReminderMegaphone: MegaphoneView {
BackupRecoveryKeyReminderCoordinator(
aep: aep,
fromViewController: fromViewController,
onSuccess: { [weak self] in
guard let self else { return }
onSuccess: {
db.write { tx in
backupSettingsStore.setLastRecoveryKeyReminderDate(Date(), tx: tx)
}
@ -56,7 +54,7 @@ class RecoveryKeyReminderMegaphone: MegaphoneView {
"BACKUP_KEY_REMINDER_SUCCESSFUL_TOAST",
comment: "Toast indicating that the Recovery Key was correct.",
)
presentToast(text: toastText, fromViewController: fromViewController)
fromViewController.presentToast(text: toastText)
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
},

View File

@ -6,7 +6,7 @@
import SignalServiceKit
import SignalUI
class RemoteMegaphone: MegaphoneView {
class RemoteMegaphone: Megaphone {
private let megaphoneModel: RemoteMegaphoneModel
init(
@ -31,7 +31,7 @@ class RemoteMegaphone: MegaphoneView {
}
if let primary = megaphoneModel.presentablePrimaryAction {
let primaryButton = MegaphoneView.Button(title: primary.presentableText) { [weak self, weak fromViewController] in
let primaryButton = Button(title: primary.presentableText) { [weak self, weak fromViewController] in
guard
let self,
let fromViewController
@ -45,7 +45,7 @@ class RemoteMegaphone: MegaphoneView {
}
if let secondary = megaphoneModel.presentableSecondaryAction {
let secondaryButton = MegaphoneView.Button(title: secondary.presentableText) { [weak self, weak fromViewController] in
let secondaryButton = Button(title: secondary.presentableText) { [weak self, weak fromViewController] in
guard
let self,
let fromViewController