mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-26 01:52:25 +00:00
[WIP] Chat Import
This commit is contained in:
parent
00cea76464
commit
04513a7624
@ -21,6 +21,7 @@ swift_library(
|
|||||||
"//submodules/RadialStatusNode:RadialStatusNode",
|
"//submodules/RadialStatusNode:RadialStatusNode",
|
||||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||||
"//submodules/ChatHistoryImportTasks:ChatHistoryImportTasks",
|
"//submodules/ChatHistoryImportTasks:ChatHistoryImportTasks",
|
||||||
|
"//submodules/MimeTypes:MimeTypes",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import RadialStatusNode
|
|||||||
import AnimatedStickerNode
|
import AnimatedStickerNode
|
||||||
import AppBundle
|
import AppBundle
|
||||||
import ZIPFoundation
|
import ZIPFoundation
|
||||||
|
import MimeTypes
|
||||||
|
|
||||||
public final class ChatImportActivityScreen: ViewController {
|
public final class ChatImportActivityScreen: ViewController {
|
||||||
private final class Node: ViewControllerTracingNode {
|
private final class Node: ViewControllerTracingNode {
|
||||||
@ -191,7 +192,7 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
|
|
||||||
if let (layout, navigationHeight) = self.validLayout {
|
if let (layout, navigationHeight) = self.validLayout {
|
||||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate)
|
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate)
|
||||||
self.radialStatus.transitionToState(.progress(color: self.presentationData.theme.list.itemAccentColor, lineWidth: 6.0, value: max(0.09, self.totalProgress), cancelEnabled: false), animated: animated, synchronous: true, completion: {})
|
self.radialStatus.transitionToState(.progress(color: self.presentationData.theme.list.itemAccentColor, lineWidth: 6.0, value: max(0.02, self.totalProgress), cancelEnabled: false), animated: animated, synchronous: true, completion: {})
|
||||||
if isDone {
|
if isDone {
|
||||||
self.radialCheck.transitionToState(.progress(color: .clear, lineWidth: 6.0, value: self.totalProgress, cancelEnabled: false), animated: false, synchronous: true, completion: {})
|
self.radialCheck.transitionToState(.progress(color: .clear, lineWidth: 6.0, value: self.totalProgress, cancelEnabled: false), animated: false, synchronous: true, completion: {})
|
||||||
self.radialCheck.transitionToState(.check(self.presentationData.theme.list.itemAccentColor), animated: animated, synchronous: true, completion: {})
|
self.radialCheck.transitionToState(.check(self.presentationData.theme.list.itemAccentColor), animated: animated, synchronous: true, completion: {})
|
||||||
@ -362,7 +363,15 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
}
|
}
|
||||||
let uploadedMedia = unpackedFile
|
let uploadedMedia = unpackedFile
|
||||||
|> mapToSignal { tempFile -> Signal<(String, Float), ImportError> in
|
|> mapToSignal { tempFile -> Signal<(String, Float), ImportError> in
|
||||||
return ChatHistoryImport.uploadMedia(account: context.account, session: session, file: tempFile, fileName: fileName, type: mediaType)
|
var mimeTypeValue = "application/binary"
|
||||||
|
let fileExtension = (tempFile.path as NSString).pathExtension
|
||||||
|
if !fileExtension.isEmpty {
|
||||||
|
if let value = TGMimeTypeMap.mimeType(forExtension: fileExtension.lowercased()) {
|
||||||
|
mimeTypeValue = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChatHistoryImport.uploadMedia(account: context.account, session: session, file: tempFile, fileName: fileName, mimeType: mimeTypeValue, type: mediaType)
|
||||||
|> mapError { _ -> ImportError in
|
|> mapError { _ -> ImportError in
|
||||||
return .generic
|
return .generic
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1351,6 +1351,9 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override open func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
override open func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||||
|
if let presentingViewController = self.presentingViewController {
|
||||||
|
presentingViewController.dismiss(animated: false, completion: nil)
|
||||||
|
}
|
||||||
if let controller = self.presentedViewController {
|
if let controller = self.presentedViewController {
|
||||||
if flag {
|
if flag {
|
||||||
UIView.animate(withDuration: 0.3, delay: 0.0, options: UIView.AnimationOptions(rawValue: 7 << 16), animations: {
|
UIView.animate(withDuration: 0.3, delay: 0.0, options: UIView.AnimationOptions(rawValue: 7 << 16), animations: {
|
||||||
|
|||||||
@ -460,7 +460,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[649453030] = { return Api.messages.MessageEditData.parse_messageEditData($0) }
|
dict[649453030] = { return Api.messages.MessageEditData.parse_messageEditData($0) }
|
||||||
dict[-886477832] = { return Api.LabeledPrice.parse_labeledPrice($0) }
|
dict[-886477832] = { return Api.LabeledPrice.parse_labeledPrice($0) }
|
||||||
dict[-438840932] = { return Api.messages.ChatFull.parse_chatFull($0) }
|
dict[-438840932] = { return Api.messages.ChatFull.parse_chatFull($0) }
|
||||||
dict[-1919636670] = { return Api.messages.HistoryImportParsed.parse_historyImportParsed($0) }
|
dict[1578088377] = { return Api.messages.HistoryImportParsed.parse_historyImportParsed($0) }
|
||||||
dict[-618540889] = { return Api.InputSecureValue.parse_inputSecureValue($0) }
|
dict[-618540889] = { return Api.InputSecureValue.parse_inputSecureValue($0) }
|
||||||
dict[-170029155] = { return Api.messages.DiscussionMessage.parse_discussionMessage($0) }
|
dict[-170029155] = { return Api.messages.DiscussionMessage.parse_discussionMessage($0) }
|
||||||
dict[1722786150] = { return Api.help.DeepLinkInfo.parse_deepLinkInfoEmpty($0) }
|
dict[1722786150] = { return Api.help.DeepLinkInfo.parse_deepLinkInfoEmpty($0) }
|
||||||
|
|||||||
@ -985,10 +985,10 @@ public struct messages {
|
|||||||
switch self {
|
switch self {
|
||||||
case .historyImportParsed(let flags, let title):
|
case .historyImportParsed(let flags, let title):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(-1919636670)
|
buffer.appendInt32(1578088377)
|
||||||
}
|
}
|
||||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||||
if Int(flags) & Int(1 << 1) != 0 {serializeString(title!, buffer: buffer, boxed: false)}
|
if Int(flags) & Int(1 << 2) != 0 {serializeString(title!, buffer: buffer, boxed: false)}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1004,9 +1004,9 @@ public struct messages {
|
|||||||
var _1: Int32?
|
var _1: Int32?
|
||||||
_1 = reader.readInt32()
|
_1 = reader.readInt32()
|
||||||
var _2: String?
|
var _2: String?
|
||||||
if Int(_1!) & Int(1 << 1) != 0 {_2 = parseString(reader) }
|
if Int(_1!) & Int(1 << 2) != 0 {_2 = parseString(reader) }
|
||||||
let _c1 = _1 != nil
|
let _c1 = _1 != nil
|
||||||
let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil
|
let _c2 = (Int(_1!) & Int(1 << 2) == 0) || _2 != nil
|
||||||
if _c1 && _c2 {
|
if _c1 && _c2 {
|
||||||
return Api.messages.HistoryImportParsed.historyImportParsed(flags: _1!, title: _2)
|
return Api.messages.HistoryImportParsed.historyImportParsed(flags: _1!, title: _2)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ public enum ChatHistoryImport {
|
|||||||
public enum ParsedInfo {
|
public enum ParsedInfo {
|
||||||
case privateChat(title: String?)
|
case privateChat(title: String?)
|
||||||
case group(title: String?)
|
case group(title: String?)
|
||||||
|
case unknown(title: String?)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum GetInfoError {
|
public enum GetInfoError {
|
||||||
@ -39,7 +40,7 @@ public enum ChatHistoryImport {
|
|||||||
} else if (flags & (1 << 1)) != 0 {
|
} else if (flags & (1 << 1)) != 0 {
|
||||||
return .single(.group(title: title))
|
return .single(.group(title: title))
|
||||||
} else {
|
} else {
|
||||||
return .fail(.parseError)
|
return .single(.unknown(title: title))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,7 +93,7 @@ public enum ChatHistoryImport {
|
|||||||
case generic
|
case generic
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func uploadMedia(account: Account, session: Session, file: TempBoxFile, fileName: String, type: MediaType) -> Signal<Float, UploadMediaError> {
|
public static func uploadMedia(account: Account, session: Session, file: TempBoxFile, fileName: String, mimeType: String, type: MediaType) -> Signal<Float, UploadMediaError> {
|
||||||
return multipartUpload(network: account.network, postbox: account.postbox, source: .tempFile(file), encrypt: false, tag: nil, hintFileSize: nil, hintFileIsLarge: false)
|
return multipartUpload(network: account.network, postbox: account.postbox, source: .tempFile(file), encrypt: false, tag: nil, hintFileSize: nil, hintFileIsLarge: false)
|
||||||
|> mapError { _ -> UploadMediaError in
|
|> mapError { _ -> UploadMediaError in
|
||||||
return .generic
|
return .generic
|
||||||
@ -107,18 +108,18 @@ public enum ChatHistoryImport {
|
|||||||
case .file, .video, .sticker, .voice:
|
case .file, .video, .sticker, .voice:
|
||||||
var attributes: [Api.DocumentAttribute] = []
|
var attributes: [Api.DocumentAttribute] = []
|
||||||
attributes.append(.documentAttributeFilename(fileName: fileName))
|
attributes.append(.documentAttributeFilename(fileName: fileName))
|
||||||
var mimeType = "application/octet-stream"
|
var resolvedMimeType = mimeType
|
||||||
switch type {
|
switch type {
|
||||||
case .video:
|
case .video:
|
||||||
mimeType = "video/mp4"
|
resolvedMimeType = "video/mp4"
|
||||||
case .sticker:
|
case .sticker:
|
||||||
mimeType = "image/webp"
|
resolvedMimeType = "image/webp"
|
||||||
case .voice:
|
case .voice:
|
||||||
mimeType = "audio/ogg"
|
resolvedMimeType = "audio/ogg"
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
inputMedia = .inputMediaUploadedDocument(flags: 0, file: inputFile, thumb: nil, mimeType: mimeType, attributes: attributes, stickers: nil, ttlSeconds: nil)
|
inputMedia = .inputMediaUploadedDocument(flags: 0, file: inputFile, thumb: nil, mimeType: resolvedMimeType, attributes: attributes, stickers: nil, ttlSeconds: nil)
|
||||||
}
|
}
|
||||||
case let .progress(value):
|
case let .progress(value):
|
||||||
return .single(value)
|
return .single(value)
|
||||||
|
|||||||
@ -85,6 +85,8 @@ public class ShareRootControllerImpl {
|
|||||||
private var observer1: AnyObject?
|
private var observer1: AnyObject?
|
||||||
private var observer2: AnyObject?
|
private var observer2: AnyObject?
|
||||||
|
|
||||||
|
private weak var navigationController: NavigationController?
|
||||||
|
|
||||||
public init(initializationData: ShareRootControllerInitializationData, getExtensionContext: @escaping () -> NSExtensionContext?) {
|
public init(initializationData: ShareRootControllerInitializationData, getExtensionContext: @escaping () -> NSExtensionContext?) {
|
||||||
self.initializationData = initializationData
|
self.initializationData = initializationData
|
||||||
self.getExtensionContext = getExtensionContext
|
self.getExtensionContext = getExtensionContext
|
||||||
@ -374,6 +376,9 @@ public class ShareRootControllerImpl {
|
|||||||
if let currentShareController = strongSelf.currentShareController {
|
if let currentShareController = strongSelf.currentShareController {
|
||||||
currentShareController.dismiss()
|
currentShareController.dismiss()
|
||||||
}
|
}
|
||||||
|
if let navigationController = strongSelf.navigationController {
|
||||||
|
navigationController.dismiss(animated: false)
|
||||||
|
}
|
||||||
strongSelf.currentShareController = shareController
|
strongSelf.currentShareController = shareController
|
||||||
strongSelf.mainWindow?.present(shareController, on: .root)
|
strongSelf.mainWindow?.present(shareController, on: .root)
|
||||||
}
|
}
|
||||||
@ -491,6 +496,7 @@ public class ShareRootControllerImpl {
|
|||||||
|
|
||||||
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
|
||||||
let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme))
|
let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme))
|
||||||
|
strongSelf.navigationController = navigationController
|
||||||
navigationController.viewControllers = [TempController(context: context)]
|
navigationController.viewControllers = [TempController(context: context)]
|
||||||
strongSelf.mainWindow?.present(navigationController, on: .root)
|
strongSelf.mainWindow?.present(navigationController, on: .root)
|
||||||
|
|
||||||
@ -677,6 +683,161 @@ public class ShareRootControllerImpl {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigationController.viewControllers = [controller]
|
||||||
|
strongSelf.mainWindow?.present(navigationController, on: .root)
|
||||||
|
case let .unknown(peerTitle):
|
||||||
|
//TODO:localize
|
||||||
|
var attemptSelectionImpl: ((Peer) -> Void)?
|
||||||
|
var createNewGroupImpl: (() -> Void)?
|
||||||
|
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.excludeDisabled, .doNotSearchMessages], hasContactSelector: true, hasGlobalSearch: false, title: "Import Chat", attemptSelection: { peer in
|
||||||
|
attemptSelectionImpl?(peer)
|
||||||
|
}, createNewGroup: {
|
||||||
|
createNewGroupImpl?()
|
||||||
|
}, pretendPresentedInModal: true))
|
||||||
|
|
||||||
|
controller.customDismiss = {
|
||||||
|
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.peerSelected = { peer in
|
||||||
|
attemptSelectionImpl?(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.navigationPresentation = .default
|
||||||
|
|
||||||
|
let beginWithPeer: (PeerId) -> Void = { peerId in
|
||||||
|
navigationController.view.endEditing(true)
|
||||||
|
navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: {
|
||||||
|
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||||
|
}, peerId: peerId, archive: archive, mainEntry: mainFile, otherEntries: otherEntries))
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptSelectionImpl = { [weak controller] peer in
|
||||||
|
controller?.inProgress = true
|
||||||
|
let _ = (ChatHistoryImport.checkPeerImport(account: context.account, peerId: peer.id)
|
||||||
|
|> deliverOnMainQueue).start(error: { error in
|
||||||
|
controller?.inProgress = false
|
||||||
|
|
||||||
|
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
let errorText: String
|
||||||
|
switch error {
|
||||||
|
case .generic:
|
||||||
|
errorText = presentationData.strings.Login_UnknownError
|
||||||
|
case .userIsNotMutualContact:
|
||||||
|
errorText = "You can only import messages into private chats with users who added you in their contact list."
|
||||||
|
}
|
||||||
|
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
|
||||||
|
})])
|
||||||
|
strongSelf.mainWindow?.present(controller, on: .root)
|
||||||
|
}, completed: {
|
||||||
|
controller?.inProgress = false
|
||||||
|
|
||||||
|
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
|
var errorText: String?
|
||||||
|
if let channel = peer as? TelegramChannel {
|
||||||
|
if channel.flags.contains(.isCreator) || channel.adminRights != nil {
|
||||||
|
} else {
|
||||||
|
errorText = "You need to be an admin of the group to import messages into it."
|
||||||
|
}
|
||||||
|
} else if let group = peer as? TelegramGroup {
|
||||||
|
switch group.role {
|
||||||
|
case .creator:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
errorText = "You need to be an admin of the group to import messages into it."
|
||||||
|
}
|
||||||
|
} else if let _ = peer as? TelegramUser {
|
||||||
|
} else {
|
||||||
|
errorText = "You can't import history into this group."
|
||||||
|
}
|
||||||
|
|
||||||
|
if let errorText = errorText {
|
||||||
|
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
|
||||||
|
})])
|
||||||
|
strongSelf.mainWindow?.present(controller, on: .root)
|
||||||
|
} else {
|
||||||
|
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
if let user = peer as? TelegramUser {
|
||||||
|
let text: String
|
||||||
|
if let title = peerTitle {
|
||||||
|
text = "Are you sure you want to import messages from **\(title)** into the chat with **\(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))**?"
|
||||||
|
} else {
|
||||||
|
text = "Are you sure you want to import messages into the chat with **\(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))**?"
|
||||||
|
}
|
||||||
|
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Import Messages", text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||||
|
}), TextAlertAction(type: .defaultAction, title: "Import", action: {
|
||||||
|
beginWithPeer(peer.id)
|
||||||
|
})], parseMarkdown: true)
|
||||||
|
strongSelf.mainWindow?.present(controller, on: .root)
|
||||||
|
} else {
|
||||||
|
let text: String
|
||||||
|
if let groupTitle = peerTitle {
|
||||||
|
text = "Are you sure you want to import messages from **\(groupTitle)** into **\(peer.debugDisplayTitle)**?"
|
||||||
|
} else {
|
||||||
|
text = "Are you sure you want to import messages into **\(peer.debugDisplayTitle)**?"
|
||||||
|
}
|
||||||
|
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Import Messages", text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||||
|
}), TextAlertAction(type: .defaultAction, title: "Import", action: {
|
||||||
|
beginWithPeer(peer.id)
|
||||||
|
})], parseMarkdown: true)
|
||||||
|
strongSelf.mainWindow?.present(controller, on: .root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
createNewGroupImpl = {
|
||||||
|
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
let resolvedGroupTitle: String
|
||||||
|
if let groupTitle = peerTitle {
|
||||||
|
resolvedGroupTitle = groupTitle
|
||||||
|
} else {
|
||||||
|
resolvedGroupTitle = "Group"
|
||||||
|
}
|
||||||
|
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Create Group and Import Messages", text: "Are you sure you want to create group **\(resolvedGroupTitle)** and import messages from another messaging app?", actions: [TextAlertAction(type: .defaultAction, title: "Create and Import", action: {
|
||||||
|
var signal: Signal<PeerId?, NoError> = createSupergroup(account: context.account, title: resolvedGroupTitle, description: nil, isForHistoryImport: true)
|
||||||
|
|> map(Optional.init)
|
||||||
|
|> `catch` { _ -> Signal<PeerId?, NoError> in
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
let progressSignal = Signal<Never, NoError> { subscriber in
|
||||||
|
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.mainWindow?.present(controller, on: .root)
|
||||||
|
}
|
||||||
|
return ActionDisposable { [weak controller] in
|
||||||
|
Queue.mainQueue().async() {
|
||||||
|
controller?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> runOn(Queue.mainQueue())
|
||||||
|
|> delay(0.15, queue: Queue.mainQueue())
|
||||||
|
let progressDisposable = progressSignal.start()
|
||||||
|
|
||||||
|
signal = signal
|
||||||
|
|> afterDisposed {
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
progressDisposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = (signal
|
||||||
|
|> deliverOnMainQueue).start(next: { peerId in
|
||||||
|
if let peerId = peerId {
|
||||||
|
beginWithPeer(peerId)
|
||||||
|
} else {
|
||||||
|
//TODO:localize
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||||
|
})], parseMarkdown: true)
|
||||||
|
strongSelf.mainWindow?.present(controller, on: .root)
|
||||||
|
}
|
||||||
|
|
||||||
navigationController.viewControllers = [controller]
|
navigationController.viewControllers = [controller]
|
||||||
strongSelf.mainWindow?.present(navigationController, on: .root)
|
strongSelf.mainWindow?.present(navigationController, on: .root)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user