Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
overtake 2021-01-21 13:48:44 +03:00
commit 5840b0828e
20 changed files with 4363 additions and 4209 deletions

View File

@ -1130,6 +1130,7 @@
"ShareFileTip.CloseTip" = "Close Tip"; "ShareFileTip.CloseTip" = "Close Tip";
"DialogList.SearchSectionDialogs" = "Chats and Contacts"; "DialogList.SearchSectionDialogs" = "Chats and Contacts";
"DialogList.SearchSectionChats" = "Chats";
"DialogList.SearchSectionGlobal" = "Global Search"; "DialogList.SearchSectionGlobal" = "Global Search";
"DialogList.SearchSectionMessages" = "Messages"; "DialogList.SearchSectionMessages" = "Messages";
@ -5900,3 +5901,5 @@ Sorry for the inconvenience.";
"DialogList.MultipleTypingPair" = "%@ and %@ are typing"; "DialogList.MultipleTypingPair" = "%@ and %@ are typing";
"Common.Save" = "Save"; "Common.Save" = "Save";
"ChatList.HeaderImportIntoAnExistingGroup" = "OR IMPORT INTO AN EXISTING GROUP";

View File

@ -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') project_path = os.path.join(build_environment.base_path, 'build-input/gen/project')
app_target = 'Telegram' 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) os.makedirs(project_path, exist_ok=True)
remove_directory('{}/Tulsi.app'.format(project_path)) remove_directory('{}/Tulsi.app'.format(project_path))
remove_directory('{project}/{target}.tulsiproj'.format(project=project_path, target=app_target)) remove_directory('{project}/{target}.tulsiproj'.format(project=project_path, target=app_target))

View File

@ -32,18 +32,24 @@ public struct ChatListNodePeersFilter: OptionSet {
public final class PeerSelectionControllerParams { public final class PeerSelectionControllerParams {
public let context: AccountContext public let context: AccountContext
public let filter: ChatListNodePeersFilter public let filter: ChatListNodePeersFilter
public let hasChatListSelector: Bool
public let hasContactSelector: Bool public let hasContactSelector: Bool
public let hasGlobalSearch: Bool
public let title: String? public let title: String?
public let attemptSelection: ((Peer) -> Void)? public let attemptSelection: ((Peer) -> Void)?
public let createNewGroup: (() -> Void)? public let createNewGroup: (() -> Void)?
public let pretendPresentedInModal: Bool
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, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false) {
self.context = context self.context = context
self.filter = filter self.filter = filter
self.hasChatListSelector = hasChatListSelector
self.hasContactSelector = hasContactSelector self.hasContactSelector = hasContactSelector
self.hasGlobalSearch = hasGlobalSearch
self.title = title self.title = title
self.attemptSelection = attemptSelection self.attemptSelection = attemptSelection
self.createNewGroup = createNewGroup self.createNewGroup = createNewGroup
self.pretendPresentedInModal = pretendPresentedInModal
} }
} }

View 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",
],
)

View File

@ -0,0 +1,364 @@
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: 186.0, height: 186.0)
let maxIconStatusSpacing: CGFloat = 62.0
let maxProgressTextSpacing: CGFloat = 33.0
let progressStatusSpacing: CGFloat = 14.0
let statusButtonSpacing: CGFloat = 19.0
self.radialStatusText.attributedText = NSAttributedString(string: "\(Int(self.totalProgress * 100.0))%", font: Font.with(size: 42.0, design: .round, weight: .semibold), 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()
override public var _presentedInModal: Bool {
get {
return true
} set(value) {
}
}
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)
}))
}
}

View File

