602 lines
23 KiB
Swift
602 lines
23 KiB
Swift
//
|
|
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import SignalServiceKit
|
|
|
|
private class StickerPackActionButton: UIView {
|
|
|
|
private let block: () -> Void
|
|
|
|
@available(*, unavailable, message: "use other constructor instead.")
|
|
required public init?(coder aDecoder: NSCoder) {
|
|
notImplemented()
|
|
}
|
|
|
|
init(actionIconName: String, block: @escaping () -> Void) {
|
|
self.block = block
|
|
|
|
super.init(frame: .zero)
|
|
|
|
configure(actionIconName: actionIconName)
|
|
}
|
|
|
|
private func configure(actionIconName: String) {
|
|
let actionIconSize: CGFloat = 20
|
|
let actionCircleSize: CGFloat = 32
|
|
let actionCircleView = CircleView(diameter: actionCircleSize)
|
|
actionCircleView.backgroundColor = Theme.washColor
|
|
let actionIcon = UIImage(named: actionIconName)?.withRenderingMode(.alwaysTemplate)
|
|
let actionIconView = UIImageView(image: actionIcon)
|
|
actionIconView.tintColor = Theme.secondaryTextAndIconColor
|
|
actionCircleView.addSubview(actionIconView)
|
|
actionIconView.autoCenterInSuperview()
|
|
actionIconView.autoSetDimensions(to: CGSize(square: actionIconSize))
|
|
|
|
self.addSubview(actionCircleView)
|
|
actionCircleView.autoPinEdgesToSuperviewEdges()
|
|
|
|
isUserInteractionEnabled = true
|
|
addGestureRecognizer(UITapGestureRecognizer(target: self,
|
|
action: #selector(didTapButton)))
|
|
}
|
|
|
|
@objc
|
|
func didTapButton(sender: UIGestureRecognizer) {
|
|
block()
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public class ManageStickersViewController: OWSTableViewController2 {
|
|
|
|
// MARK: Initializers
|
|
|
|
@objc
|
|
public required override init() {
|
|
super.init()
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
// MARK: - View Lifecycle
|
|
|
|
override public func loadView() {
|
|
super.loadView()
|
|
|
|
navigationItem.title = NSLocalizedString("STICKERS_MANAGE_VIEW_TITLE", comment: "Title for the 'manage stickers' view.")
|
|
|
|
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(didPressDismiss))
|
|
|
|
if FeatureFlags.stickerPackOrdering {
|
|
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(didPressEditButton))
|
|
}
|
|
}
|
|
|
|
override public func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
NotificationCenter.default.addObserver(self,
|
|
selector: #selector(packsDidChange),
|
|
name: StickerManager.packsDidChange,
|
|
object: nil)
|
|
|
|
defaultSeparatorInsetLeading = Self.cellHInnerMargin + iconSize + iconSpacing
|
|
|
|
updateState()
|
|
|
|
StickerManager.refreshContents()
|
|
}
|
|
|
|
private var pendingModalVC: ModalActivityIndicatorViewController?
|
|
|
|
private var needsStateUpdate = false {
|
|
didSet {
|
|
if needsStateUpdate {
|
|
updateEvent.requestNotify()
|
|
}
|
|
}
|
|
}
|
|
|
|
private var needsTableUpdate = false {
|
|
didSet {
|
|
if needsTableUpdate {
|
|
updateEvent.requestNotify()
|
|
}
|
|
}
|
|
}
|
|
|
|
private lazy var updateEvent: DebouncedEvent = {
|
|
DebouncedEvent(maxFrequencySeconds: 0.75, onQueue: .main) { [weak self] in
|
|
guard let self = self else { return }
|
|
if self.needsStateUpdate {
|
|
self.updateState()
|
|
} else if self.needsTableUpdate {
|
|
self.buildTable()
|
|
}
|
|
}
|
|
}()
|
|
|
|
private var installedStickerPackSources = [StickerPackDataSource]()
|
|
private var availableBuiltInStickerPackSources = [StickerPackDataSource]()
|
|
private var knownStickerPackSources = [StickerPackDataSource]()
|
|
|
|
private func updateState() {
|
|
// If we're presenting a modal because the user tapped install, dismiss it.
|
|
pendingModalVC?.dismiss {}
|
|
pendingModalVC = nil
|
|
|
|
// We need to recyle data sources to maintain continuity.
|
|
var oldInstalledSources = [StickerPackInfo: StickerPackDataSource]()
|
|
var oldTransientSources = [StickerPackInfo: StickerPackDataSource]()
|
|
let updateMapWithOldSources = { (map: inout [StickerPackInfo: StickerPackDataSource], sources: [StickerPackDataSource]) in
|
|
for source in sources {
|
|
guard let info = source.info else {
|
|
owsFailDebug("Source missing info.")
|
|
continue
|
|
}
|
|
map[info] = source
|
|
}
|
|
}
|
|
updateMapWithOldSources(&oldInstalledSources, installedStickerPackSources)
|
|
updateMapWithOldSources(&oldTransientSources, availableBuiltInStickerPackSources)
|
|
updateMapWithOldSources(&oldTransientSources, knownStickerPackSources)
|
|
|
|
var installedStickerPacks = [StickerPack]()
|
|
var availableBuiltInStickerPacks = [StickerPack]()
|
|
var availableKnownStickerPacks = [KnownStickerPack]()
|
|
self.databaseStorage.read { (transaction) in
|
|
let allPacks = StickerManager.allStickerPacks(transaction: transaction)
|
|
let allPackInfos = allPacks.map { $0.info }
|
|
|
|
// Only show packs with installed covers.
|
|
let packsWithCovers = allPacks.filter {
|
|
StickerManager.isStickerInstalled(stickerInfo: $0.coverInfo,
|
|
transaction: transaction)
|
|
}
|
|
// Sort sticker packs by "date saved, descending" so that we feature
|
|
// packs that the user has just learned about.
|
|
installedStickerPacks = packsWithCovers.filter { $0.isInstalled }
|
|
availableBuiltInStickerPacks = packsWithCovers.filter { !$0.isInstalled && StickerManager.isDefaultStickerPack($0.info) }
|
|
let allKnownStickerPacks = StickerManager.allKnownStickerPacks(transaction: transaction)
|
|
availableKnownStickerPacks = allKnownStickerPacks.filter { !allPackInfos.contains($0.info) }
|
|
}
|
|
|
|
let installedSource = { (info: StickerPackInfo) -> StickerPackDataSource in
|
|
if let source = oldInstalledSources[info] {
|
|
return source
|
|
}
|
|
let source = InstalledStickerPackDataSource(stickerPackInfo: info)
|
|
source.add(delegate: self)
|
|
return source
|
|
}
|
|
let transientSource = { (info: StickerPackInfo) -> StickerPackDataSource in
|
|
if let source = oldTransientSources[info] {
|
|
return source
|
|
}
|
|
// Don't download all stickers; we only need covers for this view.
|
|
let source = TransientStickerPackDataSource(stickerPackInfo: info,
|
|
shouldDownloadAllStickers: false)
|
|
source.add(delegate: self)
|
|
return source
|
|
}
|
|
let sortAvailablePacks = { (pack0: StickerPack, pack1: StickerPack) -> Bool in
|
|
return pack0.dateCreated > pack1.dateCreated
|
|
}
|
|
let sortKnownPacks = { (pack0: KnownStickerPack, pack1: KnownStickerPack) -> Bool in
|
|
return pack0.dateCreated > pack1.dateCreated
|
|
}
|
|
|
|
self.installedStickerPackSources = installedStickerPacks.sorted {
|
|
$0.dateCreated > $1.dateCreated
|
|
}.map { installedSource($0.info) }
|
|
self.availableBuiltInStickerPackSources = availableBuiltInStickerPacks.sorted(by: sortAvailablePacks)
|
|
.map { transientSource($0.info) }
|
|
self.knownStickerPackSources = availableKnownStickerPacks.sorted(by: sortKnownPacks)
|
|
.map { transientSource($0.info) }
|
|
|
|
needsStateUpdate = false
|
|
buildTable()
|
|
}
|
|
|
|
private func buildTable() {
|
|
let contents = OWSTableContents()
|
|
|
|
let installedSection = OWSTableSection()
|
|
installedSection.headerTitle = NSLocalizedString("STICKERS_MANAGE_VIEW_INSTALLED_PACKS_SECTION_TITLE", comment: "Title for the 'installed stickers' section of the 'manage stickers' view.")
|
|
if installedStickerPackSources.count < 1 {
|
|
let text = NSLocalizedString("STICKERS_MANAGE_VIEW_NO_INSTALLED_PACKS", comment: "Label indicating that the user has no installed sticker packs.")
|
|
installedSection.add(buildEmptySectionItem(labelText: text))
|
|
}
|
|
for dataSource in installedStickerPackSources {
|
|
installedSection.add(OWSTableItem(customCellBlock: { [weak self] in
|
|
guard let self = self else {
|
|
return UITableViewCell()
|
|
}
|
|
return self.buildTableCell(installedStickerPack: dataSource)
|
|
},
|
|
actionBlock: { [weak self] in
|
|
guard let packInfo = dataSource.info else {
|
|
owsFailDebug("Source missing info.")
|
|
return
|
|
}
|
|
self?.show(packInfo: packInfo)
|
|
}))
|
|
}
|
|
contents.addSection(installedSection)
|
|
|
|
let itemForAvailablePack = { (dataSource: StickerPackDataSource) -> OWSTableItem in
|
|
OWSTableItem(customCellBlock: { [weak self] in
|
|
guard let self = self else {
|
|
return UITableViewCell()
|
|
}
|
|
return self.buildTableCell(availableStickerPack: dataSource)
|
|
},
|
|
actionBlock: { [weak self] in
|
|
guard let packInfo = dataSource.info else {
|
|
owsFailDebug("Source missing info.")
|
|
return
|
|
}
|
|
self?.show(packInfo: packInfo)
|
|
})
|
|
}
|
|
if availableBuiltInStickerPackSources.count > 0 {
|
|
let section = OWSTableSection()
|
|
section.headerTitle = NSLocalizedString("STICKERS_MANAGE_VIEW_AVAILABLE_BUILT_IN_PACKS_SECTION_TITLE", comment: "Title for the 'available built-in stickers' section of the 'manage stickers' view.")
|
|
for dataSource in availableBuiltInStickerPackSources {
|
|
section.add(itemForAvailablePack(dataSource))
|
|
}
|
|
contents.addSection(section)
|
|
}
|
|
|
|
// Sticker packs whose manifest is available.
|
|
var loadedKnownStickerPackSources = [StickerPackDataSource]()
|
|
// Sticker packs whose manifest is downloading.
|
|
var loadingKnownStickerPackSources = [StickerPackDataSource]()
|
|
// Sticker packs whose manifest download failed permanently.
|
|
var failedKnownStickerPackSources = [StickerPackDataSource]()
|
|
for source in knownStickerPackSources {
|
|
guard source.getStickerPack() == nil else {
|
|
// Already loaded.
|
|
loadedKnownStickerPackSources.append(source)
|
|
continue
|
|
}
|
|
guard let info = source.info else {
|
|
owsFailDebug("Known source missing info.")
|
|
continue
|
|
}
|
|
// Hide sticker packs whose download failed permanently.
|
|
let isFailed = StickerManager.isStickerPackMissing(stickerPackInfo: info)
|
|
if isFailed {
|
|
failedKnownStickerPackSources.append(source)
|
|
} else {
|
|
loadingKnownStickerPackSources.append(source)
|
|
}
|
|
}
|
|
let knownSection = OWSTableSection()
|
|
knownSection.headerTitle = NSLocalizedString("STICKERS_MANAGE_VIEW_AVAILABLE_KNOWN_PACKS_SECTION_TITLE", comment: "Title for the 'available known stickers' section of the 'manage stickers' view.")
|
|
if knownStickerPackSources.count < 1 {
|
|
let text = NSLocalizedString("STICKERS_MANAGE_VIEW_NO_KNOWN_PACKS", comment: "Label indicating that the user has no known sticker packs.")
|
|
knownSection.add(buildEmptySectionItem(labelText: text))
|
|
}
|
|
for dataSource in loadedKnownStickerPackSources {
|
|
knownSection.add(itemForAvailablePack(dataSource))
|
|
}
|
|
if loadingKnownStickerPackSources.count > 0 {
|
|
let text = NSLocalizedString("STICKERS_MANAGE_VIEW_LOADING_KNOWN_PACKS",
|
|
comment: "Label indicating that one or more known sticker packs is loading.")
|
|
knownSection.add(buildEmptySectionItem(labelText: text))
|
|
} else if failedKnownStickerPackSources.count > 0 {
|
|
let text = NSLocalizedString("STICKERS_MANAGE_VIEW_FAILED_KNOWN_PACKS",
|
|
comment: "Label indicating that one or more known sticker packs failed to load.")
|
|
knownSection.add(buildEmptySectionItem(labelText: text))
|
|
}
|
|
contents.addSection(knownSection)
|
|
|
|
self.contents = contents
|
|
needsTableUpdate = false
|
|
}
|
|
|
|
private func buildTableCell(installedStickerPack dataSource: StickerPackDataSource) -> UITableViewCell {
|
|
let actionIconName = CurrentAppContext().isRTL ? "reply-filled-24" : "reply-filled-reversed-24"
|
|
|
|
return buildTableCell(dataSource: dataSource,
|
|
actionIconName: actionIconName) { [weak self] in
|
|
guard let packInfo = dataSource.info else {
|
|
owsFailDebug("Source missing info.")
|
|
return
|
|
}
|
|
self?.share(packInfo: packInfo)
|
|
}
|
|
}
|
|
|
|
private func buildTableCell(availableStickerPack dataSource: StickerPackDataSource) -> UITableViewCell {
|
|
if let stickerPack = dataSource.getStickerPack() {
|
|
let actionIconName = Theme.iconName(.messageActionSave)
|
|
return buildTableCell(dataSource: dataSource,
|
|
actionIconName: actionIconName) { [weak self] in
|
|
self?.install(stickerPack: stickerPack)
|
|
}
|
|
} else {
|
|
// Hide "install" button if manifest isn't downloaded yet.
|
|
return buildTableCell(dataSource: dataSource,
|
|
actionIconName: nil) { }
|
|
}
|
|
}
|
|
|
|
private let reusableCoverViewCache = NSCache<NSData, StickerReusableView>()
|
|
private func reusableCoverView(forDataSource dataSource: StickerPackDataSource) -> StickerReusableView? {
|
|
guard let packId = dataSource.info?.packId else { return nil }
|
|
|
|
let view: StickerReusableView = {
|
|
if let view = reusableCoverViewCache.object(forKey: packId as NSData) { return view }
|
|
let view = StickerReusableView()
|
|
reusableCoverViewCache.setObject(view, forKey: packId as NSData)
|
|
return view
|
|
}()
|
|
|
|
guard !view.hasStickerView else { return view }
|
|
|
|
guard let stickerInfo = dataSource.installedCoverInfo,
|
|
let imageView = imageView(forStickerInfo: stickerInfo, dataSource: dataSource) else {
|
|
view.showPlaceholder()
|
|
return view
|
|
}
|
|
|
|
view.configure(with: imageView)
|
|
|
|
return view
|
|
}
|
|
|
|
private let iconSize: CGFloat = 56
|
|
private let iconSpacing: CGFloat = 12
|
|
private func buildTableCell(dataSource: StickerPackDataSource,
|
|
actionIconName: String?,
|
|
block: @escaping () -> Void) -> UITableViewCell {
|
|
|
|
let cell = OWSTableItem.newCell()
|
|
|
|
guard let packInfo = dataSource.info else {
|
|
owsFailDebug("Source missing info.")
|
|
return cell
|
|
}
|
|
|
|
let titleValue = dataSource.title?.filterForDisplay
|
|
let authorNameValue = dataSource.author?.filterForDisplay
|
|
|
|
let iconView = reusableCoverView(forDataSource: dataSource) ?? UIView()
|
|
iconView.autoSetDimensions(to: CGSize(square: iconSize))
|
|
|
|
let title: String
|
|
if let titleValue = titleValue?.ows_stripped(),
|
|
titleValue.count > 0 {
|
|
title = titleValue
|
|
} else {
|
|
title = NSLocalizedString("STICKERS_PACK_DEFAULT_TITLE", comment: "Default title for sticker packs.")
|
|
}
|
|
let titleLabel = UILabel()
|
|
titleLabel.text = title
|
|
titleLabel.font = UIFont.ows_dynamicTypeBody.ows_semibold
|
|
titleLabel.textColor = Theme.primaryTextColor
|
|
titleLabel.lineBreakMode = .byTruncatingTail
|
|
|
|
let textStack = UIStackView(arrangedSubviews: [
|
|
titleLabel
|
|
])
|
|
textStack.axis = .vertical
|
|
textStack.alignment = .leading
|
|
textStack.spacing = 4
|
|
textStack.setContentHuggingHorizontalLow()
|
|
textStack.setCompressionResistanceHorizontalLow()
|
|
|
|
// TODO: Should we show a default author name?
|
|
|
|
let isDefaultStickerPack = StickerManager.isDefaultStickerPack(packInfo)
|
|
|
|
var authorViews = [UIView]()
|
|
if isDefaultStickerPack {
|
|
let builtInPackView = UIImageView()
|
|
builtInPackView.setTemplateImageName("check-circle-filled-16", tintColor: Theme.accentBlueColor)
|
|
builtInPackView.setCompressionResistanceHigh()
|
|
builtInPackView.setContentHuggingHigh()
|
|
authorViews.append(builtInPackView)
|
|
}
|
|
|
|
if let authorName = authorNameValue?.ows_stripped(),
|
|
authorName.count > 0 {
|
|
let authorLabel = UILabel()
|
|
authorLabel.text = authorName
|
|
authorLabel.font = isDefaultStickerPack ? UIFont.ows_dynamicTypeCaption1.ows_semibold : UIFont.ows_dynamicTypeCaption1
|
|
authorLabel.textColor = isDefaultStickerPack ? Theme.accentBlueColor : Theme.secondaryTextAndIconColor
|
|
authorLabel.lineBreakMode = .byTruncatingTail
|
|
authorViews.append(authorLabel)
|
|
}
|
|
|
|
if authorViews.count > 0 {
|
|
let authorStack = UIStackView(arrangedSubviews: authorViews)
|
|
authorStack.axis = .horizontal
|
|
authorStack.alignment = .center
|
|
authorStack.spacing = 4
|
|
textStack.addArrangedSubview(authorStack)
|
|
}
|
|
|
|
var subviews: [UIView] = [
|
|
iconView,
|
|
textStack
|
|
]
|
|
|
|
if let actionIconName = actionIconName {
|
|
let actionButton = StickerPackActionButton(actionIconName: actionIconName, block: block)
|
|
subviews.append(actionButton)
|
|
}
|
|
|
|
let stack = UIStackView(arrangedSubviews: subviews)
|
|
stack.axis = .horizontal
|
|
stack.alignment = .center
|
|
stack.spacing = iconSpacing
|
|
|
|
cell.contentView.addSubview(stack)
|
|
stack.autoPinEdgesToSuperviewMargins()
|
|
|
|
return cell
|
|
}
|
|
|
|
private func imageView(forStickerInfo stickerInfo: StickerInfo,
|
|
dataSource: StickerPackDataSource) -> UIView? {
|
|
StickerView.stickerView(forStickerInfo: stickerInfo, dataSource: dataSource)
|
|
}
|
|
|
|
private func buildEmptySectionItem(labelText: String) -> OWSTableItem {
|
|
return OWSTableItem(customCellBlock: { [weak self] in
|
|
guard let self = self else {
|
|
return UITableViewCell()
|
|
}
|
|
return self.buildEmptySectionCell(labelText: labelText)
|
|
})
|
|
}
|
|
|
|
private func buildEmptySectionCell(labelText: String) -> UITableViewCell {
|
|
let cell = OWSTableItem.newCell()
|
|
|
|
let label = UILabel()
|
|
label.text = labelText
|
|
label.font = UIFont.ows_dynamicTypeCaption1
|
|
label.textColor = Theme.secondaryTextAndIconColor
|
|
label.textAlignment = .center
|
|
label.numberOfLines = 0
|
|
label.lineBreakMode = .byWordWrapping
|
|
label.setCompressionResistanceHigh()
|
|
label.setContentHuggingHigh()
|
|
cell.contentView.addSubview(label)
|
|
label.autoPinEdgesToSuperviewMargins()
|
|
|
|
return cell
|
|
}
|
|
|
|
// MARK: Events
|
|
|
|
private func show(packInfo: StickerPackInfo) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.verbose("")
|
|
|
|
let packView = StickerPackViewController(stickerPackInfo: packInfo)
|
|
packView.present(from: self, animated: true)
|
|
}
|
|
|
|
// We need to retain a link to the send flow during the send flow.
|
|
private var sendMessageFlow: SendMessageFlow?
|
|
|
|
private func share(packInfo: StickerPackInfo) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.verbose("")
|
|
|
|
let packUrl = packInfo.shareUrl()
|
|
|
|
guard let navigationController = self.navigationController else {
|
|
owsFailDebug("Missing navigationController.")
|
|
return
|
|
}
|
|
let messageBody = MessageBody(text: packUrl, ranges: .empty)
|
|
let unapprovedContent = SendMessageUnapprovedContent.text(messageBody: messageBody)
|
|
let sendMessageFlow = SendMessageFlow(flowType: .`default`,
|
|
unapprovedContent: unapprovedContent,
|
|
useConversationComposeForSingleRecipient: true,
|
|
navigationController: navigationController,
|
|
delegate: self)
|
|
// Retain the flow until it is complete.
|
|
self.sendMessageFlow = sendMessageFlow
|
|
}
|
|
|
|
private func install(stickerPack: StickerPack) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.verbose("")
|
|
|
|
let modalVC = ModalActivityIndicatorViewController(canCancel: false, presentationDelay: 0)
|
|
modalVC.modalPresentationStyle = .overFullScreen
|
|
present(modalVC, animated: false, completion: nil)
|
|
|
|
// This will be dismissed once we receive a sticker pack update notification from StickerManager
|
|
pendingModalVC = modalVC
|
|
self.databaseStorage.asyncWrite { transaction in
|
|
StickerManager.installStickerPack(stickerPack: stickerPack,
|
|
wasLocallyInitiated: true,
|
|
transaction: transaction)
|
|
}
|
|
|
|
// or... if 6s have passed. just to be safe.
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(6)) { [weak self] in
|
|
// If the current modal isn't the one we created, we can ignore it
|
|
guard modalVC == self?.pendingModalVC else { return }
|
|
|
|
if self?.reachabilityManager.isReachable == true {
|
|
owsFailDebug("Expected to hear back from StickerManager about a newly installed sticker pack")
|
|
}
|
|
self?.updateState()
|
|
}
|
|
}
|
|
|
|
@objc func packsDidChange() {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.verbose("")
|
|
|
|
needsStateUpdate = true
|
|
}
|
|
|
|
@objc
|
|
private func didPressEditButton(sender: UIButton) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.verbose("")
|
|
|
|
// TODO:
|
|
}
|
|
|
|
@objc
|
|
private func didPressDismiss(sender: UIButton) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.verbose("")
|
|
|
|
dismiss(animated: true)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension ManageStickersViewController: StickerPackDataSourceDelegate {
|
|
public func stickerPackDataDidChange() {
|
|
AssertIsOnMainThread()
|
|
needsTableUpdate = true
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension ManageStickersViewController: SendMessageDelegate {
|
|
|
|
public func sendMessageFlowDidComplete(threads: [TSThread]) {
|
|
AssertIsOnMainThread()
|
|
|
|
sendMessageFlow = nil
|
|
|
|
navigationController?.popToViewController(self, animated: true)
|
|
}
|
|
|
|
public func sendMessageFlowDidCancel() {
|
|
AssertIsOnMainThread()
|
|
|
|
sendMessageFlow = nil
|
|
|
|
navigationController?.popToViewController(self, animated: true)
|
|
}
|
|
}
|