From da2faa59676a29db7a407cd78db099fb9bf08ec6 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Wed, 20 Jan 2021 22:25:20 +0400 Subject: [PATCH] [WIP] Chat import --- build-system/Make/ProjectGeneration.py | 8 - .../Sources/PeerSelectionController.swift | 4 +- submodules/ChatImportUI/BUILD | 27 + .../Sources/ChatImportActivityScreen.swift | 357 +++++++++ submodules/Display/Source/WindowContent.swift | 3 - .../Sources/ChatHistoryImport.swift | 40 + submodules/TelegramUI/BUILD | 1 + .../Resources/Animations/HistoryImport.tgs | Bin 0 -> 12545 bytes .../Sources/PeerSelectionController.swift | 4 +- .../Sources/PeerSelectionControllerNode.swift | 39 +- .../Sources/ShareExtensionContext.swift | 699 +++++------------- 11 files changed, 629 insertions(+), 553 deletions(-) create mode 100644 submodules/ChatImportUI/BUILD create mode 100644 submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift create mode 100644 submodules/TelegramUI/Resources/Animations/HistoryImport.tgs diff --git a/build-system/Make/ProjectGeneration.py b/build-system/Make/ProjectGeneration.py index dab2520e34..95c5baebce 100644 --- a/build-system/Make/ProjectGeneration.py +++ b/build-system/Make/ProjectGeneration.py @@ -14,14 +14,6 @@ def generate(build_environment: BuildEnvironment, disable_extensions, configurat project_path = os.path.join(build_environment.base_path, 'build-input/gen/project') app_target = 'Telegram' - ''' - TULSI_APP="build-input/gen/project/Tulsi.app" - TULSI="$TULSI_APP/Contents/MacOS/Tulsi" - - rm -rf "$GEN_DIRECTORY/${APP_TARGET}.tulsiproj" - rm -rf "$TULSI_APP" - ''' - os.makedirs(project_path, exist_ok=True) remove_directory('{}/Tulsi.app'.format(project_path)) remove_directory('{project}/{target}.tulsiproj'.format(project=project_path, target=app_target)) diff --git a/submodules/AccountContext/Sources/PeerSelectionController.swift b/submodules/AccountContext/Sources/PeerSelectionController.swift index 6ff389482b..bd5050516c 100644 --- a/submodules/AccountContext/Sources/PeerSelectionController.swift +++ b/submodules/AccountContext/Sources/PeerSelectionController.swift @@ -32,14 +32,16 @@ public struct ChatListNodePeersFilter: OptionSet { public final class PeerSelectionControllerParams { public let context: AccountContext public let filter: ChatListNodePeersFilter + public let hasChatListSelector: Bool public let hasContactSelector: Bool public let title: String? public let attemptSelection: ((Peer) -> Void)? public let createNewGroup: (() -> Void)? - public init(context: AccountContext, filter: ChatListNodePeersFilter = [.onlyWriteable], hasContactSelector: Bool = true, title: String? = nil, attemptSelection: ((Peer) -> Void)? = nil, createNewGroup: (() -> Void)? = nil) { + public init(context: AccountContext, filter: ChatListNodePeersFilter = [.onlyWriteable], hasChatListSelector: Bool = true, hasContactSelector: Bool = true, title: String? = nil, attemptSelection: ((Peer) -> Void)? = nil, createNewGroup: (() -> Void)? = nil) { self.context = context self.filter = filter + self.hasChatListSelector = hasChatListSelector self.hasContactSelector = hasContactSelector self.title = title self.attemptSelection = attemptSelection diff --git a/submodules/ChatImportUI/BUILD b/submodules/ChatImportUI/BUILD new file mode 100644 index 0000000000..5e000b6291 --- /dev/null +++ b/submodules/ChatImportUI/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatImportUI", + module_name = "ChatImportUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/Postbox:Postbox", + "//submodules/SyncCore:SyncCore", + "//submodules/TelegramCore:TelegramCore", + "//submodules/AppBundle:AppBundle", + "//third-party/ZIPFoundation:ZIPFoundation", + "//submodules/AccountContext:AccountContext", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/RadialStatusNode:RadialStatusNode", + "//submodules/AnimatedStickerNode:AnimatedStickerNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift new file mode 100644 index 0000000000..f8d77a01f0 --- /dev/null +++ b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift @@ -0,0 +1,357 @@ +import UIKit +import AsyncDisplayKit +import Display +import TelegramCore +import SyncCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import AccountContext +import PresentationDataUtils +import RadialStatusNode +import AnimatedStickerNode +import AppBundle +import ZIPFoundation + +public final class ChatImportActivityScreen: ViewController { + private final class Node: ViewControllerTracingNode { + private weak var controller: ChatImportActivityScreen? + + private let context: AccountContext + private var presentationData: PresentationData + + private let animationNode: AnimatedStickerNode + private let radialStatus: RadialStatusNode + private let radialStatusBackground: ASImageNode + private let radialStatusText: ImmediateTextNode + private let progressText: ImmediateTextNode + private let statusText: ImmediateTextNode + + private let statusButtonText: ImmediateTextNode + private let statusButton: HighlightableButtonNode + + private var validLayout: (ContainerViewLayout, CGFloat)? + + private var totalProgress: CGFloat = 0.0 + private let totalBytes: Int + private var isDone: Bool = false + + init(controller: ChatImportActivityScreen, context: AccountContext, totalBytes: Int) { + self.controller = controller + self.context = context + self.totalBytes = totalBytes + + self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + self.animationNode = AnimatedStickerNode() + + self.radialStatus = RadialStatusNode(backgroundNodeColor: .clear) + self.radialStatusBackground = ASImageNode() + self.radialStatusBackground.isUserInteractionEnabled = false + self.radialStatusBackground.displaysAsynchronously = false + self.radialStatusBackground.image = generateCircleImage(diameter: 180.0, lineWidth: 6.0, color: self.presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.2)) + + self.radialStatusText = ImmediateTextNode() + self.radialStatusText.isUserInteractionEnabled = false + self.radialStatusText.displaysAsynchronously = false + self.radialStatusText.maximumNumberOfLines = 1 + self.radialStatusText.isAccessibilityElement = false + + self.progressText = ImmediateTextNode() + self.progressText.isUserInteractionEnabled = false + self.progressText.displaysAsynchronously = false + self.progressText.maximumNumberOfLines = 1 + self.progressText.isAccessibilityElement = false + + self.statusText = ImmediateTextNode() + self.statusText.textAlignment = .center + self.statusText.isUserInteractionEnabled = false + self.statusText.displaysAsynchronously = false + self.statusText.maximumNumberOfLines = 0 + self.statusText.isAccessibilityElement = false + + self.statusButtonText = ImmediateTextNode() + self.statusButtonText.isUserInteractionEnabled = false + self.statusButtonText.displaysAsynchronously = false + self.statusButtonText.maximumNumberOfLines = 1 + self.statusButtonText.isAccessibilityElement = false + + self.statusButton = HighlightableButtonNode() + + super.init() + + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + if let path = getAppBundle().path(forResource: "HistoryImport", ofType: "tgs") { + self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 170 * 2, height: 170 * 2, playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) + self.animationNode.visibility = true + } + + self.addSubnode(self.animationNode) + self.addSubnode(self.radialStatusBackground) + self.addSubnode(self.radialStatus) + self.addSubnode(self.radialStatusText) + self.addSubnode(self.progressText) + self.addSubnode(self.statusText) + self.addSubnode(self.statusButtonText) + self.addSubnode(self.statusButton) + + self.statusButton.addTarget(self, action: #selector(self.statusButtonPressed), forControlEvents: .touchUpInside) + self.statusButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.statusButtonText.layer.removeAnimation(forKey: "opacity") + strongSelf.statusButtonText.alpha = 0.4 + } else { + strongSelf.statusButtonText.alpha = 1.0 + strongSelf.statusButtonText.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + @objc private func statusButtonPressed() { + self.controller?.cancel() + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.validLayout = (layout, navigationHeight) + + //TODO:localize + + let iconSize = CGSize(width: 170.0, height: 170.0) + let radialStatusSize = CGSize(width: 180.0, height: 180.0) + let maxIconStatusSpacing: CGFloat = 62.0 + let maxProgressTextSpacing: CGFloat = 32.0 + let progressStatusSpacing: CGFloat = 16.0 + let statusButtonSpacing: CGFloat = 16.0 + + self.radialStatusText.attributedText = NSAttributedString(string: "\(Int(self.totalProgress * 100.0))%", font: Font.with(size: 42.0, design: .round, traits: []), textColor: self.presentationData.theme.list.itemPrimaryTextColor) + let radialStatusTextSize = self.radialStatusText.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + + self.progressText.attributedText = NSAttributedString(string: "\(dataSizeString(Int(self.totalProgress * CGFloat(self.totalBytes)))) of \(dataSizeString(Int(1.0 * CGFloat(self.totalBytes))))", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) + let progressTextSize = self.progressText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude)) + + self.statusButtonText.attributedText = NSAttributedString(string: "Done", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemAccentColor) + let statusButtonTextSize = self.statusButtonText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude)) + + if !self.isDone { + self.statusText.attributedText = NSAttributedString(string: "Please keep this window open\nduring the import.", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) + } else { + self.statusText.attributedText = NSAttributedString(string: "This chat has been imported\nsuccessfully.", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) + } + let statusTextSize = self.statusText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude)) + + let contentHeight: CGFloat + var hideIcon = false + if case .compact = layout.metrics.heightClass, layout.size.width > layout.size.height { + hideIcon = true + contentHeight = radialStatusSize.height + maxProgressTextSpacing + progressTextSize.height + progressStatusSpacing + 100.0 + } else { + contentHeight = iconSize.height + maxIconStatusSpacing + radialStatusSize.height + maxProgressTextSpacing + progressTextSize.height + progressStatusSpacing + 100.0 + } + + transition.updateAlpha(node: self.animationNode, alpha: hideIcon ? 0.0 : 1.0) + + let contentOriginY = navigationHeight + floor((layout.size.height - contentHeight) / 2.0) + + self.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: contentOriginY), size: iconSize) + self.animationNode.updateLayout(size: iconSize) + + self.radialStatus.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - radialStatusSize.width) / 2.0), y: hideIcon ? contentOriginY : (contentOriginY + iconSize.height + maxIconStatusSpacing)), size: radialStatusSize) + self.radialStatusBackground.frame = self.radialStatus.frame + + self.radialStatusText.frame = CGRect(origin: CGPoint(x: self.radialStatus.frame.minX + floor((self.radialStatus.frame.width - radialStatusTextSize.width) / 2.0), y: self.radialStatus.frame.minY + floor((self.radialStatus.frame.height - radialStatusTextSize.height) / 2.0)), size: radialStatusTextSize) + + self.progressText.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - progressTextSize.width) / 2.0), y: self.radialStatus.frame.maxY + maxProgressTextSpacing), size: progressTextSize) + + if self.isDone { + self.statusText.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusTextSize.width) / 2.0), y: self.progressText.frame.minY), size: statusTextSize) + } else { + self.statusText.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusTextSize.width) / 2.0), y: self.progressText.frame.maxY + progressStatusSpacing), size: statusTextSize) + } + + let statusButtonTextFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusButtonTextSize.width) / 2.0), y: self.statusText.frame.maxY + statusButtonSpacing), size: statusButtonTextSize) + self.statusButtonText.frame = statusButtonTextFrame + self.statusButton.frame = statusButtonTextFrame.insetBy(dx: -10.0, dy: -10.0) + + self.statusButtonText.isHidden = !self.isDone + self.statusButton.isHidden = !self.isDone + self.progressText.isHidden = self.isDone + } + + func updateProgress(totalProgress: CGFloat, isDone: Bool, animated: Bool) { + self.totalProgress = totalProgress + self.isDone = isDone + + 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: self.totalProgress, cancelEnabled: false), animated: animated, synchronous: true, completion: {}) + } + } + } + + private var controllerNode: Node { + return self.displayNode as! Node + } + + private let context: AccountContext + private var presentationData: PresentationData + fileprivate let cancel: () -> Void + private let peerId: PeerId + private let archive: Archive + private let mainEntry: TempBoxFile + private let otherEntries: [(Entry, String, ChatHistoryImport.MediaType)] + + private var pendingEntries = Set() + + private let disposable = MetaDisposable() + + public init(context: AccountContext, cancel: @escaping () -> Void, peerId: PeerId, archive: Archive, mainEntry: TempBoxFile, otherEntries: [(Entry, String, ChatHistoryImport.MediaType)]) { + self.context = context + self.cancel = cancel + self.peerId = peerId + self.archive = archive + self.mainEntry = mainEntry + self.otherEntries = otherEntries + + self.pendingEntries = Set(otherEntries.map { $0.1 }) + + self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: true, hideBadge: true)) + + //TODO:localize + self.title = "Importing Chat" + + self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false) + + self.attemptNavigation = { _ in + return false + } + + self.beginImport() + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposable.dispose() + } + + @objc private func cancelPressed() { + self.cancel() + } + + override public func loadDisplayNode() { + var totalBytes: Int = 0 + if let size = fileSize(self.mainEntry.path) { + totalBytes += size + } + for entry in self.otherEntries { + totalBytes += entry.0.uncompressedSize + } + self.displayNode = Node(controller: self, context: self.context, totalBytes: totalBytes) + + self.displayNodeDidLoad() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition) + } + + private func beginImport() { + enum ImportError { + case generic + } + + let context = self.context + let archive = self.archive + let mainEntry = self.mainEntry + let otherEntries = self.otherEntries + + let resolvedPeerId: Signal + if self.peerId.namespace == Namespaces.Peer.CloudGroup { + resolvedPeerId = convertGroupToSupergroup(account: self.context.account, peerId: self.peerId) + |> mapError { _ -> ImportError in + return .generic + } + } else { + resolvedPeerId = .single(self.peerId) + } + + self.disposable.set((resolvedPeerId + |> mapToSignal { peerId -> Signal in + return ChatHistoryImport.initSession(account: context.account, peerId: peerId, file: mainEntry, mediaCount: Int32(otherEntries.count)) + |> mapError { _ -> ImportError in + return .generic + } + } + |> mapToSignal { session -> Signal in + var importSignal: Signal = .single("") + + for (entry, fileName, mediaType) in otherEntries { + let unpackedFile = Signal { subscriber in + let tempFile = TempBox.shared.tempFile(fileName: fileName) + do { + let _ = try archive.extract(entry, to: URL(fileURLWithPath: tempFile.path)) + subscriber.putNext(tempFile) + subscriber.putCompletion() + } catch { + subscriber.putError(.generic) + } + + return EmptyDisposable + } + let uploadedMedia = unpackedFile + |> mapToSignal { tempFile -> Signal in + return ChatHistoryImport.uploadMedia(account: context.account, session: session, file: tempFile, fileName: fileName, type: mediaType) + |> mapError { _ -> ImportError in + return .generic + } + |> map { _ -> String in + } + |> then(.single(fileName)) + } + + importSignal = importSignal + |> then(uploadedMedia) + } + + importSignal = importSignal + |> then(ChatHistoryImport.startImport(account: context.account, session: session) + |> mapError { _ -> ImportError in + return .generic + } + |> map { _ -> String in + }) + + return importSignal + } + |> deliverOnMainQueue).start(next: { [weak self] fileName in + guard let strongSelf = self else { + return + } + strongSelf.pendingEntries.remove(fileName) + var totalProgress: CGFloat = 1.0 + if !strongSelf.otherEntries.isEmpty { + totalProgress = CGFloat(strongSelf.otherEntries.count - strongSelf.pendingEntries.count) / CGFloat(strongSelf.otherEntries.count) + } + strongSelf.controllerNode.updateProgress(totalProgress: totalProgress, isDone: false, animated: true) + }, error: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.updateProgress(totalProgress: 0.0, isDone: false, animated: true) + }, completed: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.updateProgress(totalProgress: 1.0, isDone: true, animated: true) + })) + } +} diff --git a/submodules/Display/Source/WindowContent.swift b/submodules/Display/Source/WindowContent.swift index b5ae48c0fe..df3eec9703 100644 --- a/submodules/Display/Source/WindowContent.swift +++ b/submodules/Display/Source/WindowContent.swift @@ -3,9 +3,6 @@ import UIKit import AsyncDisplayKit import SwiftSignalKit -public func qewfqewfq() { -} - private struct WindowLayout: Equatable { let size: CGSize let metrics: LayoutMetrics diff --git a/submodules/TelegramCore/Sources/ChatHistoryImport.swift b/submodules/TelegramCore/Sources/ChatHistoryImport.swift index f367821351..75263434c1 100644 --- a/submodules/TelegramCore/Sources/ChatHistoryImport.swift +++ b/submodules/TelegramCore/Sources/ChatHistoryImport.swift @@ -123,4 +123,44 @@ public enum ChatHistoryImport { } } } + + public enum CheckPeerImportError { + case generic + case userIsNotMutualContact + } + + public static func checkPeerImport(account: Account, peerId: PeerId) -> Signal { + return account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + |> castError(CheckPeerImportError.self) + |> mapToSignal { peer -> Signal in + guard let peer = peer else { + return .fail(.generic) + } + if let inputUser = apiInputUser(peer) { + return account.network.request(Api.functions.users.getUsers(id: [inputUser])) + |> mapError { _ -> CheckPeerImportError in + return .generic + } + |> mapToSignal { result -> Signal in + guard let apiUser = result.first else { + return .fail(.generic) + } + switch apiUser { + case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _): + if (flags & (1 << 12)) == 0 { + // not mutual contact + return .fail(.userIsNotMutualContact) + } + return .complete() + case.userEmpty: + return .fail(.generic) + } + } + } else { + return .complete() + } + } + } } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 8a3eaa85ed..30e835f91f 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -214,6 +214,7 @@ swift_library( "//submodules/AudioBlob:AudioBlob", "//Telegram:GeneratedSources", "//third-party/ZIPFoundation:ZIPFoundation", + "//submodules/ChatImportUI:ChatImportUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Resources/Animations/HistoryImport.tgs b/submodules/TelegramUI/Resources/Animations/HistoryImport.tgs new file mode 100644 index 0000000000000000000000000000000000000000..e6988086789cb5b09deb80a63a8f7831a4c2b9d0 GIT binary patch literal 12545 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 Void)?, present: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) { + init(context: AccountContext, filter: ChatListNodePeersFilter, hasChatListSelector: Bool, hasContactSelector: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) { self.context = context self.present = present self.dismiss = dismiss @@ -67,7 +67,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - if hasContactSelector { + if hasChatListSelector && hasContactSelector { self.toolbarBackgroundNode = ASDisplayNode() self.toolbarBackgroundNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor @@ -145,7 +145,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } }) - if hasContactSelector { + if hasChatListSelector && hasContactSelector { self.segmentedControlNode!.selectedIndexChanged = { [weak self] index in self?.indexChanged(index) } @@ -155,6 +155,9 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.addSubnode(self.segmentedControlNode!) } + if !hasChatListSelector && hasContactSelector { + self.indexChanged(1) + } self.readyValue.set(self.chatListNode.ready) } @@ -316,10 +319,6 @@ final class PeerSelectionControllerNode: ASDisplayNode { } private func indexChanged(_ index: Int) { - guard let (layout, navigationHeight, actualNavigationHeight) = self.containerLayout else { - return - } - let contactListActive = index == 1 if contactListActive != self.contactListActive { self.contactListActive = contactListActive @@ -338,6 +337,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } contactListNode.openPeer = { [weak self] peer, _ in if case let .peer(peer, _, _) = peer { + self?.contactListNode?.listNode.clearHighlightAnimated(true) self?.requestOpenPeer?(peer) } } @@ -360,17 +360,26 @@ final class PeerSelectionControllerNode: ASDisplayNode { contactListNode.contentScrollingEnded = { [weak self] listView in return self?.contentScrollingEnded?(listView) ?? false } - self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, actualNavigationBarHeight: actualNavigationHeight, transition: .immediate) - let _ = (contactListNode.ready |> deliverOnMainQueue).start(next: { [weak self] _ in - if let strongSelf = self { - if let contactListNode = strongSelf.contactListNode { - strongSelf.insertSubnode(contactListNode, aboveSubnode: strongSelf.chatListNode) + if let (layout, navigationHeight, actualNavigationHeight) = self.containerLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, actualNavigationBarHeight: actualNavigationHeight, transition: .immediate) + + let _ = (contactListNode.ready |> deliverOnMainQueue).start(next: { [weak self] _ in + if let strongSelf = self { + if let contactListNode = strongSelf.contactListNode { + strongSelf.insertSubnode(contactListNode, aboveSubnode: strongSelf.chatListNode) + } + strongSelf.chatListNode.removeFromSupernode() + strongSelf.recursivelyEnsureDisplaySynchronously(true) } - strongSelf.chatListNode.removeFromSupernode() - strongSelf.recursivelyEnsureDisplaySynchronously(true) + }) + } else { + if let contactListNode = self.contactListNode { + self.insertSubnode(contactListNode, aboveSubnode: self.chatListNode) } - }) + self.chatListNode.removeFromSupernode() + self.recursivelyEnsureDisplaySynchronously(true) + } } } else if let contactListNode = self.contactListNode { contactListNode.enableUpdates = false diff --git a/submodules/TelegramUI/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Sources/ShareExtensionContext.swift index cbe65e2235..1f45384557 100644 --- a/submodules/TelegramUI/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Sources/ShareExtensionContext.swift @@ -20,445 +20,11 @@ import Intents import MobileCoreServices import OverlayStatusController import PresentationDataUtils - +import ChatImportUI import ZIPFoundation private let inForeground = ValuePromise(false, ignoreRepeated: true) -private final class LinearProgressNode: ASDisplayNode { - private let trackingNode: HierarchyTrackingNode - private let backgroundNode: ASImageNode - private let barNode: ASImageNode - private let shimmerNode: ASImageNode - private let shimmerClippingNode: ASDisplayNode - - private var currentProgress: CGFloat = 0.0 - private var currentProgressAnimation: (from: CGFloat, to: CGFloat, startTime: Double, completion: () -> Void)? - - private var shimmerPhase: CGFloat = 0.0 - - private var inHierarchyValue: Bool = false - private var shouldAnimate: Bool = false - - private let animator: ConstantDisplayLinkAnimator - - override init() { - var updateInHierarchy: ((Bool) -> Void)? - self.trackingNode = HierarchyTrackingNode { value in - updateInHierarchy?(value) - } - - var animationStep: (() -> Void)? - self.animator = ConstantDisplayLinkAnimator { - animationStep?() - } - - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - - self.barNode = ASImageNode() - self.barNode.isLayerBacked = true - - self.shimmerNode = ASImageNode() - self.shimmerNode.contentMode = .scaleToFill - self.shimmerClippingNode = ASDisplayNode() - self.shimmerClippingNode.clipsToBounds = true - - super.init() - - self.addSubnode(trackingNode) - self.addSubnode(self.backgroundNode) - self.addSubnode(self.barNode) - - self.shimmerClippingNode.addSubnode(self.shimmerNode) - self.addSubnode(self.shimmerClippingNode) - - updateInHierarchy = { [weak self] value in - guard let strongSelf = self else { - return - } - if strongSelf.inHierarchyValue != value { - strongSelf.inHierarchyValue = value - strongSelf.updateAnimations() - } - } - - animationStep = { [weak self] in - self?.update() - } - } - - func updateTheme(theme: PresentationTheme) { - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 3.0, color: theme.list.itemAccentColor.withMultipliedAlpha(0.2)) - self.barNode.image = generateStretchableFilledCircleImage(diameter: 3.0, color: theme.list.itemAccentColor) - self.shimmerNode.image = generateImage(CGSize(width: 100.0, height: 3.0), opaque: false, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - let foregroundColor = theme.list.plainBackgroundColor.withAlphaComponent(0.4) - - let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor - let peakColor = foregroundColor.cgColor - - var locations: [CGFloat] = [0.0, 0.5, 1.0] - let colors: [CGColor] = [transparentColor, peakColor, transparentColor] - - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! - - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) - }) - } - - func updateProgress(value: CGFloat, completion: @escaping () -> Void = {}) { - if self.currentProgress.isEqual(to: value) { - self.currentProgressAnimation = nil - completion() - } else { - if value.isEqual(to: 1.0) { - self.shimmerNode.alpha = 0.0 - } - self.currentProgressAnimation = (self.currentProgress, value, CACurrentMediaTime(), completion) - } - } - - private func updateAnimations() { - let shouldAnimate = self.inHierarchyValue - if shouldAnimate != self.shouldAnimate { - self.shouldAnimate = shouldAnimate - self.animator.isPaused = !shouldAnimate - } - } - - private func update() { - if let (fromValue, toValue, startTime, completion) = self.currentProgressAnimation { - let duration: Double = 0.15 - let timestamp = CACurrentMediaTime() - let t = CGFloat((timestamp - startTime) / duration) - if t >= 1.0 { - self.currentProgress = toValue - self.currentProgressAnimation = nil - completion() - } else { - let clippedT = max(0.0, t) - self.currentProgress = (1.0 - clippedT) * fromValue + clippedT * toValue - } - - var progressWidth: CGFloat = self.bounds.width * self.currentProgress - if progressWidth < 6.0 { - progressWidth = 0.0 - } - let progressFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: progressWidth, height: 3.0)) - self.barNode.frame = progressFrame - self.shimmerClippingNode.frame = progressFrame - } - self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.width, height: 3.0)) - - self.shimmerPhase += 3.5 - let shimmerWidth: CGFloat = 160.0 - let shimmerOffset = self.shimmerPhase.remainder(dividingBy: self.bounds.width + shimmerWidth / 2.0) - self.shimmerNode.frame = CGRect(origin: CGPoint(x: shimmerOffset - shimmerWidth / 2.0, y: 0.0), size: CGSize(width: shimmerWidth, height: 3.0)) - } -} - -private final class ChatImportProgressController: ViewController { - private final class Node: ViewControllerTracingNode { - private weak var controller: ChatImportProgressController? - - private let context: AccountContext - private var presentationData: PresentationData - - private let statusText: ImmediateTextNode - private let statusButtonText: ImmediateTextNode - private let statusButton: HighlightableButtonNode - - private let messagesProgressText: ImmediateTextNode - private let messagesProgressNode: LinearProgressNode - - private let mediaProgressText: ImmediateTextNode - private let mediaProgressNode: LinearProgressNode - - private var validLayout: (ContainerViewLayout, CGFloat)? - - private let mediaCount: Int - private var mediaProgress: Int - private var messagesProgress: CGFloat = 0.0 - private var isDone: Bool = false - - init(controller: ChatImportProgressController, context: AccountContext, mediaCount: Int) { - self.controller = controller - self.context = context - - self.mediaCount = mediaCount - self.mediaProgress = 0 - - self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - - self.messagesProgressText = ImmediateTextNode() - self.messagesProgressText.isUserInteractionEnabled = false - self.messagesProgressText.displaysAsynchronously = false - self.messagesProgressText.maximumNumberOfLines = 1 - self.messagesProgressText.isAccessibilityElement = false - - self.mediaProgressText = ImmediateTextNode() - self.mediaProgressText.isUserInteractionEnabled = false - self.mediaProgressText.displaysAsynchronously = false - self.mediaProgressText.maximumNumberOfLines = 1 - self.mediaProgressText.isAccessibilityElement = false - - self.statusText = ImmediateTextNode() - self.statusText.textAlignment = .center - self.statusText.isUserInteractionEnabled = false - self.statusText.displaysAsynchronously = false - self.statusText.maximumNumberOfLines = 0 - self.statusText.isAccessibilityElement = false - - self.statusButtonText = ImmediateTextNode() - self.statusButtonText.isUserInteractionEnabled = false - self.statusButtonText.displaysAsynchronously = false - self.statusButtonText.maximumNumberOfLines = 1 - self.statusButtonText.isAccessibilityElement = false - - self.statusButton = HighlightableButtonNode() - - self.messagesProgressNode = LinearProgressNode() - self.messagesProgressNode.updateTheme(theme: self.presentationData.theme) - - self.mediaProgressNode = LinearProgressNode() - self.mediaProgressNode.updateTheme(theme: self.presentationData.theme) - - super.init() - - self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor - - self.addSubnode(self.messagesProgressText) - self.addSubnode(self.messagesProgressNode) - self.addSubnode(self.mediaProgressText) - self.addSubnode(self.mediaProgressNode) - self.addSubnode(self.statusText) - self.addSubnode(self.statusButtonText) - self.addSubnode(self.statusButton) - - self.statusButton.addTarget(self, action: #selector(self.statusButtonPressed), forControlEvents: .touchUpInside) - self.statusButton.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.statusButtonText.layer.removeAnimation(forKey: "opacity") - strongSelf.statusButtonText.alpha = 0.4 - } else { - strongSelf.statusButtonText.alpha = 1.0 - strongSelf.statusButtonText.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - } - } - - @objc private func statusButtonPressed() { - self.controller?.cancel() - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.validLayout = (layout, navigationHeight) - - //TODO:localize - - self.messagesProgressText.attributedText = NSAttributedString(string: "Message Texts", font: Font.regular(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - let messagesProgressTextSize = self.messagesProgressText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude)) - - self.mediaProgressText.attributedText = NSAttributedString(string: "\(self.mediaProgress) media out of \(self.mediaCount)", font: Font.regular(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - let mediaProgressTextSize = self.mediaProgressText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude)) - - self.statusButtonText.attributedText = NSAttributedString(string: "Done", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemAccentColor) - let statusButtonTextSize = self.statusButtonText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude)) - - var statusTextOffset: CGFloat = 0.0 - let statusButtonSpacing: CGFloat = 10.0 - - if !self.isDone { - self.statusText.attributedText = NSAttributedString(string: "Please keep this window open\nduring the import.", font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) - } else { - self.statusText.attributedText = NSAttributedString(string: "This chat has been imported\nsuccessfully.", font: Font.semibold(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - statusTextOffset -= statusButtonTextSize.height - statusButtonSpacing - } - let statusTextSize = self.statusText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude)) - - let mediaProgressHeight: CGFloat = 4.0 - let progressSpacing: CGFloat = 16.0 - let sectionSpacing: CGFloat = 50.0 - - let contentOriginY = navigationHeight + floor((layout.size.height - navigationHeight - messagesProgressTextSize.height - progressSpacing - mediaProgressHeight - sectionSpacing - mediaProgressTextSize.height - progressSpacing - mediaProgressHeight) / 2.0) - - let messagesProgressTextFrame = CGRect(origin: CGPoint(x: 16.0, y: contentOriginY), size: messagesProgressTextSize) - self.messagesProgressText.frame = messagesProgressTextFrame - let messagesProgressFrame = CGRect(origin: CGPoint(x: 16.0, y: messagesProgressTextFrame.maxY + progressSpacing), size: CGSize(width: layout.size.width - 16.0 * 2.0, height: mediaProgressHeight)) - self.messagesProgressNode.frame = messagesProgressFrame - - let mediaProgressTextFrame = CGRect(origin: CGPoint(x: 16.0, y: messagesProgressFrame.maxY + sectionSpacing), size: mediaProgressTextSize) - self.mediaProgressText.frame = mediaProgressTextFrame - let mediaProgressFrame = CGRect(origin: CGPoint(x: 16.0, y: mediaProgressTextFrame.maxY + progressSpacing), size: CGSize(width: layout.size.width - 16.0 * 2.0, height: mediaProgressHeight)) - self.mediaProgressNode.frame = mediaProgressFrame - - let statusTextFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusTextSize.width) / 2.0), y: layout.size.height - layout.intrinsicInsets.bottom - 16.0 - statusTextSize.height + statusTextOffset), size: statusTextSize) - self.statusText.frame = statusTextFrame - - let statusButtonTextFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusButtonTextSize.width) / 2.0), y: statusTextFrame.maxY + statusButtonSpacing), size: statusButtonTextSize) - self.statusButtonText.frame = statusButtonTextFrame - - self.statusButtonText.isHidden = !self.isDone - self.statusButton.isHidden = !self.isDone - self.statusButton.frame = statusButtonTextFrame.insetBy(dx: -10.0, dy: -10.0) - } - - func updateProgress(mediaProgress: Int, messagesProgress: CGFloat, isDone: Bool, animated: Bool) { - self.mediaProgress = mediaProgress - self.isDone = isDone - - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate) - self.messagesProgressNode.updateProgress(value: messagesProgress) - self.mediaProgressNode.updateProgress(value: CGFloat(mediaProgress) / CGFloat(self.mediaCount)) - } - } - } - - private var controllerNode: Node { - return self.displayNode as! Node - } - - private let context: AccountContext - private var presentationData: PresentationData - let cancel: () -> Void - private let peerId: PeerId - private let archive: Archive - private let mainEntry: TempBoxFile - private let otherEntries: [(Entry, String, ChatHistoryImport.MediaType)] - - private var pendingEntries = Set() - - private let disposable = MetaDisposable() - - init(context: AccountContext, cancel: @escaping () -> Void, peerId: PeerId, archive: Archive, mainEntry: TempBoxFile, otherEntries: [(Entry, String, ChatHistoryImport.MediaType)]) { - self.context = context - self.cancel = cancel - self.peerId = peerId - self.archive = archive - self.mainEntry = mainEntry - self.otherEntries = otherEntries - - self.pendingEntries = Set(otherEntries.map { $0.1 }) - - self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) - - //TODO:localize - self.title = "Importing Chat" - - self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false) - - self.attemptNavigation = { _ in - return false - } - - self.beginImport() - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.disposable.dispose() - } - - @objc private func cancelPressed() { - self.cancel() - } - - override func loadDisplayNode() { - self.displayNode = Node(controller: self, context: self.context, mediaCount: self.otherEntries.count) - - self.displayNodeDidLoad() - } - - override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition) - } - - private func beginImport() { - enum ImportError { - case generic - } - - let context = self.context - let archive = self.archive - let otherEntries = self.otherEntries - self.disposable.set((ChatHistoryImport.initSession(account: self.context.account, peerId: self.peerId, file: self.mainEntry, mediaCount: Int32(otherEntries.count)) - |> mapError { _ -> ImportError in - return .generic - } - |> mapToSignal { session -> Signal in - var importSignal: Signal = .single("") - - for (entry, fileName, mediaType) in otherEntries { - let unpackedFile = Signal { subscriber in - let tempFile = TempBox.shared.tempFile(fileName: fileName) - do { - let _ = try archive.extract(entry, to: URL(fileURLWithPath: tempFile.path)) - subscriber.putNext(tempFile) - subscriber.putCompletion() - } catch { - subscriber.putError(.generic) - } - - return EmptyDisposable - } - let uploadedMedia = unpackedFile - |> mapToSignal { tempFile -> Signal in - return ChatHistoryImport.uploadMedia(account: context.account, session: session, file: tempFile, fileName: fileName, type: mediaType) - |> mapError { _ -> ImportError in - return .generic - } - |> map { _ -> String in - } - |> then(.single(fileName)) - } - - importSignal = importSignal - |> then(uploadedMedia) - } - - importSignal = importSignal - |> then(ChatHistoryImport.startImport(account: context.account, session: session) - |> mapError { _ -> ImportError in - return .generic - } - |> map { _ -> String in - }) - - return importSignal - } - |> deliverOnMainQueue).start(next: { [weak self] fileName in - guard let strongSelf = self else { - return - } - strongSelf.pendingEntries.remove(fileName) - strongSelf.controllerNode.updateProgress(mediaProgress: strongSelf.otherEntries.count - strongSelf.pendingEntries.count, messagesProgress: 1.0, isDone: false, animated: true) - }, error: { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.controllerNode.updateProgress(mediaProgress: 0, messagesProgress: 0.0, isDone: false, animated: true) - }, completed: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.controllerNode.updateProgress(mediaProgress: strongSelf.otherEntries.count, messagesProgress: 1.0, isDone: true, animated: true) - })) - } -} - private final class InternalContext { let sharedContext: SharedAccountContextImpl let wakeupManager: SharedWakeupManager @@ -847,6 +413,10 @@ public class ShareRootControllerImpl { let stickerRegex = try! NSRegularExpression(pattern: "[\\d]+-STICKER-.*?\\.webp") let voiceRegex = try! NSRegularExpression(pattern: "[\\d]+-AUDIO-.*?\\.opus") + let groupVerificationRegexList = [ + try! NSRegularExpression(pattern: "created this group"), + try! NSRegularExpression(pattern: "created group “(.*?)”"), + ] let groupCreationRegexList = [ try! NSRegularExpression(pattern: "created group “(.*?)”"), try! NSRegularExpression(pattern: "] (.*?): ‎Messages and calls are end-to-end encrypted") @@ -867,14 +437,23 @@ public class ShareRootControllerImpl { let _ = try archive.extract(entry, to: URL(fileURLWithPath: tempFile.path)) if let fileContents = try? String(contentsOfFile: tempFile.path) { let fullRange = NSRange(fileContents.startIndex ..< fileContents.endIndex, in: fileContents) - for regex in groupCreationRegexList { - if groupTitle != nil { + var isGroup = false + for regex in groupVerificationRegexList { + if let _ = regex.firstMatch(in: fileContents, options: [], range: fullRange) { + isGroup = true break } - if let match = regex.firstMatch(in: fileContents, options: [], range: fullRange) { - let range = match.range(at: 1) - if let mappedRange = Range(range, in: fileContents) { - groupTitle = String(fileContents[mappedRange]) + } + if isGroup { + for regex in groupCreationRegexList { + if groupTitle != nil { + break + } + if let match = regex.firstMatch(in: fileContents, options: [], range: fullRange) { + let range = match.range(at: 1) + if let mappedRange = Range(range, in: fileContents) { + groupTitle = String(fileContents[mappedRange]) + } } } } @@ -902,103 +481,173 @@ public class ShareRootControllerImpl { } } catch { } - if let mainFile = mainFile, let groupTitle = groupTitle { - let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 } - let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme)) - - //TODO:localize - var attemptSelectionImpl: ((Peer) -> Void)? - var createNewGroupImpl: (() -> Void)? - let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyGroups, .onlyManageable, .excludeDisabled], hasContactSelector: false, title: "Import Chat", attemptSelection: { peer in - attemptSelectionImpl?(peer) - }, createNewGroup: { - createNewGroupImpl?() - })) - - controller.peerSelected = { peer in - attemptSelectionImpl?(peer) - } - - controller.navigationPresentation = .default - - let beginWithPeer: (PeerId) -> Void = { peerId in - navigationController.pushViewController(ChatImportProgressController(context: context, cancel: { + if let mainFile = mainFile { + if let groupTitle = groupTitle { + let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 } + let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme)) + + //TODO:localize + var attemptSelectionImpl: ((Peer) -> Void)? + var createNewGroupImpl: (() -> Void)? + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyGroups, .onlyManageable, .excludeDisabled], hasContactSelector: false, title: "Import Chat", attemptSelection: { peer in + attemptSelectionImpl?(peer) + }, createNewGroup: { + createNewGroupImpl?() + })) + + controller.customDismiss = { self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) - }, peerId: peerId, archive: archive, mainEntry: mainFile, otherEntries: otherEntries)) - } - - attemptSelectionImpl = { peer in - 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 { - 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 } - let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Import Messages", text: "Are you sure you want to import messages from \(groupTitle) into \(peer.debugDisplayTitle)?", actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - }), TextAlertAction(type: .defaultAction, title: "Import", action: { - beginWithPeer(peer.id) - })]) - strongSelf.mainWindow?.present(controller, on: .root) + controller.peerSelected = { peer in + attemptSelectionImpl?(peer) } - } - - createNewGroupImpl = { - let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 } - let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Create Group and Import Messages", text: "Are you sure you want to create group \(groupTitle) and import messages from another messaging app?", actions: [TextAlertAction(type: .defaultAction, title: "Create and Import", action: { - var signal: Signal = createSupergroup(account: context.account, title: groupTitle, description: nil) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) + + controller.navigationPresentation = .default + + let beginWithPeer: (PeerId) -> Void = { peerId in + navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: { + self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) + }, peerId: peerId, archive: archive, mainEntry: mainFile, otherEntries: otherEntries)) + } + + attemptSelectionImpl = { peer in + 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 { + errorText = "You can't import history into this group." } - 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) + 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 } + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Import Messages", text: "Are you sure you want to import messages from **\(groupTitle)** into **\(peer.debugDisplayTitle)**?", 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 controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Create Group and Import Messages", text: "Are you sure you want to create group **\(groupTitle)** and import messages from another messaging app?", actions: [TextAlertAction(type: .defaultAction, title: "Create and Import", action: { + var signal: Signal = createSupergroup(account: context.account, title: groupTitle, description: nil) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) } - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() + + 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() + |> 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 + 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) + } else { + let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 } + let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme)) + + //TODO:localize + var attemptSelectionImpl: ((Peer) -> Void)? + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyPrivateChats, .excludeDisabled], hasChatListSelector: false, hasContactSelector: true, title: "Import Chat", attemptSelection: { peer in + attemptSelectionImpl?(peer) + })) + + 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.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 } + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Import Messages", text: "Are you sure you want to import messages into the chat with **\(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))**?", 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) }) - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - })]) - strongSelf.mainWindow?.present(controller, on: .root) + } + + navigationController.viewControllers = [controller] + strongSelf.mainWindow?.present(navigationController, on: .root) } - - navigationController.viewControllers = [controller] - strongSelf.mainWindow?.present(navigationController, on: .root) } else { beginShare() return