@ -25,6 +25,7 @@ public enum ChatListSearchItemHeaderType {
case groupMembers case groupMembers
case activeVoiceChats case activeVoiceChats
case recentCalls case recentCalls
case orImportIntoAnExistingGroup
fileprivate func title(strings: PresentationStrings) -> String { fileprivate func title(strings: PresentationStrings) -> String {
switch self { switch self {
@ -68,6 +69,8 @@ public enum ChatListSearchItemHeaderType {
return strings.CallList_ActiveVoiceChatsHeader return strings.CallList_ActiveVoiceChatsHeader
case .recentCalls: case .recentCalls:
return strings.CallList_RecentCallsHeader return strings.CallList_RecentCallsHeader
case .orImportIntoAnExistingGroup:
return strings.ChatList_HeaderImportIntoAnExistingGroup
} }
} }
@ -113,6 +116,8 @@ public enum ChatListSearchItemHeaderType {
return .activeVoiceChats return .activeVoiceChats
case .recentCalls: case .recentCalls:
return .recentCalls return .recentCalls
case .orImportIntoAnExistingGroup:
return .orImportIntoAnExistingGroup
} }
} }
} }
@ -142,6 +147,7 @@ private enum ChatListSearchItemHeaderId: Int32 {
case groupMembers case groupMembers
case activeVoiceChats case activeVoiceChats
case recentCalls case recentCalls
case orImportIntoAnExistingGroup
} }
public final class ChatListSearchItemHeader: ListViewItemHeader { public final class ChatListSearchItemHeader: ListViewItemHeader {

View File

@ -429,7 +429,13 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case .collapse: case .collapse:
actionTitle = strings.ChatList_Search_ShowLess actionTitle = strings.ChatList_Search_ShowLess
} }
header = ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings, actionTitle: actionTitle, action: actionTitle == nil ? nil : { let headerType: ChatListSearchItemHeaderType
if filter.contains(.onlyGroups) {
headerType = .chats
} else {
headerType = .localPeers
}
header = ChatListSearchItemHeader(type: headerType, theme: theme, strings: strings, actionTitle: actionTitle, action: actionTitle == nil ? nil : {
toggleExpandLocalResults() toggleExpandLocalResults()
}) })
} }

View File

