mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Chat import
This commit is contained in:
parent
347cc10a6e
commit
da2faa5967
@ -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))
|
||||
|
@ -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
|
||||
|
27
submodules/ChatImportUI/BUILD
Normal file
27
submodules/ChatImportUI/BUILD
Normal file
@ -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",
|
||||
],
|
||||
)
|
357
submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift
Normal file
357
submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift
Normal file
@ -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<String>()
|
||||
|
||||
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<PeerId, ImportError>
|
||||
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<ChatHistoryImport.Session, ImportError> in
|
||||
return ChatHistoryImport.initSession(account: context.account, peerId: peerId, file: mainEntry, mediaCount: Int32(otherEntries.count))
|
||||
|> mapError { _ -> ImportError in
|
||||
return .generic
|
||||
}
|
||||
}
|
||||
|> mapToSignal { session -> Signal<String, ImportError> in
|
||||
var importSignal: Signal<String, ImportError> = .single("")
|
||||
|
||||
for (entry, fileName, mediaType) in otherEntries {
|
||||
let unpackedFile = Signal<TempBoxFile, ImportError> { 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<String, ImportError> 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)
|
||||
}))
|
||||
}
|
||||
}
|
@ -3,9 +3,6 @@ import UIKit
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
public func qewfqewfq() {
|
||||
}
|
||||
|
||||
private struct WindowLayout: Equatable {
|
||||
let size: CGSize
|
||||
let metrics: LayoutMetrics
|
||||
|
@ -123,4 +123,44 @@ public enum ChatHistoryImport {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum CheckPeerImportError {
|
||||
case generic
|
||||
case userIsNotMutualContact
|
||||
}
|
||||
|
||||
public static func checkPeerImport(account: Account, peerId: PeerId) -> Signal<Never, CheckPeerImportError> {
|
||||
return account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(peerId)
|
||||
}
|
||||
|> castError(CheckPeerImportError.self)
|
||||
|> mapToSignal { peer -> Signal<Never, CheckPeerImportError> 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<Never, CheckPeerImportError> 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -214,6 +214,7 @@ swift_library(
|
||||
"//submodules/AudioBlob:AudioBlob",
|
||||
"//Telegram:GeneratedSources",
|
||||
"//third-party/ZIPFoundation:ZIPFoundation",
|
||||
"//submodules/ChatImportUI:ChatImportUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
BIN
submodules/TelegramUI/Resources/Animations/HistoryImport.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/HistoryImport.tgs
Normal file
Binary file not shown.
@ -54,6 +54,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
||||
return self._ready
|
||||
}
|
||||
|
||||
private let hasChatListSelector: Bool
|
||||
private let hasContactSelector: Bool
|
||||
|
||||
private var searchContentNode: NavigationBarSearchContentNode?
|
||||
@ -61,6 +62,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
||||
public init(_ params: PeerSelectionControllerParams) {
|
||||
self.context = params.context
|
||||
self.filter = params.filter
|
||||
self.hasChatListSelector = params.hasChatListSelector
|
||||
self.hasContactSelector = params.hasContactSelector
|
||||
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.attemptSelection = params.attemptSelection
|
||||
@ -124,7 +126,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = PeerSelectionControllerNode(context: self.context, filter: self.filter, hasContactSelector: hasContactSelector, createNewGroup: self.createNewGroup, present: { [weak self] c, a in
|
||||
self.displayNode = PeerSelectionControllerNode(context: self.context, filter: self.filter, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, createNewGroup: self.createNewGroup, present: { [weak self] c, a in
|
||||
self?.present(c, in: .window(.root), with: a)
|
||||
}, dismiss: { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
|
@ -59,7 +59,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
return self.readyValue.get()
|
||||
}
|
||||
|
||||
init(context: AccountContext, filter: ChatListNodePeersFilter, hasContactSelector: Bool, createNewGroup: (() -> 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
|
||||
|
@ -20,445 +20,11 @@ import Intents
|
||||
import MobileCoreServices
|
||||
import OverlayStatusController
|
||||
import PresentationDataUtils
|
||||
|
||||
import ChatImportUI
|
||||
import ZIPFoundation
|
||||
|
||||
private let inForeground = ValuePromise<Bool>(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<String>()
|
||||
|
||||
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<String, ImportError> in
|
||||
var importSignal: Signal<String, ImportError> = .single("")
|
||||
|
||||
for (entry, fileName, mediaType) in otherEntries {
|
||||
let unpackedFile = Signal<TempBoxFile, ImportError> { 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<String, ImportError> 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<PeerId?, NoError> = createSupergroup(account: context.account, title: groupTitle, description: nil)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<PeerId?, NoError> 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<Never, NoError> { 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<PeerId?, NoError> = createSupergroup(account: context.account, title: groupTitle, description: nil)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<PeerId?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
|
||||
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()
|
||||
|> 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user