From 04513a76244e5394440ee2c7dad9867e16f32c7c Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 22 Jan 2021 21:59:03 +0400 Subject: [PATCH] [WIP] Chat Import --- submodules/ChatImportUI/BUILD | 1 + .../Sources/ChatImportActivityScreen.swift | 13 +- .../Navigation/NavigationController.swift | 3 + submodules/TelegramApi/Sources/Api0.swift | 2 +- submodules/TelegramApi/Sources/Api1.swift | 8 +- .../Sources/ChatHistoryImport.swift | 15 +- .../Sources/ShareExtensionContext.swift | 161 ++++++++++++++++++ 7 files changed, 189 insertions(+), 14 deletions(-) diff --git a/submodules/ChatImportUI/BUILD b/submodules/ChatImportUI/BUILD index f926455101..016041c6fb 100644 --- a/submodules/ChatImportUI/BUILD +++ b/submodules/ChatImportUI/BUILD @@ -21,6 +21,7 @@ swift_library( "//submodules/RadialStatusNode:RadialStatusNode", "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/ChatHistoryImportTasks:ChatHistoryImportTasks", + "//submodules/MimeTypes:MimeTypes", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift index 79a36bacd3..7a37202915 100644 --- a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift +++ b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift @@ -12,6 +12,7 @@ import RadialStatusNode import AnimatedStickerNode import AppBundle import ZIPFoundation +import MimeTypes public final class ChatImportActivityScreen: ViewController { private final class Node: ViewControllerTracingNode { @@ -191,7 +192,7 @@ public final class ChatImportActivityScreen: ViewController { if let (layout, navigationHeight) = self.validLayout { 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 { 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: {}) @@ -362,7 +363,15 @@ public final class ChatImportActivityScreen: ViewController { } let uploadedMedia = unpackedFile |> 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 return .generic } diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index 7386e862b4..cb17bba6e2 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -1351,6 +1351,9 @@ open class NavigationController: UINavigationController, ContainableController, } 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 flag { UIView.animate(withDuration: 0.3, delay: 0.0, options: UIView.AnimationOptions(rawValue: 7 << 16), animations: { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 1a2d35f1b1..af8395f39b 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -460,7 +460,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[649453030] = { return Api.messages.MessageEditData.parse_messageEditData($0) } dict[-886477832] = { return Api.LabeledPrice.parse_labeledPrice($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[-170029155] = { return Api.messages.DiscussionMessage.parse_discussionMessage($0) } dict[1722786150] = { return Api.help.DeepLinkInfo.parse_deepLinkInfoEmpty($0) } diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index 50aca08ddb..1368d2c662 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -985,10 +985,10 @@ public struct messages { switch self { case .historyImportParsed(let flags, let title): if boxed { - buffer.appendInt32(-1919636670) + buffer.appendInt32(1578088377) } 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 } } @@ -1004,9 +1004,9 @@ public struct messages { var _1: Int32? _1 = reader.readInt32() 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 _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil + let _c2 = (Int(_1!) & Int(1 << 2) == 0) || _2 != nil if _c1 && _c2 { return Api.messages.HistoryImportParsed.historyImportParsed(flags: _1!, title: _2) } diff --git a/submodules/TelegramCore/Sources/ChatHistoryImport.swift b/submodules/TelegramCore/Sources/ChatHistoryImport.swift index cc646444cd..d9610ef932 100644 --- a/submodules/TelegramCore/Sources/ChatHistoryImport.swift +++ b/submodules/TelegramCore/Sources/ChatHistoryImport.swift @@ -19,6 +19,7 @@ public enum ChatHistoryImport { public enum ParsedInfo { case privateChat(title: String?) case group(title: String?) + case unknown(title: String?) } public enum GetInfoError { @@ -39,7 +40,7 @@ public enum ChatHistoryImport { } else if (flags & (1 << 1)) != 0 { return .single(.group(title: title)) } else { - return .fail(.parseError) + return .single(.unknown(title: title)) } } } @@ -92,7 +93,7 @@ public enum ChatHistoryImport { case generic } - public static func uploadMedia(account: Account, session: Session, file: TempBoxFile, fileName: String, type: MediaType) -> Signal { + public static func uploadMedia(account: Account, session: Session, file: TempBoxFile, fileName: String, mimeType: String, type: MediaType) -> Signal { return multipartUpload(network: account.network, postbox: account.postbox, source: .tempFile(file), encrypt: false, tag: nil, hintFileSize: nil, hintFileIsLarge: false) |> mapError { _ -> UploadMediaError in return .generic @@ -107,18 +108,18 @@ public enum ChatHistoryImport { case .file, .video, .sticker, .voice: var attributes: [Api.DocumentAttribute] = [] attributes.append(.documentAttributeFilename(fileName: fileName)) - var mimeType = "application/octet-stream" + var resolvedMimeType = mimeType switch type { case .video: - mimeType = "video/mp4" + resolvedMimeType = "video/mp4" case .sticker: - mimeType = "image/webp" + resolvedMimeType = "image/webp" case .voice: - mimeType = "audio/ogg" + resolvedMimeType = "audio/ogg" default: 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): return .single(value) diff --git a/submodules/TelegramUI/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Sources/ShareExtensionContext.swift index 8e63c65980..091c0396da 100644 --- a/submodules/TelegramUI/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Sources/ShareExtensionContext.swift @@ -85,6 +85,8 @@ public class ShareRootControllerImpl { private var observer1: AnyObject? private var observer2: AnyObject? + private weak var navigationController: NavigationController? + public init(initializationData: ShareRootControllerInitializationData, getExtensionContext: @escaping () -> NSExtensionContext?) { self.initializationData = initializationData self.getExtensionContext = getExtensionContext @@ -374,6 +376,9 @@ public class ShareRootControllerImpl { if let currentShareController = strongSelf.currentShareController { currentShareController.dismiss() } + if let navigationController = strongSelf.navigationController { + navigationController.dismiss(animated: false) + } strongSelf.currentShareController = shareController strongSelf.mainWindow?.present(shareController, on: .root) } @@ -491,6 +496,7 @@ public class ShareRootControllerImpl { let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 } let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme)) + strongSelf.navigationController = navigationController navigationController.viewControllers = [TempController(context: context)] 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 = createSupergroup(account: context.account, title: resolvedGroupTitle, description: nil, isForHistoryImport: true) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let progressSignal = Signal { 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] strongSelf.mainWindow?.present(navigationController, on: .root) }