@ -250,7 +250,14 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
switch mode { switch mode {
case let .peers(_, _, additionalCategories, _): case let .peers(_, _, additionalCategories, _):
if !additionalCategories.isEmpty { if !additionalCategories.isEmpty {
header = ChatListSearchItemHeader(type: .chats, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) let headerType: ChatListSearchItemHeaderType
if case .action = additionalCategories[0].appearance {
// TODO: hack, generalize
headerType = .orImportIntoAnExistingGroup
} else {
headerType = .chats
}
header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
} }
default: default:
break break
@ -320,7 +327,14 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
switch mode { switch mode {
case let .peers(_, _, additionalCategories, _): case let .peers(_, _, additionalCategories, _):
if !additionalCategories.isEmpty { if !additionalCategories.isEmpty {
header = ChatListSearchItemHeader(type: .chats, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) let headerType: ChatListSearchItemHeaderType
if case .action = additionalCategories[0].appearance {
// TODO: hack, generalize
headerType = .orImportIntoAnExistingGroup
} else {
headerType = .chats
}
header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
} }
default: default:
break break

View File

@ -33,7 +33,7 @@ public struct Font {
case bold case bold
} }
public static func with(size: CGFloat, design: Design = .regular, traits: Traits = []) -> UIFont { public static func with(size: CGFloat, design: Design = .regular, weight: Weight = .regular, traits: Traits = []) -> UIFont {
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {
let descriptor = UIFont.systemFont(ofSize: size).fontDescriptor let descriptor = UIFont.systemFont(ofSize: size).fontDescriptor
var symbolicTraits = descriptor.symbolicTraits var symbolicTraits = descriptor.symbolicTraits
@ -63,6 +63,15 @@ public struct Font {
default: default:
updatedDescriptor = updatedDescriptor?.withDesign(.default) updatedDescriptor = updatedDescriptor?.withDesign(.default)
} }
switch weight {
case .semibold:
let fontTraits = [UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold]
updatedDescriptor = updatedDescriptor?.addingAttributes([
UIFontDescriptor.AttributeName.traits: fontTraits
])
default:
break
}
if let updatedDescriptor = updatedDescriptor { if let updatedDescriptor = updatedDescriptor {
return UIFont(descriptor: updatedDescriptor, size: size) return UIFont(descriptor: updatedDescriptor, size: size)
} else { } else {

View File

@ -139,7 +139,7 @@ public enum TabBarItemContextActionType {
} }
open var navigationPresentation: ViewControllerNavigationPresentation = .default open var navigationPresentation: ViewControllerNavigationPresentation = .default
var _presentedInModal: Bool = false open var _presentedInModal: Bool = false
public var presentedOverCoveringView: Bool = false public var presentedOverCoveringView: Bool = false

View File

@ -3,9 +3,6 @@ import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import SwiftSignalKit import SwiftSignalKit
public func qewfqewfq() {
}
private struct WindowLayout: Equatable { private struct WindowLayout: Equatable {
let size: CGSize let size: CGSize
let metrics: LayoutMetrics let metrics: LayoutMetrics

View File

@ -12,7 +12,7 @@ public enum CreateChannelError {
case serverProvided(String) case serverProvided(String)
} }
private func createChannel(account: Account, title: String, description: String?, isSupergroup:Bool, location: (latitude: Double, longitude: Double, address: String)? = nil) -> Signal<PeerId, CreateChannelError> { private func createChannel(account: Account, title: String, description: String?, isSupergroup:Bool, location: (latitude: Double, longitude: Double, address: String)? = nil, isForHistoryImport: Bool = false) -> Signal<PeerId, CreateChannelError> {
return account.postbox.transaction { transaction -> Signal<PeerId, CreateChannelError> in return account.postbox.transaction { transaction -> Signal<PeerId, CreateChannelError> in
var flags: Int32 = 0 var flags: Int32 = 0
if isSupergroup { if isSupergroup {
@ -20,6 +20,9 @@ private func createChannel(account: Account, title: String, description: String?
} else { } else {
flags |= (1 << 0) flags |= (1 << 0)
} }
if isForHistoryImport {
flags |= (1 << 3)
}
var geoPoint: Api.InputGeoPoint? var geoPoint: Api.InputGeoPoint?
var address: String? var address: String?
@ -69,8 +72,8 @@ public func createChannel(account: Account, title: String, description: String?)
return createChannel(account: account, title: title, description: description, isSupergroup: false) return createChannel(account: account, title: title, description: description, isSupergroup: false)
} }
public func createSupergroup(account: Account, title: String, description: String?, location: (latitude: Double, longitude: Double, address: String)? = nil) -> Signal<PeerId, CreateChannelError> { public func createSupergroup(account: Account, title: String, description: String?, location: (latitude: Double, longitude: Double, address: String)? = nil, isForHistoryImport: Bool = false) -> Signal<PeerId, CreateChannelError> {
return createChannel(account: account, title: title, description: description, isSupergroup: true, location: location) return createChannel(account: account, title: title, description: description, isSupergroup: true, location: location, isForHistoryImport: isForHistoryImport)
} }
public enum DeleteChannelError { public enum DeleteChannelError {
@ -81,7 +84,7 @@ public func deleteChannel(account: Account, peerId: PeerId) -> Signal<Void, Dele
return account.postbox.transaction { transaction -> Api.InputChannel? in return account.postbox.transaction { transaction -> Api.InputChannel? in
return transaction.getPeer(peerId).flatMap(apiInputChannel) return transaction.getPeer(peerId).flatMap(apiInputChannel)
} }
|> mapError { _ -> DeleteChannelError in return .generic } |> mapError { _ -> DeleteChannelError in }
|> mapToSignal { inputChannel -> Signal<Void, DeleteChannelError> in |> mapToSignal { inputChannel -> Signal<Void, DeleteChannelError> in
if let inputChannel = inputChannel { if let inputChannel = inputChannel {
return account.network.request(Api.functions.channels.deleteChannel(channel: inputChannel)) return account.network.request(Api.functions.channels.deleteChannel(channel: inputChannel))

View File

@ -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()
}
}
}
} }

View File

@ -214,6 +214,7 @@ swift_library(
"//submodules/AudioBlob:AudioBlob", "//submodules/AudioBlob:AudioBlob",
"//Telegram:GeneratedSources", "//Telegram:GeneratedSources",
"//third-party/ZIPFoundation:ZIPFoundation", "//third-party/ZIPFoundation:ZIPFoundation",
"//submodules/ChatImportUI:ChatImportUI",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -54,17 +54,37 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
return self._ready return self._ready
} }
private let hasChatListSelector: Bool
private let hasContactSelector: Bool private let hasContactSelector: Bool
private let hasGlobalSearch: Bool
private let pretendPresentedInModal: Bool
override public var _presentedInModal: Bool {
get {
if self.pretendPresentedInModal {
return true
} else {
return super._presentedInModal
}
} set(value) {
if !self.pretendPresentedInModal {
super._presentedInModal = value
}
}
}
private var searchContentNode: NavigationBarSearchContentNode? private var searchContentNode: NavigationBarSearchContentNode?
public init(_ params: PeerSelectionControllerParams) { public init(_ params: PeerSelectionControllerParams) {
self.context = params.context self.context = params.context
self.filter = params.filter self.filter = params.filter
self.hasChatListSelector = params.hasChatListSelector
self.hasContactSelector = params.hasContactSelector self.hasContactSelector = params.hasContactSelector
self.hasGlobalSearch = params.hasGlobalSearch
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.attemptSelection = params.attemptSelection self.attemptSelection = params.attemptSelection
self.createNewGroup = params.createNewGroup self.createNewGroup = params.createNewGroup
self.pretendPresentedInModal = params.pretendPresentedInModal
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
@ -124,7 +144,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
} }
override public func loadDisplayNode() { 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, hasGlobalSearch: self.hasGlobalSearch, createNewGroup: self.createNewGroup, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a) self?.present(c, in: .window(.root), with: a)
}, dismiss: { [weak self] in }, dismiss: { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil) self?.presentingViewController?.dismiss(animated: false, completion: nil)

View File

@ -19,6 +19,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
private let present: (ViewController, Any?) -> Void private let present: (ViewController, Any?) -> Void
private let dismiss: () -> Void private let dismiss: () -> Void
private let filter: ChatListNodePeersFilter private let filter: ChatListNodePeersFilter
private let hasGlobalSearch: Bool
var inProgress: Bool = false { var inProgress: Bool = false {
didSet { didSet {
@ -59,15 +60,16 @@ final class PeerSelectionControllerNode: ASDisplayNode {
return self.readyValue.get() 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, hasGlobalSearch: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) {
self.context = context self.context = context
self.present = present self.present = present
self.dismiss = dismiss self.dismiss = dismiss
self.filter = filter self.filter = filter
self.hasGlobalSearch = hasGlobalSearch
self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
if hasContactSelector { if hasChatListSelector && hasContactSelector {
self.toolbarBackgroundNode = ASDisplayNode() self.toolbarBackgroundNode = ASDisplayNode()
self.toolbarBackgroundNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor self.toolbarBackgroundNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor
@ -145,7 +147,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
} }
}) })
if hasContactSelector { if hasChatListSelector && hasContactSelector {
self.segmentedControlNode!.selectedIndexChanged = { [weak self] index in self.segmentedControlNode!.selectedIndexChanged = { [weak self] index in
self?.indexChanged(index) self?.indexChanged(index)
} }
@ -155,6 +157,9 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.addSubnode(self.segmentedControlNode!) self.addSubnode(self.segmentedControlNode!)
} }
if !hasChatListSelector && hasContactSelector {
self.indexChanged(1)
}
self.readyValue.set(self.chatListNode.ready) self.readyValue.set(self.chatListNode.ready)
} }
@ -261,7 +266,11 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}, placeholder: placeholderNode) }, placeholder: placeholderNode)
} else if let contactListNode = self.contactListNode, contactListNode.supernode != nil { } else if let contactListNode = self.contactListNode, contactListNode.supernode != nil {
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: true, categories: [.cloudContacts, .global], addContact: nil, openPeer: { [weak self] peer in var categories: ContactsSearchCategories = [.cloudContacts]
if self.hasGlobalSearch {
categories.insert(.global)
}
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: true, categories: categories, addContact: nil, openPeer: { [weak self] peer in
if let strongSelf = self { if let strongSelf = self {
switch peer { switch peer {
case let .peer(peer, _, _): case let .peer(peer, _, _):
@ -316,10 +325,6 @@ final class PeerSelectionControllerNode: ASDisplayNode {
} }
private func indexChanged(_ index: Int) { private func indexChanged(_ index: Int) {
guard let (layout, navigationHeight, actualNavigationHeight) = self.containerLayout else {
return
}
let contactListActive = index == 1 let contactListActive = index == 1
if contactListActive != self.contactListActive { if contactListActive != self.contactListActive {
self.contactListActive = contactListActive self.contactListActive = contactListActive
@ -338,6 +343,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
} }
contactListNode.openPeer = { [weak self] peer, _ in contactListNode.openPeer = { [weak self] peer, _ in
if case let .peer(peer, _, _) = peer { if case let .peer(peer, _, _) = peer {
self?.contactListNode?.listNode.clearHighlightAnimated(true)
self?.requestOpenPeer?(peer) self?.requestOpenPeer?(peer)
} }
} }
@ -360,17 +366,26 @@ final class PeerSelectionControllerNode: ASDisplayNode {
contactListNode.contentScrollingEnded = { [weak self] listView in contactListNode.contentScrollingEnded = { [weak self] listView in
return self?.contentScrollingEnded?(listView) ?? false 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 (layout, navigationHeight, actualNavigationHeight) = self.containerLayout {
if let strongSelf = self { self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, actualNavigationBarHeight: actualNavigationHeight, transition: .immediate)
if let contactListNode = strongSelf.contactListNode {
strongSelf.insertSubnode(contactListNode, aboveSubnode: strongSelf.chatListNode) 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 { } else if let contactListNode = self.contactListNode {
contactListNode.enableUpdates = false contactListNode.enableUpdates = false

View File

@ -20,445 +20,11 @@ import Intents
import MobileCoreServices import MobileCoreServices
import OverlayStatusController import OverlayStatusController
import PresentationDataUtils import PresentationDataUtils
import ChatImportUI
import ZIPFoundation import ZIPFoundation
private let inForeground = ValuePromise<Bool>(false, ignoreRepeated: true) 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 { private final class InternalContext {
let sharedContext: SharedAccountContextImpl let sharedContext: SharedAccountContextImpl
let wakeupManager: SharedWakeupManager let wakeupManager: SharedWakeupManager
@ -847,6 +413,10 @@ public class ShareRootControllerImpl {
let stickerRegex = try! NSRegularExpression(pattern: "[\\d]+-STICKER-.*?\\.webp") let stickerRegex = try! NSRegularExpression(pattern: "[\\d]+-STICKER-.*?\\.webp")
let voiceRegex = try! NSRegularExpression(pattern: "[\\d]+-AUDIO-.*?\\.opus") let voiceRegex = try! NSRegularExpression(pattern: "[\\d]+-AUDIO-.*?\\.opus")
let groupVerificationRegexList = [
try! NSRegularExpression(pattern: "created this group"),
try! NSRegularExpression(pattern: "created group “(.*?)”"),
]
let groupCreationRegexList = [ let groupCreationRegexList = [
try! NSRegularExpression(pattern: "created group “(.*?)”"), try! NSRegularExpression(pattern: "created group “(.*?)”"),
try! NSRegularExpression(pattern: "] (.*?): Messages and calls are end-to-end encrypted") 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)) let _ = try archive.extract(entry, to: URL(fileURLWithPath: tempFile.path))
if let fileContents = try? String(contentsOfFile: tempFile.path) { if let fileContents = try? String(contentsOfFile: tempFile.path) {
let fullRange = NSRange(fileContents.startIndex ..< fileContents.endIndex, in: fileContents) let fullRange = NSRange(fileContents.startIndex ..< fileContents.endIndex, in: fileContents)
for regex in groupCreationRegexList { var isGroup = false
if groupTitle != nil { for regex in groupVerificationRegexList {
if let _ = regex.firstMatch(in: fileContents, options: [], range: fullRange) {
isGroup = true
break break
} }
if let match = regex.firstMatch(in: fileContents, options: [], range: fullRange) { }
let range = match.range(at: 1) if isGroup {
if let mappedRange = Range(range, in: fileContents) { for regex in groupCreationRegexList {
groupTitle = String(fileContents[mappedRange]) 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 { } catch {
} }
if let mainFile = mainFile, let groupTitle = groupTitle { if let mainFile = mainFile {
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 } if let groupTitle = groupTitle {
let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme)) let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme))
//TODO:localize
var attemptSelectionImpl: ((Peer) -> Void)? //TODO:localize
var createNewGroupImpl: (() -> Void)? var attemptSelectionImpl: ((Peer) -> Void)?
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyGroups, .onlyManageable, .excludeDisabled], hasContactSelector: false, title: "Import Chat", attemptSelection: { peer in var createNewGroupImpl: (() -> Void)?
attemptSelectionImpl?(peer) let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyGroups, .onlyManageable, .excludeDisabled, .doNotSearchMessages], hasContactSelector: false, hasGlobalSearch: false, title: "Import Chat", attemptSelection: { peer in
}, createNewGroup: { attemptSelectionImpl?(peer)
createNewGroupImpl?() }, createNewGroup: {
})) createNewGroupImpl?()
}, pretendPresentedInModal: true))
controller.peerSelected = { peer in
attemptSelectionImpl?(peer) controller.customDismiss = {
}
controller.navigationPresentation = .default
let beginWithPeer: (PeerId) -> Void = { peerId in
navigationController.pushViewController(ChatImportProgressController(context: context, cancel: {
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) 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 { controller.peerSelected = { peer in
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 } attemptSelectionImpl?(peer)
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.navigationPresentation = .default
createNewGroupImpl = {
let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 } let beginWithPeer: (PeerId) -> Void = { peerId in
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: { navigationController.pushViewController(ChatImportActivityScreen(context: context, cancel: {
var signal: Signal<PeerId?, NoError> = createSupergroup(account: context.account, title: groupTitle, description: nil) self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
|> map(Optional.init) }, peerId: peerId, archive: archive, mainEntry: mainFile, otherEntries: otherEntries))
|> `catch` { _ -> Signal<PeerId?, NoError> in }
return .single(nil)
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 } if let errorText = errorText {
let progressSignal = Signal<Never, NoError> { subscriber in let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
if let strongSelf = self { })])
strongSelf.mainWindow?.present(controller, on: .root) 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, isForHistoryImport: true)
|> map(Optional.init)
|> `catch` { _ -> Signal<PeerId?, NoError> in
return .single(nil)
} }
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() { let presentationData = context.sharedContext.currentPresentationData.with { $0 }
controller?.dismiss() 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())
|> runOn(Queue.mainQueue()) |> delay(0.15, queue: Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start()
let progressDisposable = progressSignal.start()
signal = signal
signal = signal |> afterDisposed {
|> afterDisposed { Queue.mainQueue().async {
Queue.mainQueue().async { progressDisposable.dispose()
progressDisposable.dispose() }
} }
} let _ = (signal
let _ = (signal |> deliverOnMainQueue).start(next: { peerId in
|> deliverOnMainQueue).start(next: { peerId in if let peerId = peerId {
if let peerId = peerId { beginWithPeer(peerId)
beginWithPeer(peerId) } else {
} else { //TODO:localize
//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, .doNotSearchMessages], hasChatListSelector: false, hasContactSelector: true, hasGlobalSearch: false, title: "Import Chat", attemptSelection: { peer in
attemptSelectionImpl?(peer)
}, pretendPresentedInModal: true))
controller.customDismiss = {
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
}
controller.peerSelected = { peer in
attemptSelectionImpl?(peer)
}
controller.navigationPresentation = .default
let beginWithPeer: (PeerId) -> Void = { peerId in
navigationController.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 { } else {
beginShare() beginShare()
return return