Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SettingsUI",
module_name = "SettingsUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/MtProtoKit:MtProtoKit",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/ActivityIndicator:ActivityIndicator",
"//submodules/AlertUI:AlertUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/AvatarNode:AvatarNode",
"//submodules/CallListUI:CallListUI",
"//submodules/ChatListSearchItemNode:ChatListSearchItemNode",
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
"//submodules/ChatListUI:ChatListUI",
"//submodules/ContactsPeerItem:ContactsPeerItem",
"//submodules/CountrySelectionUI:CountrySelectionUI",
"//submodules/DeviceAccess:DeviceAccess",
"//submodules/DeviceLocationManager:DeviceLocationManager",
"//submodules/GalleryUI:GalleryUI",
"//submodules/Geocoding:Geocoding",
"//submodules/ItemListUI:ItemListUI",
"//submodules/ItemListStickerPackItem:ItemListStickerPackItem",
"//submodules/ItemListPeerItem:ItemListPeerItem",
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
"//submodules/ItemListAvatarAndNameInfoItem:ItemListAvatarAndNameInfoItem",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/LegacyUI:LegacyUI",
"//submodules/LegacyMediaPickerUI:LegacyMediaPickerUI",
"//submodules/ListSectionHeaderNode:ListSectionHeaderNode",
"//submodules/LocalMediaResources:LocalMediaResources",
"//submodules/LocalizedPeerData:LocalizedPeerData",
"//submodules/LocalAuth:LocalAuth",
"//submodules/MapResourceToAvatarSizes:MapResourceToAvatarSizes",
"//submodules/MediaResources:MediaResources",
"//submodules/MergeLists:MergeLists",
"//submodules/NotificationSoundSelectionUI:NotificationSoundSelectionUI",
"//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/PasswordSetupUI:PasswordSetupUI",
"//submodules/PassportUI:PassportUI",
"//submodules/PasscodeUI:PasscodeUI",
"//submodules/PeerAvatarGalleryUI:PeerAvatarGalleryUI",
"//submodules/PhoneInputNode:PhoneInputNode",
"//submodules/PhotoResources:PhotoResources",
"//submodules/ProgressNavigationButtonNode:ProgressNavigationButtonNode",
"//submodules/RadialStatusNode:RadialStatusNode",
"//submodules/SearchBarNode:SearchBarNode",
"//submodules/SearchUI:SearchUI",
"//submodules/ShareController:ShareController",
"//submodules/StickerPackPreviewUI:StickerPackPreviewUI",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/TelegramNotices:TelegramNotices",
"//submodules/TelegramCallsUI:TelegramCallsUI",
"//submodules/TextFormat:TextFormat",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/UrlEscaping:UrlEscaping",
"//submodules/WebSearchUI:WebSearchUI",
"//submodules/UrlHandling:UrlHandling",
"//submodules/HexColor:HexColor",
"//submodules/QrCode:QrCode",
"//submodules/WallpaperResources:WallpaperResources",
"//submodules/AuthorizationUtils:AuthorizationUtils",
"//submodules/AuthorizationUI:AuthorizationUI",
"//submodules/InstantPageUI:InstantPageUI",
"//submodules/CheckNode:CheckNode",
"//submodules/CounterControllerTitleView:CounterControllerTitleView",
"//submodules/GridMessageSelectionNode:GridMessageSelectionNode",
"//submodules/InstantPageCache:InstantPageCache",
"//submodules/AppBundle:AppBundle",
"//submodules/ContextUI:ContextUI",
"//submodules/Markdown:Markdown",
"//submodules/UndoUI:UndoUI",
"//submodules/DeleteChatPeerActionSheetItem:DeleteChatPeerActionSheetItem",
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
"//submodules/OpenInExternalAppUI:OpenInExternalAppUI",
"//submodules/AccountUtils:AccountUtils",
"//submodules/UIKitRuntimeUtils:UIKitRuntimeUtils",
"//submodules/DebugSettingsUI:DebugSettingsUI",
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
"//submodules/WebPBinding:WebPBinding",
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
"//submodules/TranslateUI:TranslateUI",
"//submodules/QrCodeUI:QrCodeUI",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/FetchManagerImpl:FetchManagerImpl",
"//submodules/ListMessageItem:ListMessageItem",
"//submodules/PaymentMethodUI:PaymentMethodUI",
"//submodules/PremiumUI:PremiumUI",
"//submodules/InviteLinksUI:InviteLinksUI",
"//submodules/HorizontalPeerItem:HorizontalPeerItem",
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
"//submodules/PersistentStringHash:PersistentStringHash",
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
"//submodules/TelegramUI/Components/ChatTimerScreen",
"//submodules/AnimatedAvatarSetNode",
"//submodules/TelegramUI/Components/StorageUsageScreen",
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
"//submodules/MediaPickerUI:MediaPickerUI",
"//submodules/ImageBlur:ImageBlur",
"//submodules/AttachmentUI:AttachmentUI",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen",
"//submodules/TelegramUI/Components/PeerInfo/MessagePriceItem",
"//submodules/TelegramUI/Components/Settings/PeerNameColorScreen",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/TelegramUI/Components/Settings/QuickReactionSetupController",
"//submodules/TelegramUI/Components/Settings/ThemeCarouselItem",
"//submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem",
"//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen",
"//submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode",
"//submodules/TelegramUI/Components/Settings/WallpaperGridScreen",
"//submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen",
"//submodules/TelegramUI/Components/Settings/GenerateThemeName",
"//submodules/TelegramUI/Components/Settings/PeerNameColorItem",
"//submodules/TelegramUI/Components/Settings/PasskeysScreen",
"//submodules/TelegramUI/Components/FaceScanScreen",
"//submodules/ComponentFlow",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/SliderComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,216 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import UndoUI
private final class ArchiveSettingsControllerArguments {
let updateUnmuted: (Bool) -> Void
let updateFolders: (Bool) -> Void
let updateUnknown: (Bool?) -> Void
init(
updateUnmuted: @escaping (Bool) -> Void,
updateFolders: @escaping (Bool) -> Void,
updateUnknown: @escaping (Bool?) -> Void
) {
self.updateUnmuted = updateUnmuted
self.updateFolders = updateFolders
self.updateUnknown = updateUnknown
}
}
private enum ArchiveSettingsSection: Int32 {
case unmuted
case folders
case unknown
}
private enum ArchiveSettingsControllerEntry: ItemListNodeEntry {
case unmutedHeader
case unmutedValue(Bool)
case unmutedFooter
case foldersHeader
case foldersValue(Bool)
case foldersFooter
case unknownHeader
case unknownValue(isOn: Bool, isLocked: Bool)
case unknownFooter
var section: ItemListSectionId {
switch self {
case .unmutedHeader, .unmutedValue, .unmutedFooter:
return ArchiveSettingsSection.unmuted.rawValue
case .foldersHeader, .foldersValue, .foldersFooter:
return ArchiveSettingsSection.folders.rawValue
case .unknownHeader, .unknownValue, .unknownFooter:
return ArchiveSettingsSection.unknown.rawValue
}
}
var stableId: Int32 {
switch self {
case .unmutedHeader:
return 0
case .unmutedValue:
return 1
case .unmutedFooter:
return 2
case .foldersHeader:
return 3
case .foldersValue:
return 4
case .foldersFooter:
return 5
case .unknownHeader:
return 6
case .unknownValue:
return 7
case .unknownFooter:
return 8
}
}
static func <(lhs: ArchiveSettingsControllerEntry, rhs: ArchiveSettingsControllerEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ArchiveSettingsControllerArguments
switch self {
case .unmutedHeader:
return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ArchiveSettings_UnmutedChatsHeader, sectionId: self.section)
case let .unmutedValue(value):
return ItemListSwitchItem(presentationData: presentationData, title: presentationData.strings.ArchiveSettings_KeepArchived, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateUnmuted(value)
})
case .unmutedFooter:
return ItemListTextItem(presentationData: presentationData, text: .markdown(presentationData.strings.ArchiveSettings_UnmutedChatsFooter), sectionId: self.section)
case .foldersHeader:
return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ArchiveSettings_FolderChatsHeader, sectionId: self.section)
case let .foldersValue(value):
return ItemListSwitchItem(presentationData: presentationData, title: presentationData.strings.ArchiveSettings_KeepArchived, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateFolders(value)
})
case .foldersFooter:
return ItemListTextItem(presentationData: presentationData, text: .markdown(presentationData.strings.ArchiveSettings_FolderChatsFooter), sectionId: self.section)
case .unknownHeader:
return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ArchiveSettings_UnknownChatsHeader, sectionId: self.section)
case let .unknownValue(isOn, isLocked):
return ItemListSwitchItem(presentationData: presentationData, title: presentationData.strings.ArchiveSettings_AutomaticallyArchive, value: isOn, enableInteractiveChanges: !isLocked, enabled: true, displayLocked: isLocked, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateUnknown(value)
}, activatedWhileDisabled: {
arguments.updateUnknown(nil)
})
case .unknownFooter:
return ItemListTextItem(presentationData: presentationData, text: .markdown(presentationData.strings.ArchiveSettings_UnknownChatsFooter), sectionId: self.section)
}
}
}
private func archiveSettingsControllerEntries(
presentationData: PresentationData,
settings: GlobalPrivacySettings,
isPremium: Bool,
isPremiumEnabled: Bool
) -> [ArchiveSettingsControllerEntry] {
var entries: [ArchiveSettingsControllerEntry] = []
entries.append(.unmutedHeader)
entries.append(.unmutedValue(settings.keepArchivedUnmuted))
entries.append(.unmutedFooter)
if !settings.keepArchivedUnmuted {
entries.append(.foldersHeader)
entries.append(.foldersValue(settings.keepArchivedFolders))
entries.append(.foldersFooter)
}
if isPremium || isPremiumEnabled {
entries.append(.unknownHeader)
entries.append(.unknownValue(isOn: isPremium && settings.automaticallyArchiveAndMuteNonContacts, isLocked: !isPremium))
entries.append(.unknownFooter)
}
return entries
}
public func archiveSettingsController(context: AccountContext) -> ViewController {
let updateDisposable = MetaDisposable()
updateDisposable.set(context.engine.privacy.requestAccountPrivacySettings().start())
var presentUndoImpl: ((UndoOverlayContent) -> Void)?
var presentPremiumImpl: (() -> Void)?
let arguments = ArchiveSettingsControllerArguments(
updateUnmuted: { value in
let _ = context.engine.privacy.updateAccountKeepArchivedUnmuted(value: value).start()
},
updateFolders: { value in
let _ = context.engine.privacy.updateAccountKeepArchivedFolders(value: value).start()
},
updateUnknown: { value in
if let value {
let _ = context.engine.privacy.updateAccountAutoArchiveChats(value: value).start()
} else {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentUndoImpl?(.premiumPaywall(title: nil, text: presentationData.strings.ArchiveSettings_TooltipPremiumRequired, customUndoText: nil, timeout: nil, linkAction: { _ in
presentPremiumImpl?()
}))
}
}
)
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.GlobalPrivacy()),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App()),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
)
|> deliverOnMainQueue
|> map { presentationData, settings, appConfiguration, accountPeer -> (ItemListControllerState, (ItemListNodeState, Any)) in
let isPremium = accountPeer?.isPremium ?? false
let isPremiumDisabled = PremiumConfiguration.with(appConfiguration: appConfiguration).isPremiumDisabled
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ArchiveSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: archiveSettingsControllerEntries(
presentationData: presentationData,
settings: settings,
isPremium: isPremium,
isPremiumEnabled: !isPremiumDisabled
), style: .blocks, animateChanges: true)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
controller.navigationPresentation = .modal
presentUndoImpl = { [weak controller] content in
guard let controller else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
controller.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, action: { _ in
return false
}), in: .current)
}
presentPremiumImpl = { [weak controller] in
guard let controller else {
return
}
let premiumController = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
controller.push(premiumController)
}
return controller
}
@@ -0,0 +1,553 @@
import Foundation
import UIKit
import Display
import Postbox
import SwiftSignalKit
import AsyncDisplayKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import ChatListUI
import WallpaperResources
import LegacyComponents
import ItemListUI
import WallpaperBackgroundNode
private func generateMaskImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let gradientColors = [color.withAlphaComponent(0.0).cgColor, color.cgColor, color.cgColor] as CFArray
var locations: [CGFloat] = [0.0, 0.75, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 80.0), options: CGGradientDrawingOptions())
})
}
private final class BubbleSettingsControllerNode: ASDisplayNode, ASScrollViewDelegate {
private let context: AccountContext
private var presentationThemeSettings: PresentationThemeSettings
private var presentationData: PresentationData
private let referenceTimestamp: Int32
private let scrollNode: ASScrollNode
private let maskNode: ASImageNode
private let chatBackgroundNode: WallpaperBackgroundNode
private let messagesContainerNode: ASDisplayNode
private var dateHeaderNode: ListViewItemHeaderNode?
private var messageNodes: [ListViewItemNode]?
private let toolbarNode: BubbleSettingsToolbarNode
private var validLayout: (ContainerViewLayout, CGFloat)?
init(context: AccountContext, presentationThemeSettings: PresentationThemeSettings, dismiss: @escaping () -> Void, apply: @escaping (PresentationChatBubbleSettings) -> Void) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationThemeSettings = presentationThemeSettings
let calendar = Calendar(identifier: .gregorian)
var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: Date())
components.hour = 13
components.minute = 0
components.second = 0
self.referenceTimestamp = Int32(calendar.date(from: components)?.timeIntervalSince1970 ?? 0.0)
self.scrollNode = ASScrollNode()
self.chatBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: false)
self.chatBackgroundNode.displaysAsynchronously = false
self.messagesContainerNode = ASDisplayNode()
self.messagesContainerNode.clipsToBounds = true
self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
self.chatBackgroundNode.update(wallpaper: self.presentationData.chatWallpaper, animated: false)
self.chatBackgroundNode.updateBubbleTheme(bubbleTheme: self.presentationData.theme, bubbleCorners: self.presentationData.chatBubbleCorners)
self.toolbarNode = BubbleSettingsToolbarNode(presentationThemeSettings: self.presentationThemeSettings, presentationData: self.presentationData)
self.maskNode = ASImageNode()
self.maskNode.displaysAsynchronously = false
self.maskNode.displayWithoutProcessing = true
self.maskNode.contentMode = .scaleToFill
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.maskNode.image = generateMaskImage(color: self.presentationData.theme.chatList.backgroundColor)
self.addSubnode(self.scrollNode)
self.addSubnode(self.toolbarNode)
self.scrollNode.addSubnode(self.chatBackgroundNode)
self.scrollNode.addSubnode(self.messagesContainerNode)
self.toolbarNode.cancel = {
dismiss()
}
var dismissed = false
self.toolbarNode.done = { [weak self] in
guard let strongSelf = self else {
return
}
if !dismissed {
dismissed = true
apply(strongSelf.presentationThemeSettings.chatBubbleSettings)
}
}
self.toolbarNode.updateMergeBubbleCorners = { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.presentationThemeSettings.chatBubbleSettings.mergeBubbleCorners = value
strongSelf.updatePresentationThemeSettings(strongSelf.presentationThemeSettings)
}
self.toolbarNode.updateCornerRadius = { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.presentationThemeSettings.chatBubbleSettings.mainRadius = Int32(value)
strongSelf.presentationThemeSettings.chatBubbleSettings.auxiliaryRadius = Int32(value / 2)
strongSelf.updatePresentationThemeSettings(strongSelf.presentationThemeSettings)
}
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.isPagingEnabled = true
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.alwaysBounceHorizontal = false
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
}
func animateIn(completion: (() -> Void)? = nil) {
if let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass {
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
}
func animateOut(completion: (() -> Void)? = nil) {
if let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass {
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
completion?()
})
} else {
completion?()
}
}
private func updateMessagesLayout(layout: ContainerViewLayout, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
let headerItem = self.context.sharedContext.makeChatMessageDateHeaderItem(context: self.context, timestamp: self.referenceTimestamp, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder)
var items: [ListViewItem] = []
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1))
let otherPeerId = self.context.account.peerId
var peers = SimpleDictionary<PeerId, Peer>()
var messages = SimpleDictionary<MessageId, Message>()
peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .preset(.blue), backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .preset(.blue), backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3)
messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA="
let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)]
let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes, alternativeRepresentations: [])
let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
let message4 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
let width: CGFloat
if case .regular = layout.metrics.widthClass {
width = layout.size.width
} else {
width = layout.size.width
}
let params = ListViewItemLayoutParams(width: width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height)
if let messageNodes = self.messageNodes {
for i in 0 ..< items.count {
let itemNode = messageNodes[i]
items[i].updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
apply(ListViewItemApply(isOnScreen: true))
})
}
} else {
var messageNodes: [ListViewItemNode] = []
for i in 0 ..< items.count {
var itemNode: ListViewItemNode?
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
itemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
itemNode!.isUserInteractionEnabled = false
messageNodes.append(itemNode!)
self.messagesContainerNode.addSubnode(itemNode!)
if let extractedBackgroundNode = itemNode!.extractedBackgroundNode {
extractedBackgroundNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
self.messagesContainerNode.insertSubnode(extractedBackgroundNode, at: 0)
}
}
self.messageNodes = messageNodes
}
var bottomOffset: CGFloat = 9.0 + bottomInset
if let messageNodes = self.messageNodes {
for itemNode in messageNodes {
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: itemNode.frame.size))
if let extractedBackgroundNode = itemNode.extractedBackgroundNode {
transition.updateFrame(node: extractedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: itemNode.frame.size))
}
bottomOffset += itemNode.frame.height
itemNode.updateFrame(itemNode.frame, within: layout.size)
}
}
let dateHeaderNode: ListViewItemHeaderNode
if let currentDateHeaderNode = self.dateHeaderNode {
dateHeaderNode = currentDateHeaderNode
headerItem.updateNode(dateHeaderNode, previous: nil, next: headerItem)
} else {
dateHeaderNode = headerItem.node(synchronousLoad: true)
dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
self.messagesContainerNode.addSubnode(dateHeaderNode)
self.dateHeaderNode = dateHeaderNode
}
transition.updateFrame(node: dateHeaderNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: CGSize(width: layout.size.width, height: headerItem.height)))
dateHeaderNode.updateLayout(size: self.messagesContainerNode.frame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: .immediate)
}
func updatePresentationThemeSettings(_ presentationThemeSettings: PresentationThemeSettings) {
let chatBubbleCorners = PresentationChatBubbleCorners(mainRadius: CGFloat(presentationThemeSettings.chatBubbleSettings.mainRadius), auxiliaryRadius: CGFloat(presentationThemeSettings.chatBubbleSettings.auxiliaryRadius), mergeBubbleCorners: presentationThemeSettings.chatBubbleSettings.mergeBubbleCorners)
self.presentationData = self.presentationData.withChatBubbleCorners(chatBubbleCorners)
self.toolbarNode.updatePresentationData(presentationData: self.presentationData)
self.toolbarNode.updatePresentationThemeSettings(presentationThemeSettings: self.presentationThemeSettings)
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
self.recursivelyEnsureDisplaySynchronously(true)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
let bounds = CGRect(origin: CGPoint(), size: layout.size)
self.scrollNode.frame = bounds
let toolbarHeight = self.toolbarNode.updateLayout(width: layout.size.width, bottomInset: layout.intrinsicInsets.bottom, layout: layout, transition: transition)
var chatFrame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)
let bottomInset: CGFloat
chatFrame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)
self.scrollNode.view.contentSize = CGSize(width: bounds.width, height: bounds.height)
bottomInset = 37.0
self.chatBackgroundNode.frame = chatFrame
self.chatBackgroundNode.updateLayout(size: chatFrame.size, displayMode: .aspectFill, transition: transition)
self.messagesContainerNode.frame = chatFrame
transition.updateFrame(node: self.toolbarNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: toolbarHeight + layout.intrinsicInsets.bottom)))
self.updateMessagesLayout(layout: layout, bottomInset: toolbarHeight + bottomInset, transition: transition)
transition.updateFrame(node: self.maskNode, frame: CGRect(x: 0.0, y: layout.size.height - toolbarHeight - 80.0, width: bounds.width, height: 80.0))
}
}
final class BubbleSettingsController: ViewController {
private let context: AccountContext
private var controllerNode: BubbleSettingsControllerNode {
return self.displayNode as! BubbleSettingsControllerNode
}
private var didPlayPresentationAnimation = false
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var presentationThemeSettings: PresentationThemeSettings
private var presentationThemeSettingsDisposable: Disposable?
private var disposable: Disposable?
private var applyDisposable = MetaDisposable()
public init(context: AccountContext, presentationThemeSettings: PresentationThemeSettings) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationThemeSettings = presentationThemeSettings
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationTheme: self.presentationData.theme, presentationStrings: self.presentationData.strings))
self.blocksBackgroundWhenInOverlay = true
self.acceptsFocusWhenInOverlay = true
self.navigationPresentation = .modal
self.navigationItem.title = self.presentationData.strings.Appearance_BubbleCorners_Title
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView())
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.presentationData = presentationData
}
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
self.presentationThemeSettingsDisposable?.dispose()
self.disposable?.dispose()
self.applyDisposable.dispose()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation {
self.didPlayPresentationAnimation = true
if case .modalSheet = presentationArguments.presentationAnimation {
self.controllerNode.animateIn()
}
}
}
override public func loadDisplayNode() {
super.loadDisplayNode()
self.displayNode = BubbleSettingsControllerNode(context: self.context, presentationThemeSettings: self.presentationThemeSettings, dismiss: { [weak self] in
if let strongSelf = self {
strongSelf.dismiss()
}
}, apply: { [weak self] chatBubbleSettings in
if let strongSelf = self {
strongSelf.apply(chatBubbleSettings: chatBubbleSettings)
}
})
self.displayNodeDidLoad()
}
private func apply(chatBubbleSettings: PresentationChatBubbleSettings) {
let _ = (updatePresentationThemeSettingsInteractively(accountManager: self.context.sharedContext.accountManager, { current in
var current = current
current.chatBubbleSettings = chatBubbleSettings
return current
})
|> deliverOnMainQueue).start(completed: { [weak self] in
self?.dismiss()
})
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}
private enum TextSelectionCustomMode {
case list
case chat
}
private final class BubbleSettingsToolbarNode: ASDisplayNode {
private var presentationThemeSettings: PresentationThemeSettings
private var presentationData: PresentationData
private let cancelButton = HighlightableButtonNode()
private let doneButton = HighlightableButtonNode()
private let separatorNode = ASDisplayNode()
private let topSeparatorNode = ASDisplayNode()
private var switchItemNode: ItemListSwitchItemNode
private var cornerRadiusItemNode: BubbleSettingsRadiusItemNode
private(set) var customMode: TextSelectionCustomMode = .chat
var cancel: (() -> Void)?
var done: (() -> Void)?
var updateMergeBubbleCorners: ((Bool) -> Void)?
var updateCornerRadius: ((Int32) -> Void)?
init(presentationThemeSettings: PresentationThemeSettings, presentationData: PresentationData) {
self.presentationThemeSettings = presentationThemeSettings
self.presentationData = presentationData
self.switchItemNode = ItemListSwitchItemNode(type: .regular)
self.cornerRadiusItemNode = BubbleSettingsRadiusItemNode()
super.init()
self.cancelButton.accessibilityTraits = [.button]
self.doneButton.accessibilityTraits = [.button]
self.addSubnode(self.switchItemNode)
self.addSubnode(self.cornerRadiusItemNode)
self.addSubnode(self.cancelButton)
self.addSubnode(self.doneButton)
self.addSubnode(self.separatorNode)
self.addSubnode(self.topSeparatorNode)
self.updatePresentationData(presentationData: self.presentationData)
self.cancelButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.cancelButton.backgroundColor = strongSelf.presentationData.theme.list.itemHighlightedBackgroundColor
} else {
UIView.animate(withDuration: 0.3, animations: {
strongSelf.cancelButton.backgroundColor = .clear
})
}
}
}
self.doneButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.doneButton.backgroundColor = strongSelf.presentationData.theme.list.itemHighlightedBackgroundColor
} else {
UIView.animate(withDuration: 0.3, animations: {
strongSelf.doneButton.backgroundColor = .clear
})
}
}
}
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
self.doneButton.addTarget(self, action: #selector(self.donePressed), forControlEvents: .touchUpInside)
}
func setDoneEnabled(_ enabled: Bool) {
self.doneButton.alpha = enabled ? 1.0 : 0.4
self.doneButton.isUserInteractionEnabled = enabled
}
func setCustomMode(_ customMode: TextSelectionCustomMode) {
self.customMode = customMode
}
func updatePresentationData(presentationData: PresentationData) {
self.backgroundColor = presentationData.theme.rootController.tabBar.backgroundColor
self.separatorNode.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor
self.topSeparatorNode.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor
self.cancelButton.setTitle(presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: presentationData.theme.list.itemPrimaryTextColor, for: [])
self.doneButton.setTitle(presentationData.strings.Wallpaper_Set, with: Font.regular(17.0), with: presentationData.theme.list.itemPrimaryTextColor, for: [])
self.cancelButton.accessibilityLabel = presentationData.strings.Common_Cancel
self.doneButton.accessibilityLabel = presentationData.strings.Wallpaper_Set
}
func updatePresentationThemeSettings(presentationThemeSettings: PresentationThemeSettings) {
self.presentationThemeSettings = presentationThemeSettings
}
func updateLayout(width: CGFloat, bottomInset: CGFloat, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat {
var contentHeight: CGFloat = 0.0
let switchItem = ItemListSwitchItem(presentationData: ItemListPresentationData(self.presentationData), title: self.presentationData.strings.Appearance_BubbleCorners_AdjustAdjacent, value: self.presentationThemeSettings.chatBubbleSettings.mergeBubbleCorners, disableLeadingInset: true, sectionId: 0, style: .blocks, updated: { [weak self] value in
self?.updateMergeBubbleCorners?(value)
})
let cornerRadiusItem = BubbleSettingsRadiusItem(theme: self.presentationData.theme, value: Int(self.presentationData.chatBubbleCorners.mainRadius), enabled: true, disableLeadingInset: false, displayIcons: false, disableDecorations: true, force: false, sectionId: 0, updated: { [weak self] value in
self?.updateCornerRadius?(Int32(max(8, min(16, value))))
})
/*switchItem.updateNode(async: { f in
f()
}, node: {
return self.switchItemNode
}, params: ListViewItemLayoutParams(width: width, leftInset: layout.intrinsicInsets.left, rightInset: layout.intrinsicInsets.right, availableHeight: 1000.0), previousItem: nil, nextItem: cornerRadiusItem, animation: .None, completion: { layout, apply in
self.switchItemNode.contentSize = layout.contentSize
self.switchItemNode.insets = layout.insets
transition.updateFrame(node: self.switchItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: layout.contentSize))
contentHeight += layout.contentSize.height
apply(ListViewItemApply(isOnScreen: true))
})*/
cornerRadiusItem.updateNode(async: { f in
f()
}, node: {
return self.cornerRadiusItemNode
}, params: ListViewItemLayoutParams(width: width, leftInset: layout.intrinsicInsets.left, rightInset: layout.intrinsicInsets.right, availableHeight: 1000.0), previousItem: switchItem, nextItem: nil, animation: .None, completion: { layout, apply in
self.cornerRadiusItemNode.contentSize = layout.contentSize
self.cornerRadiusItemNode.insets = layout.insets
transition.updateFrame(node: self.cornerRadiusItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: layout.contentSize))
contentHeight += layout.contentSize.height
apply(ListViewItemApply(isOnScreen: true))
})
self.cancelButton.frame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: floor(width / 2.0), height: 49.0))
self.doneButton.frame = CGRect(origin: CGPoint(x: floor(width / 2.0), y: contentHeight), size: CGSize(width: width - floor(width / 2.0), height: 49.0))
contentHeight += 49.0
self.topSeparatorNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel))
let resultHeight = contentHeight + bottomInset
self.separatorNode.frame = CGRect(origin: CGPoint(x: floor(width / 2.0), y: self.cancelButton.frame.minY), size: CGSize(width: UIScreenPixel, height: resultHeight - self.cancelButton.frame.minY))
return resultHeight
}
@objc func cancelPressed() {
self.cancel?()
}
@objc func donePressed() {
self.doneButton.isUserInteractionEnabled = false
self.done?()
}
}
@@ -0,0 +1,70 @@
import Foundation
import SwiftSignalKit
import TelegramCore
import AccountContext
import InstantPageUI
import InstantPageCache
import UrlHandling
import TelegramUIPreferences
func faqSearchableItems(context: AccountContext, resolvedUrl: Signal<ResolvedUrl?, NoError>, suggestAccountDeletion: Bool) -> Signal<[SettingsSearchableItem], NoError> {
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
return resolvedUrl
|> map { resolvedUrl -> [SettingsSearchableItem] in
var results: [SettingsSearchableItem] = []
var nextIndex: Int32 = 2
if let resolvedUrl = resolvedUrl, case let .instantView(webPage, _) = resolvedUrl {
if case let .Loaded(content) = webPage.content, let instantPage = content.instantPage?._parse() {
var processingQuestions = false
var currentSection: String?
outer: for block in instantPage.blocks {
if !processingQuestions {
switch block {
case .blockQuote:
if results.isEmpty {
processingQuestions = true
}
default:
break
}
} else {
switch block {
case let .paragraph(text):
if case .bold = text {
currentSection = text.plainText
} else if case .concat = text {
processingQuestions = false
}
case let .list(items, false):
if let currentSection = currentSection {
for item in items {
if case let .text(itemText, _) = item, case let .url(text, url, _) = itemText {
let (_, anchor) = extractAnchor(string: url)
var index = nextIndex
if suggestAccountDeletion && (anchor?.contains("delete-my-account") ?? false) {
index = 1
} else {
nextIndex += 1
}
let item = SettingsSearchableItem(id: .faq(index), title: text.plainText, alternate: [], icon: .faq, breadcrumbs: [strings.SettingsSearch_FAQ, currentSection], present: { context, _, present in
let controller = context.sharedContext.makeInstantPageController(context: context, webPage: webPage, anchor: anchor, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .channel))
present(.push, controller)
})
if index == 1 {
results.insert(item, at: 0)
} else {
results.append(item)
}
}
}
}
default:
break
}
}
}
}
}
return results
}
}
@@ -0,0 +1,351 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import AccountContext
import AlertUI
import PresentationDataUtils
import AuthorizationUtils
import PhoneNumberFormat
private final class ChangePhoneNumberCodeControllerArguments {
let context: AccountContext
let updateEntryText: (String) -> Void
let next: () -> Void
init(context: AccountContext, updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void) {
self.context = context
self.updateEntryText = updateEntryText
self.next = next
}
}
private enum ChangePhoneNumberCodeSection: Int32 {
case code
}
private enum ChangePhoneNumberCodeTag: ItemListItemTag {
case input
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? ChangePhoneNumberCodeTag {
switch self {
case .input:
if case .input = other {
return true
} else {
return false
}
}
} else {
return false
}
}
}
private enum ChangePhoneNumberCodeEntry: ItemListNodeEntry {
case codeEntry(PresentationTheme, PresentationStrings, String, String)
case codeInfo(PresentationTheme, String)
var section: ItemListSectionId {
return ChangePhoneNumberCodeSection.code.rawValue
}
var stableId: Int32 {
switch self {
case .codeEntry:
return 1
case .codeInfo:
return 2
}
}
static func ==(lhs: ChangePhoneNumberCodeEntry, rhs: ChangePhoneNumberCodeEntry) -> Bool {
switch lhs {
case let .codeEntry(lhsTheme, lhsStrings, lhsTitle, lhsText):
if case let .codeEntry(rhsTheme, rhsStrings, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsTitle == rhsTitle, lhsText == rhsText {
return true
} else {
return false
}
case let .codeInfo(lhsTheme, lhsText):
if case let .codeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: ChangePhoneNumberCodeEntry, rhs: ChangePhoneNumberCodeEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChangePhoneNumberCodeControllerArguments
switch self {
case let .codeEntry(theme, _, title, text):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: title, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .number, spacing: 10.0, tag: ChangePhoneNumberCodeTag.input, sectionId: self.section, textUpdated: { updatedText in
arguments.updateEntryText(updatedText)
}, action: {
arguments.next()
})
case let .codeInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
}
}
private struct ChangePhoneNumberCodeControllerState: Equatable {
let codeText: String
let checking: Bool
init(codeText: String, checking: Bool) {
self.codeText = codeText
self.checking = checking
}
static func ==(lhs: ChangePhoneNumberCodeControllerState, rhs: ChangePhoneNumberCodeControllerState) -> Bool {
if lhs.codeText != rhs.codeText {
return false
}
if lhs.checking != rhs.checking {
return false
}
return true
}
func withUpdatedCodeText(_ codeText: String) -> ChangePhoneNumberCodeControllerState {
return ChangePhoneNumberCodeControllerState(codeText: codeText, checking: self.checking)
}
func withUpdatedChecking(_ checking: Bool) -> ChangePhoneNumberCodeControllerState {
return ChangePhoneNumberCodeControllerState(codeText: self.codeText, checking: checking)
}
func withUpdatedNextMethodTimeout(_ nextMethodTimeout: Int32?) -> ChangePhoneNumberCodeControllerState {
return ChangePhoneNumberCodeControllerState(codeText: self.codeText, checking: self.checking)
}
func withUpdatedCodeData(_ codeData: ChangeAccountPhoneNumberData) -> ChangePhoneNumberCodeControllerState {
return ChangePhoneNumberCodeControllerState(codeText: self.codeText, checking: self.checking)
}
}
private func changePhoneNumberCodeControllerEntries(presentationData: PresentationData, state: ChangePhoneNumberCodeControllerState, codeData: ChangeAccountPhoneNumberData, timeout: Int32?, strings: PresentationStrings, phoneNumber: String) -> [ChangePhoneNumberCodeEntry] {
var entries: [ChangePhoneNumberCodeEntry] = []
entries.append(.codeEntry(presentationData.theme, presentationData.strings, presentationData.strings.ChangePhoneNumberCode_CodePlaceholder, state.codeText))
var text = authorizationCurrentOptionText(codeData.type, phoneNumber: phoneNumber, email: nil, strings: presentationData.strings, primaryColor: presentationData.theme.list.itemPrimaryTextColor, accentColor: presentationData.theme.list.itemAccentColor).string
if let nextType = codeData.nextType {
text += "\n\n" + authorizationNextOptionText(currentType: codeData.type, nextType: nextType, timeout: timeout, strings: presentationData.strings, primaryColor: .black, accentColor: .black).0.string
}
entries.append(.codeInfo(presentationData.theme, text))
return entries
}
private func timeoutSignal(codeData: ChangeAccountPhoneNumberData) -> Signal<Int32?, NoError> {
if let _ = codeData.nextType, let timeout = codeData.timeout {
return Signal { subscriber in
let value = Atomic<Int32>(value: timeout)
subscriber.putNext(timeout)
let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: {
subscriber.putNext(value.modify { value in
return max(0, value - 1)
})
}, queue: Queue.mainQueue())
timer.start()
return ActionDisposable {
timer.invalidate()
}
}
} else {
return .single(nil)
}
}
public protocol ChangePhoneNumberCodeController: AnyObject {
func applyCode(_ code: Int)
}
private final class ChangePhoneNumberCodeControllerImpl: ItemListController, ChangePhoneNumberCodeController {
private let applyCodeImpl: (Int) -> Void
init(context: AccountContext, state: Signal<(ItemListControllerState, (ItemListNodeState, Any)), NoError>, applyCodeImpl: @escaping (Int) -> Void) {
self.applyCodeImpl = applyCodeImpl
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(presentationData: ItemListPresentationData(presentationData), updatedPresentationData: context.sharedContext.presentationData |> map(ItemListPresentationData.init(_:)), state: state, tabBarItem: nil)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func applyCode(_ code: Int) {
self.applyCodeImpl(code)
}
}
func changePhoneNumberCodeController(context: AccountContext, phoneNumber: String, codeData: ChangeAccountPhoneNumberData) -> ViewController {
let initialState = ChangePhoneNumberCodeControllerState(codeText: "", checking: false)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((ChangePhoneNumberCodeControllerState) -> ChangePhoneNumberCodeControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
let actionsDisposable = DisposableSet()
let changePhoneDisposable = MetaDisposable()
actionsDisposable.add(changePhoneDisposable)
let nextTypeDisposable = MetaDisposable()
actionsDisposable.add(nextTypeDisposable)
let currentDataPromise = Promise<ChangeAccountPhoneNumberData>()
currentDataPromise.set(.single(codeData))
let timeout = Promise<Int32?>()
timeout.set(timeoutSignal(codeData: codeData))
let resendCode = currentDataPromise.get()
|> mapToSignal { [weak currentDataPromise] data -> Signal<Void, NoError> in
if let _ = data.nextType {
return timeout.get()
|> filter { $0 == 0 }
|> take(1)
|> mapToSignal { _ -> Signal<Void, NoError> in
return Signal { subscriber in
return context.engine.accountData.requestNextChangeAccountPhoneNumberVerification(
phoneNumber: phoneNumber,
phoneCodeHash: data.hash,
apiId: context.sharedContext.networkArguments.apiId,
apiHash: context.sharedContext.networkArguments.apiHash,
firebaseSecretStream: context.sharedContext.firebaseSecretStream
).start(next: { next in
currentDataPromise?.set(.single(next))
}, error: { error in
})
}
}
} else {
return .complete()
}
}
nextTypeDisposable.set(resendCode.start())
let checkCode: () -> Void = {
var code: String?
updateState { state in
if state.checking || state.codeText.isEmpty {
return state
} else {
code = state.codeText
return state.withUpdatedChecking(true)
}
}
if let code = code {
changePhoneDisposable.set((context.engine.accountData.requestChangeAccountPhoneNumber(phoneNumber: phoneNumber, phoneCodeHash: codeData.hash, phoneCode: code) |> deliverOnMainQueue).start(error: { error in
updateState {
return $0.withUpdatedChecking(false)
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let alertText: String
switch error {
case .generic:
alertText = presentationData.strings.Login_UnknownError
case .invalidCode:
alertText = presentationData.strings.Login_InvalidCodeError
case .codeExpired:
alertText = presentationData.strings.Login_CodeExpiredError
case .limitExceeded:
alertText = presentationData.strings.Login_CodeFloodError
}
presentControllerImpl?(textAlertController(context: context, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, completed: {
updateState {
return $0.withUpdatedChecking(false)
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .success), nil)
let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: ServerProvidedSuggestion.validatePhoneNumber.id).start()
dismissImpl?()
}))
}
}
let arguments = ChangePhoneNumberCodeControllerArguments(context: context, updateEntryText: { updatedText in
var initiateCheck = false
updateState { state in
if state.codeText.count < 5 && updatedText.count == 5 {
initiateCheck = true
}
return state.withUpdatedCodeText(updatedText)
}
if initiateCheck {
checkCode()
}
}, next: {
checkCode()
})
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get() |> deliverOnMainQueue, currentDataPromise.get() |> deliverOnMainQueue, timeout.get() |> deliverOnMainQueue)
|> deliverOnMainQueue
|> map { presentationData, state, data, timeout -> (ItemListControllerState, (ItemListNodeState, Any)) in
var rightNavigationButton: ItemListNavigationButton?
if state.checking {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
} else {
var nextEnabled = true
if state.codeText.isEmpty {
nextEnabled = false
}
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Next), style: .bold, enabled: nextEnabled, action: {
checkCode()
})
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(formatPhoneNumber(context: context, number: phoneNumber)), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: changePhoneNumberCodeControllerEntries(presentationData: presentationData, state: state, codeData: data, timeout: timeout, strings: presentationData.strings, phoneNumber: phoneNumber), style: .blocks, focusItemTag: ChangePhoneNumberCodeTag.input, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ChangePhoneNumberCodeControllerImpl(context: context, state: signal, applyCodeImpl: { code in
updateState { state in
return state.withUpdatedCodeText("\(code)")
}
checkCode()
})
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
dismissImpl = { [weak controller] in
(controller?.navigationController as? NavigationController)?.popToRoot(animated: true)
}
return controller
}
@@ -0,0 +1,198 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import ProgressNavigationButtonNode
import AccountContext
import AlertUI
import PresentationDataUtils
import CountrySelectionUI
import PhoneNumberFormat
import CoreTelephony
import MessageUI
import AuthorizationUI
public func ChangePhoneNumberController(context: AccountContext) -> ViewController {
var dismissImpl: (() -> Void)?
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let requestDisposable = MetaDisposable()
let changePhoneDisposable = MetaDisposable()
let controller = AuthorizationSequencePhoneEntryController(sharedContext: context.sharedContext, account: nil, countriesConfiguration: context.currentCountriesConfiguration.with { $0 }, apiId: 0, apiHash: "", isTestingEnvironment: false, otherAccountPhoneNumbers: (nil, []), network: context.account.network, presentationData: presentationData, openUrl: { _ in }, back: {
dismissImpl?()
})
controller.loginWithNumber = { [weak controller] phoneNumber, _ in
controller?.inProgress = true
let authorizationPushConfiguration = context.sharedContext.authorizationPushConfiguration
|> take(1)
|> timeout(2.0, queue: .mainQueue(), alternate: .single(nil))
requestDisposable.set((
authorizationPushConfiguration
|> castError(RequestChangeAccountPhoneNumberVerificationError.self)
|> mapToSignal { authorizationPushConfiguration in
return context.engine.accountData.requestChangeAccountPhoneNumberVerification(
apiId: context.sharedContext.networkArguments.apiId,
apiHash: context.sharedContext.networkArguments.apiHash,
phoneNumber: phoneNumber,
pushNotificationConfiguration: authorizationPushConfiguration,
firebaseSecretStream: context.sharedContext.firebaseSecretStream
)
}
|> deliverOnMainQueue).start(next: { [weak controller] next in
controller?.inProgress = false
var dismissImpl: (() -> Void)?
let codeController = AuthorizationSequenceCodeEntryController(presentationData: presentationData, back: {
dismissImpl?()
})
codeController.loginWithCode = { [weak codeController] code in
codeController?.inProgress = true
changePhoneDisposable.set((context.engine.accountData.requestChangeAccountPhoneNumber(phoneNumber: phoneNumber, phoneCodeHash: next.hash, phoneCode: code)
|> deliverOnMainQueue).start(error: { [weak codeController] error in
if case .invalidCode = error {
codeController?.animateError(text: presentationData.strings.Login_WrongCodeError)
} else {
var resetCode = false
let text: String
switch error {
case .generic:
text = presentationData.strings.Login_UnknownError
case .invalidCode:
resetCode = true
text = presentationData.strings.Login_InvalidCodeError
case .codeExpired:
resetCode = true
text = presentationData.strings.Login_CodeExpiredError
case .limitExceeded:
resetCode = true
text = presentationData.strings.Login_CodeFloodError
}
if resetCode {
codeController?.resetCode()
}
codeController?.present(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}
}, completed: { [weak codeController] in
codeController?.present(OverlayStatusController(theme: presentationData.theme, type: .success), in: .window(.root))
let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: ServerProvidedSuggestion.validatePhoneNumber.id).start()
if let navigationController = codeController?.navigationController as? NavigationController {
var viewControllers = navigationController.viewControllers
viewControllers.removeAll(where: { c in
if c is AuthorizationSequencePhoneEntryController {
return true
} else if c is AuthorizationSequenceCodeEntryController {
return true
} else {
return false
}
})
navigationController.setViewControllers(viewControllers, animated: true)
}
}))
}
codeController.requestNextOption = { [weak codeController] in
guard let codeController else {
return
}
let carrier = CTCarrier()
let mnc = carrier.mobileNetworkCode ?? "none"
let _ = context.engine.auth.reportMissingCode(phoneNumber: phoneNumber, phoneCodeHash: next.hash, mnc: mnc).start()
AuthorizationSequenceController.presentDidNotGetCodeUI(controller: codeController, presentationData: context.sharedContext.currentPresentationData.with({ $0 }), phoneNumber: phoneNumber, mnc: mnc)
}
codeController.openFragment = { url in
context.sharedContext.applicationBindings.openUrl(url)
}
codeController.updateData(number: formatPhoneNumber(context: context, number: phoneNumber), email: nil, codeType: next.type, nextType: nil, timeout: next.timeout, termsOfService: nil, previousCodeType: nil, isPrevious: false)
dismissImpl = { [weak codeController] in
codeController?.dismiss()
}
controller?.push(codeController)
}, error: { [weak controller] error in
controller?.inProgress = false
let text: String
var actions: [TextAlertAction] = []
switch error {
case .limitExceeded:
text = presentationData.strings.Login_CodeFloodError
actions.append(TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}))
case .invalidPhoneNumber:
text = presentationData.strings.Login_InvalidPhoneError
actions.append(TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}))
case .phoneNumberOccupied:
text = presentationData.strings.ChangePhone_ErrorOccupied(formatPhoneNumber(context: context, number: phoneNumber)).string
actions.append(TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}))
case .phoneBanned:
text = presentationData.strings.Login_PhoneBannedError
actions.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {}))
actions.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in
let formattedNumber = formatPhoneNumber(context: context, number: phoneNumber)
let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
let systemVersion = UIDevice.current.systemVersion
let locale = Locale.current.identifier
let carrier = CTCarrier()
let mnc = carrier.mobileNetworkCode ?? "none"
if MFMailComposeViewController.canSendMail() {
let composeController = MFMailComposeViewController()
composeController.setToRecipients(["recover@telegram.org"])
composeController.setSubject(presentationData.strings.Login_PhoneBannedEmailSubject(formattedNumber).string)
composeController.setMessageBody(presentationData.strings.Login_PhoneBannedEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string, isHTML: false)
composeController.mailComposeDelegate = controller
controller?.view.window?.rootViewController?.present(composeController, animated: true, completion: nil)
} else {
controller?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}
}))
case .generic:
text = presentationData.strings.Login_UnknownError
actions.append(TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}))
}
controller?.dismissConfirmation()
controller?.present(textAlertController(context: context, title: nil, text: text, actions: actions), in: .window(.root))
}))
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
Queue.mainQueue().justDispatch {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { accountPeer in
guard let accountPeer, case let .user(user) = accountPeer else {
return
}
let initialCountryCode: Int32
if let phone = user.phone {
if let (_, countryCode) = lookupCountryIdByNumber(phone, configuration: context.currentCountriesConfiguration.with { $0 }), let codeValue = Int32(countryCode.code) {
initialCountryCode = codeValue
} else {
initialCountryCode = AuthorizationSequenceCountrySelectionController.defaultCountryCode()
}
} else {
initialCountryCode = AuthorizationSequenceCountrySelectionController.defaultCountryCode()
}
controller.updateData(countryCode: initialCountryCode, countryName: nil, number: "")
controller.updateCountryCode()
})
}
return controller
}
@@ -0,0 +1,279 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import PhoneInputNode
import CountrySelectionUI
private func generateCountryButtonBackground(color: UIColor, strokeColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 56, height: 44.0 + 6.0), rotatedContext: { size, context in
let arrowSize: CGFloat = 6.0
let lineWidth = UIScreenPixel
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height - arrowSize)))
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize))
context.closePath()
context.fillPath()
context.setStrokeColor(strokeColor.cgColor)
context.setLineWidth(lineWidth)
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize - lineWidth / 2.0))
context.addLine(to: CGPoint(x: 15.0, y: size.height - arrowSize - lineWidth / 2.0))
context.strokePath()
context.move(to: CGPoint(x: 0.0, y: lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width, y: lineWidth / 2.0))
context.strokePath()
})?.stretchableImage(withLeftCapWidth: 55, topCapHeight: 1)
}
private func generateCountryButtonHighlightedBackground(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 56.0, height: 44.0 + 6.0), rotatedContext: { size, context in
let arrowSize: CGFloat = 6.0
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height - arrowSize)))
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize))
context.closePath()
context.fillPath()
})?.stretchableImage(withLeftCapWidth: 55, topCapHeight: 2)
}
private func generatePhoneInputBackground(color: UIColor, strokeColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 82.0, height: 44.0), rotatedContext: { size, context in
let lineWidth = UIScreenPixel
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(strokeColor.cgColor)
context.setLineWidth(lineWidth)
context.move(to: CGPoint(x: 0.0, y: size.height - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width, y: size.height - lineWidth / 2.0))
context.strokePath()
context.move(to: CGPoint(x: size.width - 2.0 + lineWidth / 2.0, y: size.height - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 2.0 + lineWidth / 2.0, y: 0.0))
context.strokePath()
})?.stretchableImage(withLeftCapWidth: 81, topCapHeight: 2)
}
final class ChangePhoneNumberControllerNode: ASDisplayNode {
private let titleNode: ASTextNode
private let noticeNode: ASTextNode
private let countryButton: ASButtonNode
private let phoneBackground: ASImageNode
private let phoneInputNode: PhoneInputNode
var currentNumber: String {
return self.phoneInputNode.number
}
var codeAndNumber: (Int32?, String?, String) {
get {
return self.phoneInputNode.codeAndNumber
} set(value) {
self.phoneInputNode.codeAndNumber = value
}
}
var preferredCountryIdForCode: [String: String] = [:]
var selectCountryCode: (() -> Void)?
var inProgress: Bool = false {
didSet {
self.phoneInputNode.enableEditing = !self.inProgress
self.phoneInputNode.alpha = self.inProgress ? 0.6 : 1.0
self.countryButton.isEnabled = !self.inProgress
}
}
var presentationData: PresentationData
init(presentationData: PresentationData) {
self.presentationData = presentationData
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChangePhoneNumberNumber_NewNumber, font: Font.regular(14.0), textColor: self.presentationData.theme.list.sectionHeaderTextColor)
self.noticeNode = ASTextNode()
self.noticeNode.isUserInteractionEnabled = false
self.noticeNode.displaysAsynchronously = false
self.noticeNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChangePhoneNumberNumber_Help, font: Font.regular(14.0), textColor: self.presentationData.theme.list.freeTextColor)
self.countryButton = ASButtonNode()
self.countryButton.setBackgroundImage(generateCountryButtonBackground(color: self.presentationData.theme.list.itemBlocksBackgroundColor, strokeColor: self.presentationData.theme.list.itemBlocksSeparatorColor), for: [])
self.countryButton.setBackgroundImage(generateCountryButtonHighlightedBackground(color: self.presentationData.theme.list.itemHighlightedBackgroundColor), for: .highlighted)
self.phoneBackground = ASImageNode()
self.phoneBackground.image = generatePhoneInputBackground(color: self.presentationData.theme.list.itemBlocksBackgroundColor, strokeColor: self.presentationData.theme.list.itemBlocksSeparatorColor)
self.phoneBackground.displaysAsynchronously = false
self.phoneBackground.displayWithoutProcessing = true
self.phoneBackground.isLayerBacked = true
self.phoneInputNode = PhoneInputNode(fontSize: 17.0)
self.phoneInputNode.countryCodeField.textField.textColor = self.presentationData.theme.list.itemPrimaryTextColor
self.phoneInputNode.countryCodeField.textField.keyboardAppearance = self.presentationData.theme.rootController.keyboardColor.keyboardAppearance
self.phoneInputNode.countryCodeField.textField.tintColor = self.presentationData.theme.list.itemAccentColor
self.phoneInputNode.numberField.textField.textColor = self.presentationData.theme.list.itemPrimaryTextColor
self.phoneInputNode.numberField.textField.keyboardAppearance = self.presentationData.theme.rootController.keyboardColor.keyboardAppearance
self.phoneInputNode.numberField.textField.tintColor = self.presentationData.theme.list.itemAccentColor
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
self.addSubnode(self.titleNode)
self.addSubnode(self.noticeNode)
self.addSubnode(self.phoneBackground)
self.addSubnode(self.countryButton)
self.addSubnode(self.phoneInputNode)
self.countryButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 15.0, bottom: 4.0, right: 0.0)
self.countryButton.contentHorizontalAlignment = .left
self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: self.presentationData.strings.Login_PhonePlaceholder, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPlaceholderTextColor)
self.countryButton.addTarget(self, action: #selector(self.countryPressed), forControlEvents: .touchUpInside)
let processNumberChange: (String) -> Bool = { [weak self] number in
guard let strongSelf = self else {
return false
}
if let (country, _) = AuthorizationSequenceCountrySelectionController.lookupCountryIdByNumber(number, preferredCountries: strongSelf.preferredCountryIdForCode) {
let flagString = emojiFlagForISOCountryCode(country.id)
let localizedName: String = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(country.id, strings: strongSelf.presentationData.strings) ?? country.name
strongSelf.countryButton.setTitle("\(flagString) \(localizedName)", with: Font.regular(17.0), with: strongSelf.presentationData.theme.list.itemPrimaryTextColor, for: [])
let maskFont = Font.with(size: 17.0, design: .regular, traits: [.monospacedNumbers])
if let mask = AuthorizationSequenceCountrySelectionController.lookupPatternByNumber(number, preferredCountries: strongSelf.preferredCountryIdForCode).flatMap({ NSAttributedString(string: $0, font: maskFont, textColor: strongSelf.presentationData.theme.list.itemPlaceholderTextColor) }) {
strongSelf.phoneInputNode.numberField.textField.attributedPlaceholder = nil
strongSelf.phoneInputNode.mask = mask
} else {
strongSelf.phoneInputNode.mask = nil
strongSelf.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: strongSelf.presentationData.strings.Login_PhonePlaceholder, font: Font.regular(17.0), textColor: strongSelf.presentationData.theme.list.itemPlaceholderTextColor)
}
return true
} else {
return false
}
}
self.phoneInputNode.numberTextUpdated = { [weak self] number in
if let strongSelf = self {
let _ = processNumberChange(strongSelf.phoneInputNode.number)
}
}
self.phoneInputNode.countryCodeUpdated = { [weak self] code, name in
if let strongSelf = self {
if let name = name {
strongSelf.preferredCountryIdForCode[code] = name
}
if processNumberChange(strongSelf.phoneInputNode.number) {
} else if let code = Int(code), let name = name, let countryName = countryCodeAndIdToName[CountryCodeAndId(code: code, id: name)] {
let localizedName: String = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(name, strings: strongSelf.presentationData.strings) ?? countryName
strongSelf.countryButton.setTitle(localizedName, with: Font.regular(17.0), with: strongSelf.presentationData.theme.list.itemPrimaryTextColor, for: [])
} else if let code = Int(code), let (_, countryName) = countryCodeToIdAndName[code] {
strongSelf.countryButton.setTitle(countryName, with: Font.regular(17.0), with: strongSelf.presentationData.theme.list.itemPrimaryTextColor, for: [])
} else {
strongSelf.countryButton.setTitle(strongSelf.presentationData.strings.Login_CountryCode, with: Font.regular(17.0), with: strongSelf.presentationData.theme.list.itemPrimaryTextColor, for: [])
}
}
}
self.phoneInputNode.customFormatter = { number in
if let (_, code) = AuthorizationSequenceCountrySelectionController.lookupCountryIdByNumber(number, preferredCountries: [:]) {
return code.code
} else {
return nil
}
}
let countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String
var countryCodeAndId: (Int32, String) = (1, "US")
if let countryId = countryId {
let normalizedId = countryId.uppercased()
for (code, idAndName) in countryCodeToIdAndName {
if idAndName.0 == normalizedId {
countryCodeAndId = (Int32(code), idAndName.0.uppercased())
break
}
}
}
self.phoneInputNode.number = "+\(countryCodeAndId.0)"
}
func updateCountryCode() {
self.phoneInputNode.codeAndNumber = self.codeAndNumber
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
var insets = layout.insets(options: [.statusBar, .input])
insets.left = layout.safeInsets.left
insets.right = layout.safeInsets.right
let countryButtonHeight: CGFloat = 44.0
let inputFieldsHeight: CGFloat = 44.0
let titleSize = self.titleNode.measure(CGSize(width: layout.size.width - 28.0 - insets.left - insets .right, height: CGFloat.greatestFiniteMagnitude))
let noticeSize = self.noticeNode.measure(CGSize(width: layout.size.width - 28.0 - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude))
let navigationHeight: CGFloat = 97.0 + insets.top + navigationBarHeight
let inputHeight = countryButtonHeight + inputFieldsHeight
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: 15.0 + insets.left, y: navigationHeight - titleSize.height - 8.0), size: titleSize))
transition.updateFrame(node: self.countryButton, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: layout.size.width, height: 44.0 + 6.0)))
transition.updateFrame(node: self.phoneBackground, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight + 44.0), size: CGSize(width: layout.size.width, height: 44.0)))
let countryCodeFrame = CGRect(origin: CGPoint(x: 11.0, y: navigationHeight + 44.0 + 1.0), size: CGSize(width: 67.0, height: 44.0))
let numberFrame = CGRect(origin: CGPoint(x: 92.0, y: navigationHeight + 44.0 + 1.0), size: CGSize(width: layout.size.width - 70.0 - 8.0, height: 44.0))
let placeholderFrame = numberFrame.offsetBy(dx: 0.0, dy: 8.0)
let phoneInputFrame = countryCodeFrame.union(numberFrame)
transition.updateFrame(node: self.phoneInputNode, frame: phoneInputFrame)
transition.updateFrame(node: self.phoneInputNode.countryCodeField, frame: countryCodeFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY))
transition.updateFrame(node: self.phoneInputNode.numberField, frame: numberFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY))
transition.updateFrame(node: self.phoneInputNode.placeholderNode, frame: placeholderFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY))
transition.updateFrame(node: self.noticeNode, frame: CGRect(origin: CGPoint(x: 15.0 + insets.left, y: navigationHeight + inputHeight + 8.0), size: noticeSize))
}
func activateInput() {
self.phoneInputNode.numberField.textField.becomeFirstResponder()
}
func animateError() {
self.phoneInputNode.countryCodeField.layer.addShakeAnimation()
self.phoneInputNode.numberField.layer.addShakeAnimation()
}
@objc func countryPressed() {
self.selectCountryCode?()
}
}
@@ -0,0 +1,383 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
enum AutomaticDownloadConnectionType {
case cellular
case wifi
var automaticDownloadNetworkType: MediaAutoDownloadNetworkType {
switch self {
case .cellular:
return .cellular
case .wifi:
return .wifi
}
}
}
private final class AutodownloadMediaConnectionTypeControllerArguments {
let toggleMaster: (Bool) -> Void
let changePreset: (AutomaticDownloadDataUsage) -> Void
let customize: (AutomaticDownloadCategory) -> Void
init(toggleMaster: @escaping (Bool) -> Void, changePreset: @escaping (AutomaticDownloadDataUsage) -> Void, customize: @escaping (AutomaticDownloadCategory) -> Void) {
self.toggleMaster = toggleMaster
self.changePreset = changePreset
self.customize = customize
}
}
private enum AutodownloadMediaCategorySection: Int32 {
case master
case dataUsage
case types
}
private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry {
case master(PresentationTheme, String, Bool)
case dataUsageHeader(PresentationTheme, String)
case dataUsageItem(PresentationTheme, PresentationStrings, AutomaticDownloadDataUsage, Int?, Bool)
case typesHeader(PresentationTheme, String)
case photos(PresentationTheme, String, String, Bool)
case stories(PresentationTheme, String, String, Bool)
case videos(PresentationTheme, String, String, Bool)
case files(PresentationTheme, String, String, Bool)
case voiceMessagesInfo(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .master:
return AutodownloadMediaCategorySection.master.rawValue
case .dataUsageHeader, .dataUsageItem:
return AutodownloadMediaCategorySection.dataUsage.rawValue
case .typesHeader, .photos, .stories, .videos, .files, .voiceMessagesInfo:
return AutodownloadMediaCategorySection.types.rawValue
}
}
var stableId: Int32 {
switch self {
case .master:
return 0
case .dataUsageHeader:
return 1
case .dataUsageItem:
return 2
case .typesHeader:
return 3
case .photos:
return 4
case .stories:
return 5
case .videos:
return 6
case .files:
return 7
case .voiceMessagesInfo:
return 8
}
}
static func ==(lhs: AutodownloadMediaCategoryEntry, rhs: AutodownloadMediaCategoryEntry) -> Bool {
switch lhs {
case let .master(lhsTheme, lhsText, lhsValue):
if case let .master(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .dataUsageHeader(lhsTheme, lhsText):
if case let .dataUsageHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .dataUsageItem(lhsTheme, lhsStrings, lhsValue, lhsCustomPosition, lhsEnabled):
if case let .dataUsageItem(rhsTheme, rhsStrings, rhsValue, rhsCustomPosition, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsStrings == rhsStrings, lhsValue == rhsValue, lhsCustomPosition == rhsCustomPosition, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .typesHeader(lhsTheme, lhsText):
if case let .typesHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .photos(lhsTheme, lhsText, lhsValue, lhsEnabled):
if case let .photos(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .stories(lhsTheme, lhsText, lhsValue, lhsEnabled):
if case let .stories(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .videos(lhsTheme, lhsText, lhsValue, lhsEnabled):
if case let .videos(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .files(lhsTheme, lhsText, lhsValue, lhsEnabled):
if case let .files(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .voiceMessagesInfo(lhsTheme, lhsText):
if case let .voiceMessagesInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: AutodownloadMediaCategoryEntry, rhs: AutodownloadMediaCategoryEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! AutodownloadMediaConnectionTypeControllerArguments
switch self {
case let .master(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleMaster(value)
})
case let .dataUsageHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .dataUsageItem(theme, strings, value, customPosition, enabled):
return AutodownloadDataUsagePickerItem(theme: theme, strings: strings, systemStyle: .glass, value: value, customPosition: customPosition, enabled: enabled, sectionId: self.section, updated: { preset in
arguments.changePreset(preset)
})
case let .typesHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .photos(_, text, value, enabled):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Photos")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: {
arguments.customize(.photo)
})
case let .stories(_, text, value, enabled):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Stories")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: {
arguments.customize(.story)
})
case let .videos(_, text, value, enabled):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Videos")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: {
arguments.customize(.video)
})
case let .files(_, text, value, enabled):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Files")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: {
arguments.customize(.file)
})
case let .voiceMessagesInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
}
}
private struct AutomaticDownloadPeers {
let contacts: Bool
let otherPrivate: Bool
let groups: Bool
let channels: Bool
let size: Int64?
init(category: MediaAutoDownloadCategory) {
self.contacts = category.contacts
self.otherPrivate = category.otherPrivate
self.groups = category.groups
self.channels = category.channels
self.size = category.sizeLimit
}
}
private func stringForAutomaticDownloadPeers(strings: PresentationStrings, decimalSeparator: String, peers: AutomaticDownloadPeers, category: AutomaticDownloadCategory) -> String {
if case .story = category {
if peers.contacts && peers.otherPrivate {
return strings.AutoDownloadSettings_OnForAll
} else if peers.contacts {
return strings.AutoDownloadSettings_OnForContacts
} else {
return strings.AutoDownloadSettings_OffForAll
}
}
var size: String?
if var peersSize = peers.size, category == .video || category == .file {
if peersSize == Int32.max {
peersSize = 1536 * 1024 * 1024
}
size = autodownloadDataSizeString(Int64(peersSize), decimalSeparator: decimalSeparator)
}
if peers.contacts && peers.otherPrivate && peers.groups && peers.channels {
if let size = size {
return strings.AutoDownloadSettings_UpToForAll(size).string
} else {
return strings.AutoDownloadSettings_OnForAll
}
} else {
var types: [String] = []
if peers.contacts {
types.append(strings.AutoDownloadSettings_TypeContacts)
}
if peers.otherPrivate {
types.append(strings.AutoDownloadSettings_TypePrivateChats)
}
if peers.groups {
types.append(strings.AutoDownloadSettings_TypeGroupChats)
}
if peers.channels {
types.append(strings.AutoDownloadSettings_TypeChannels)
}
if types.isEmpty {
return strings.AutoDownloadSettings_OffForAll
}
var string: String = ""
for i in 0 ..< types.count {
if !string.isEmpty {
if i == types.count - 1 {
string.append(strings.AutoDownloadSettings_LastDelimeter)
} else {
string.append(strings.AutoDownloadSettings_Delimeter)
}
}
string.append(types[i])
}
if let size = size {
return strings.AutoDownloadSettings_UpToFor(size, string).string
} else {
return strings.AutoDownloadSettings_OnFor(string).string
}
}
}
private func autodownloadMediaConnectionTypeControllerEntries(presentationData: PresentationData, connectionType: AutomaticDownloadConnectionType, settings: MediaAutoDownloadSettings) -> [AutodownloadMediaCategoryEntry] {
var entries: [AutodownloadMediaCategoryEntry] = []
let connection = settings.connectionSettings(for: connectionType.automaticDownloadNetworkType)
let categories = effectiveAutodownloadCategories(settings: settings, networkType: connectionType.automaticDownloadNetworkType)
let master = connection.enabled
let photo = AutomaticDownloadPeers(category: categories.photo)
let video = AutomaticDownloadPeers(category: categories.video)
let file = AutomaticDownloadPeers(category: categories.file)
let stories = AutomaticDownloadPeers(category: categories.stories)
entries.append(.master(presentationData.theme, presentationData.strings.AutoDownloadSettings_AutoDownload, master))
entries.append(.dataUsageHeader(presentationData.theme, presentationData.strings.AutoDownloadSettings_DataUsage))
var customPosition: Int?
if let custom = connection.custom {
let sortedPresets = [settings.presets.low, settings.presets.medium, settings.presets.high, custom].sorted()
customPosition = sortedPresets.firstIndex(of: custom) ?? 0
}
entries.append(.dataUsageItem(presentationData.theme, presentationData.strings, AutomaticDownloadDataUsage(preset: connection.preset), customPosition, master))
entries.append(.typesHeader(presentationData.theme, presentationData.strings.AutoDownloadSettings_MediaTypes))
entries.append(.photos(presentationData.theme, presentationData.strings.AutoDownloadSettings_Photos, stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: photo, category: .photo), master))
entries.append(.stories(presentationData.theme, presentationData.strings.AutoDownloadSettings_Stories, stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: stories, category: .story), master))
entries.append(.videos(presentationData.theme, presentationData.strings.AutoDownloadSettings_Videos, stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: video, category: .video), master))
entries.append(.files(presentationData.theme, presentationData.strings.AutoDownloadSettings_Files, stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: file, category: .file), master))
entries.append(.voiceMessagesInfo(presentationData.theme, presentationData.strings.AutoDownloadSettings_VoiceMessagesInfo))
return entries
}
func autodownloadMediaConnectionTypeController(context: AccountContext, connectionType: AutomaticDownloadConnectionType) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)?
let arguments = AutodownloadMediaConnectionTypeControllerArguments(toggleMaster: { value in
let _ = updateMediaDownloadSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings
switch connectionType {
case .cellular:
settings.cellular.enabled = value
case .wifi:
settings.wifi.enabled = value
}
return settings
}).start()
}, changePreset: { value in
let _ = updateMediaDownloadSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings
let preset: MediaAutoDownloadPreset
switch value {
case .low:
preset = .low
case .medium:
preset = .medium
case .high:
preset = .high
case .custom:
preset = .custom
}
switch connectionType {
case .cellular:
settings.cellular.preset = preset
case .wifi:
settings.wifi.preset = preset
}
return settings
}).start()
}, customize: { category in
let controller = autodownloadMediaCategoryController(context: context, connectionType: connectionType, category: category)
pushControllerImpl?(controller)
})
let signal = combineLatest(context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]))
|> deliverOnMainQueue
|> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in
var automaticMediaDownloadSettings: MediaAutoDownloadSettings
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]?.get(MediaAutoDownloadSettings.self) {
automaticMediaDownloadSettings = value
} else {
automaticMediaDownloadSettings = MediaAutoDownloadSettings.defaultSettings
}
var autodownloadSettings: AutodownloadSettings
if let value = sharedData.entries[SharedDataKeys.autodownloadSettings]?.get(AutodownloadSettings.self) {
autodownloadSettings = value
automaticMediaDownloadSettings = automaticMediaDownloadSettings.updatedWithAutodownloadSettings(autodownloadSettings)
} else {
autodownloadSettings = .defaultSettings
}
let title: String
switch connectionType {
case .cellular:
title = presentationData.strings.AutoDownloadSettings_CellularTitle
case .wifi:
title = presentationData.strings.AutoDownloadSettings_WifiTitle
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: autodownloadMediaConnectionTypeControllerEntries(presentationData: presentationData, connectionType: connectionType, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
pushControllerImpl = { [weak controller] c in
if let controller = controller {
(controller.navigationController as? NavigationController)?.pushViewController(c)
}
}
return controller
}
@@ -0,0 +1,353 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramUIPreferences
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import ComponentFlow
import SliderComponent
enum AutomaticDownloadDataUsage: Int {
case low
case medium
case high
case custom
init(preset: MediaAutoDownloadPreset) {
switch preset {
case .low:
self = .low
case .medium:
self = .medium
case .high:
self = .high
case .custom:
self = .custom
}
}
}
final class AutodownloadDataUsagePickerItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let systemStyle: ItemListSystemStyle
let value: AutomaticDownloadDataUsage
let customPosition: Int?
let enabled: Bool
let sectionId: ItemListSectionId
let updated: (AutomaticDownloadDataUsage) -> Void
init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle = .legacy, value: AutomaticDownloadDataUsage, customPosition: Int?, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (AutomaticDownloadDataUsage) -> Void) {
self.theme = theme
self.strings = strings
self.systemStyle = systemStyle
self.value = value
self.customPosition = customPosition
self.enabled = enabled
self.sectionId = sectionId
self.updated = updated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = AutodownloadDataUsagePickerItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? AutodownloadDataUsagePickerItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private final class AutodownloadDataUsagePickerItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let lowTextNode: TextNode
private let mediumTextNode: TextNode
private let highTextNode: TextNode
private let customTextNode: TextNode
private let slider = ComponentView<Empty>()
private let activateArea: AccessibilityAreaNode
private var item: AutodownloadDataUsagePickerItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.lowTextNode = TextNode()
self.lowTextNode.isUserInteractionEnabled = false
self.lowTextNode.displaysAsynchronously = false
self.mediumTextNode = TextNode()
self.mediumTextNode.isUserInteractionEnabled = false
self.mediumTextNode.displaysAsynchronously = false
self.highTextNode = TextNode()
self.highTextNode.isUserInteractionEnabled = false
self.highTextNode.displaysAsynchronously = false
self.customTextNode = TextNode()
self.customTextNode.isUserInteractionEnabled = false
self.customTextNode.displaysAsynchronously = false
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.lowTextNode)
self.addSubnode(self.mediumTextNode)
self.addSubnode(self.highTextNode)
self.addSubnode(self.customTextNode)
self.addSubnode(self.activateArea)
// self.activateArea.increment = { [weak self] in
// if let self {
// self.sliderView?.increase()
// }
// }
//
// self.activateArea.decrement = { [weak self] in
// if let self {
// self.sliderView?.decrease()
// }
// }
}
func asyncLayout() -> (_ item: AutodownloadDataUsagePickerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeLowTextLayout = TextNode.asyncLayout(self.lowTextNode)
let makeMediumTextLayout = TextNode.asyncLayout(self.mediumTextNode)
let makeHighTextLayout = TextNode.asyncLayout(self.highTextNode)
let makeCustomTextLayout = TextNode.asyncLayout(self.customTextNode)
return { item, params, neighbors in
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let (lowTextLayout, lowTextApply) = makeLowTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.AutoDownloadSettings_DataUsageLow, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (mediumTextLayout, mediumTextApply) = makeMediumTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.AutoDownloadSettings_DataUsageMedium, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (highTextLayout, highTextApply) = makeHighTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.AutoDownloadSettings_DataUsageHigh, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (customTextLayout, customTextApply) = makeCustomTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.AutoDownloadSettings_DataUsageCustom, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0 //params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
let _ = lowTextApply()
let _ = mediumTextApply()
let _ = highTextApply()
let _ = customTextApply()
var textNodes: [(TextNode, CGSize)] = [(strongSelf.lowTextNode, lowTextLayout.size),
(strongSelf.mediumTextNode, mediumTextLayout.size),
(strongSelf.highTextNode, highTextLayout.size)]
if let customPosition = item.customPosition {
textNodes.insert((strongSelf.customTextNode, customTextLayout.size), at: customPosition)
}
let delta = (params.width - params.leftInset - params.rightInset - 25.0 * 2.0) / CGFloat(textNodes.count - 1)
for i in 0 ..< textNodes.count {
let (textNode, textSize) = textNodes[i]
let leftEdge = params.leftInset + 18.0
let rightEdge = params.width - params.rightInset - 18.0
let position = params.leftInset + 25.0 + delta * CGFloat(i)
let origin = max(leftEdge, min(rightEdge - textSize.width, position - textSize.width / 2.0))
textNode.frame = CGRect(origin: CGPoint(x: origin, y: 15.0), size: textSize)
}
var valueCount = 3
var value = item.value.rawValue
if let customPosition = item.customPosition {
valueCount += 1
if case .custom = item.value {
value = customPosition
} else {
if value >= customPosition {
value += 1
}
}
}
let sliderSize = strongSelf.slider.update(
transition: .immediate,
component: AnyComponent(
SliderComponent(
content: .discrete(.init(
valueCount: valueCount,
value: value,
markPositions: true,
valueUpdated: { [weak self] position in
guard let self else {
return
}
var value: AutomaticDownloadDataUsage?
if let customPosition = self.item?.customPosition {
if position == customPosition {
value = .custom
} else {
value = AutomaticDownloadDataUsage(rawValue: position > customPosition ? (position - 1) : position)
}
} else {
value = AutomaticDownloadDataUsage(rawValue: position)
}
if let value = value {
self.item?.updated(value)
}
}
)),
useNative: true,
trackBackgroundColor: item.theme.list.itemSwitchColors.frameColor,
trackForegroundColor: item.theme.list.itemAccentColor
)
),
environment: {},
containerSize: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0)
)
if let sliderView = strongSelf.slider.view {
if sliderView.superview == nil {
strongSelf.view.addSubview(sliderView)
}
sliderView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - sliderSize.width) / 2.0), y: 37.0), size: sliderSize)
sliderView.isUserInteractionEnabled = item.enabled
sliderView.alpha = item.enabled ? 1.0 : 0.4
sliderView.layer.allowsGroupOpacity = !item.enabled
}
strongSelf.activateArea.accessibilityLabel = item.strings.AutoDownloadSettings_DataUsage
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
}
})
}
}
private func updateAccessibilityLabels() {
// guard let item = self.item else {
// return
// }
// var textNodes: [TextNode] = [self.lowTextNode, self.mediumTextNode, self.highTextNode]
// if let customPosition = item.customPosition {
// textNodes.insert(self.customTextNode, at: customPosition)
// }
// if let value = self.sliderView?.value {
// self.activateArea.accessibilityValue = textNodes[Int(value)].cachedLayout?.attributedString?.string ?? ""
// }
// var accessibilityTraits: UIAccessibilityTraits = [.adjustable]
// if item.enabled {
// } else {
// accessibilityTraits.insert(.notEnabled)
// }
// self.activateArea.accessibilityTraits = accessibilityTraits
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,502 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
public func autodownloadDataSizeString(_ size: Int64, decimalSeparator: String = ".") -> String {
if size >= 1024 * 1024 * 1024 {
var remainder = (size % (1024 * 1024 * 1024)) / (1024 * 1024 * 102)
if remainder == 10 {
remainder = 9
}
while remainder != 0 && remainder % 10 == 0 {
remainder /= 10
}
if remainder != 0 {
return "\(size / (1024 * 1024 * 1024))\(decimalSeparator)\(remainder) GB"
} else {
return "\(size / (1024 * 1024 * 1024)) GB"
}
} else if size >= 1024 * 1024 {
var remainder = (size % (1024 * 1024)) / (1024 * 102)
if remainder == 10 {
remainder = 9
}
while remainder != 0 && remainder % 10 == 0 {
remainder /= 10
}
if size < 10 * 1024 * 1024 {
return "\(size / (1024 * 1024))\(decimalSeparator)\(remainder) MB"
} else {
return "\(size / (1024 * 1024)) MB"
}
} else if size >= 1024 {
return "\(size / 1024) KB"
} else {
return "\(size) B"
}
}
enum AutomaticDownloadCategory {
case photo
case video
case file
case story
}
private enum AutomaticDownloadPeerType {
case contact
case otherPrivate
case group
case channel
}
private final class AutodownloadMediaCategoryControllerArguments {
let togglePeer: (AutomaticDownloadPeerType) -> Void
let adjustSize: (Int64) -> Void
let toggleVideoPreload: () -> Void
init(togglePeer: @escaping (AutomaticDownloadPeerType) -> Void, adjustSize: @escaping (Int64) -> Void, toggleVideoPreload: @escaping () -> Void) {
self.togglePeer = togglePeer
self.adjustSize = adjustSize
self.toggleVideoPreload = toggleVideoPreload
}
}
private enum AutodownloadMediaCategorySection: Int32 {
case peer
case size
}
private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry {
case peerHeader(PresentationTheme, String)
case peerContacts(PresentationTheme, String, Bool)
case peerOtherPrivate(PresentationTheme, String, Bool)
case peerGroups(PresentationTheme, String, Bool)
case peerChannels(PresentationTheme, String, Bool)
case sizeHeader(PresentationTheme, String)
case sizeItem(PresentationTheme, PresentationStrings, String, String, Int64)
case sizePreload(PresentationTheme, String, Bool, Bool)
case sizePreloadInfo(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .peerHeader, .peerContacts, .peerOtherPrivate, .peerGroups, .peerChannels:
return AutodownloadMediaCategorySection.peer.rawValue
case .sizeHeader, .sizeItem, .sizePreload, .sizePreloadInfo:
return AutodownloadMediaCategorySection.size.rawValue
}
}
var stableId: Int32 {
switch self {
case .peerHeader:
return 0
case .peerContacts:
return 1
case .peerOtherPrivate:
return 2
case .peerGroups:
return 3
case .peerChannels:
return 4
case .sizeHeader:
return 5
case .sizeItem:
return 6
case .sizePreload:
return 7
case .sizePreloadInfo:
return 8
}
}
static func ==(lhs: AutodownloadMediaCategoryEntry, rhs: AutodownloadMediaCategoryEntry) -> Bool {
switch lhs {
case let .peerHeader(lhsTheme, lhsText):
if case let .peerHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .peerContacts(lhsTheme, lhsText, lhsValue):
if case let .peerContacts(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .peerOtherPrivate(lhsTheme, lhsText, lhsValue):
if case let .peerOtherPrivate(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .peerGroups(lhsTheme, lhsText, lhsValue):
if case let .peerGroups(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .peerChannels(lhsTheme, lhsText, lhsValue):
if case let .peerChannels(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .sizeHeader(lhsTheme, lhsText):
if case let .sizeHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .sizeItem(lhsTheme, lhsStrings, lhsDecimalSeparator, lhsText, lhsValue):
if case let .sizeItem(rhsTheme, rhsStrings, rhsDecimalSeparator, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDecimalSeparator == rhsDecimalSeparator, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .sizePreload(lhsTheme, lhsText, lhsValue, lhsEnabled):
if case let .sizePreload(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .sizePreloadInfo(lhsTheme, lhsText):
if case let .sizePreloadInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: AutodownloadMediaCategoryEntry, rhs: AutodownloadMediaCategoryEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! AutodownloadMediaCategoryControllerArguments
switch self {
case let .peerHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .peerContacts(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
arguments.togglePeer(.contact)
})
case let .peerOtherPrivate(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
arguments.togglePeer(.otherPrivate)
})
case let .peerGroups(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
arguments.togglePeer(.group)
})
case let .peerChannels(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
arguments.togglePeer(.channel)
})
case let .sizeHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .sizeItem(theme, strings, decimalSeparator, text, value):
return AutodownloadSizeLimitItem(theme: theme, strings: strings, systemStyle: .glass, decimalSeparator: decimalSeparator, text: text, value: value, range: nil, sectionId: self.section, updated: { value in
arguments.adjustSize(value)
})
case let .sizePreload(_, text, value, enabled):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value && enabled, enableInteractiveChanges: true, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleVideoPreload()
})
case let .sizePreloadInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
}
}
private struct AutomaticDownloadPeers {
let contacts: Bool
let otherPrivate: Bool
let groups: Bool
let channels: Bool
init(category: MediaAutoDownloadCategory) {
self.contacts = category.contacts
self.otherPrivate = category.otherPrivate
self.groups = category.groups
self.channels = category.channels
}
}
private func autodownloadMediaCategoryControllerEntries(presentationData: PresentationData, connectionType: AutomaticDownloadConnectionType, category: AutomaticDownloadCategory, settings: MediaAutoDownloadSettings) -> [AutodownloadMediaCategoryEntry] {
var entries: [AutodownloadMediaCategoryEntry] = []
let categories = effectiveAutodownloadCategories(settings: settings, networkType: connectionType.automaticDownloadNetworkType)
let peers: AutomaticDownloadPeers
let size: Int64
let predownload: Bool
switch category {
case .photo:
peers = AutomaticDownloadPeers(category: categories.photo)
size = categories.photo.sizeLimit
predownload = categories.photo.predownload
case .video:
peers = AutomaticDownloadPeers(category: categories.video)
size = categories.video.sizeLimit
predownload = categories.video.predownload
case .file:
peers = AutomaticDownloadPeers(category: categories.file)
size = categories.file.sizeLimit
predownload = categories.file.predownload
case .story:
peers = AutomaticDownloadPeers(category: categories.stories)
size = categories.stories.sizeLimit
predownload = categories.stories.predownload
}
let downloadTitle: String
var sizeTitle: String?
switch category {
case .photo:
downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadPhotos
case .video:
downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadVideos
sizeTitle = presentationData.strings.AutoDownloadSettings_MaxVideoSize
case .file:
downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadFiles
sizeTitle = presentationData.strings.AutoDownloadSettings_MaxFileSize
case .story:
downloadTitle = presentationData.strings.AutoDownloadSettings_StoriesSectionHeader
sizeTitle = presentationData.strings.AutoDownloadSettings_MaxFileSize
}
if case .story = category {
entries.append(.peerContacts(presentationData.theme, presentationData.strings.AutoDownloadSettings_Contacts, peers.contacts))
if peers.contacts {
entries.append(.peerOtherPrivate(presentationData.theme, presentationData.strings.AutoDownloadSettings_StoriesArchivedContacts, peers.otherPrivate))
}
} else {
entries.append(.peerHeader(presentationData.theme, downloadTitle))
entries.append(.peerContacts(presentationData.theme, presentationData.strings.AutoDownloadSettings_Contacts, peers.contacts))
entries.append(.peerOtherPrivate(presentationData.theme, presentationData.strings.AutoDownloadSettings_PrivateChats, peers.otherPrivate))
entries.append(.peerGroups(presentationData.theme, presentationData.strings.AutoDownloadSettings_GroupChats, peers.groups))
entries.append(.peerChannels(presentationData.theme, presentationData.strings.AutoDownloadSettings_Channels, peers.channels))
}
switch category {
case .video, .file:
if let sizeTitle = sizeTitle {
entries.append(.sizeHeader(presentationData.theme, sizeTitle))
}
let sizeText: String
if size == Int64.max {
sizeText = autodownloadDataSizeString(1536 * 1024 * 1024, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
} else {
sizeText = autodownloadDataSizeString(Int64(size), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
}
let text = presentationData.strings.AutoDownloadSettings_UpTo(sizeText).string
entries.append(.sizeItem(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat.decimalSeparator, text, size))
if #available(iOSApplicationExtension 10.3, *), category == .video {
entries.append(.sizePreload(presentationData.theme, presentationData.strings.AutoDownloadSettings_PreloadVideo, predownload, size > 2 * 1024 * 1024))
entries.append(.sizePreloadInfo(presentationData.theme, presentationData.strings.AutoDownloadSettings_PreloadVideoInfo(sizeText).string))
}
default:
break
}
return entries
}
func autodownloadMediaCategoryController(context: AccountContext, connectionType: AutomaticDownloadConnectionType, category: AutomaticDownloadCategory) -> ViewController {
let arguments = AutodownloadMediaCategoryControllerArguments(togglePeer: { type in
let _ = updateMediaDownloadSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings
var categories = effectiveAutodownloadCategories(settings: settings, networkType: connectionType.automaticDownloadNetworkType)
switch category {
case .photo:
switch type {
case .contact:
categories.photo.contacts = !categories.photo.contacts
case .otherPrivate:
categories.photo.otherPrivate = !categories.photo.otherPrivate
case .group:
categories.photo.groups = !categories.photo.groups
case .channel:
categories.photo.channels = !categories.photo.channels
}
case .video:
switch type {
case .contact:
categories.video.contacts = !categories.video.contacts
case .otherPrivate:
categories.video.otherPrivate = !categories.video.otherPrivate
case .group:
categories.video.groups = !categories.video.groups
case .channel:
categories.video.channels = !categories.video.channels
}
case .file:
switch type {
case .contact:
categories.file.contacts = !categories.file.contacts
case .otherPrivate:
categories.file.otherPrivate = !categories.file.otherPrivate
case .group:
categories.file.groups = !categories.file.groups
case .channel:
categories.file.channels = !categories.file.channels
}
case .story:
switch type {
case .contact:
categories.stories.contacts = !categories.stories.contacts
case .otherPrivate:
categories.stories.otherPrivate = !categories.stories.otherPrivate
case .group:
categories.stories.groups = !categories.stories.groups
case .channel:
categories.stories.channels = !categories.stories.channels
}
}
switch connectionType {
case .cellular:
settings.cellular.preset = .custom
settings.cellular.custom = categories
case .wifi:
settings.wifi.preset = .custom
settings.wifi.custom = categories
}
return settings
}).start()
}, adjustSize: { size in
let _ = updateMediaDownloadSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings
var categories = effectiveAutodownloadCategories(settings: settings, networkType: connectionType.automaticDownloadNetworkType)
switch category {
case .photo:
categories.photo.sizeLimit = size
case .video:
categories.video.sizeLimit = size
case .file:
categories.file.sizeLimit = size
case .story:
categories.stories.sizeLimit = size
}
switch connectionType {
case .cellular:
settings.cellular.preset = .custom
settings.cellular.custom = categories
case .wifi:
settings.wifi.preset = .custom
settings.wifi.custom = categories
}
return settings
}).start()
}, toggleVideoPreload: {
let _ = updateMediaDownloadSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings
var categories = effectiveAutodownloadCategories(settings: settings, networkType: connectionType.automaticDownloadNetworkType)
switch category {
case .photo:
categories.photo.predownload = !categories.photo.predownload
case .video:
categories.video.predownload = !categories.video.predownload
case .file:
categories.file.predownload = !categories.file.predownload
case .story:
categories.stories.predownload = !categories.stories.predownload
}
switch connectionType {
case .cellular:
settings.cellular.preset = .custom
settings.cellular.custom = categories
case .wifi:
settings.wifi.preset = .custom
settings.wifi.custom = categories
}
return settings
}).start()
})
let currentAutodownloadSettings = {
return context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings])
|> take(1)
|> map { sharedData -> MediaAutoDownloadSettings in
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]?.get(MediaAutoDownloadSettings.self) {
return value
} else {
return .defaultSettings
}
}
}
let initialValuePromise: Promise<MediaAutoDownloadSettings> = Promise()
initialValuePromise.set(currentAutodownloadSettings())
let signal = combineLatest(context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings])) |> deliverOnMainQueue
|> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in
var automaticMediaDownloadSettings: MediaAutoDownloadSettings
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]?.get(MediaAutoDownloadSettings.self) {
automaticMediaDownloadSettings = value
} else {
automaticMediaDownloadSettings = .defaultSettings
}
var autodownloadSettings: AutodownloadSettings
if let value = sharedData.entries[SharedDataKeys.autodownloadSettings]?.get(AutodownloadSettings.self) {
autodownloadSettings = value
automaticMediaDownloadSettings = automaticMediaDownloadSettings.updatedWithAutodownloadSettings(autodownloadSettings)
} else {
autodownloadSettings = .defaultSettings
}
let title: String
switch category {
case .photo:
title = presentationData.strings.AutoDownloadSettings_PhotosTitle
case .video:
title = presentationData.strings.AutoDownloadSettings_VideosTitle
case .file:
title = presentationData.strings.AutoDownloadSettings_DocumentsTitle
case .story:
title = presentationData.strings.AutoDownloadSettings_StoriesTitle
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: autodownloadMediaCategoryControllerEntries(presentationData: presentationData, connectionType: connectionType, category: category, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: true)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
controller.didDisappear = { _ in
let _ = (combineLatest(initialValuePromise.get() |> take(1), currentAutodownloadSettings())
|> mapToSignal { initialValue, currentValue -> Signal<Void, NoError> in
let initialConnection = initialValue.connectionSettings(for: connectionType.automaticDownloadNetworkType)
let currentConnection = currentValue.connectionSettings(for: connectionType.automaticDownloadNetworkType)
if currentConnection != initialConnection, let categories = currentConnection.custom, currentConnection.preset == .custom {
let preset: SavedAutodownloadPreset
switch connectionType {
case .cellular:
preset = .medium
case .wifi:
preset = .high
}
let settings = AutodownloadPresetSettings(disabled: false, photoSizeMax: categories.photo.sizeLimit, videoSizeMax: categories.video.sizeLimit, fileSizeMax: categories.file.sizeLimit, preloadLargeVideo: categories.video.predownload, lessDataForPhoneCalls: false, videoUploadMaxbitrate: 0)
return saveAutodownloadSettings(account: context.account, preset: preset, settings: settings)
}
return .complete()
}).start()
}
return controller
}
@@ -0,0 +1,308 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import LegacyComponents
import ItemListUI
import PresentationDataUtils
private let autodownloadSizeValues: [(CGFloat, Int64)] = [
(0.000, 512 * 1024),
(0.257, 1024 * 1024),
(0.520, 10 * 1024 * 1024),
(0.763, 100 * 1024 * 1024),
(1.000, 1536 * 1024 * 1024)
]
private func sliderValue(for size: Int64) -> CGFloat {
for i in 1 ..< autodownloadSizeValues.count {
let (previousValue, previousValueSize) = autodownloadSizeValues[i - 1]
let (value, valueSize) = autodownloadSizeValues[i]
if valueSize > size {
return previousValue + CGFloat(size - previousValueSize) / CGFloat(valueSize - previousValueSize) * (value - previousValue)
} else if previousValueSize == size {
return previousValue
} else if valueSize == size || i == autodownloadSizeValues.count - 1 {
return value
}
}
return 0.0
}
private func sizeValue(for sliderValue: CGFloat) -> Int64 {
for i in 1 ..< autodownloadSizeValues.count {
let (previousValue, previousValueSize) = autodownloadSizeValues[i - 1]
let (value, valueSize) = autodownloadSizeValues[i]
if value > sliderValue {
let delta = (sliderValue - previousValue) / (value - previousValue) * CGFloat(valueSize - previousValueSize)
return previousValueSize + Int64(delta)
} else if previousValue == sliderValue {
return previousValueSize
} else if value == sliderValue || i == autodownloadSizeValues.count - 1 {
return valueSize
}
}
return 0
}
final class AutodownloadSizeLimitItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let systemStyle: ItemListSystemStyle
let decimalSeparator: String
let text: String
let value: Int64
let range: Range<Int64>?
let sectionId: ItemListSectionId
let updated: (Int64) -> Void
init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle = .legacy, decimalSeparator: String, text: String, value: Int64, range: Range<Int64>?, sectionId: ItemListSectionId, updated: @escaping (Int64) -> Void) {
self.theme = theme
self.strings = strings
self.systemStyle = systemStyle
self.decimalSeparator = decimalSeparator
self.text = text
self.value = value
self.range = range
self.sectionId = sectionId
self.updated = updated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = AutodownloadSizeLimitItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? AutodownloadSizeLimitItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private final class AutodownloadSizeLimitItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let minTextNode: TextNode
private let maxTextNode: TextNode
private let textNode: TextNode
private var sliderView: TGPhotoEditorSliderView?
private var item: AutodownloadSizeLimitItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.minTextNode = TextNode()
self.minTextNode.isUserInteractionEnabled = false
self.minTextNode.displaysAsynchronously = false
self.maxTextNode = TextNode()
self.maxTextNode.isUserInteractionEnabled = false
self.maxTextNode.displaysAsynchronously = false
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.textNode)
self.addSubnode(self.minTextNode)
self.addSubnode(self.maxTextNode)
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 2.0
sliderView.lineSize = 4.0
sliderView.dotSize = 5.0
sliderView.minimumValue = 0.0
sliderView.maximumValue = 1.0
sliderView.startValue = 0.0
sliderView.displayEdges = true
sliderView.disablesInteractiveTransitionGestureRecognizer = true
if let item = self.item, let params = self.layoutParams {
sliderView.value = sliderValue(for: item.value)
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.startColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
}
self.view.addSubview(sliderView)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
}
func asyncLayout() -> (_ item: AutodownloadSizeLimitItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let makeMinTextLayout = TextNode.asyncLayout(self.minTextNode)
let makeMaxTextLayout = TextNode.asyncLayout(self.maxTextNode)
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let formatting = DataSizeStringFormatting(strings: item.strings, decimalSeparator: item.decimalSeparator)
let range = item.range ?? (512 * 1024 ..< 1536 * 1024 * 1024)
let (minTextLayout, minTextApply) = makeMinTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: dataSizeString(range.lowerBound, formatting: formatting), font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (maxTextLayout, maxTextApply) = makeMaxTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: dataSizeString(range.upperBound, formatting: formatting), font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - separatorRightInset, height: separatorHeight))
let _ = textApply()
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.size.width) / 2.0), y: 12.0), size: textLayout.size)
let _ = minTextApply()
strongSelf.minTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 16.0, y: 16.0), size: minTextLayout.size)
let _ = maxTextApply()
strongSelf.maxTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 16.0 - maxTextLayout.size.width, y: 16.0), size: maxTextLayout.size)
if let sliderView = strongSelf.sliderView {
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
}
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func sliderValueChanged() {
guard let sliderView = self.sliderView else {
return
}
let value = sizeValue(for: sliderView.value)
self.item?.updated(value)
}
}
@@ -0,0 +1,235 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import ActivityIndicator
final class CalculatingCacheSizeItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let title: String
let sectionId: ItemListSectionId
let style: ItemListStyle
init(theme: PresentationTheme, title: String, sectionId: ItemListSectionId, style: ItemListStyle) {
self.theme = theme
self.title = title
self.sectionId = sectionId
self.style = style
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = CalculatingCacheSizeItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? CalculatingCacheSizeItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private let titleFont = Font.regular(14.0)
private final class CalculatingCacheSizeItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private var activityIndicator: ActivityIndicator?
private let titleNode: TextNode
private var item: CalculatingCacheSizeItem?
var tag: Any? {
return self.item?.tag
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
}
func asyncLayout() -> (_ item: CalculatingCacheSizeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
switch item.style {
case .plain:
itemBackgroundColor = item.theme.list.plainBackgroundColor
itemSeparatorColor = item.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0 + 24.0)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0 + 24.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
let activityIndicator: ActivityIndicator
if let current = strongSelf.activityIndicator {
activityIndicator = current
} else {
activityIndicator = ActivityIndicator(type: .custom(item.theme.list.itemAccentColor, 20.0, 2.0, false), speed: ActivityIndicatorSpeed.slow)
strongSelf.activityIndicator = activityIndicator
strongSelf.addSubnode(activityIndicator)
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
activityIndicator.type = .custom(item.theme.list.itemAccentColor, 20.0, 2.0, false)
}
let _ = titleApply()
let leftInset: CGFloat
switch item.style {
case .plain:
leftInset = 35.0 + params.leftInset
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
leftInset = 16.0 + params.leftInset
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 16.0 + params.leftInset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((params.width - params.leftInset - params.rightInset - titleLayout.size.width) / 2.0), y: 11.0 + 24.0), size: titleLayout.size)
activityIndicator.frame = CGRect(origin: CGPoint(x: floor((params.width - 20.0) / 2.0), y: 8.0), size: CGSize(width: 20.0, height: 20.0))
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,321 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import UndoUI
enum ItemType: CaseIterable {
case autoplayVideo
case autoplayGif
case loopStickers
case loopEmoji
case fullTranslucency
case autodownloadInBackground
case extendBackgroundWork
var settingsKeyPath: WritableKeyPath<EnergyUsageSettings, Bool> {
switch self {
case .autoplayVideo:
return \.autoplayVideo
case .autoplayGif:
return \.autoplayGif
case .loopStickers:
return \.loopStickers
case .loopEmoji:
return \.loopEmoji
case .fullTranslucency:
return \.fullTranslucency
case .extendBackgroundWork:
return \.extendBackgroundWork
case .autodownloadInBackground:
return \.autodownloadInBackground
}
}
func title(strings: PresentationStrings) -> (String, String, String) {
switch self {
case .autoplayVideo:
return (
"Settings/Power/PowerIconVideo",
strings.PowerSavingScreen_OptionAutoplayVideoTitle,
strings.PowerSavingScreen_OptionAutoplayVideoText
)
case .autoplayGif:
return (
"Settings/Power/PowerIconGif",
strings.PowerSavingScreen_OptionAutoplayGifTitle,
strings.PowerSavingScreen_OptionAutoplayGifText
)
case .loopStickers:
return (
"Settings/Power/PowerIconStickers",
strings.PowerSavingScreen_OptionAutoplayStickersTitle,
strings.PowerSavingScreen_OptionAutoplayStickersText
)
case .loopEmoji:
return (
"Settings/Power/PowerIconEmoji",
strings.PowerSavingScreen_OptionAutoplayEmojiTitle,
strings.PowerSavingScreen_OptionAutoplayEmojiText
)
case .fullTranslucency:
return (
"Settings/Power/PowerIconEffects",
strings.PowerSavingScreen_OptionAutoplayEffectsTitle,
strings.PowerSavingScreen_OptionAutoplayEffectsText
)
case .extendBackgroundWork:
return (
"Settings/Power/PowerIconBackgroundTime",
strings.PowerSavingScreen_OptionBackgroundTitle,
strings.PowerSavingScreen_OptionBackgroundText
)
case .autodownloadInBackground:
return (
"Settings/Power/PowerIconMedia",
strings.PowerSavingScreen_OptionPreloadTitle,
strings.PowerSavingScreen_OptionPreloadText
)
}
}
}
private final class EnergeSavingSettingsScreenArguments {
let updateThreshold: (Int32) -> Void
let toggleItem: (ItemType) -> Void
let displayDisabledTooltip: () -> Void
init(updateThreshold: @escaping (Int32) -> Void, toggleItem: @escaping (ItemType) -> Void, displayDisabledTooltip: @escaping () -> Void) {
self.updateThreshold = updateThreshold
self.toggleItem = toggleItem
self.displayDisabledTooltip = displayDisabledTooltip
}
}
private enum EnergeSavingSettingsScreenSection: Int32 {
case all
case items
}
private enum EnergeSavingSettingsScreenEntry: ItemListNodeEntry {
enum StableId: Hashable {
case allHeader
case all
case allFooter
case itemsHeader
case item(ItemType)
}
case allHeader(Bool?)
case all(Int32)
case allFooter(String)
case item(index: Int, type: ItemType, value: Bool, enabled: Bool)
case itemsHeader
var section: ItemListSectionId {
switch self {
case .allHeader, .all, .allFooter:
return EnergeSavingSettingsScreenSection.all.rawValue
case .item, .itemsHeader:
return EnergeSavingSettingsScreenSection.items.rawValue
}
}
var sortIndex: Int {
switch self {
case .allHeader:
return -4
case .all:
return -3
case .allFooter:
return -2
case .itemsHeader:
return -1
case let .item(index, _, _, _):
return index
}
}
var stableId: StableId {
switch self {
case .allHeader:
return .allHeader
case .all:
return .all
case .allFooter:
return .allFooter
case .itemsHeader:
return .itemsHeader
case let .item(_, type, _, _):
return .item(type)
}
}
static func <(lhs: EnergeSavingSettingsScreenEntry, rhs: EnergeSavingSettingsScreenEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! EnergeSavingSettingsScreenArguments
switch self {
case let .allHeader(value):
let text: String
if let value {
if value {
text = presentationData.strings.PowerSavingScreen_ToggleHeaderOn
} else {
text = presentationData.strings.PowerSavingScreen_ToggleHeaderOff
}
} else {
text = presentationData.strings.PowerSavingScreen_ToggleHeaderEmpty
}
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .all(value):
return EnergyUsageBatteryLevelItem(
systemStyle: .glass,
theme: presentationData.theme,
strings: presentationData.strings,
value: value,
sectionId: self.section,
updated: { value in
arguments.updateThreshold(value)
}
)
case let .allFooter(text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case .itemsHeader:
return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.PowerSavingScreen_OptionsHeader, sectionId: self.section)
case let .item(_, type, value, enabled):
let (iconName, title, text) = type.title(strings: presentationData.strings)
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: iconName)?.precomposed(), title: title, text: text, value: value, enableInteractiveChanges: true, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleItem(type)
}, activatedWhileDisabled: {
arguments.displayDisabledTooltip()
})
}
}
}
private func energeSavingSettingsScreenEntries(
presentationData: PresentationData,
settings: MediaAutoDownloadSettings
) -> [EnergeSavingSettingsScreenEntry] {
var entries: [EnergeSavingSettingsScreenEntry] = []
let isOn = automaticEnergyUsageShouldBeOnNow(settings: settings)
let allIsOn: Bool?
if settings.energyUsageSettings.activationThreshold <= 4 || settings.energyUsageSettings.activationThreshold >= 96 {
allIsOn = nil
} else {
allIsOn = isOn
}
entries.append(.allHeader(allIsOn))
entries.append(.all(settings.energyUsageSettings.activationThreshold))
let allText: String
if settings.energyUsageSettings.activationThreshold <= 4 {
allText = presentationData.strings.PowerSaving_AllDescriptionNever
} else if settings.energyUsageSettings.activationThreshold >= 96 {
allText = presentationData.strings.PowerSaving_AllDescriptionAlways
} else {
allText = presentationData.strings.PowerSaving_AllDescriptionLimit("\(settings.energyUsageSettings.activationThreshold)").string
}
entries.append(.allFooter(allText))
let itemsEnabled: Bool
if settings.energyUsageSettings.activationThreshold <= 4 {
itemsEnabled = true
} else if settings.energyUsageSettings.activationThreshold >= 96 {
itemsEnabled = false
} else if isOn {
itemsEnabled = false
} else {
itemsEnabled = true
}
entries.append(.itemsHeader)
for type in ItemType.allCases {
entries.append(.item(index: entries.count, type: type, value: settings.energyUsageSettings[keyPath: type.settingsKeyPath] && itemsEnabled, enabled: itemsEnabled))
}
return entries
}
public func energySavingSettingsScreen(context: AccountContext) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)?
let _ = pushControllerImpl
var displayTooltipImpl: ((UndoOverlayContent) -> Void)?
let arguments = EnergeSavingSettingsScreenArguments(
updateThreshold: { value in
let _ = updateMediaDownloadSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings
settings.energyUsageSettings.activationThreshold = max(4, min(96, value))
return settings
}).start()
},
toggleItem: { type in
let _ = updateMediaDownloadSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings
settings.energyUsageSettings[keyPath: type.settingsKeyPath] = !settings.energyUsageSettings[keyPath: type.settingsKeyPath]
return settings
}).start()
},
displayDisabledTooltip: {
let text: String
if context.sharedContext.currentAutomaticMediaDownloadSettings.energyUsageSettings.activationThreshold >= 96 {
text = (context.sharedContext.currentPresentationData.with { $0 }).strings.PowerSavingScreen_OptionChangeAlertAlways
} else {
text = (context.sharedContext.currentPresentationData.with { $0 }).strings.PowerSavingScreen_OptionChangeAlertConditional
}
displayTooltipImpl?(.universal(animation: "lowbattery_30", scale: 1.0, colors: [:], title: nil, text: text, customUndoText: nil, timeout: 5.0))
}
)
let signal = combineLatest(
context.sharedContext.presentationData,
context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]))
|> deliverOnMainQueue
|> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in
var automaticMediaDownloadSettings: MediaAutoDownloadSettings
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]?.get(MediaAutoDownloadSettings.self) {
automaticMediaDownloadSettings = value
} else {
automaticMediaDownloadSettings = MediaAutoDownloadSettings.defaultSettings
}
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
title: .text(presentationData.strings.PowerSavingScreen_Title),
leftNavigationButton: nil,
rightNavigationButton: nil,
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
animateChanges: false
)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: energeSavingSettingsScreenEntries(presentationData: presentationData, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: true)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
pushControllerImpl = { [weak controller] c in
if let controller = controller {
(controller.navigationController as? NavigationController)?.pushViewController(c)
}
}
displayTooltipImpl = { [weak controller] c in
if let controller = controller {
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
controller.present(UndoOverlayController(presentationData: presentationData, content: c, elevatedLayout: false, action: { _ in return false }), in: .current)
}
}
return controller
}
@@ -0,0 +1,320 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AppBundle
import ComponentFlow
import SliderComponent
class EnergyUsageBatteryLevelItem: ListViewItem, ItemListItem {
let systemStyle: ItemListSystemStyle
let theme: PresentationTheme
let strings: PresentationStrings
let value: Int32
let sectionId: ItemListSectionId
let updated: (Int32) -> Void
init(systemStyle: ItemListSystemStyle = .legacy, theme: PresentationTheme, strings: PresentationStrings, value: Int32, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.systemStyle = systemStyle
self.theme = theme
self.strings = strings
self.value = value
self.sectionId = sectionId
self.updated = updated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = EnergyUsageBatteryLevelItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? EnergyUsageBatteryLevelItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private func rescaleBatteryValueToSlider(_ value: CGFloat) -> CGFloat {
var result = (value - 0.04) / (0.96 - 0.04)
result = max(0.0, min(1.0, result))
return result
}
private func rescaleSliderToBatteryValue(_ value: CGFloat) -> CGFloat {
return 0.04 + (0.96 - 0.04) * value
}
class EnergyUsageBatteryLevelItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let leftTextNode: ImmediateTextNode
private let rightTextNode: ImmediateTextNode
private let centerTextNode: ImmediateTextNode
private let centerMeasureTextNode: ImmediateTextNode
private let slider = ComponentView<Empty>()
private let batteryImage: UIImage?
private let batteryBackgroundNode: ASImageNode
private let batteryForegroundNode: ASImageNode
private var item: EnergyUsageBatteryLevelItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.leftTextNode = ImmediateTextNode()
self.rightTextNode = ImmediateTextNode()
self.centerTextNode = ImmediateTextNode()
self.centerMeasureTextNode = ImmediateTextNode()
self.batteryImage = UIImage(bundleImageName: "Settings/UsageBatteryFrame")
self.batteryBackgroundNode = ASImageNode()
self.batteryForegroundNode = ASImageNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.leftTextNode)
self.addSubnode(self.rightTextNode)
self.addSubnode(self.centerTextNode)
self.addSubnode(self.batteryBackgroundNode)
self.addSubnode(self.batteryForegroundNode)
}
func asyncLayout() -> (_ item: EnergyUsageBatteryLevelItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, neighbors in
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
var verticalInset: CGFloat = 0.0
if case .glass = item.systemStyle {
verticalInset = 4.0
}
contentSize = CGSize(width: params.width, height: 88.0 + verticalInset * 2.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))
strongSelf.leftTextNode.attributedText = NSAttributedString(string: item.strings.PowerSaving_BatteryLevelLimit_Off, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor)
strongSelf.rightTextNode.attributedText = NSAttributedString(string: item.strings.PowerSaving_BatteryLevelLimit_On, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor)
let centralText: String
let centralMeasureText: String
if item.value <= 4 {
centralText = item.strings.PowerSaving_BatteryLevelLimit_AlwaysOff
centralMeasureText = centralText
strongSelf.batteryBackgroundNode.isHidden = true
} else if item.value >= 96 {
centralText = item.strings.PowerSaving_BatteryLevelLimit_AlwaysOn
centralMeasureText = centralText
strongSelf.batteryBackgroundNode.isHidden = true
} else {
centralText = item.strings.PowerSaving_BatteryLevelLimit_WhenBelow("\(item.value)").string
centralMeasureText = item.strings.PowerSaving_BatteryLevelLimit_WhenBelow("99").string
strongSelf.batteryBackgroundNode.isHidden = false
}
strongSelf.batteryForegroundNode.isHidden = strongSelf.batteryBackgroundNode.isHidden
strongSelf.centerTextNode.attributedText = NSAttributedString(string: centralText, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor)
strongSelf.centerMeasureTextNode.attributedText = NSAttributedString(string: centralMeasureText, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor)
let leftTextSize = strongSelf.leftTextNode.updateLayout(CGSize(width: 100.0, height: 100.0))
let rightTextSize = strongSelf.rightTextNode.updateLayout(CGSize(width: 100.0, height: 100.0))
let centerTextSize = strongSelf.centerTextNode.updateLayout(CGSize(width: 200.0, height: 100.0))
let centerMeasureTextSize = strongSelf.centerMeasureTextNode.updateLayout(CGSize(width: 200.0, height: 100.0))
let sideInset: CGFloat = 18.0
strongSelf.leftTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + sideInset, y: 15.0 + verticalInset), size: leftTextSize)
strongSelf.rightTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.leftInset - sideInset - rightTextSize.width, y: 15.0 + verticalInset), size: rightTextSize)
var centerFrame = CGRect(origin: CGPoint(x: floor((params.width - centerMeasureTextSize.width) / 2.0), y: 11.0 + verticalInset), size: centerTextSize)
if !strongSelf.batteryBackgroundNode.isHidden {
centerFrame.origin.x -= 12.0
}
strongSelf.centerTextNode.frame = centerFrame
if let frameImage = strongSelf.batteryImage {
strongSelf.batteryBackgroundNode.image = generateImage(frameImage.size, rotatedContext: { size, context in
UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size))
if let image = generateTintedImage(image: frameImage, color: item.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.9)) {
image.draw(in: CGRect(origin: CGPoint(), size: size))
let contentRect = CGRect(origin: CGPoint(x: 3.0, y: (size.height - 9.0) * 0.5), size: CGSize(width: 20.8, height: 9.0))
context.addPath(UIBezierPath(roundedRect: contentRect, cornerRadius: 2.0).cgPath)
context.clip()
}
UIGraphicsPopContext()
})
strongSelf.batteryForegroundNode.image = generateImage(frameImage.size, rotatedContext: { size, context in
UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size))
let contentRect = CGRect(origin: CGPoint(x: 3.0, y: (size.height - 9.0) * 0.5), size: CGSize(width: 20.8, height: 9.0))
context.addPath(UIBezierPath(roundedRect: contentRect, cornerRadius: 2.0).cgPath)
context.clip()
context.setFillColor(UIColor.white.cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: contentRect.origin, size: CGSize(width: contentRect.width * CGFloat(item.value) / 100.0, height: contentRect.height)), cornerRadius: 1.0).cgPath)
context.fillPath()
UIGraphicsPopContext()
})
let batteryColor: UIColor
if item.value <= 20 {
batteryColor = UIColor(rgb: 0xFF3B30)
} else {
batteryColor = item.theme.list.itemSwitchColors.positiveColor
}
if strongSelf.batteryForegroundNode.layer.layerTintColor == nil {
strongSelf.batteryForegroundNode.layer.layerTintColor = batteryColor.cgColor
} else {
ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut).updateTintColor(layer: strongSelf.batteryForegroundNode.layer, color: batteryColor)
}
strongSelf.batteryBackgroundNode.frame = CGRect(origin: CGPoint(x: centerFrame.minX + centerMeasureTextSize.width + 4.0, y: floor(centerFrame.midY - frameImage.size.height * 0.5)), size: frameImage.size)
strongSelf.batteryForegroundNode.frame = strongSelf.batteryBackgroundNode.frame
}
let sliderSize = strongSelf.slider.update(
transition: .immediate,
component: AnyComponent(
SliderComponent(
content: .continuous(.init(
value: rescaleBatteryValueToSlider(CGFloat(item.value) / 100.0),
minValue: nil,
valueUpdated: { [weak self] value in
self?.item?.updated(Int32(rescaleSliderToBatteryValue(value) * 100.0))
}
)),
useNative: true,
trackBackgroundColor: item.theme.list.itemSwitchColors.frameColor,
trackForegroundColor: item.theme.list.itemAccentColor
)
),
environment: {},
containerSize: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0)
)
if let sliderView = strongSelf.slider.view {
if sliderView.superview == nil {
strongSelf.view.addSubview(sliderView)
}
sliderView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - sliderSize.width) / 2.0), y: 36.0 + verticalInset), size: sliderSize)
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,321 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import ItemListPeerItem
import AccountContext
import TelegramIntents
import AccountUtils
private final class IntentsSettingsControllerArguments {
let context: AccountContext
let updateSettings: (@escaping (IntentsSettings) -> IntentsSettings) -> Void
let resetAll: () -> Void
init(context: AccountContext, updateSettings: @escaping (@escaping (IntentsSettings) -> IntentsSettings) -> Void, resetAll: @escaping () -> Void) {
self.context = context
self.updateSettings = updateSettings
self.resetAll = resetAll
}
}
private enum IntentsSettingsSection: Int32 {
case account
case chats
case suggest
case reset
}
private enum IntentsSettingsControllerEntry: ItemListNodeEntry {
case accountHeader(PresentationTheme, String)
case account(PresentationTheme, EnginePeer, Bool, Int32)
case accountInfo(PresentationTheme, String)
case chatsHeader(PresentationTheme, String)
case contacts(PresentationTheme, String, Bool)
case savedMessages(PresentationTheme, String, Bool)
case privateChats(PresentationTheme, String, Bool)
case groups(PresentationTheme, String, Bool)
case chatsInfo(PresentationTheme, String)
case suggestHeader(PresentationTheme, String)
case suggestAll(PresentationTheme, String, Bool)
case suggestOnlyShared(PresentationTheme, String, Bool)
case resetAll(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .accountHeader, .account, .accountInfo:
return IntentsSettingsSection.account.rawValue
case .chatsHeader, .contacts, .savedMessages, .privateChats, .groups, .chatsInfo:
return IntentsSettingsSection.chats.rawValue
case .suggestHeader, .suggestAll, .suggestOnlyShared:
return IntentsSettingsSection.suggest.rawValue
case .resetAll:
return IntentsSettingsSection.reset.rawValue
}
}
var stableId: Int32 {
switch self {
case .accountHeader:
return 0
case let .account(_, _, _, index):
return 1 + index
case .accountInfo:
return 1000
case .chatsHeader:
return 1001
case .contacts:
return 1002
case .savedMessages:
return 1003
case .privateChats:
return 1004
case .groups:
return 1005
case .chatsInfo:
return 1006
case .suggestHeader:
return 1007
case .suggestAll:
return 1008
case .suggestOnlyShared:
return 1009
case .resetAll:
return 1010
}
}
static func ==(lhs: IntentsSettingsControllerEntry, rhs: IntentsSettingsControllerEntry) -> Bool {
switch lhs {
case let .accountHeader(lhsTheme, lhsText):
if case let .accountHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .account(lhsTheme, lhsPeer, lhsSelected, lhsIndex):
if case let .account(rhsTheme, rhsPeer, rhsSelected, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsPeer == rhsPeer, lhsSelected == rhsSelected, lhsIndex == rhsIndex {
return true
} else {
return false
}
case let .accountInfo(lhsTheme, lhsText):
if case let .accountInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .chatsHeader(lhsTheme, lhsText):
if case let .chatsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .contacts(lhsTheme, lhsText, lhsValue):
if case let .contacts(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .savedMessages(lhsTheme, lhsText, lhsValue):
if case let .savedMessages(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .privateChats(lhsTheme, lhsText, lhsValue):
if case let .privateChats(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .groups(lhsTheme, lhsText, lhsValue):
if case let .groups(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .chatsInfo(lhsTheme, lhsText):
if case let .chatsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .suggestHeader(lhsTheme, lhsText):
if case let .suggestHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .suggestAll(lhsTheme, lhsText, lhsValue):
if case let .suggestAll(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .suggestOnlyShared(lhsTheme, lhsText, lhsValue):
if case let .suggestOnlyShared(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .resetAll(lhsTheme, lhsText):
if case let .resetAll(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: IntentsSettingsControllerEntry, rhs: IntentsSettingsControllerEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! IntentsSettingsControllerArguments
switch self {
case let .accountHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .account(_, peer, selected, _):
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: .firstLast, context: arguments.context.sharedContext.makeTempAccountContext(account: arguments.context.account), peer: peer, height: .generic, aliasHandling: .standard, nameStyle: .plain, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: false), revealOptions: nil, switchValue: ItemListPeerItemSwitch(value: selected, style: .check), enabled: true, selectable: true, sectionId: self.section, action: {
arguments.updateSettings { $0.withUpdatedAccount(peer.id) }
}, setPeerIdWithRevealedOptions: { _, _ in}, removePeer: { _ in })
case let .accountInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .chatsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .contacts(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { $0.withUpdatedContacts(value) }
})
case let .savedMessages(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { $0.withUpdatedSavedMessages(value) }
})
case let .privateChats(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { $0.withUpdatedPrivateChats(value) }
})
case let .groups(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateSettings { $0.withUpdatedGroups(value) }
})
case let .chatsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .suggestHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .suggestAll(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateSettings { $0.withUpdatedOnlyShared(false) }
})
case let .suggestOnlyShared(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateSettings { $0.withUpdatedOnlyShared(true) }
})
case let .resetAll(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.resetAll()
})
}
}
}
private func intentsSettingsControllerEntries(context: AccountContext, presentationData: PresentationData, settings: IntentsSettings, accounts: [(Account, EnginePeer)]) -> [IntentsSettingsControllerEntry] {
var entries: [IntentsSettingsControllerEntry] = []
if accounts.count > 1 {
entries.append(.accountHeader(presentationData.theme, presentationData.strings.IntentsSettings_MainAccount.uppercased()))
var index: Int32 = 0
for (_, peer) in accounts {
entries.append(.account(presentationData.theme, peer, peer.id == settings.account, index))
index += 1
}
entries.append(.accountInfo(presentationData.theme, presentationData.strings.IntentsSettings_MainAccountInfo))
}
entries.append(.chatsHeader(presentationData.theme, presentationData.strings.IntentsSettings_SuggestedChats.uppercased()))
entries.append(.contacts(presentationData.theme, presentationData.strings.IntentsSettings_SuggestedChatsContacts, settings.contacts))
entries.append(.savedMessages(presentationData.theme, presentationData.strings.IntentsSettings_SuggestedChatsSavedMessages, settings.savedMessages))
entries.append(.privateChats(presentationData.theme, presentationData.strings.IntentsSettings_SuggestedChatsPrivateChats, settings.privateChats))
entries.append(.groups(presentationData.theme, presentationData.strings.IntentsSettings_SuggestedChatsGroups, settings.groups))
entries.append(.chatsInfo(presentationData.theme, presentationData.strings.IntentsSettings_SuggestedAndSpotlightChatsInfo))
entries.append(.suggestHeader(presentationData.theme, presentationData.strings.IntentsSettings_SuggestBy.uppercased()))
entries.append(.suggestAll(presentationData.theme, presentationData.strings.IntentsSettings_SuggestByAll, !settings.onlyShared))
entries.append(.suggestOnlyShared(presentationData.theme, presentationData.strings.IntentsSettings_SuggestByShare, settings.onlyShared))
entries.append(.resetAll(presentationData.theme, presentationData.strings.IntentsSettings_ResetAll))
return entries
}
public func intentsSettingsController(context: AccountContext) -> ViewController {
var presentControllerImpl: ((ViewController) -> Void)?
let arguments = IntentsSettingsControllerArguments(context: context, updateSettings: { f in
let _ = updateIntentsSettingsInteractively(accountManager: context.sharedContext.accountManager, f).start(next: { previous, updated in
guard let previous = previous, let updated = updated else {
return
}
if previous.contacts && !updated.contacts {
deleteAllSendMessageIntents()
}
if previous.savedMessages && !updated.savedMessages {
deleteAllSendMessageIntents()
}
if previous.privateChats && !updated.privateChats {
deleteAllSendMessageIntents()
}
if previous.groups && !updated.groups {
deleteAllSendMessageIntents()
}
if previous.account != updated.account, let _ = previous.account {
deleteAllSendMessageIntents()
}
})
}, resetAll: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.IntentsSettings_Reset, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
deleteAllSendMessageIntents()
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet)
})
let signal = combineLatest(context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.intentsSettings]), activeAccountsAndPeers(context: context, includePrimary: true))
|> deliverOnMainQueue
|> map { presentationData, sharedData, accounts -> (ItemListControllerState, (ItemListNodeState, Any)) in
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.intentsSettings]?.get(IntentsSettings.self) ?? IntentsSettings.defaultSettings
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.IntentsSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: intentsSettingsControllerEntries(context: context, presentationData: presentationData, settings: settings, accounts: accounts.1.map { ($0.0.account, $0.1) }), style: .blocks, animateChanges: false)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
presentControllerImpl = { [weak controller] c in
controller?.present(c, in: .window(.root))
}
return controller
}
@@ -0,0 +1,307 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramUIPreferences
import TelegramPresentationData
import LegacyComponents
import ItemListUI
import PresentationDataUtils
private func stringForKeepMediaTimeout(strings: PresentationStrings, timeout: Int32) -> String {
if timeout > 1 * 31 * 24 * 60 * 60 {
return strings.ClearCache_Forever
} else {
return timeIntervalString(strings: strings, value: timeout)
}
}
private let keepMediaTimeoutValues: [Int32] = [
3 * 24 * 60 * 60,
7 * 24 * 60 * 60,
1 * 31 * 24 * 60 * 60,
Int32.max
]
final class KeepMediaDurationPickerItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let value: Int32
let sectionId: ItemListSectionId
let updated: (Int32) -> Void
init(theme: PresentationTheme, strings: PresentationStrings, value: Int32, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.value = value
self.sectionId = sectionId
self.updated = updated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = KeepMediaDurationPickerItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? KeepMediaDurationPickerItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private final class KeepMediaDurationPickerItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let textNodes: [TextNode]
private var sliderView: TGPhotoEditorSliderView?
private var item: KeepMediaDurationPickerItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
var textNodes: [TextNode] = []
for _ in 0 ..< 4 {
let textNode = TextNode()
textNode.isUserInteractionEnabled = false
textNode.displaysAsynchronously = false
textNodes.append(textNode)
}
self.textNodes = textNodes
super.init(layerBacked: false, dynamicBounce: false)
for textNode in textNodes {
self.addSubnode(textNode)
}
}
func updateSliderView() {
if let sliderView = self.sliderView, let item = self.item {
sliderView.maximumValue = 3.0
sliderView.positionsCount = 4
let value = keepMediaTimeoutValues.firstIndex(where: { $0 == item.value }) ?? 0
sliderView.value = CGFloat(value)
}
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 2.0
sliderView.lineSize = 4.0
sliderView.dotSize = 5.0
sliderView.minimumValue = 0.0
sliderView.maximumValue = 3.0
sliderView.startValue = 0.0
sliderView.disablesInteractiveTransitionGestureRecognizer = true
sliderView.positionsCount = 4
sliderView.useLinesForPositions = true
if let item = self.item, let params = self.layoutParams {
let value = keepMediaTimeoutValues.firstIndex(where: { $0 == item.value }) ?? 0
sliderView.value = CGFloat(value)
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor.blitOver(item.theme.list.itemBlocksBackgroundColor, alpha: 1.0)
sliderView.startColor = item.theme.list.itemSwitchColors.frameColor.blitOver(item.theme.list.itemBlocksBackgroundColor, alpha: 1.0)
sliderView.trackColor = item.theme.list.itemAccentColor.blitOver(item.theme.list.itemBlocksBackgroundColor, alpha: 1.0)
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
}
self.view.addSubview(sliderView)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
self.updateSliderView()
}
func asyncLayout() -> (_ item: KeepMediaDurationPickerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
var makeTextLayouts: [(TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)] = []
for textNode in self.textNodes {
makeTextLayouts.append(TextNode.asyncLayout(textNode))
}
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
var textLayouts: [TextNodeLayout] = []
var textApplies: [() -> TextNode] = []
for i in 0 ..< makeTextLayouts.count {
let makeTextLayout = makeTextLayouts[i]
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: stringForKeepMediaTimeout(strings: item.strings, timeout: keepMediaTimeoutValues[i]), font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
textLayouts.append(textLayout)
textApplies.append(textApply)
}
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
for apply in textApplies {
let _ = apply()
}
var textNodes: [(TextNode, CGSize)] = []
for (node, size) in zip(strongSelf.textNodes, textLayouts.map { $0.size }) {
textNodes.append((node, size))
}
let delta = (params.width - params.leftInset - params.rightInset - 18.0 * 2.0) / CGFloat(textNodes.count - 1)
for i in 0 ..< textNodes.count {
let (textNode, textSize) = textNodes[i]
var position = params.leftInset + 18.0 + delta * CGFloat(i)
if i == textNodes.count - 1 {
position -= textSize.width
} else if i > 0 {
position -= textSize.width / 2.0
}
textNode.frame = CGRect(origin: CGPoint(x: position, y: 15.0), size: textSize)
}
if let sliderView = strongSelf.sliderView {
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.startColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
}
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
strongSelf.updateSliderView()
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc private func sliderValueChanged() {
guard let sliderView = self.sliderView else {
return
}
let position = Int(sliderView.value)
let value = keepMediaTimeoutValues[position]
self.item?.updated(value)
}
}
@@ -0,0 +1,322 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramUIPreferences
import TelegramPresentationData
import LegacyComponents
import ItemListUI
import PresentationDataUtils
private func totalDiskSpace() -> Int64 {
do {
let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String)
return (systemAttributes[FileAttributeKey.systemSize] as? NSNumber)?.int64Value ?? 0
} catch {
return 0
}
}
private func stringForCacheSize(strings: PresentationStrings, size: Int32) -> String {
if size > 100 {
return strings.Cache_NoLimit
} else {
return dataSizeString(Int64(size) * 1024 * 1024 * 1024, formatting: DataSizeStringFormatting(strings: strings, decimalSeparator: "."))
}
}
private let maximumCacheSizeValues: [Int32] = {
let diskSpace = totalDiskSpace()
if diskSpace > 100 * 1024 * 1024 * 1024 {
return [5, 20, 50, Int32.max]
} else if diskSpace > 50 * 1024 * 1024 * 1024 {
return [5, 16, 32, Int32.max]
} else if diskSpace > 24 * 1024 * 1024 * 1024 {
return [2, 8, 16, Int32.max]
} else {
return [1, 4, 8, Int32.max]
}
}()
final class MaximumCacheSizePickerItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let value: Int32
let sectionId: ItemListSectionId
let updated: (Int32) -> Void
init(theme: PresentationTheme, strings: PresentationStrings, value: Int32, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.value = value
self.sectionId = sectionId
self.updated = updated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = MaximumCacheSizePickerItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? MaximumCacheSizePickerItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private final class MaximumCacheSizePickerItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let textNodes: [TextNode]
private var sliderView: TGPhotoEditorSliderView?
private var item: MaximumCacheSizePickerItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
var textNodes: [TextNode] = []
for _ in 0 ..< 4 {
let textNode = TextNode()
textNode.isUserInteractionEnabled = false
textNode.displaysAsynchronously = false
textNodes.append(textNode)
}
self.textNodes = textNodes
super.init(layerBacked: false, dynamicBounce: false)
for textNode in textNodes {
self.addSubnode(textNode)
}
}
func updateSliderView() {
if let sliderView = self.sliderView, let item = self.item {
sliderView.maximumValue = 3.0
sliderView.positionsCount = 4
let value = maximumCacheSizeValues.firstIndex(where: { $0 == item.value }) ?? 0
sliderView.value = CGFloat(value)
}
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 2.0
sliderView.lineSize = 4.0
sliderView.dotSize = 5.0
sliderView.minimumValue = 0.0
sliderView.maximumValue = 3.0
sliderView.startValue = 0.0
sliderView.disablesInteractiveTransitionGestureRecognizer = true
sliderView.positionsCount = 4
sliderView.useLinesForPositions = true
if let item = self.item, let params = self.layoutParams {
let value = maximumCacheSizeValues.firstIndex(where: { $0 == item.value }) ?? 0
sliderView.value = CGFloat(value)
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.startColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
}
self.view.addSubview(sliderView)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
self.updateSliderView()
}
func asyncLayout() -> (_ item: MaximumCacheSizePickerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
var makeTextLayouts: [(TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)] = []
for textNode in self.textNodes {
makeTextLayouts.append(TextNode.asyncLayout(textNode))
}
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
var textLayouts: [TextNodeLayout] = []
var textApplies: [() -> TextNode] = []
for i in 0 ..< makeTextLayouts.count {
let makeTextLayout = makeTextLayouts[i]
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: stringForCacheSize(strings: item.strings, size: maximumCacheSizeValues[i]), font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
textLayouts.append(textLayout)
textApplies.append(textApply)
}
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
for apply in textApplies {
let _ = apply()
}
var textNodes: [(TextNode, CGSize)] = []
for (node, size) in zip(strongSelf.textNodes, textLayouts.map { $0.size }) {
textNodes.append((node, size))
}
let delta = (params.width - params.leftInset - params.rightInset - 18.0 * 2.0) / CGFloat(textNodes.count - 1)
for i in 0 ..< textNodes.count {
let (textNode, textSize) = textNodes[i]
var position = params.leftInset + 18.0 + delta * CGFloat(i)
if i == textNodes.count - 1 {
position -= textSize.width
} else if i > 0 {
position -= textSize.width / 2.0
}
textNode.frame = CGRect(origin: CGPoint(x: position, y: 15.0), size: textSize)
}
if let sliderView = strongSelf.sliderView {
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.startColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
}
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
strongSelf.updateSliderView()
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc private func sliderValueChanged() {
guard let sliderView = self.sliderView else {
return
}
let position = Int(sliderView.value)
let value = maximumCacheSizeValues[position]
self.item?.updated(value)
}
}
@@ -0,0 +1,534 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import MtProtoKit
import ItemListUI
import PresentationDataUtils
import AccountContext
import UrlEscaping
import ShareController
private final class ProxySettingsControllerArguments {
let toggleEnabled: (Bool) -> Void
let addNewServer: () -> Void
let activateServer: (ProxyServerSettings) -> Void
let editServer: (ProxyServerSettings) -> Void
let removeServer: (ProxyServerSettings) -> Void
let setServerWithRevealedOptions: (ProxyServerSettings?, ProxyServerSettings?) -> Void
let toggleUseForCalls: (Bool) -> Void
let shareProxyList: () -> Void
init(toggleEnabled: @escaping (Bool) -> Void, addNewServer: @escaping () -> Void, activateServer: @escaping (ProxyServerSettings) -> Void, editServer: @escaping (ProxyServerSettings) -> Void, removeServer: @escaping (ProxyServerSettings) -> Void, setServerWithRevealedOptions: @escaping (ProxyServerSettings?, ProxyServerSettings?) -> Void, toggleUseForCalls: @escaping (Bool) -> Void, shareProxyList: @escaping () -> Void) {
self.toggleEnabled = toggleEnabled
self.addNewServer = addNewServer
self.activateServer = activateServer
self.editServer = editServer
self.removeServer = removeServer
self.setServerWithRevealedOptions = setServerWithRevealedOptions
self.toggleUseForCalls = toggleUseForCalls
self.shareProxyList = shareProxyList
}
}
private enum ProxySettingsControllerSection: Int32 {
case enabled
case servers
case share
case calls
}
private enum ProxyServerAvailabilityStatus: Equatable {
case checking
case notAvailable
case available(Int32)
}
private struct DisplayProxyServerStatus: Equatable {
let activity: Bool
let text: String
let textActive: Bool
}
private enum ProxySettingsControllerEntryId: Equatable, Hashable {
case index(Int)
case server(String, Int32, ProxyServerConnection)
}
private enum ProxySettingsControllerEntry: ItemListNodeEntry {
case enabled(PresentationTheme, String, Bool, Bool)
case serversHeader(PresentationTheme, String)
case addServer(PresentationTheme, String, Bool)
case server(Int, PresentationTheme, PresentationStrings, ProxyServerSettings, Bool, DisplayProxyServerStatus, ProxySettingsServerItemEditing, Bool)
case shareProxyList(PresentationTheme, String)
case useForCalls(PresentationTheme, String, Bool)
case useForCallsInfo(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .enabled:
return ProxySettingsControllerSection.enabled.rawValue
case .serversHeader, .addServer, .server:
return ProxySettingsControllerSection.servers.rawValue
case .shareProxyList:
return ProxySettingsControllerSection.share.rawValue
case .useForCalls, .useForCallsInfo:
return ProxySettingsControllerSection.calls.rawValue
}
}
var stableId: ProxySettingsControllerEntryId {
switch self {
case .enabled:
return .index(0)
case .serversHeader:
return .index(1)
case .addServer:
return .index(2)
case let .server(_, _, _, settings, _, _, _, _):
return .server(settings.host, settings.port, settings.connection)
case .shareProxyList:
return .index(3)
case .useForCalls:
return .index(4)
case .useForCallsInfo:
return .index(5)
}
}
static func ==(lhs: ProxySettingsControllerEntry, rhs: ProxySettingsControllerEntry) -> Bool {
switch lhs {
case let .enabled(lhsTheme, lhsText, lhsValue, lhsCreatesNew):
if case let .enabled(rhsTheme, rhsText, rhsValue, rhsCreatesNew) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsCreatesNew == rhsCreatesNew {
return true
} else {
return false
}
case let .serversHeader(lhsTheme, lhsText):
if case let .serversHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .addServer(lhsTheme, lhsText, lhsEditing):
if case let .addServer(rhsTheme, rhsText, rhsEditing) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEditing == rhsEditing {
return true
} else {
return false
}
case let .server(lhsIndex, lhsTheme, lhsStrings, lhsSettings, lhsActive, lhsStatus, lhsEditing, lhsEnabled):
if case let .server(rhsIndex, rhsTheme, rhsStrings, rhsSettings, rhsActive, rhsStatus, rhsEditing, rhsEnabled) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsSettings == rhsSettings, lhsActive == rhsActive, lhsStatus == rhsStatus, lhsEditing == rhsEditing, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .shareProxyList(lhsTheme, lhsText):
if case let .shareProxyList(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .useForCalls(lhsTheme, lhsText, lhsValue):
if case let .useForCalls(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .useForCallsInfo(lhsTheme, lhsText):
if case let .useForCallsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: ProxySettingsControllerEntry, rhs: ProxySettingsControllerEntry) -> Bool {
switch lhs {
case .enabled:
switch rhs {
case .enabled:
return false
default:
return true
}
case .serversHeader:
switch rhs {
case .enabled, .serversHeader:
return false
default:
return true
}
case .addServer:
switch rhs {
case .enabled, .serversHeader, .addServer:
return false
default:
return true
}
case let .server(lhsIndex, _, _, _, _, _, _, _):
switch rhs {
case .enabled, .serversHeader, .addServer:
return false
case let .server(rhsIndex, _, _, _, _, _, _, _):
return lhsIndex < rhsIndex
default:
return true
}
case .shareProxyList:
switch rhs {
case .enabled, .serversHeader, .addServer, .server, .shareProxyList:
return false
default:
return true
}
case .useForCalls:
switch rhs {
case .enabled, .serversHeader, .addServer, .server, .shareProxyList, .useForCalls:
return false
default:
return true
}
case .useForCallsInfo:
return false
}
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ProxySettingsControllerArguments
switch self {
case let .enabled(_, text, value, createsNew):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: !createsNew, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
if createsNew {
arguments.addNewServer()
} else {
arguments.toggleEnabled(value)
}
})
case let .serversHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .addServer(_, text, _):
return ProxySettingsActionItem(presentationData: presentationData, systemStyle: .glass, title: text, icon: .add, sectionId: self.section, editing: false, action: {
arguments.addNewServer()
})
case let .server(_, theme, strings, settings, active, status, editing, enabled):
return ProxySettingsServerItem(theme: theme, strings: strings, systemStyle: .glass, server: settings, activity: status.activity, active: active, color: enabled ? .accent : .secondary, label: status.text, labelAccent: status.textActive, editing: editing, sectionId: self.section, action: {
arguments.activateServer(settings)
}, infoAction: {
arguments.editServer(settings)
}, setServerWithRevealedOptions: { lhs, rhs in
arguments.setServerWithRevealedOptions(lhs, rhs)
}, removeServer: { _ in
arguments.removeServer(settings)
})
case let .shareProxyList(_, text):
return ProxySettingsActionItem(presentationData: presentationData, systemStyle: .glass, title: text, sectionId: self.section, editing: false, action: {
arguments.shareProxyList()
})
case let .useForCalls(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleUseForCalls(value)
})
case let .useForCallsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
}
}
private func proxySettingsControllerEntries(theme: PresentationTheme, strings: PresentationStrings, state: ProxySettingsControllerState, proxySettings: ProxySettings, statuses: [ProxyServerSettings: ProxyServerStatus], connectionStatus: ConnectionStatus) -> [ProxySettingsControllerEntry] {
var entries: [ProxySettingsControllerEntry] = []
entries.append(.enabled(theme, strings.ChatSettings_ConnectionType_UseProxy, proxySettings.enabled, proxySettings.servers.isEmpty))
entries.append(.serversHeader(theme, strings.SocksProxySetup_SavedProxies))
entries.append(.addServer(theme, strings.SocksProxySetup_AddProxy, state.editing))
var index = 0
for server in proxySettings.servers {
let status: ProxyServerStatus = statuses[server] ?? .checking
let displayStatus: DisplayProxyServerStatus
if proxySettings.enabled && server == proxySettings.activeServer {
switch connectionStatus {
case .waitingForNetwork:
displayStatus = DisplayProxyServerStatus(activity: true, text: strings.State_WaitingForNetwork.lowercased(), textActive: false)
case .connecting, .updating:
displayStatus = DisplayProxyServerStatus(activity: true, text: strings.SocksProxySetup_ProxyStatusConnecting, textActive: false)
case .online:
var text = strings.SocksProxySetup_ProxyStatusConnected
if case let .available(rtt) = status {
let pingTime: Int = Int(rtt * 1000.0)
text = text + ", \(strings.SocksProxySetup_ProxyStatusPing("\(pingTime)").string)"
}
displayStatus = DisplayProxyServerStatus(activity: false, text: text, textActive: true)
}
} else {
var text: String
switch server.connection {
case .socks5:
text = strings.ChatSettings_ConnectionType_UseSocks5
case .mtp:
text = strings.SocksProxySetup_ProxyTelegram
}
switch status {
case .notAvailable:
text = text + ", " + strings.SocksProxySetup_ProxyStatusUnavailable
displayStatus = DisplayProxyServerStatus(activity: false, text: text, textActive: false)
case .checking:
text = text + ", " + strings.SocksProxySetup_ProxyStatusChecking
displayStatus = DisplayProxyServerStatus(activity: false, text: text, textActive: false)
case let .available(rtt):
let pingTime: Int = Int(rtt * 1000.0)
text = text + ", \(strings.SocksProxySetup_ProxyStatusPing("\(pingTime)").string)"
displayStatus = DisplayProxyServerStatus(activity: false, text: text, textActive: false)
}
}
entries.append(.server(index, theme, strings, server, server == proxySettings.activeServer, displayStatus, ProxySettingsServerItemEditing(editable: true, editing: state.editing, revealed: state.revealedServer == server), proxySettings.enabled))
index += 1
}
if !proxySettings.servers.isEmpty {
entries.append(.shareProxyList(theme, strings.SocksProxySetup_ShareProxyList))
}
if let activeServer = proxySettings.activeServer, case .socks5 = activeServer.connection {
entries.append(.useForCalls(theme, strings.SocksProxySetup_UseForCalls, proxySettings.useForCalls))
entries.append(.useForCallsInfo(theme, strings.SocksProxySetup_UseForCallsHelp))
}
return entries
}
private struct ProxySettingsControllerState: Equatable {
var editing: Bool = false
var revealedServer: ProxyServerSettings? = nil
}
public enum ProxySettingsControllerMode {
case `default`
case modal
}
public func proxySettingsController(context: AccountContext, mode: ProxySettingsControllerMode = .default) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
return proxySettingsController(accountManager: context.sharedContext.accountManager, sharedContext: context.sharedContext, context: context, postbox: context.account.postbox, network: context.account.network, mode: mode, presentationData: presentationData, updatedPresentationData: context.sharedContext.presentationData)
}
public func proxySettingsController(accountManager: AccountManager<TelegramAccountManagerTypes>, sharedContext: SharedAccountContext, context: AccountContext? = nil, postbox: Postbox, network: Network, mode: ProxySettingsControllerMode, presentationData: PresentationData, updatedPresentationData: Signal<PresentationData, NoError>) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
let stateValue = Atomic(value: ProxySettingsControllerState())
let statePromise = ValuePromise<ProxySettingsControllerState>(stateValue.with { $0 })
let updateState: ((ProxySettingsControllerState) -> ProxySettingsControllerState) -> Void = { f in
var changed = false
let value = stateValue.modify { current in
let updated = f(current)
if updated != current {
changed = true
}
return updated
}
if changed {
statePromise.set(value)
}
}
var shareProxyListImpl: (() -> Void)?
let arguments = ProxySettingsControllerArguments(toggleEnabled: { value in
let _ = updateProxySettingsInteractively(accountManager: accountManager, { current in
var current = current
current.enabled = value
return current
}).start()
}, addNewServer: {
pushControllerImpl?(proxyServerSettingsController(sharedContext: sharedContext, presentationData: presentationData, updatedPresentationData: updatedPresentationData, accountManager: accountManager, network: network, currentSettings: nil))
}, activateServer: { server in
let _ = updateProxySettingsInteractively(accountManager: accountManager, { current in
var current = current
if current.activeServer != server {
if let _ = current.servers.firstIndex(of: server) {
current.activeServer = server
current.enabled = true
}
}
return current
}).start()
}, editServer: { server in
pushControllerImpl?(proxyServerSettingsController(sharedContext: sharedContext, presentationData: presentationData, updatedPresentationData: updatedPresentationData, accountManager: accountManager, network: network, currentSettings: server))
}, removeServer: { server in
let _ = updateProxySettingsInteractively(accountManager: accountManager, { current in
var current = current
if let index = current.servers.firstIndex(of: server) {
current.servers.remove(at: index)
if current.activeServer == server {
current.activeServer = nil
current.enabled = false
}
}
return current
}).start()
}, setServerWithRevealedOptions: { server, fromServer in
updateState { state in
var state = state
if (server == nil && fromServer == state.revealedServer) || (server != nil && fromServer == nil) {
state.revealedServer = server
}
return state
}
}, toggleUseForCalls: { value in
let _ = updateProxySettingsInteractively(accountManager: accountManager, { current in
var current = current
current.useForCalls = value
return current
}).start()
}, shareProxyList: {
shareProxyListImpl?()
})
let proxySettings = Promise<ProxySettings>()
proxySettings.set(accountManager.sharedData(keys: [SharedDataKeys.proxySettings])
|> map { sharedData -> ProxySettings in
if let value = sharedData.entries[SharedDataKeys.proxySettings]?.get(ProxySettings.self) {
return value
} else {
return ProxySettings.defaultSettings
}
})
let statusesContext = ProxyServersStatuses(network: network, servers: proxySettings.get()
|> map { proxySettings -> [ProxyServerSettings] in
return proxySettings.servers
})
let signal = combineLatest(updatedPresentationData, statePromise.get(), proxySettings.get(), statusesContext.statuses(), network.connectionStatus)
|> map { presentationData, state, proxySettings, statuses, connectionStatus -> (ItemListControllerState, (ItemListNodeState, Any)) in
var leftNavigationButton: ItemListNavigationButton?
if case .modal = mode {
leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
}
let rightNavigationButton: ItemListNavigationButton?
if proxySettings.servers.isEmpty {
rightNavigationButton = nil
} else if state.editing {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
updateState { state in
var state = state
state.editing = false
return state
}
})
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: {
updateState { state in
var state = state
state.editing = true
return state
}
})
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.SocksProxySetup_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: proxySettingsControllerEntries(theme: presentationData.theme, strings: presentationData.strings, state: state, proxySettings: proxySettings, statuses: statuses, connectionStatus: connectionStatus), style: .blocks)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(presentationData: ItemListPresentationData(presentationData), updatedPresentationData: updatedPresentationData |> map(ItemListPresentationData.init(_:)), state: signal, tabBarItem: nil)
controller.navigationPresentation = .modal
pushControllerImpl = { [weak controller] c in
(controller?.navigationController as? NavigationController)?.pushViewController(c)
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [ProxySettingsControllerEntry]) -> Signal<Bool, NoError> in
let fromEntry = entries[fromIndex]
guard case let .server(_, _, _, fromServer, _, _, _, _) = fromEntry else {
return .single(false)
}
var referenceServer: ProxyServerSettings?
var beforeAll = false
var afterAll = false
if toIndex < entries.count {
switch entries[toIndex] {
case let .server(_, _, _, toServer, _, _, _, _):
referenceServer = toServer
default:
if entries[toIndex] < fromEntry {
beforeAll = true
} else {
afterAll = true
}
}
} else {
afterAll = true
}
return updateProxySettingsInteractively(accountManager: accountManager, { current in
var current = current
if let index = current.servers.firstIndex(of: fromServer) {
current.servers.remove(at: index)
}
if let referenceServer = referenceServer {
var inserted = false
for i in 0 ..< current.servers.count {
if current.servers[i] == referenceServer {
if fromIndex < toIndex {
current.servers.insert(fromServer, at: i + 1)
} else {
current.servers.insert(fromServer, at: i)
}
inserted = true
break
}
}
if !inserted {
current.servers.append(fromServer)
}
} else if beforeAll {
current.servers.insert(fromServer, at: 0)
} else if afterAll {
current.servers.append(fromServer)
}
return current
})
})
shareProxyListImpl = { [weak controller] in
guard let context = context, let strongController = controller else {
return
}
let _ = (proxySettings.get()
|> take(1)
|> deliverOnMainQueue).start(next: { settings in
var result = ""
for server in settings.servers {
if !result.isEmpty {
result += "\n\n"
}
var string: String
switch server.connection {
case let .mtp(secret):
let secret = MTProxySecret.parseData(secret)?.serializeToString() ?? ""
string = "https://t.me/proxy?server=\(server.host)&port=\(server.port)"
string += "&secret=\((secret as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")"
case let .socks5(username, password):
string = "https://t.me/socks?server=\(server.host)&port=\(server.port)"
if let username = username, let password = password {
string += "&user=\((username as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")&pass=\((password as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")"
}
}
result += string
}
presentExternalShare(context: context, text: result, parentController: strongController)
})
}
return controller
}
@@ -0,0 +1,440 @@
import Foundation
import UIKit
import Display
import TelegramCore
import Postbox
import AsyncDisplayKit
import UIKit
import SwiftSignalKit
import TelegramPresentationData
import ActivityIndicator
import OverlayStatusController
import AccountContext
import PresentationDataUtils
import UrlEscaping
public final class ProxyServerActionSheetController: ActionSheetController {
private var presentationDisposable: Disposable?
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private var isDismissed: Bool = false
convenience public init(context: AccountContext, server: ProxyServerSettings) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.init(presentationData: presentationData, accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, server: server, updatedPresentationData: context.sharedContext.presentationData)
}
public init(presentationData: PresentationData, accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox, network: Network, server: ProxyServerSettings, updatedPresentationData: Signal<PresentationData, NoError>?) {
let sheetTheme = ActionSheetControllerTheme(presentationData: presentationData)
super.init(theme: sheetTheme)
self._ready.set(.single(true))
var items: [ActionSheetItem] = []
if case .mtp = server.connection {
items.append(ActionSheetTextItem(title: presentationData.strings.SocksProxySetup_AdNoticeHelp))
}
items.append(ProxyServerInfoItem(strings: presentationData.strings, network: network, server: server))
items.append(ProxyServerActionItem(accountManager:accountManager, postbox: postbox, network: network, presentationData: presentationData, server: server, dismiss: { [weak self] success in
guard let strongSelf = self, !strongSelf.isDismissed else {
return
}
strongSelf.isDismissed = true
if success {
strongSelf.present(OverlayStatusController(theme: presentationData.theme, type: .shieldSuccess(presentationData.strings.SocksProxySetup_ProxyEnabled, false)), in: .window(.root))
}
strongSelf.dismissAnimated()
}, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}))
self.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { [weak self] in
self?.dismissAnimated()
})
])
])
if let updatedPresentationData = updatedPresentationData {
self.presentationDisposable = updatedPresentationData.start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData)
}
})
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDisposable?.dispose()
}
}
private final class ProxyServerInfoItem: ActionSheetItem {
private let strings: PresentationStrings
private let network: Network
private let server: ProxyServerSettings
init(strings: PresentationStrings, network: Network, server: ProxyServerSettings) {
self.strings = strings
self.network = network
self.server = server
}
func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
return ProxyServerInfoItemNode(theme: theme, strings: self.strings, network: self.network, server: self.server)
}
func updateNode(_ node: ActionSheetItemNode) {
}
}
private enum ProxyServerInfoStatusType {
case generic(String)
case failed(String)
}
private final class ProxyServerInfoItemNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
private let strings: PresentationStrings
private let textFont: UIFont
private let network: Network
private let server: ProxyServerSettings
private let fieldNodes: [(ImmediateTextNode, ImmediateTextNode)]
private let statusTextNode: ImmediateTextNode
private let statusDisposable = MetaDisposable()
init(theme: ActionSheetControllerTheme, strings: PresentationStrings, network: Network, server: ProxyServerSettings) {
self.theme = theme
self.strings = strings
self.network = network
self.server = server
self.textFont = Font.regular(floor(theme.baseFontSize * 16.0 / 17.0))
var fieldNodes: [(ImmediateTextNode, ImmediateTextNode)] = []
let serverTitleNode = ImmediateTextNode()
serverTitleNode.isUserInteractionEnabled = false
serverTitleNode.displaysAsynchronously = false
serverTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Hostname, font: textFont, textColor: theme.secondaryTextColor)
let serverTextNode = ImmediateTextNode()
serverTextNode.isUserInteractionEnabled = false
serverTextNode.displaysAsynchronously = false
serverTextNode.attributedText = NSAttributedString(string: urlEncodedStringFromString(server.host), font: textFont, textColor: theme.primaryTextColor)
fieldNodes.append((serverTitleNode, serverTextNode))
let portTitleNode = ImmediateTextNode()
portTitleNode.isUserInteractionEnabled = false
portTitleNode.displaysAsynchronously = false
portTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Port, font: textFont, textColor: theme.secondaryTextColor)
let portTextNode = ImmediateTextNode()
portTextNode.isUserInteractionEnabled = false
portTextNode.displaysAsynchronously = false
portTextNode.attributedText = NSAttributedString(string: "\(server.port)", font: textFont, textColor: theme.primaryTextColor)
fieldNodes.append((portTitleNode, portTextNode))
switch server.connection {
case let .socks5(username, password):
if let username = username {
let usernameTitleNode = ImmediateTextNode()
usernameTitleNode.isUserInteractionEnabled = false
usernameTitleNode.displaysAsynchronously = false
usernameTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Username, font: textFont, textColor: theme.secondaryTextColor)
let usernameTextNode = ImmediateTextNode()
usernameTextNode.isUserInteractionEnabled = false
usernameTextNode.displaysAsynchronously = false
usernameTextNode.attributedText = NSAttributedString(string: username, font: textFont, textColor: theme.primaryTextColor)
fieldNodes.append((usernameTitleNode, usernameTextNode))
}
if let password = password {
let passwordTitleNode = ImmediateTextNode()
passwordTitleNode.isUserInteractionEnabled = false
passwordTitleNode.displaysAsynchronously = false
passwordTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Password, font: textFont, textColor: theme.secondaryTextColor)
let passwordTextNode = ImmediateTextNode()
passwordTextNode.isUserInteractionEnabled = false
passwordTextNode.displaysAsynchronously = false
passwordTextNode.attributedText = NSAttributedString(string: password, font: textFont, textColor: theme.primaryTextColor)
fieldNodes.append((passwordTitleNode, passwordTextNode))
}
case .mtp:
let passwordTitleNode = ImmediateTextNode()
passwordTitleNode.isUserInteractionEnabled = false
passwordTitleNode.displaysAsynchronously = false
passwordTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Secret, font: textFont, textColor: theme.secondaryTextColor)
let passwordTextNode = ImmediateTextNode()
passwordTextNode.isUserInteractionEnabled = false
passwordTextNode.displaysAsynchronously = false
passwordTextNode.attributedText = NSAttributedString(string: "•••••", font: textFont, textColor: theme.primaryTextColor)
fieldNodes.append((passwordTitleNode, passwordTextNode))
}
let statusTitleNode = ImmediateTextNode()
statusTitleNode.isUserInteractionEnabled = false
statusTitleNode.displaysAsynchronously = false
statusTitleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_Status, font: textFont, textColor: theme.secondaryTextColor)
let statusTextNode = ImmediateTextNode()
statusTextNode.isUserInteractionEnabled = false
statusTextNode.displaysAsynchronously = false
statusTextNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_ProxyStatusChecking, font: textFont, textColor: theme.primaryTextColor)
fieldNodes.append((statusTitleNode, statusTextNode))
self.fieldNodes = fieldNodes
self.statusTextNode = statusTextNode
super.init(theme: theme)
for (lhs, rhs) in fieldNodes {
self.addSubnode(lhs)
self.addSubnode(rhs)
}
}
deinit {
self.statusDisposable.dispose()
}
override func didLoad() {
super.didLoad()
let statusesContext = ProxyServersStatuses(network: network, servers: .single([self.server]))
self.statusDisposable.set((statusesContext.statuses()
|> map { return $0.first?.value }
|> distinctUntilChanged
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self, let status = status {
let statusType: ProxyServerInfoStatusType
switch status {
case .checking:
statusType = .generic(strongSelf.strings.SocksProxySetup_ProxyStatusChecking)
case let .available(rtt):
let pingTime = Int(rtt * 1000.0)
statusType = .generic(strongSelf.strings.SocksProxySetup_ProxyStatusPing("\(pingTime)").string)
case .notAvailable:
statusType = .failed(strongSelf.strings.SocksProxySetup_ProxyStatusUnavailable)
}
strongSelf.setStatus(statusType)
}
}))
}
func setStatus(_ status: ProxyServerInfoStatusType) {
let attributedString: NSAttributedString
switch status {
case let .generic(text):
attributedString = NSAttributedString(string: text, font: textFont, textColor: theme.primaryTextColor)
case let .failed(text):
attributedString = NSAttributedString(string: text, font: textFont, textColor: theme.destructiveActionTextColor)
}
self.statusTextNode.attributedText = attributedString
self.requestLayoutUpdate()
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 36.0 * CGFloat(self.fieldNodes.count) + 12.0)
var offset: CGFloat = 15.0
for (lhs, rhs) in self.fieldNodes {
let lhsSize = lhs.updateLayout(CGSize(width: size.width - 18.0 * 2.0, height: CGFloat.greatestFiniteMagnitude))
lhs.frame = CGRect(origin: CGPoint(x: 18, y: offset), size: lhsSize)
let rhsSize = rhs.updateLayout(CGSize(width: max(1.0, size.width - 18 * 2.0 - lhsSize.width - 4.0), height: CGFloat.greatestFiniteMagnitude))
rhs.frame = CGRect(origin: CGPoint(x: size.width - 18 - rhsSize.width, y: offset), size: rhsSize)
offset += 36.0
}
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
}
private final class ProxyServerActionItem: ActionSheetItem {
private let accountManager: AccountManager<TelegramAccountManagerTypes>
private let postbox: Postbox
private let network: Network
private let presentationData: PresentationData
private let server: ProxyServerSettings
private let dismiss: (Bool) -> Void
private let present: (ViewController, Any?) -> Void
init(accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox, network: Network, presentationData: PresentationData, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.accountManager = accountManager
self.postbox = postbox
self.network = network
self.presentationData = presentationData
self.server = server
self.dismiss = dismiss
self.present = present
}
func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
return ProxyServerActionItemNode(accountManager: self.accountManager, postbox: self.postbox, network: self.network, presentationData: self.presentationData, theme: theme, server: self.server, dismiss: self.dismiss, present: self.present)
}
func updateNode(_ node: ActionSheetItemNode) {
}
}
private final class ProxyServerActionItemNode: ActionSheetItemNode {
private let accountManager: AccountManager<TelegramAccountManagerTypes>
private let postbox: Postbox
private let network: Network
private let presentationData: PresentationData
private let theme: ActionSheetControllerTheme
private let server: ProxyServerSettings
private let dismiss: (Bool) -> Void
private let present: (ViewController, Any?) -> Void
private let buttonNode: HighlightableButtonNode
private let titleNode: ImmediateTextNode
private let activityIndicator: ActivityIndicator
private let disposable = MetaDisposable()
private var revertSettings: ProxySettings?
init(accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox, network: Network, presentationData: PresentationData, theme: ActionSheetControllerTheme, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.accountManager = accountManager
self.postbox = postbox
self.network = network
self.theme = theme
self.presentationData = presentationData
self.server = server
self.dismiss = dismiss
self.present = present
let titleFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0))
self.titleNode = ImmediateTextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: presentationData.strings.SocksProxySetup_ConnectAndSave, font: titleFont, textColor: theme.controlAccentColor)
self.activityIndicator = ActivityIndicator(type: .custom(theme.controlAccentColor, 22.0, 1.5, false))
self.activityIndicator.isHidden = true
self.buttonNode = HighlightableButtonNode()
super.init(theme: theme)
self.addSubnode(self.titleNode)
self.addSubnode(self.activityIndicator)
self.addSubnode(self.buttonNode)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor
} else {
UIView.animate(withDuration: 0.3, animations: {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor
})
}
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
deinit {
self.disposable.dispose()
if let revertSettings = self.revertSettings {
let _ = updateProxySettingsInteractively(accountManager: self.accountManager, { _ in
return revertSettings
})
}
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 57.0)
self.buttonNode.frame = CGRect(origin: CGPoint(), size: size)
let labelSize = self.titleNode.updateLayout(CGSize(width: max(1.0, size.width - 10.0), height: size.height))
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
let activitySize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0))
self.titleNode.frame = titleFrame
self.activityIndicator.frame = CGRect(origin: CGPoint(x: 14.0, y: titleFrame.minY - 0.0), size: activitySize)
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
@objc private func buttonPressed() {
let proxyServerSettings = self.server
let _ = (self.accountManager.transaction { transaction -> ProxySettings in
var currentSettings: ProxySettings?
let _ = updateProxySettingsInteractively(transaction: transaction, { settings in
currentSettings = settings
var settings = settings
if let index = settings.servers.firstIndex(of: proxyServerSettings) {
settings.servers[index] = proxyServerSettings
settings.activeServer = proxyServerSettings
} else {
settings.servers.insert(proxyServerSettings, at: 0)
settings.activeServer = proxyServerSettings
}
settings.enabled = true
return settings
})
return currentSettings ?? ProxySettings.defaultSettings
} |> deliverOnMainQueue).start(next: { [weak self] previousSettings in
if let strongSelf = self {
strongSelf.revertSettings = previousSettings
strongSelf.buttonNode.isUserInteractionEnabled = false
strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.SocksProxySetup_Connecting, font: Font.regular(20.0), textColor: strongSelf.theme.primaryTextColor)
strongSelf.activityIndicator.isHidden = false
strongSelf.requestLayoutUpdate()
let signal = strongSelf.network.connectionStatus
|> filter { status in
switch status {
case let .online(proxyAddress):
if proxyAddress == proxyServerSettings.host {
return true
} else {
return false
}
default:
return false
}
}
|> map { _ -> Bool in
return true
}
|> timeout(15.0, queue: Queue.mainQueue(), alternate: .single(false))
|> deliverOnMainQueue
strongSelf.disposable.set(signal.start(next: { value in
if let strongSelf = self {
strongSelf.activityIndicator.isHidden = true
strongSelf.revertSettings = nil
if value {
strongSelf.dismiss(true)
} else {
let _ = updateProxySettingsInteractively(accountManager: strongSelf.accountManager, { _ in
return previousSettings
})
strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.SocksProxySetup_ConnectAndSave, font: Font.regular(20.0), textColor: strongSelf.theme.controlAccentColor)
strongSelf.buttonNode.isUserInteractionEnabled = true
strongSelf.requestLayoutUpdate()
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.SocksProxySetup_FailedToConnect, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil)
}
}
}))
}
})
}
}
@@ -0,0 +1,392 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import MtProtoKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import UrlEscaping
import UrlHandling
import ShareController
private func shareLink(for server: ProxyServerSettings) -> String {
var link: String
switch server.connection {
case let .mtp(secret):
let secret = MTProxySecret.parseData(secret)?.serializeToString() ?? ""
link = "https://t.me/proxy?server=\(server.host)&port=\(server.port)"
link += "&secret=\(secret.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")"
case let .socks5(username, password):
link = "https://t.me/socks?server=\(server.host)&port=\(server.port)"
link += "&user=\(username?.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")&pass=\(password?.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")"
}
return link
}
private final class ProxyServerSettingsControllerArguments {
let updateState: ((ProxyServerSettingsControllerState) -> ProxyServerSettingsControllerState) -> Void
let share: () -> Void
let usePasteboardSettings: () -> Void
init(updateState: @escaping ((ProxyServerSettingsControllerState) -> ProxyServerSettingsControllerState) -> Void, share: @escaping () -> Void, usePasteboardSettings: @escaping () -> Void) {
self.updateState = updateState
self.share = share
self.usePasteboardSettings = usePasteboardSettings
}
}
private enum ProxySettingsSection: Int32 {
case pasteboard
case mode
case connection
case credentials
case share
}
private enum ProxySettingsEntry: ItemListNodeEntry {
case usePasteboardSettings(PresentationTheme, String)
case usePasteboardInfo(PresentationTheme, String)
case modeSocks5(PresentationTheme, String, Bool)
case modeMtp(PresentationTheme, String, Bool)
case connectionHeader(PresentationTheme, String)
case connectionServer(PresentationTheme, PresentationStrings, String, String)
case connectionPort(PresentationTheme, PresentationStrings, String, String)
case credentialsHeader(PresentationTheme, String)
case credentialsUsername(PresentationTheme, PresentationStrings, String, String)
case credentialsPassword(PresentationTheme, PresentationStrings, String, String)
case credentialsSecret(PresentationTheme, PresentationStrings, String, String)
case share(PresentationTheme, String, Bool)
var section: ItemListSectionId {
switch self {
case .usePasteboardSettings, .usePasteboardInfo:
return ProxySettingsSection.pasteboard.rawValue
case .modeSocks5, .modeMtp:
return ProxySettingsSection.mode.rawValue
case .connectionHeader, .connectionServer, .connectionPort:
return ProxySettingsSection.connection.rawValue
case .credentialsHeader, .credentialsUsername, .credentialsPassword, .credentialsSecret:
return ProxySettingsSection.credentials.rawValue
case .share:
return ProxySettingsSection.share.rawValue
}
}
var stableId: Int32 {
switch self {
case .usePasteboardSettings:
return 0
case .usePasteboardInfo:
return 1
case .modeSocks5:
return 2
case .modeMtp:
return 3
case .connectionHeader:
return 4
case .connectionServer:
return 5
case .connectionPort:
return 6
case .credentialsHeader:
return 7
case .credentialsUsername:
return 8
case .credentialsPassword:
return 9
case .credentialsSecret:
return 10
case .share:
return 12
}
}
static func <(lhs: ProxySettingsEntry, rhs: ProxySettingsEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ProxyServerSettingsControllerArguments
switch self {
case let .usePasteboardSettings(_, title):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.usePasteboardSettings()
})
case let .usePasteboardInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .modeSocks5(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateState { state in
var state = state
state.mode = .socks5
return state
}
})
case let .modeMtp(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateState { state in
var state = state
state.mode = .mtp
return state
}
})
case let .connectionHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .connectionServer(_, _, placeholder, text):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), sectionId: self.section, textUpdated: { value in
arguments.updateState { current in
var state = current
state.host = value
return state
}
}, action: {})
case let .connectionPort(_, _, placeholder, text):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(), text: text, placeholder: placeholder, type: .number, sectionId: self.section, textUpdated: { value in
arguments.updateState { current in
var state = current
state.port = value
return state
}
}, action: {})
case let .credentialsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .credentialsUsername(_, _, placeholder, text):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(), text: text, placeholder: placeholder, sectionId: self.section, textUpdated: { value in
arguments.updateState { current in
var state = current
state.username = value
return state
}
}, action: {})
case let .credentialsPassword(_, _, placeholder, text):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(), text: text, placeholder: placeholder, type: .password, sectionId: self.section, textUpdated: { value in
arguments.updateState { current in
var state = current
state.password = value
return state
}
}, action: {})
case let .credentialsSecret(_, _, placeholder, text):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), sectionId: self.section, textUpdated: { value in
arguments.updateState { current in
var state = current
state.secret = value
return state
}
}, action: {})
case let .share(_, text, enabled):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.share()
})
}
}
}
private enum ProxyServerSettingsControllerMode {
case socks5
case mtp
}
private struct ProxyServerSettingsControllerState: Equatable {
var mode: ProxyServerSettingsControllerMode
var host: String
var port: String
var username: String
var password: String
var secret: String
var isComplete: Bool {
if self.host.isEmpty || self.port.isEmpty || Int(self.port) == nil {
return false
}
switch self.mode {
case .socks5:
break
case .mtp:
let secretIsValid = MTProxySecret.parse(self.secret) != nil
if !secretIsValid {
return false
}
}
return true
}
}
private func proxyServerSettingsControllerEntries(presentationData: PresentationData, state: ProxyServerSettingsControllerState, pasteboardSettings: ProxyServerSettings?) -> [ProxySettingsEntry] {
var entries: [ProxySettingsEntry] = []
if let _ = pasteboardSettings {
entries.append(.usePasteboardSettings(presentationData.theme, presentationData.strings.SocksProxySetup_PasteFromClipboard))
}
entries.append(.modeSocks5(presentationData.theme, presentationData.strings.SocksProxySetup_ProxySocks5, state.mode == .socks5))
entries.append(.modeMtp(presentationData.theme, presentationData.strings.SocksProxySetup_ProxyTelegram, state.mode == .mtp))
entries.append(.connectionHeader(presentationData.theme, presentationData.strings.SocksProxySetup_Connection.uppercased()))
entries.append(.connectionServer(presentationData.theme, presentationData.strings, presentationData.strings.SocksProxySetup_Hostname, state.host))
entries.append(.connectionPort(presentationData.theme, presentationData.strings, presentationData.strings.SocksProxySetup_Port, state.port))
switch state.mode {
case .socks5:
entries.append(.credentialsHeader(presentationData.theme, presentationData.strings.SocksProxySetup_Credentials))
entries.append(.credentialsUsername(presentationData.theme, presentationData.strings, presentationData.strings.SocksProxySetup_Username, state.username))
entries.append(.credentialsPassword(presentationData.theme, presentationData.strings, presentationData.strings.SocksProxySetup_Password, state.password))
case .mtp:
entries.append(.credentialsHeader(presentationData.theme, presentationData.strings.SocksProxySetup_RequiredCredentials))
entries.append(.credentialsSecret(presentationData.theme, presentationData.strings, presentationData.strings.SocksProxySetup_SecretPlaceholder, state.secret))
}
entries.append(.share(presentationData.theme, presentationData.strings.Conversation_ContextMenuShare, state.isComplete))
return entries
}
private func proxyServerSettings(with state: ProxyServerSettingsControllerState) -> ProxyServerSettings? {
if state.isComplete, let port = Int32(state.port) {
switch state.mode {
case .socks5:
return ProxyServerSettings(host: state.host, port: port, connection: .socks5(username: state.username.isEmpty ? nil : state.username, password: state.password.isEmpty ? nil : state.password))
case .mtp:
let parsedSecret = MTProxySecret.parse(state.secret)
if let parsedSecret = parsedSecret {
return ProxyServerSettings(host: state.host, port: port, connection: .mtp(secret: parsedSecret.serialize()))
}
}
}
return nil
}
public func proxyServerSettingsController(context: AccountContext, currentSettings: ProxyServerSettings? = nil) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
return proxyServerSettingsController(sharedContext: context.sharedContext, context: context, presentationData: presentationData, updatedPresentationData: context.sharedContext.presentationData, accountManager: context.sharedContext.accountManager, network: context.account.network, currentSettings: currentSettings)
}
func proxyServerSettingsController(sharedContext: SharedAccountContext, context: AccountContext? = nil, presentationData: PresentationData, updatedPresentationData: Signal<PresentationData, NoError>, accountManager: AccountManager<TelegramAccountManagerTypes>, network: Network, currentSettings: ProxyServerSettings?) -> ViewController {
var currentMode: ProxyServerSettingsControllerMode = .socks5
var currentUsername: String?
var currentPassword: String?
var currentSecret: String?
var pasteboardSettings: ProxyServerSettings?
if let currentSettings = currentSettings {
switch currentSettings.connection {
case let .socks5(username, password):
currentUsername = username
currentPassword = password
currentMode = .socks5
case let .mtp(secret):
currentSecret = hexString(secret)
currentMode = .mtp
}
} else {
if let proxy = parseProxyUrl(sharedContext: sharedContext, url: UIPasteboard.general.string ?? "") {
if let secret = proxy.secret, let parsedSecret = MTProxySecret.parseData(secret) {
pasteboardSettings = ProxyServerSettings(host: proxy.host, port: proxy.port, connection: .mtp(secret: parsedSecret.serialize()))
} else {
pasteboardSettings = ProxyServerSettings(host: proxy.host, port: proxy.port, connection: .socks5(username: proxy.username, password: proxy.password))
}
}
}
let initialState = ProxyServerSettingsControllerState(mode: currentMode, host: currentSettings?.host ?? "", port: (currentSettings?.port).flatMap { "\($0)" } ?? "", username: currentUsername ?? "", password: currentPassword ?? "", secret: currentSecret ?? "")
let stateValue = Atomic(value: initialState)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let updateState: ((ProxyServerSettingsControllerState) -> ProxyServerSettingsControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var dismissImpl: (() -> Void)?
var shareImpl: (() -> Void)?
let arguments = ProxyServerSettingsControllerArguments(updateState: { f in
updateState(f)
}, share: {
shareImpl?()
}, usePasteboardSettings: {
if let pasteboardSettings = pasteboardSettings {
updateState { state in
var state = state
state.host = pasteboardSettings.host
state.port = "\(pasteboardSettings.port)"
switch pasteboardSettings.connection {
case let .socks5(username, password):
state.mode = .socks5
state.username = username ?? ""
state.password = password ?? ""
case let .mtp(secret):
state.mode = .mtp
state.secret = hexString(secret)
}
return state
}
}
})
let signal = combineLatest(updatedPresentationData, statePromise.get())
|> deliverOnMainQueue
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: state.isComplete, action: {
if let proxyServerSettings = proxyServerSettings(with: state) {
let _ = (updateProxySettingsInteractively(accountManager: accountManager, { settings in
var settings = settings
if let currentSettings = currentSettings {
if let index = settings.servers.firstIndex(of: currentSettings) {
settings.servers[index] = proxyServerSettings
if settings.activeServer == currentSettings {
settings.activeServer = proxyServerSettings
}
}
} else {
settings.servers.append(proxyServerSettings)
if settings.servers.count == 1 {
settings.activeServer = proxyServerSettings
}
}
return settings
}) |> deliverOnMainQueue).start(completed: {
dismissImpl?()
})
}
})
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.SocksProxySetup_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: proxyServerSettingsControllerEntries(presentationData: presentationData, state: state, pasteboardSettings: pasteboardSettings), style: .blocks, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(presentationData: ItemListPresentationData(presentationData), updatedPresentationData: updatedPresentationData |> map(ItemListPresentationData.init(_:)), state: signal, tabBarItem: nil)
controller.navigationPresentation = .modal
presentControllerImpl = { [weak controller] c, d in
controller?.present(c, in: .window(.root), with: d)
}
dismissImpl = { [weak controller] in
let _ = controller?.dismiss()
}
shareImpl = { [weak controller] in
let state = stateValue.with { $0 }
guard let server = proxyServerSettings(with: state) else {
return
}
let link = shareLink(for: server)
controller?.view.endEditing(true)
let controller = ShareProxyServerActionSheetController(presentationData: presentationData, updatedPresentationData: updatedPresentationData, link: link)
presentControllerImpl?(controller, nil)
}
return controller
}
@@ -0,0 +1,288 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
enum ProxySettingsActionIcon {
case none
case add
}
final class ProxySettingsActionItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let title: String
let icon: ProxySettingsActionIcon
let editing: Bool
let sectionId: ItemListSectionId
let action: () -> Void
init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, title: String, icon: ProxySettingsActionIcon = .none, sectionId: ItemListSectionId, editing: Bool, action: @escaping () -> Void) {
self.presentationData = presentationData
self.systemStyle = systemStyle
self.title = title
self.icon = icon
self.editing = editing
self.sectionId = sectionId
self.action = action
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ProxySettingsActionItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(false) })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ProxySettingsActionItemNode {
let makeLayout = nodeValue.asyncLayout()
var animated = true
if case .None = animation {
animated = false
}
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply(animated)
})
}
}
}
}
}
var selectable: Bool = true
func selected(listView: ListView){
listView.clearHighlightAnimated(true)
self.action()
}
}
private final class ProxySettingsActionItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private let iconNode: ASImageNode
private let titleNode: TextNode
private var item: ProxySettingsActionItem?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false)
self.isAccessibilityElement = true
self.addSubnode(self.iconNode)
self.addSubnode(self.titleNode)
}
func asyncLayout() -> (_ item: ProxySettingsActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let leftInset: CGFloat = (item.icon != .none ? 50.0 : 16.0) + params.leftInset
let editingOffset: CGFloat = (item.editing ? 38.0 : 0.0)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - editingOffset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
verticalInset = 15.0
case .legacy:
verticalInset = 11.0
}
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
let icon = item.icon == .add ? PresentationResourcesItemList.plusIconImage(item.presentationData.theme) : nil
return (layout, { [weak self] animated in
if let strongSelf = self {
strongSelf.item = item
strongSelf.accessibilityLabel = item.title
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let _ = titleApply()
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
strongSelf.iconNode.image = icon
if let image = icon {
transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + editingOffset + floor((leftInset - params.leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size))
}
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset + editingOffset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset, y: floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0) + 1.0), size: titleLayout.size))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel))
}
})
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,560 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import ActivityIndicator
import UrlEscaping
private let activitySize = CGSize(width: 24.0, height: 24.0)
struct ProxySettingsServerItemEditing: Equatable {
let editable: Bool
let editing: Bool
let revealed: Bool
}
final class ProxySettingsServerItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let systemStyle: ItemListSystemStyle
let server: ProxyServerSettings
let activity: Bool
let active: Bool
let color: ItemListCheckboxItemColor
let label: String
let labelAccent: Bool
let editing: ProxySettingsServerItemEditing
let sectionId: ItemListSectionId
let action: () -> Void
let infoAction: () -> Void
let setServerWithRevealedOptions: (ProxyServerSettings?, ProxyServerSettings?) -> Void
let removeServer: (ProxyServerSettings) -> Void
init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle = .legacy, server: ProxyServerSettings, activity: Bool, active: Bool, color: ItemListCheckboxItemColor, label: String, labelAccent: Bool, editing: ProxySettingsServerItemEditing, sectionId: ItemListSectionId, action: @escaping () -> Void, infoAction: @escaping () -> Void, setServerWithRevealedOptions: @escaping (ProxyServerSettings?, ProxyServerSettings?) -> Void, removeServer: @escaping (ProxyServerSettings) -> Void) {
self.theme = theme
self.strings = strings
self.systemStyle = systemStyle
self.server = server
self.activity = activity
self.active = active
self.color = color
self.label = label
self.labelAccent = labelAccent
self.editing = editing
self.sectionId = sectionId
self.action = action
self.infoAction = infoAction
self.setServerWithRevealedOptions = setServerWithRevealedOptions
self.removeServer = removeServer
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ProxySettingsServerItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(false) })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ProxySettingsServerItemNode {
let makeLayout = nodeValue.asyncLayout()
var animated = true
if case .None = animation {
animated = false
}
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply(animated)
})
}
}
}
}
}
var selectable: Bool = true
func selected(listView: ListView){
listView.clearHighlightAnimated(true)
self.action()
}
}
private let titleFont = Font.regular(17.0)
private let statusFont = Font.regular(14.0)
private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private let titleNode: TextNode
private let infoIconNode: ASImageNode
private let infoButtonNode: HighlightableButtonNode
private let statusNode: TextNode
private let checkNode: ASImageNode
private let activityNode: ActivityIndicator
private let activateArea: AccessibilityAreaNode
private var editableControlNode: ItemListEditableControlNode?
private var reorderControlNode: ItemListEditableReorderControlNode?
private var item: ProxySettingsServerItem?
private var layoutParams: ListViewItemLayoutParams?
override var canBeSelected: Bool {
if self.editableControlNode != nil {
return false
}
return true
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.infoIconNode = ASImageNode()
self.infoIconNode.isLayerBacked = true
self.infoIconNode.displayWithoutProcessing = true
self.infoIconNode.displaysAsynchronously = false
self.checkNode = ASImageNode()
self.checkNode.isLayerBacked = true
self.checkNode.displayWithoutProcessing = true
self.checkNode.displaysAsynchronously = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.statusNode = TextNode()
self.statusNode.isUserInteractionEnabled = false
self.statusNode.contentMode = .left
self.statusNode.contentsScale = UIScreen.main.scale
self.activityNode = ActivityIndicator(type: .custom(.blue, activitySize.width, 2.0, false))
self.activityNode.isHidden = true
self.activateArea = AccessibilityAreaNode()
self.infoButtonNode = HighlightableButtonNode()
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.statusNode)
self.addSubnode(self.checkNode)
self.addSubnode(self.infoIconNode)
self.addSubnode(self.activityNode)
self.addSubnode(self.infoButtonNode)
self.addSubnode(self.activateArea)
self.infoButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.infoIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.infoIconNode.alpha = 0.4
} else {
strongSelf.infoIconNode.alpha = 1.0
strongSelf.infoIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.infoButtonNode.addTarget(self, action: #selector(self.infoButtonPressed), forControlEvents: .touchUpInside)
self.activateArea.activate = { [weak self] in
self?.item?.action()
return true
}
}
func asyncLayout() -> (_ item: ProxySettingsServerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
let currentItem = self.item
return { item, params, neighbors in
var updateInfoIconImage: UIImage?
var updateCheckImage: UIImage?
var updatedTheme: PresentationTheme?
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
updateInfoIconImage = PresentationResourcesCallList.infoButton(item.theme)
}
if currentItem?.theme !== item.theme || currentItem?.color != item.color {
switch item.color {
case .accent:
updateCheckImage = PresentationResourcesItemList.checkIconImage(item.theme)
case .secondary:
updateCheckImage = PresentationResourcesItemList.secondaryCheckIconImage(item.theme)
}
}
let peerRevealOptions: [ItemListRevealOption]
if item.editing.editable {
peerRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]
} else {
peerRevealOptions = []
}
let titleAttributedString = NSMutableAttributedString()
titleAttributedString.append(NSAttributedString(string: urlEncodedStringFromString(item.server.host), font: titleFont, textColor: item.theme.list.itemPrimaryTextColor))
titleAttributedString.append(NSAttributedString(string: ":\(item.server.port)", font: titleFont, textColor: item.theme.list.itemSecondaryTextColor))
let statusAttributedString = NSAttributedString(string: item.label, font: statusFont, textColor: item.labelAccent ? item.theme.list.itemAccentColor : item.theme.list.itemSecondaryTextColor)
var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)?
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)?
let editingOffset: CGFloat
var reorderInset: CGFloat = 0.0
if item.editing.editing {
let sizeAndApply = editableControlLayout(item.theme, false)
editableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0
let reorderSizeAndApply = reorderControlLayout(item.theme)
reorderControlSizeAndApply = reorderSizeAndApply
reorderInset = reorderSizeAndApply.0
} else {
editingOffset = 0.0
}
let leftInset: CGFloat = 50.0 + params.leftInset
let rightInset: CGFloat = params.rightInset + max(reorderInset, 55.0)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var verticalInset: CGFloat = 0.0
if case .glass = item.systemStyle {
verticalInset = 4.0
}
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let contentSize = CGSize(width: params.width, height: 64.0 + verticalInset * 2.0)
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] animated in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.infoButtonNode.accessibilityLabel = item.strings.Conversation_Info
strongSelf.activateArea.accessibilityLabel = "\(titleAttributedString.string)\n\(statusAttributedString.string)"
if item.active {
strongSelf.activateArea.accessibilityValue = item.strings.ProxyServer_VoiceOver_Active
} else {
strongSelf.activateArea.accessibilityValue = ""
}
if let updateInfoIconImage = updateInfoIconImage {
strongSelf.infoIconNode.image = updateInfoIconImage
}
if let updateCheckImage = updateCheckImage {
strongSelf.checkNode.image = updateCheckImage
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
strongSelf.activityNode.type = .custom(item.theme.list.itemAccentColor, activitySize.width, 2.0, false)
}
let revealOffset = strongSelf.revealOffset
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
if let editableControlSizeAndApply = editableControlSizeAndApply {
let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height))
if strongSelf.editableControlNode == nil {
let editableControlNode = editableControlSizeAndApply.1(layout.contentSize.height)
editableControlNode.tapped = {
if let strongSelf = self {
strongSelf.setRevealOptionsOpened(true, animated: true)
strongSelf.revealOptionsInteractivelyOpened()
}
}
strongSelf.editableControlNode = editableControlNode
strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.titleNode)
editableControlNode.frame = editableControlFrame
transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY))
editableControlNode.alpha = 0.0
transition.updateAlpha(node: editableControlNode, alpha: 1.0)
} else {
strongSelf.editableControlNode?.frame = editableControlFrame
}
strongSelf.editableControlNode?.isHidden = !item.editing.editable
} else if let editableControlNode = strongSelf.editableControlNode {
var editableControlFrame = editableControlNode.frame
editableControlFrame.origin.x = -editableControlFrame.size.width
strongSelf.editableControlNode = nil
transition.updateAlpha(node: editableControlNode, alpha: 0.0)
transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in
editableControlNode?.removeFromSupernode()
})
}
if let reorderControlSizeAndApply = reorderControlSizeAndApply {
if strongSelf.reorderControlNode == nil {
let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate)
strongSelf.reorderControlNode = reorderControlNode
strongSelf.addSubnode(reorderControlNode)
reorderControlNode.alpha = 0.0
transition.updateAlpha(node: reorderControlNode, alpha: 1.0)
}
let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0, y: 0.0), size: CGSize(width: reorderControlSizeAndApply.0, height: layout.contentSize.height))
strongSelf.reorderControlNode?.frame = reorderControlFrame
} else if let reorderControlNode = strongSelf.reorderControlNode {
strongSelf.reorderControlNode = nil
transition.updateAlpha(node: reorderControlNode, alpha: 0.0, completion: { [weak reorderControlNode] _ in
reorderControlNode?.removeFromSupernode()
})
}
let _ = titleApply()
let _ = statusApply()
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.addSubnode(strongSelf.maskNode)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset + editingOffset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 12.0 + verticalInset), size: titleLayout.size))
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 37.0 + verticalInset), size: statusLayout.size))
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 0.0), size: CGSize(width: params.width - params.rightInset - 56.0 - (leftInset + revealOffset + editingOffset), height: layout.contentSize.height))
transition.updateAlpha(node: strongSelf.infoIconNode, alpha: item.editing.editing ? 0.0 : 1.0)
if let checkImage = strongSelf.checkNode.image {
transition.updateFrame(node: strongSelf.checkNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + floor((50.0 - checkImage.size.width) / 2.0), y: floor((layout.contentSize.height - checkImage.size.height) / 2.0)), size: checkImage.size))
}
transition.updateFrame(node: strongSelf.activityNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + floor((50.0 - activitySize.width) / 2.0), y: floor((layout.contentSize.height - activitySize.height) / 2.0)), size: activitySize))
strongSelf.checkNode.isHidden = !item.active || item.activity
strongSelf.activityNode.isHidden = !item.activity
if let infoImage = strongSelf.infoIconNode.image {
transition.updateFrame(node: strongSelf.infoIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 55.0 + floor((55.0 - infoImage.size.width) / 2.0), y: floor((layout.contentSize.height - infoImage.size.height) / 2.0)), size: infoImage.size))
}
strongSelf.infoButtonNode.isUserInteractionEnabled = revealOffset.isZero && !item.editing.editing
strongSelf.infoButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 55.0, y: 0.0), size: CGSize(width: 55.0, height: layout.contentSize.height))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel))
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
strongSelf.setRevealOptions((left: [], right: peerRevealOptions))
strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated)
}
})
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
guard let params = self.layoutParams else {
return
}
let leftInset: CGFloat = 50.0 + params.leftInset
let editingOffset: CGFloat
if let editableControlNode = self.editableControlNode {
editingOffset = editableControlNode.bounds.size.width
var editableControlFrame = editableControlNode.frame
editableControlFrame.origin.x = params.leftInset + offset
transition.updateFrame(node: editableControlNode, frame: editableControlFrame)
} else {
editingOffset = 0.0
}
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + offset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size))
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + offset + editingOffset, y: self.statusNode.frame.minY), size: self.statusNode.bounds.size))
var checkFrame = self.checkNode.frame
checkFrame.origin.x = params.leftInset + offset + editingOffset + floor((50.0 - checkFrame.width) / 2.0)
transition.updateFrame(node: self.checkNode, frame: checkFrame)
var activityFrame = self.activityNode.frame
activityFrame.origin.x = params.leftInset + offset + editingOffset + floor((50.0 - activityFrame.width) / 2.0)
transition.updateFrame(node: self.activityNode, frame: activityFrame)
var infoIconFrame = self.infoIconNode.frame
infoIconFrame.origin.x = offset + params.width - params.rightInset - 55.0 + floor((55.0 - infoIconFrame.width) / 2.0)
transition.updateFrame(node: self.infoIconNode, frame: infoIconFrame)
self.infoButtonNode.isUserInteractionEnabled = offset.isZero
}
override func revealOptionsInteractivelyOpened() {
if let item = self.item {
item.setServerWithRevealedOptions(item.server, nil)
}
}
override func revealOptionsInteractivelyClosed() {
if let item = self.item {
item.setServerWithRevealedOptions(nil, item.server)
}
}
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
if let item = self.item {
item.removeServer(item.server)
}
}
override func isReorderable(at point: CGPoint) -> Bool {
if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point), !self.isDisplayingRevealedOptions {
return true
}
return false
}
@objc private func infoButtonPressed() {
self.item?.infoAction()
}
}
@@ -0,0 +1,748 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import ItemListPeerActionItem
import ItemListAvatarAndNameInfoItem
import ItemListPeerItem
private enum MediaType {
case photo
case video
}
private final class SaveIncomingMediaControllerArguments {
let context: AccountContext
let toggle: (MediaType) -> Void
let updateMaximumVideoSize: (Int64) -> Void
let openAddException: () -> Void
let openPeerMenu: (EnginePeer) -> Void
let setPeerIdWithRevealedOptions: (EnginePeer.Id?, EnginePeer.Id?) -> Void
let deletePeer: (EnginePeer.Id) -> Void
let deleteAllExceptions: () -> Void
init(context: AccountContext, toggle: @escaping (MediaType) -> Void, updateMaximumVideoSize: @escaping (Int64) -> Void, openAddException: @escaping () -> Void, openPeerMenu: @escaping (EnginePeer) -> Void, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, deletePeer: @escaping (EnginePeer.Id) -> Void, deleteAllExceptions: @escaping () -> Void) {
self.context = context
self.toggle = toggle
self.updateMaximumVideoSize = updateMaximumVideoSize
self.openAddException = openAddException
self.openPeerMenu = openPeerMenu
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.deletePeer = deletePeer
self.deleteAllExceptions = deleteAllExceptions
}
}
enum SaveIncomingMediaSection: ItemListSectionId {
case peer
case mediaTypes
case videoSize
case exceptions
case deleteAllExceptions
}
private enum SaveIncomingMediaEntry: ItemListNodeEntry {
enum StableId: Hashable {
case peer
case typesHeader
case typePhotos
case typeVideos
case typesInfo
case videoSizeHeader
case videoSize
case videoInfo
case exceptionsHeader
case addException
case exceptionItem(EnginePeer.Id)
case deleteAllExceptions
}
case peer(peer: EnginePeer, presence: EnginePeer.Presence?)
case typesHeader(String)
case typePhotos(String, Bool)
case typeVideos(String, Bool)
case typesInfo(String)
case videoSizeHeader(String)
case videoSize(decimalSeparator: String, text: String, value: Int64)
case videoInfo(String)
case exceptionsHeader(String)
case addException(String)
case exceptionItem(index: Int, peer: EnginePeer, label: String)
case deleteAllExceptions(String)
var section: ItemListSectionId {
switch self {
case .peer:
return SaveIncomingMediaSection.peer.rawValue
case .typesHeader, .typePhotos, .typeVideos, .typesInfo:
return SaveIncomingMediaSection.mediaTypes.rawValue
case .videoSizeHeader, .videoSize, .videoInfo:
return SaveIncomingMediaSection.videoSize.rawValue
case .exceptionsHeader, .addException, .exceptionItem:
return SaveIncomingMediaSection.exceptions.rawValue
case .deleteAllExceptions:
return SaveIncomingMediaSection.deleteAllExceptions.rawValue
}
}
var stableId: StableId {
switch self {
case .peer:
return .peer
case .typesHeader:
return .typesHeader
case .typePhotos:
return .typePhotos
case .typeVideos:
return .typeVideos
case .typesInfo:
return .typesInfo
case .videoSizeHeader:
return .videoSizeHeader
case .videoSize:
return .videoSize
case .videoInfo:
return .videoInfo
case .exceptionsHeader:
return .exceptionsHeader
case .addException:
return .addException
case let .exceptionItem(_, peer, _):
return .exceptionItem(peer.id)
case .deleteAllExceptions:
return .deleteAllExceptions
}
}
var sortIndex: Int {
switch self {
case .peer:
return 0
case .typesHeader:
return 1
case .typePhotos:
return 2
case .typeVideos:
return 3
case .typesInfo:
return 4
case .videoSizeHeader:
return 5
case .videoSize:
return 6
case .videoInfo:
return 7
case .exceptionsHeader:
return 8
case .addException:
return 9
case let .exceptionItem(index, _, _):
return 100 + index
case .deleteAllExceptions:
return 100000
}
}
static func <(lhs: SaveIncomingMediaEntry, rhs: SaveIncomingMediaEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! SaveIncomingMediaControllerArguments
switch self {
case let .peer(peer, presence):
return ItemListAvatarAndNameInfoItem(
itemContext: .accountContext(arguments.context),
presentationData: presentationData,
systemStyle: .glass,
dateTimeFormat: PresentationDateTimeFormat(),
mode: .generic,
peer: peer,
presence: presence,
memberCount: nil,
state: ItemListAvatarAndNameInfoItemState(),
sectionId: self.section,
style: .blocks(withTopInset: true, withExtendedBottomInset: false),
editingNameUpdated: { _ in
},
avatarTapped: {
}
)
case let .typesHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .typePhotos(title, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/DataPhotos"), title: title, value: value, sectionId: self.section, style: .blocks, updated: { _ in
arguments.toggle(.photo)
})
case let .typeVideos(title, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/DataVideo"), title: title, value: value, sectionId: self.section, style: .blocks, updated: { _ in
arguments.toggle(.video)
})
case let .typesInfo(text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .videoSizeHeader(title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .videoSize(decimalSeparator, text, size):
return AutodownloadSizeLimitItem(theme: presentationData.theme, strings: presentationData.strings, systemStyle: .glass, decimalSeparator: decimalSeparator, text: text, value: size, range: nil/*2 * 1024 * 1024 ..< (4 * 1024 * 1024 * 1024)*/, sectionId: self.section, updated: { value in
arguments.updateMaximumVideoSize(value)
})
case let .videoInfo(text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .exceptionsHeader(title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .addException(title):
let icon: UIImage? = PresentationResourcesItemList.createGroupIcon(presentationData.theme)
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: icon, title: title, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: {
arguments.openAddException()
})
case let .exceptionItem(_, peer, label):
return ItemListPeerItem(
presentationData: presentationData,
systemStyle: .glass,
dateTimeFormat: PresentationDateTimeFormat(),
nameDisplayOrder: .firstLast,
context: arguments.context,
peer: peer,
height: .generic,
aliasHandling: .threatSelfAsSaved,
nameColor: .primary,
presence: nil,
text: .text(label, .secondary),
label: .none,
editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: false),
revealOptions: ItemListPeerItemRevealOptions(options: [ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Common_Delete, action: {
arguments.deletePeer(peer.id)
})]),
switchValue: nil,
enabled: true,
selectable: true,
sectionId: self.section,
action: {
arguments.openPeerMenu(peer)
},
setPeerIdWithRevealedOptions: { lhs, rhs in
arguments.setPeerIdWithRevealedOptions(lhs, rhs)
},
removePeer: { id in
arguments.deletePeer(id)
}
)
/*return ItemListDisclosureItem(presentationData: presentationData, icon: nil, context: arguments.context, iconPeer: peer, title: peer.displayTitle(strings: presentationData.strings, displayOrder: .firstLast), enabled: true, titleFont: .bold, label: label, labelStyle: .detailText, additionalDetailLabel: nil, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
arguments.openPeerMenu(peer)
}, tag: nil)*/
case let .deleteAllExceptions(title):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.deleteAllExceptions()
})
}
}
}
private func saveIncomingMediaControllerEntries(presentationData: PresentationData, scope: SaveIncomingMediaScope, state: SaveIncomingMediaControllerState, peer: EnginePeer?, peerPresence: EnginePeer.Presence?, configuration: MediaAutoSaveConfiguration, exceptions: [MediaAutoSaveSettings.ExceptionItem], autosaveExceptionPeers: [EnginePeer.Id: EnginePeer?]) -> [SaveIncomingMediaEntry] {
var entries: [SaveIncomingMediaEntry] = []
if case .peer = scope, let peer {
entries.append(.peer(peer: peer, presence: peerPresence))
}
entries.append(.typesHeader(presentationData.strings.Autosave_TypesSection))
entries.append(.typePhotos(presentationData.strings.Autosave_TypePhoto, configuration.photo))
entries.append(.typeVideos(presentationData.strings.Autosave_TypeVideo, configuration.video))
entries.append(.typesInfo(presentationData.strings.Autosave_TypesInfo))
if configuration.video {
let sizeText: String
if configuration.maximumVideoSize == Int64.max {
sizeText = autodownloadDataSizeString(1536 * 1024 * 1024, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
} else {
sizeText = autodownloadDataSizeString(configuration.maximumVideoSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
}
let text = presentationData.strings.AutoDownloadSettings_UpTo(sizeText).string
entries.append(.videoSizeHeader(presentationData.strings.Autosave_VideoSizeSection))
entries.append(.videoSize(decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, text: text, value: configuration.maximumVideoSize))
entries.append(.videoInfo(presentationData.strings.Autosave_VideoInfo(sizeText).string))
}
if case let .peerType(peerType) = scope {
var filteredExceptions: [(EnginePeer, MediaAutoSaveConfiguration)] = []
for exception in exceptions {
guard let maybeExceptionPeer = autosaveExceptionPeers[exception.id], let exceptionPeer = maybeExceptionPeer else {
continue
}
let peerTypeValue: AutomaticSaveIncomingPeerType
switch exceptionPeer {
case .user, .secretChat:
peerTypeValue = .privateChats
case .legacyGroup:
peerTypeValue = .groups
case let .channel(channel):
if case .broadcast = channel.info {
peerTypeValue = .channels
} else {
peerTypeValue = .groups
}
}
if peerTypeValue == peerType {
filteredExceptions.append((exceptionPeer, exception.configuration))
}
}
if filteredExceptions.isEmpty {
entries.append(.exceptionsHeader(presentationData.strings.Autosave_ExceptionsSection))
} else {
entries.append(.exceptionsHeader(presentationData.strings.Notifications_CategoryExceptions(Int32(filteredExceptions.count)).uppercased()))
}
entries.append(.addException(presentationData.strings.Autosave_AddException))
var index = 0
for (exceptionPeer, exceptionConfiguration) in filteredExceptions {
var label = ""
if exceptionConfiguration.photo {
if !label.isEmpty {
label.append(", ")
}
label.append(presentationData.strings.Settings_AutosaveMediaPhoto)
} else {
if !label.isEmpty {
label.append(", ")
}
label.append(presentationData.strings.Settings_AutosaveMediaNoPhoto)
}
if exceptionConfiguration.video {
if !label.isEmpty {
label.append(", ")
}
label.append(presentationData.strings.Settings_AutosaveMediaVideo(dataSizeString(Int(exceptionConfiguration.maximumVideoSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string)
} else {
if !label.isEmpty {
label.append(", ")
}
label.append(presentationData.strings.Settings_AutosaveMediaNoVideo)
}
entries.append(.exceptionItem(index: index, peer: exceptionPeer, label: label))
index += 1
}
if !filteredExceptions.isEmpty {
entries.append(.deleteAllExceptions(presentationData.strings.Autosave_DeleteAllExceptions))
}
}
return entries
}
enum SaveIncomingMediaScope {
case peer(EnginePeer.Id)
case addPeer(id: EnginePeer.Id, completion: (MediaAutoSaveConfiguration) -> Void)
case peerType(AutomaticSaveIncomingPeerType)
}
private struct SaveIncomingMediaControllerState: Equatable {
var pendingConfiguration: MediaAutoSaveConfiguration = .default
var peerIdWithOptions: EnginePeer.Id?
}
func saveIncomingMediaController(context: AccountContext, scope: SaveIncomingMediaScope) -> ViewController {
let stateValue = Atomic(value: SaveIncomingMediaControllerState())
let statePromise = ValuePromise<SaveIncomingMediaControllerState>(stateValue.with { $0 })
let updateState: ((SaveIncomingMediaControllerState) -> SaveIncomingMediaControllerState) -> Void = { f in
var changed = false
let value = stateValue.modify { current in
let updated = f(current)
if updated != current {
changed = true
}
return updated
}
if changed {
statePromise.set(value)
}
}
var pushController: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController) -> Void)?
var dismiss: (() -> Void)?
let arguments = SaveIncomingMediaControllerArguments(
context: context,
toggle: { type in
if case .addPeer = scope {
updateState { state in
var state = state
switch type {
case .photo:
state.pendingConfiguration.photo = !state.pendingConfiguration.photo
case .video:
state.pendingConfiguration.video = !state.pendingConfiguration.video
}
return state
}
} else {
let _ = updateMediaAutoSaveSettingsInteractively(account: context.account, { settings in
var settings = settings
switch scope {
case let .peer(peerId):
if let index = settings.exceptions.firstIndex(where: { $0.id == peerId }) {
switch type {
case .photo:
settings.exceptions[index].configuration.photo = !settings.exceptions[index].configuration.photo
case .video:
settings.exceptions[index].configuration.video = !settings.exceptions[index].configuration.video
}
}
case .addPeer:
break
case let .peerType(peerType):
let mappedType: MediaAutoSaveSettings.PeerType
switch peerType {
case .privateChats:
mappedType = .users
case .groups:
mappedType = .groups
case .channels:
mappedType = .channels
}
var current = settings.configurations[mappedType] ?? .default
switch type {
case .photo:
current.photo = !current.photo
case .video:
current.video = !current.video
}
settings.configurations[mappedType] = current
}
return settings
}).start()
}
},
updateMaximumVideoSize: { value in
if case .addPeer = scope {
updateState { state in
var state = state
state.pendingConfiguration.maximumVideoSize = value
return state
}
} else {
let _ = updateMediaAutoSaveSettingsInteractively(account: context.account, { settings in
var settings = settings
switch scope {
case let .peer(peerId):
if let index = settings.exceptions.firstIndex(where: { $0.id == peerId }) {
settings.exceptions[index].configuration.maximumVideoSize = value
}
case .addPeer:
break
case let .peerType(peerType):
let mappedType: MediaAutoSaveSettings.PeerType
switch peerType {
case .privateChats:
mappedType = .users
case .groups:
mappedType = .groups
case .channels:
mappedType = .channels
}
var current = settings.configurations[mappedType] ?? .default
current.maximumVideoSize = value
settings.configurations[mappedType] = current
}
return settings
}).start()
}
},
openAddException: {
guard case let .peerType(peerType) = scope else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var filter: ChatListNodePeersFilter = [.excludeRecent, .doNotSearchMessages, .removeSearchHeader]
switch peerType {
case .groups:
filter.insert(.onlyGroups)
case .privateChats:
filter.insert(.onlyPrivateChats)
filter.insert(.excludeSecretChats)
case .channels:
filter.insert(.onlyChannels)
}
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: filter, hasContactSelector: false, title: presentationData.strings.Notifications_AddExceptionTitle))
controller.peerSelected = { [weak controller] peer, _ in
let peerId = peer.id
let preferencesKey: PostboxViewKey = .preferences(keys: Set([ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings]))
let preferences = context.account.postbox.combinedView(keys: [preferencesKey])
|> map { views -> MediaAutoSaveSettings in
guard let view = views.views[preferencesKey] as? PreferencesView else {
return .default
}
return view.values[ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings]?.get(MediaAutoSaveSettings.self) ?? MediaAutoSaveSettings.default
}
let _ = (preferences
|> take(1)
|> deliverOnMainQueue).start(next: { settings in
if settings.exceptions.contains(where: { $0.id == peerId }) {
guard let controller = controller, let navigationController = controller.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers
controllers = controllers.filter { item in
if item === controller {
return false
}
return true
}
controllers.append(saveIncomingMediaController(context: context, scope: .peer(peerId)))
navigationController.setViewControllers(controllers, animated: true)
} else {
var dismissAll: (() -> Void)?
let exceptionController = saveIncomingMediaController(context: context, scope: .addPeer(id: peerId, completion: { configuration in
let _ = updateMediaAutoSaveSettingsInteractively(account: context.account, { settings in
var settings = settings
settings.exceptions.removeAll(where: { $0.id == peerId })
settings.exceptions.insert(MediaAutoSaveSettings.ExceptionItem(id: peerId, configuration: configuration), at: 0)
return settings
}).start()
dismissAll?()
}))
controller?.push(exceptionController)
dismissAll = { [weak exceptionController] in
guard let exceptionController = exceptionController else {
return
}
guard let navigationController = exceptionController.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers
controllers = controllers.filter { item in
if item === exceptionController || item === controller {
return false
}
return true
}
navigationController.setViewControllers(controllers, animated: true)
}
}
})
}
pushController?(controller)
},
openPeerMenu: { peer in
pushController?(saveIncomingMediaController(context: context, scope: .peer(peer.id)))
},
setPeerIdWithRevealedOptions: { itemId, fromItemId in
updateState { state in
var state = state
if (itemId == nil && fromItemId == state.peerIdWithOptions) || (itemId != nil && fromItemId == nil) {
state.peerIdWithOptions = itemId
}
return state
}
},
deletePeer: { id in
let _ = updateMediaAutoSaveSettingsInteractively(account: context.account, { settings in
var settings = settings
settings.exceptions.removeAll(where: { $0.id == id })
return settings
}).start()
},
deleteAllExceptions: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Autosave_DeleteAllExceptions, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let _ = updateMediaAutoSaveSettingsInteractively(account: context.account, { settings in
var settings = settings
settings.exceptions.removeAll()
return settings
}).start()
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet)
}
)
let preferencesKey: PostboxViewKey = .preferences(keys: Set([ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings]))
let preferences = context.account.postbox.combinedView(keys: [preferencesKey])
|> map { views -> MediaAutoSaveSettings in
guard let view = views.views[preferencesKey] as? PreferencesView else {
return .default
}
return view.values[ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings]?.get(MediaAutoSaveSettings.self) ?? MediaAutoSaveSettings.default
}
let peer: Signal<(EnginePeer?, EnginePeer.Presence?), NoError>
switch scope {
case let .peer(id):
peer = context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: id),
TelegramEngine.EngineData.Item.Peer.Presence(id: id)
)
case let .addPeer(id, _):
peer = context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: id),
TelegramEngine.EngineData.Item.Peer.Presence(id: id)
)
default:
peer = .single((nil, nil))
}
let autosaveExceptionPeers: Signal<[EnginePeer.Id: EnginePeer?], NoError> = preferences
|> mapToSignal { mediaAutoSaveSettings -> Signal<[EnginePeer.Id: EnginePeer?], NoError> in
let peerIds = mediaAutoSaveSettings.exceptions.map(\.id)
return context.engine.data.get(EngineDataMap(
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))
))
}
struct StoredState {
var entryCount: Int
var hasVideo: Bool
}
let previousState = Atomic<StoredState?>(value: nil)
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), preferences, peer, autosaveExceptionPeers)
|> deliverOnMainQueue
|> map { presentationData, state, mediaAutoSaveSettings, peer, autosaveExceptionPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in
var rightButton: ItemListNavigationButton?
switch scope {
case .peer, .addPeer:
rightButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
switch scope {
case let .addPeer(_, completion):
let configuration = stateValue.with({ $0 }).pendingConfiguration
completion(configuration)
default:
dismiss?()
}
})
default:
break
}
let configuration: MediaAutoSaveConfiguration
var exceptions: [MediaAutoSaveSettings.ExceptionItem] = []
let title: String
switch scope {
case let .peer(id):
if let data = mediaAutoSaveSettings.exceptions.first(where: { $0.id == id }) {
configuration = data.configuration
} else {
configuration = .default
}
title = presentationData.strings.Autosave_Exception
case .addPeer:
configuration = state.pendingConfiguration
title = presentationData.strings.Autosave_AddException
case let .peerType(peerType):
exceptions = mediaAutoSaveSettings.exceptions
switch peerType {
case .privateChats:
configuration = mediaAutoSaveSettings.configurations[.users] ?? .default
title = presentationData.strings.Notifications_PrivateChats
case .groups:
configuration = mediaAutoSaveSettings.configurations[.groups] ?? .default
title = presentationData.strings.Notifications_GroupChats
case .channels:
configuration = mediaAutoSaveSettings.configurations[.channels] ?? .default
title = presentationData.strings.Notifications_Channels
}
}
let entries = saveIncomingMediaControllerEntries(presentationData: presentationData, scope: scope, state: state, peer: peer.0, peerPresence: peer.1, configuration: configuration, exceptions: exceptions, autosaveExceptionPeers: autosaveExceptionPeers)
var animateChanges = false
let storedState = StoredState(
entryCount: entries.count,
hasVideo: entries.contains(where: { entry in
switch entry {
case .videoSize:
return true
default:
return false
}
})
)
if let previous = previousState.swap(storedState) {
if previous.entryCount > storedState.entryCount || previous.hasVideo != storedState.hasVideo {
animateChanges = true
}
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: nil, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
switch scope {
case .peer, .addPeer:
controller.navigationPresentation = .modal
default:
break
}
pushController = { [weak controller] c in
controller?.push(c)
}
presentControllerImpl = { [weak controller] c in
controller?.present(c, in: .window(.root))
}
dismiss = { [weak controller] in
controller?.dismiss()
}
return controller
}
@@ -0,0 +1,185 @@
import Foundation
import UIKit
import Display
import TelegramCore
import AsyncDisplayKit
import UIKit
import SwiftSignalKit
import TelegramPresentationData
import QrCode
import ShareController
public final class ShareProxyServerActionSheetController: ActionSheetController {
private var presentationDisposable: Disposable?
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private var isDismissed: Bool = false
public init(presentationData: PresentationData, updatedPresentationData: Signal<PresentationData, NoError>, link: String) {
let sheetTheme = ActionSheetControllerTheme(presentationData: presentationData)
super.init(theme: sheetTheme)
let presentActivityController: (Any) -> Void = { [weak self] item in
let activityController = UIActivityViewController(activityItems: [item], applicationActivities: nil)
if let window = self?.view.window, let rootViewController = window.rootViewController {
activityController.popoverPresentationController?.sourceView = window
activityController.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
rootViewController.present(activityController, animated: true, completion: nil)
}
}
var items: [ActionSheetItem] = []
items.append(ProxyServerQRCodeItem(strings: presentationData.strings, link: link, ready: { [weak self] in
self?._ready.set(.single(true))
}))
items.append(ActionSheetButtonItem(title: presentationData.strings.SocksProxySetup_ShareQRCode, action: { [weak self] in
self?.dismissAnimated()
let _ = (qrCode(string: link, color: .black, backgroundColor: .white, icon: .proxy)
|> map { _, generator -> UIImage? in
let imageSize = CGSize(width: 768.0, height: 768.0)
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), scale: 1.0))
return context?.generateImage()
}
|> deliverOnMainQueue).start(next: { image in
if let image = image {
presentActivityController(image)
}
})
}))
items.append(ActionSheetButtonItem(title: presentationData.strings.SocksProxySetup_ShareLink, action: { [weak self] in
self?.dismissAnimated()
presentActivityController(link)
}))
self.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { [weak self] in
self?.dismissAnimated()
})
])
])
self.presentationDisposable = updatedPresentationData.start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData)
}
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDisposable?.dispose()
}
}
private final class ProxyServerQRCodeItem: ActionSheetItem {
private let strings: PresentationStrings
private let link: String
private let ready: () -> Void
init(strings: PresentationStrings, link: String, ready: @escaping () -> Void = {}) {
self.strings = strings
self.link = link
self.ready = ready
}
func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
return ProxyServerQRCodeItemNode(theme: theme, strings: self.strings, link: self.link, ready: self.ready)
}
func updateNode(_ node: ActionSheetItemNode) {
}
}
private final class ProxyServerQRCodeItemNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
private let strings: PresentationStrings
private let link: String
private let label: ASTextNode
private let imageNode: TransformImageNode
private let ready: () -> Void
private var cachedHasLabel = true
private var cachedHasImage = true
init(theme: ActionSheetControllerTheme, strings: PresentationStrings, link: String, ready: @escaping () -> Void = {}) {
self.theme = theme
self.strings = strings
self.link = link
self.ready = ready
let textFont = Font.regular(floor(theme.baseFontSize * 13.0 / 17.0))
self.label = ASTextNode()
self.label.isUserInteractionEnabled = false
self.label.maximumNumberOfLines = 0
self.label.displaysAsynchronously = false
self.label.truncationMode = .byTruncatingTail
self.label.isUserInteractionEnabled = false
self.label.attributedText = NSAttributedString(string: strings.SocksProxySetup_ShareQRCodeInfo, font: textFont, textColor: self.theme.secondaryTextColor, paragraphAlignment: .center)
self.imageNode = TransformImageNode()
self.imageNode.clipsToBounds = true
self.imageNode.setSignal(qrCode(string: link, color: .black, backgroundColor: .white, icon: .proxy) |> map { $0.1 }, attemptSynchronously: true)
self.imageNode.cornerRadius = 14.0
super.init(theme: theme)
self.addSubnode(self.label)
self.addSubnode(self.imageNode)
}
override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let imageInset: CGFloat = 44.0
let side = constrainedSize.width - imageInset * 2.0
var imageSize = CGSize(width: side, height: side)
let makeLayout = self.imageNode.asyncLayout()
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil))
apply()
var labelSize = self.label.measure(CGSize(width: max(1.0, constrainedSize.width - 64.0), height: constrainedSize.height))
self.cachedHasImage = constrainedSize.width < constrainedSize.height
if !self.cachedHasImage {
imageSize = CGSize()
}
self.ready()
self.cachedHasLabel = constrainedSize.height > 480 || !self.cachedHasImage
if !self.cachedHasLabel {
labelSize = CGSize()
}
let size = CGSize(width: constrainedSize.width, height: 14.0 + (labelSize.height > 0.0 ? labelSize.height + 14.0 : 0.0) + (imageSize.height > 0.0 ? imageSize.height + 14.0 : 8.0))
let inset: CGFloat = 32.0
let spacing: CGFloat = 18.0
if self.cachedHasLabel {
labelSize = self.label.measure(CGSize(width: max(1.0, size.width - inset * 2.0), height: size.height))
self.label.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: spacing), size: labelSize)
} else {
labelSize = CGSize()
}
if !self.cachedHasImage {
imageSize = CGSize()
} else {
imageSize = CGSize(width: size.width - imageInset * 2.0, height: size.width - imageInset * 2.0)
}
let imageOrigin = CGPoint(x: imageInset, y: self.label.frame.maxY + spacing - 4.0)
self.imageNode.frame = CGRect(origin: imageOrigin, size: imageSize)
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
}
@@ -0,0 +1,516 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import AccountContext
import ItemListPeerItem
import UndoUI
import ContextUI
import ItemListPeerActionItem
private enum StorageUsageExceptionsEntryTag: Hashable, ItemListItemTag {
case peer(EnginePeer.Id)
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? StorageUsageExceptionsEntryTag, self == other {
return true
} else {
return false
}
}
}
private final class StorageUsageExceptionsScreenArguments {
let context: AccountContext
let openAddException: () -> Void
let openPeerMenu: (EnginePeer.Id, Int32) -> Void
init(
context: AccountContext,
openAddException: @escaping () -> Void,
openPeerMenu: @escaping (EnginePeer.Id, Int32) -> Void
) {
self.context = context
self.openAddException = openAddException
self.openPeerMenu = openPeerMenu
}
}
private enum StorageUsageExceptionsSection: Int32 {
case add
case items
}
private enum StorageUsageExceptionsEntry: ItemListNodeEntry {
enum SortIndex: Equatable, Comparable {
case index(Int)
case peer(index: Int, peerId: EnginePeer.Id)
static func <(lhs: SortIndex, rhs: SortIndex) -> Bool {
switch lhs {
case let .index(index):
if case let .index(rhsIndex) = rhs {
return index < rhsIndex
} else {
return true
}
case let .peer(index, peerId):
if case let .peer(rhsIndex, rhsPeerId) = rhs {
if index != rhsIndex {
return index < rhsIndex
} else {
return peerId < rhsPeerId
}
} else {
return false
}
}
}
}
enum StableId: Hashable {
case index(Int)
case peer(EnginePeer.Id)
}
case addException(String)
case exceptionsHeader(String)
case peer(index: Int, peer: FoundPeer, value: Int32)
var section: ItemListSectionId {
switch self {
case .addException:
return StorageUsageExceptionsSection.add.rawValue
case .exceptionsHeader, .peer:
return StorageUsageExceptionsSection.items.rawValue
}
}
var stableId: StableId {
switch self {
case .addException:
return .index(0)
case .exceptionsHeader:
return .index(1)
case let .peer(_, peer, _):
return .peer(peer.peer.id)
}
}
var sortIndex: SortIndex {
switch self {
case .addException:
return .index(0)
case .exceptionsHeader:
return .index(1)
case let .peer(index, peer, _):
return .peer(index: index, peerId: peer.peer.id)
}
}
static func ==(lhs: StorageUsageExceptionsEntry, rhs: StorageUsageExceptionsEntry) -> Bool {
switch lhs {
case let .addException(text):
if case .addException(text) = rhs {
return true
} else {
return false
}
case let .exceptionsHeader(text):
if case .exceptionsHeader(text) = rhs {
return true
} else {
return false
}
case let .peer(index, peer, value):
if case .peer(index, peer, value) = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: StorageUsageExceptionsEntry, rhs: StorageUsageExceptionsEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! StorageUsageExceptionsScreenArguments
switch self {
case let .addException(text):
let icon: UIImage? = PresentationResourcesItemList.createGroupIcon(presentationData.theme)
return ItemListPeerActionItem(presentationData: presentationData, icon: icon, title: text, alwaysPlain: false, sectionId: self.section, editing: false, action: {
arguments.openAddException()
})
case let .exceptionsHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .peer(_, peer, value):
var additionalDetailLabel: String?
if let subscribers = peer.subscribers {
additionalDetailLabel = presentationData.strings.VoiceChat_Panel_Members(subscribers)
}
let optionText: String
if value == Int32.max {
optionText = presentationData.strings.ClearCache_Forever
} else {
optionText = timeIntervalString(strings: presentationData.strings, value: value)
}
let title: String
if peer.peer.id == arguments.context.account.peerId {
title = presentationData.strings.DialogList_SavedMessages
} else {
title = EnginePeer(peer.peer).displayTitle(strings: presentationData.strings, displayOrder: .firstLast)
}
return ItemListDisclosureItem(presentationData: presentationData, icon: nil, context: arguments.context, iconPeer: EnginePeer(peer.peer), title: title, enabled: true, titleFont: .bold, label: optionText, labelStyle: .text, additionalDetailLabel: additionalDetailLabel, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: {
arguments.openPeerMenu(peer.peer.id, value)
}, tag: StorageUsageExceptionsEntryTag.peer(peer.peer.id))
}
}
}
private struct StorageUsageExceptionsState: Equatable {
}
private func storageUsageExceptionsScreenEntries(
presentationData: PresentationData,
peerExceptions: [(peer: FoundPeer, value: Int32)],
state: StorageUsageExceptionsState
) -> [StorageUsageExceptionsEntry] {
var entries: [StorageUsageExceptionsEntry] = []
entries.append(.addException(presentationData.strings.Notification_Exceptions_AddException))
if !peerExceptions.isEmpty {
entries.append(.exceptionsHeader(presentationData.strings.Notifications_CategoryExceptions(Int32(peerExceptions.count)).uppercased()))
var index = 100
for item in peerExceptions {
entries.append(.peer(index: index, peer: item.peer, value: item.value))
index += 1
}
}
return entries
}
public func storageUsageExceptionsScreen(
context: AccountContext,
category: CacheStorageSettings.PeerStorageCategory,
isModal: Bool = false
) -> ViewController {
let statePromise = ValuePromise(StorageUsageExceptionsState())
let stateValue = Atomic(value: StorageUsageExceptionsState())
let updateState: ((StorageUsageExceptionsState) -> StorageUsageExceptionsState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let _ = updateState
let cacheSettingsPromise = Promise<CacheStorageSettings>()
cacheSettingsPromise.set(context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings])
|> map { sharedData -> CacheStorageSettings in
let cacheSettings: CacheStorageSettings
if let value = sharedData.entries[SharedDataKeys.cacheStorageSettings]?.get(CacheStorageSettings.self) {
cacheSettings = value
} else {
cacheSettings = CacheStorageSettings.defaultSettings
}
return cacheSettings
})
let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings]))
let accountSpecificSettings: Signal<AccountSpecificCacheStorageSettings, NoError> = context.account.postbox.combinedView(keys: [viewKey])
|> map { views -> AccountSpecificCacheStorageSettings in
let cacheSettings: AccountSpecificCacheStorageSettings
if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) {
cacheSettings = value
} else {
cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings
}
return cacheSettings
}
|> distinctUntilChanged
let peerExceptions: Signal<[(peer: FoundPeer, value: Int32)], NoError> = accountSpecificSettings
|> mapToSignal { accountSpecificSettings -> Signal<[(peer: FoundPeer, value: Int32)], NoError> in
return context.account.postbox.transaction { transaction -> [(peer: FoundPeer, value: Int32)] in
var result: [(peer: FoundPeer, value: Int32)] = []
for item in accountSpecificSettings.peerStorageTimeoutExceptions {
let peerId = item.key
let value = item.value
guard let peer = transaction.getPeer(peerId) else {
continue
}
let peerCategory: CacheStorageSettings.PeerStorageCategory
var subscriberCount: Int32?
if peer is TelegramUser {
peerCategory = .privateChats
} else if peer is TelegramGroup {
peerCategory = .groups
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData {
subscriberCount = (cachedData.participants?.participants.count).flatMap(Int32.init)
}
} else if let channel = peer as? TelegramChannel {
if case .group = channel.info {
peerCategory = .groups
} else {
peerCategory = .channels
}
if peerCategory == category {
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
subscriberCount = cachedData.participantsSummary.memberCount
}
}
} else {
continue
}
if peerCategory != category {
continue
}
result.append((peer: FoundPeer(peer: peer, subscribers: subscriberCount), value: value))
}
return result.sorted(by: { lhs, rhs in
if lhs.value != rhs.value {
return lhs.value < rhs.value
}
return lhs.peer.peer.debugDisplayTitle < rhs.peer.peer.debugDisplayTitle
})
}
}
var presentControllerImpl: ((ViewController, PresentationContextType, Any?) -> Void)?
let _ = presentControllerImpl
var pushControllerImpl: ((ViewController) -> Void)?
var findPeerReferenceNode: ((EnginePeer.Id) -> ItemListDisclosureItemNode?)?
let _ = findPeerReferenceNode
var presentInGlobalOverlay: ((ViewController) -> Void)?
let _ = presentInGlobalOverlay
let actionDisposables = DisposableSet()
let clearDisposable = MetaDisposable()
actionDisposables.add(clearDisposable)
let arguments = StorageUsageExceptionsScreenArguments(
context: context,
openAddException: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var filter: ChatListNodePeersFilter = [.excludeRecent, .doNotSearchMessages, .removeSearchHeader]
switch category {
case .groups:
filter.insert(.onlyGroups)
case .privateChats:
filter.insert(.onlyPrivateChats)
filter.insert(.excludeSecretChats)
case .channels:
filter.insert(.onlyChannels)
case .stories:
filter.insert(.onlyPrivateChats)
}
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: filter, hasContactSelector: false, title: presentationData.strings.Notifications_AddExceptionTitle))
controller.peerSelected = { [weak controller] peer, _ in
let peerId = peer.id
let _ = updateAccountSpecificCacheStorageSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
for i in 0 ..< settings.peerStorageTimeoutExceptions.count {
if settings.peerStorageTimeoutExceptions[i].key == peerId {
settings.peerStorageTimeoutExceptions.remove(at: i)
break
}
}
settings.peerStorageTimeoutExceptions.append(AccountSpecificCacheStorageSettings.Value(key: peerId, value: Int32.max))
return settings
}).start()
controller?.dismiss()
}
pushControllerImpl?(controller)
},
openPeerMenu: { peerId, currentValue in
let applyValue: (Int32?) -> Void = { value in
let _ = updateAccountSpecificCacheStorageSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
if let value = value {
var found = false
for i in 0 ..< settings.peerStorageTimeoutExceptions.count {
if settings.peerStorageTimeoutExceptions[i].key == peerId {
found = true
settings.peerStorageTimeoutExceptions[i] = AccountSpecificCacheStorageSettings.Value(key: peerId, value: value)
break
}
}
if !found {
settings.peerStorageTimeoutExceptions.append(AccountSpecificCacheStorageSettings.Value(key: peerId, value: value))
}
} else {
for i in 0 ..< settings.peerStorageTimeoutExceptions.count {
if settings.peerStorageTimeoutExceptions[i].key == peerId {
settings.peerStorageTimeoutExceptions.remove(at: i)
break
}
}
}
return settings
}).start()
}
var subItems: [ContextMenuItem] = []
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let presetValues: [Int32] = [
Int32.max,
31 * 24 * 60 * 60,
7 * 24 * 60 * 60,
1 * 24 * 60 * 60
]
for value in presetValues {
let optionText: String
if value == Int32.max {
optionText = presentationData.strings.ClearCache_Forever
} else {
optionText = timeIntervalString(strings: presentationData.strings, value: value)
}
subItems.append(.action(ContextMenuActionItem(text: optionText, icon: { theme in
if currentValue == value {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
} else {
return nil
}
}, action: { _, f in
applyValue(value)
f(.default)
})))
}
subItems.append(.separator)
subItems.append(.action(ContextMenuActionItem(text: presentationData.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { _, f in
f(.default)
applyValue(nil)
})))
if let sourceNode = findPeerReferenceNode?(peerId) {
let items: Signal<ContextController.Items, NoError> = .single(ContextController.Items(content: .list(subItems)))
let source: ContextContentSource = .reference(StorageUsageExceptionsContextReferenceContentSource(sourceView: sourceNode.labelNode.view))
let contextController = ContextController(
presentationData: presentationData,
source: source,
items: items,
gesture: nil
)
sourceNode.updateHasContextMenu(hasContextMenu: true)
contextController.dismissed = { [weak sourceNode] in
sourceNode?.updateHasContextMenu(hasContextMenu: false)
}
presentInGlobalOverlay?(contextController)
}
}
)
let _ = cacheSettingsPromise
var dismissImpl: (() -> Void)?
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
peerExceptions,
statePromise.get()
)
|> deliverOnMainQueue
|> map { presentationData, peerExceptions, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = isModal ? ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
}) : nil
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Notifications_ExceptionsTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: storageUsageExceptionsScreenEntries(presentationData: presentationData, peerExceptions: peerExceptions, state: state), style: .blocks, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionDisposables.dispose()
}
let controller = ItemListController(context: context, state: signal)
if isModal {
controller.navigationPresentation = .modal
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
}
presentControllerImpl = { [weak controller] c, contextType, a in
controller?.present(c, in: contextType, with: a)
}
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
presentInGlobalOverlay = { [weak controller] c in
controller?.presentInGlobalOverlay(c, with: nil)
}
findPeerReferenceNode = { [weak controller] peerId in
guard let controller else {
return nil
}
let targetTag: StorageUsageExceptionsEntryTag = .peer(peerId)
var resultItemNode: ItemListItemNode?
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode {
if let tag = itemNode.tag, tag.isEqual(to: targetTag) {
resultItemNode = itemNode
return
}
}
}
if let resultItemNode = resultItemNode as? ItemListDisclosureItemNode {
return resultItemNode
} else {
return nil
}
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
return controller
}
private final class StorageUsageExceptionsContextReferenceContentSource: ContextReferenceContentSource {
private let sourceView: UIView
init(sourceView: UIView) {
self.sourceView = sourceView
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0))
}
}
@@ -0,0 +1,320 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramUIPreferences
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
struct StorageUsageCategory: Equatable {
let title: String
let size: Int64
let fraction: CGFloat
let color: UIColor
}
final class StorageUsageItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let dateTimeFormat: PresentationDateTimeFormat
let categories: [StorageUsageCategory]
let sectionId: ItemListSectionId
init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, categories: [StorageUsageCategory], sectionId: ItemListSectionId) {
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.categories = categories
self.sectionId = sectionId
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = StorageUsageItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? StorageUsageItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private func generateDotImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 8.0, height: 8.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(color.cgColor)
context.fillEllipse(in: bounds)
})
}
private func generateLineMaskImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 8.0, height: 8.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.setFillColor(color.cgColor)
context.fill(bounds)
context.setBlendMode(.clear)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: bounds)
})?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 4)
}
private final class StorageUsageItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let lineMaskNode: ASImageNode
private var lineNodes: [ASDisplayNode]
private var descriptionNodes: [(ASImageNode, TextNode)]
private var item: StorageUsageItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.lineMaskNode = ASImageNode()
self.lineMaskNode.displaysAsynchronously = false
self.lineMaskNode.displayWithoutProcessing = true
self.lineMaskNode.contentMode = .scaleToFill
self.lineNodes = []
self.descriptionNodes = []
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.lineMaskNode)
}
func asyncLayout() -> (_ item: StorageUsageItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { [weak self] item, params, neighbors in
if let strongSelf = self, strongSelf.lineNodes.count != item.categories.count {
for node in strongSelf.lineNodes {
node.removeFromSupernode()
}
strongSelf.lineNodes = []
for pair in strongSelf.descriptionNodes {
pair.0.removeFromSupernode()
pair.1.removeFromSupernode()
}
strongSelf.descriptionNodes = []
for _ in item.categories {
let lineNode = ASDisplayNode()
strongSelf.insertSubnode(lineNode, belowSubnode: strongSelf.lineMaskNode)
strongSelf.lineNodes.append(lineNode)
let dotNode = ASImageNode()
dotNode.displaysAsynchronously = false
dotNode.displayWithoutProcessing = true
strongSelf.addSubnode(dotNode)
let textNode = TextNode()
strongSelf.addSubnode(textNode)
strongSelf.descriptionNodes.append((dotNode, textNode))
}
}
var makeNodesLayout: [(TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)] = []
if let strongSelf = self {
for nodes in strongSelf.descriptionNodes {
let makeTextLayout = TextNode.asyncLayout(nodes.1)
makeNodesLayout.append(makeTextLayout)
}
}
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
var textFramesApplies: [(CGRect, () -> TextNode)] = []
let inset: CGFloat = 16.0
let horizontalSpacing: CGFloat = 32.0
let verticalSpacing: CGFloat = 22.0
var textOrigin: CGPoint = CGPoint(x: params.leftInset + horizontalSpacing, y: 52.0)
for i in 0 ..< item.categories.count {
let makeTextLayout = makeNodesLayout[i]
let category = item.categories[i]
let attributedString = NSMutableAttributedString(string: category.title, font: Font.regular(14.0), textColor: item.theme.list.itemPrimaryTextColor, paragraphAlignment: .natural)
attributedString.append(NSAttributedString(string: "\(dataSizeString(category.size, forceDecimal: true, formatting: DataSizeStringFormatting(strings: item.strings, decimalSeparator: item.dateTimeFormat.decimalSeparator)))", font: Font.bold(14.0), textColor: item.theme.list.itemPrimaryTextColor))
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 60.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var textFrame = CGRect(origin: textOrigin, size: textLayout.size)
if textFrame.maxX > params.width - params.rightInset - inset {
textFrame.origin = CGPoint(x: params.leftInset + horizontalSpacing, y: textOrigin.y + verticalSpacing)
}
textOrigin = CGPoint(x: textFrame.maxX + horizontalSpacing, y: textFrame.minY)
textFramesApplies.append((textFrame, textApply))
}
contentSize = CGSize(width: params.width, height: textOrigin.y + 34.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if themeUpdated {
strongSelf.lineMaskNode.image = generateLineMaskImage(color: item.theme.list.itemBlocksBackgroundColor)
}
for (_, textApply) in textFramesApplies {
let _ = textApply()
}
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
let lineInset: CGFloat = params.leftInset + 12.0
var lineOrigin = CGPoint(x: lineInset, y: 16.0)
let lineWidth = params.width - lineOrigin.x * 2.0
strongSelf.lineMaskNode.frame = CGRect(origin: lineOrigin, size: CGSize(width: lineWidth, height: 21.0))
for i in 0 ..< strongSelf.lineNodes.count {
let lineNode = strongSelf.lineNodes[i]
let category = item.categories[i]
lineNode.backgroundColor = category.color
var categoryWidth = max(floor(lineWidth * category.fraction), 2.0)
if i == strongSelf.lineNodes.count - 1 {
categoryWidth = max(0.0, lineWidth - (lineOrigin.x - lineInset))
}
let lineRect = CGRect(origin: lineOrigin, size: CGSize(width: categoryWidth, height: 21.0))
lineNode.frame = lineRect
lineOrigin.x += lineRect.width + 1.0
}
for i in 0 ..< strongSelf.descriptionNodes.count {
let dotNode = strongSelf.descriptionNodes[i].0
let textNode = strongSelf.descriptionNodes[i].1
let textFrame = textFramesApplies[i].0
let category = item.categories[i]
if dotNode.image == nil || themeUpdated {
dotNode.image = generateDotImage(color: category.color)
}
dotNode.frame = CGRect(x: textFrame.minX - 16.0, y: textFrame.minY + 4.0, width: 8.0, height: 8.0)
textNode.frame = textFrame
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,165 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
private final class VoiceCallDataSavingControllerArguments {
let updateSelection: (VoiceCallDataSaving) -> Void
init(updateSelection: @escaping (VoiceCallDataSaving) -> Void) {
self.updateSelection = updateSelection
}
}
private enum VoiceCallDataSavingSection: Int32 {
case dataSaving
}
private enum VoiceCallDataSavingEntry: ItemListNodeEntry {
case never(PresentationTheme, String, Bool)
case cellular(PresentationTheme, String, Bool)
case always(PresentationTheme, String, Bool)
case info(PresentationTheme, String)
var section: ItemListSectionId {
return VoiceCallDataSavingSection.dataSaving.rawValue
}
var stableId: Int32 {
switch self {
case .never:
return 0
case .cellular:
return 1
case .always:
return 2
case .info:
return 3
}
}
static func ==(lhs: VoiceCallDataSavingEntry, rhs: VoiceCallDataSavingEntry) -> Bool {
switch lhs {
case let .never(lhsTheme, lhsText, lhsValue):
if case let .never(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .cellular(lhsTheme, lhsText, lhsValue):
if case let .cellular(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .always(lhsTheme, lhsText, lhsValue):
if case let .always(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .info(lhsTheme, lhsText):
if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: VoiceCallDataSavingEntry, rhs: VoiceCallDataSavingEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! VoiceCallDataSavingControllerArguments
switch self {
case let .never(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateSelection(.never)
})
case let .cellular(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateSelection(.cellular)
})
case let .always(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateSelection(.always)
})
case let .info(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
}
}
private func stringForDataSavingOption(_ option: VoiceCallDataSaving, strings: PresentationStrings) -> String {
switch option {
case .never:
return strings.CallSettings_Never
case .cellular:
return strings.CallSettings_OnMobile
case .always:
return strings.CallSettings_Always
default:
return ""
}
}
private func voiceCallDataSavingControllerEntries(presentationData: PresentationData, dataSaving: VoiceCallDataSaving) -> [VoiceCallDataSavingEntry] {
var entries: [VoiceCallDataSavingEntry] = []
entries.append(.never(presentationData.theme, stringForDataSavingOption(.never, strings: presentationData.strings), dataSaving == .never))
entries.append(.cellular(presentationData.theme, stringForDataSavingOption(.cellular, strings: presentationData.strings), dataSaving == .cellular))
entries.append(.always(presentationData.theme, stringForDataSavingOption(.always, strings: presentationData.strings), dataSaving == .always))
entries.append(.info(presentationData.theme, presentationData.strings.CallSettings_UseLessDataLongDescription))
return entries
}
func voiceCallDataSavingController(context: AccountContext) -> ViewController {
let sharedSettings = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.voiceCallSettings])
|> map { sharedData -> (VoiceCallSettings, AutodownloadSettings) in
let voiceCallSettings: VoiceCallSettings
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.voiceCallSettings]?.get(VoiceCallSettings.self) {
voiceCallSettings = value
} else {
voiceCallSettings = VoiceCallSettings.defaultSettings
}
let autodownloadSettings: AutodownloadSettings
if let value = sharedData.entries[SharedDataKeys.autodownloadSettings]?.get(AutodownloadSettings.self) {
autodownloadSettings = value
} else {
autodownloadSettings = AutodownloadSettings.defaultSettings
}
return (voiceCallSettings, autodownloadSettings)
}
let arguments = VoiceCallDataSavingControllerArguments(updateSelection: { option in
let _ = updateVoiceCallSettingsSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
var current = current
current.dataSaving = option
return current
}).start()
})
let signal = combineLatest(context.sharedContext.presentationData, sharedSettings) |> deliverOnMainQueue
|> map { presentationData, sharedSettings -> (ItemListControllerState, (ItemListNodeState, Any)) in
let dataSaving = effectiveDataSaving(for: sharedSettings.0, autodownloadSettings: sharedSettings.1)
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.CallSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: voiceCallDataSavingControllerEntries(presentationData: presentationData, dataSaving: dataSaving), style: .blocks, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
return controller
}
@@ -0,0 +1,471 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import AccountContext
import UrlEscaping
import ActivityIndicator
private final class WebBrowserDomainInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
private var theme: PresentationTheme
private let backgroundNode: ASImageNode
fileprivate let textInputNode: EditableTextNode
private let placeholderNode: ASTextNode
var updateHeight: (() -> Void)?
var complete: (() -> Void)?
var textChanged: ((String) -> Void)?
private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0)
private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0)
var text: String {
get {
return self.textInputNode.attributedText?.string ?? ""
}
set {
self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor)
self.placeholderNode.isHidden = !newValue.isEmpty
}
}
var placeholder: String = "" {
didSet {
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
}
}
init(theme: PresentationTheme, placeholder: String) {
self.theme = theme
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
self.textInputNode = EditableTextNode()
self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor]
self.textInputNode.clipsToBounds = true
self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0)
self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.textInputNode.keyboardType = .URL
self.textInputNode.autocapitalizationType = .none
self.textInputNode.returnKeyType = .done
self.textInputNode.autocorrectionType = .no
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
self.placeholderNode = ASTextNode()
self.placeholderNode.isUserInteractionEnabled = false
self.placeholderNode.displaysAsynchronously = false
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
super.init()
self.textInputNode.delegate = self
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textInputNode)
self.addSubnode(self.placeholderNode)
}
func updateTheme(_ theme: PresentationTheme) {
self.theme = theme
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
let textFieldHeight = self.calculateTextFieldMetrics(width: width)
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom))
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size)
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height)))
return panelHeight
}
func activateInput() {
self.textInputNode.becomeFirstResponder()
}
func deactivateInput() {
self.textInputNode.resignFirstResponder()
}
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
self.updateTextNodeText(animated: true)
self.textChanged?(editableTextNode.textView.text)
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty
}
private let domainRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.?)*([a-zA-Z]*)?(:)?(/)?$", options: [])
private let pathRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}/", options: [])
var inProgress = false
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if self.inProgress {
return false
}
if text == "\n" {
self.complete?()
return false
}
if let domainRegex = self.domainRegex, let pathRegex = self.pathRegex {
let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text)
let domainMatches = domainRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count))
let pathMatches = pathRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count))
if domainMatches.count > 0, pathMatches.count == 0 {
return true
} else {
return false
}
}
return true
}
private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height))
return min(61.0, max(33.0, unboundTextFieldHeight))
}
private func updateTextNodeText(animated: Bool) {
let backgroundInsets = self.backgroundInsets
let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width)
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
if !self.bounds.size.height.isEqual(to: panelHeight) {
self.updateHeight?()
}
}
@objc func clearPressed() {
self.textInputNode.attributedText = nil
self.deactivateInput()
}
}
private final class WebBrowserDomainAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let titleNode: ASTextNode
private let textNode: ASTextNode
let activityIndicator: ActivityIndicator
let inputFieldNode: WebBrowserDomainInputFieldNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private let disposable = MetaDisposable()
private var validLayout: CGSize?
private let hapticFeedback = HapticFeedback()
var complete: (() -> Void)? {
didSet {
self.inputFieldNode.complete = self.complete
}
}
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction]) {
self.strings = strings
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 2
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 2
self.activityIndicator = ActivityIndicator(type: .custom(ptheme.rootController.navigationBar.secondaryTextColor, 20.0, 1.5, false), speed: .slow)
self.activityIndicator.isHidden = true
self.inputFieldNode = WebBrowserDomainInputFieldNode(theme: ptheme, placeholder: strings.WebBrowser_Exceptions_Create_Placeholder)
self.inputFieldNode.text = ""
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.inputFieldNode)
self.addSubnode(self.activityIndicator)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
self.actionNodes.last?.actionEnabled = false
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.inputFieldNode.updateHeight = { [weak self] in
if let strongSelf = self {
if let _ = strongSelf.validLayout {
strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring))
}
}
}
self.inputFieldNode.textChanged = { [weak self] text in
if let strongSelf = self, let lastNode = strongSelf.actionNodes.last {
lastNode.actionEnabled = !text.isEmpty
}
}
self.updateTheme(theme)
}
deinit {
self.disposable.dispose()
}
var link: String {
return self.inputFieldNode.text
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.strings.WebBrowser_Exceptions_Create_Title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: self.strings.WebBrowser_Exceptions_Create_Text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
let hadValidLayout = self.validLayout != nil
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let spacing: CGFloat = 5.0
let titleSize = self.titleNode.measure(measureSize)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 4.0
let textSize = self.textNode.measure(measureSize)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 6.0 + spacing
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
var contentWidth = max(titleSize.width, minActionsWidth)
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultWidth = contentWidth + insets.left + insets.right
let inputFieldWidth = resultWidth
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
let inputHeight = inputFieldHeight
let inputFrame = CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)
transition.updateFrame(node: self.inputFieldNode, frame: inputFrame)
transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0)
let activitySize = CGSize(width: 20.0, height: 20.0)
transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: inputFrame.maxX - activitySize.width - 23.0, y: inputFrame.midY - activitySize.height / 2.0 - 3.0), size: activitySize))
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
if !hadValidLayout {
self.inputFieldNode.activateInput()
}
return resultSize
}
func animateError() {
self.inputFieldNode.layer.addShakeAnimation()
self.hapticFeedback.error()
}
}
public func webBrowserDomainController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, apply: @escaping (String?) -> Void) -> AlertController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
var dismissImpl: ((Bool) -> Void)?
var applyImpl: (() -> Void)?
var inProgress = false
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
if !inProgress {
dismissImpl?(true)
apply(nil)
}
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: {
if !inProgress {
applyImpl?()
}
})]
let contentNode = WebBrowserDomainAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions)
contentNode.complete = {
applyImpl?()
}
applyImpl = { [weak contentNode] in
guard let contentNode = contentNode else {
return
}
inProgress = true
contentNode.inputFieldNode.inProgress = true
contentNode.activityIndicator.isHidden = false
let updatedLink = explicitUrl(contentNode.link)
if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true]) {
apply(updatedLink)
} else {
contentNode.animateError()
}
}
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in
controller?.theme = AlertControllerTheme(presentationData: presentationData)
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
dismissImpl = { [weak controller] animated in
contentNode.inputFieldNode.deactivateInput()
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}
@@ -0,0 +1,358 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import TelegramCore
import AccountContext
import ItemListUI
import PhotoResources
private enum RevealOptionKey: Int32 {
case delete
}
final class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let context: AccountContext
let title: String
let label: String
let icon: TelegramMediaImage?
let sectionId: ItemListSectionId
let style: ItemListStyle
let deleted: (() -> Void)?
init(
presentationData: ItemListPresentationData,
systemStyle: ItemListSystemStyle,
context: AccountContext,
title: String,
label: String,
icon: TelegramMediaImage?,
sectionId: ItemListSectionId,
style: ItemListStyle,
deleted: (() -> Void)?
) {
self.presentationData = presentationData
self.systemStyle = systemStyle
self.context = context
self.title = title
self.label = label
self.icon = icon
self.sectionId = sectionId
self.style = style
self.deleted = deleted
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = WebBrowserDomainExceptionItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? WebBrowserDomainExceptionItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
var selectable: Bool = false
func selected(listView: ListView){
}
}
final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
let iconNode: TransformImageNode
let titleNode: TextNode
let labelNode: TextNode
private let activateArea: AccessibilityAreaNode
private var item: WebBrowserDomainExceptionItem?
private var layoutParams: ListViewItemLayoutParams?
override public var canBeSelected: Bool {
return false
}
var tag: ItemListItemTag? = nil
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.iconNode = TransformImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.iconNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.activateArea)
}
func asyncLayout() -> (_ item: WebBrowserDomainExceptionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let leftInset = 16.0 + params.leftInset + 46.0
let titleColor: UIColor = item.presentationData.theme.list.itemPrimaryTextColor
let labelColor: UIColor = item.presentationData.theme.list.itemAccentColor
let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
let maxTitleWidth: CGFloat = params.width - params.rightInset - 20.0 - leftInset
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: labelColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
verticalInset = 11.0
case .legacy:
verticalInset = 11.0
}
let titleSpacing: CGFloat = 1.0
let height: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = item.title
strongSelf.activateArea.accessibilityValue = item.label
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
}
let iconSize = CGSize(width: 40.0, height: 40.0)
var imageSize = iconSize
if currentItem?.icon?.id != item.icon?.id, let icon = item.icon {
strongSelf.iconNode.setSignal(chatMessagePhoto(mediaBox: item.context.sharedContext.accountManager.mediaBox, userLocation: .other, photoReference: .standalone(media: icon)))
}
if let icon = item.icon, let dimensions = largestImageRepresentation(icon.representations)?.dimensions.cgSize {
imageSize = dimensions.aspectFilled(imageSize)
}
let _ = titleApply()
let _ = labelApply()
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))
}
var centralContentHeight: CGFloat = titleLayout.size.height
centralContentHeight += titleSpacing
centralContentHeight += labelLayout.size.height
let titleFrame = CGRect(origin: CGPoint(x: leftInset + strongSelf.revealOffset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
let labelFrame = CGRect(origin: CGPoint(x: leftInset + strongSelf.revealOffset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + 11.0 + strongSelf.revealOffset, y: floorToScreenPixels((contentSize.height - iconSize.height) / 2.0)), size: iconSize)
strongSelf.iconNode.frame = iconFrame
strongSelf.iconNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 7.0), imageSize: imageSize, boundingSize: iconSize, intrinsicInsets: .zero))()
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
var revealOptions: [ItemListRevealOption] = []
revealOptions.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor))
strongSelf.setRevealOptions((left: [], right: revealOptions))
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
if let params = self.layoutParams {
let leftInset: CGFloat = 16.0 + params.leftInset + 46.0
var iconFrame = self.iconNode.frame
iconFrame.origin.x = params.leftInset + 11.0 + offset
transition.updateFrame(node: self.iconNode, frame: iconFrame)
var titleFrame = self.titleNode.frame
titleFrame.origin.x = leftInset + offset
transition.updateFrame(node: self.titleNode, frame: titleFrame)
var subtitleFrame = self.labelNode.frame
subtitleFrame.origin.x = leftInset + offset
transition.updateFrame(node: self.labelNode, frame: subtitleFrame)
}
}
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
if let item = self.item {
switch option.key {
case RevealOptionKey.delete.rawValue:
item.deleted?()
default:
break
}
}
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
}
}
@@ -0,0 +1,328 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PhotoResources
import OpenInExternalAppUI
import AccountContext
import AppBundle
class WebBrowserItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let title: String
let application: OpenInApplication?
let checked: Bool
public let sectionId: ItemListSectionId
let action: () -> Void
public init(context: AccountContext, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle, title: String, application: OpenInApplication?, checked: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) {
self.context = context
self.presentationData = presentationData
self.systemStyle = systemStyle
self.title = title
self.application = application
self.checked = checked
self.sectionId = sectionId
self.action = action
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = WebBrowserItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? WebBrowserItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
public var selectable: Bool = true
public func selected(listView: ListView){
listView.clearHighlightAnimated(true)
self.action()
}
}
private final class WebBrowserItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private let activateArea: AccessibilityAreaNode
private let iconNode: TransformImageNode
private let checkIconNode: ASImageNode
private let titleNode: TextNode
private var item: WebBrowserItem?
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.checkIconNode = ASImageNode()
self.checkIconNode.isLayerBacked = true
self.checkIconNode.displayWithoutProcessing = true
self.checkIconNode.displaysAsynchronously = false
self.iconNode = TransformImageNode()
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.iconNode)
self.addSubnode(self.checkIconNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.activateArea)
self.activateArea.activate = { [weak self] in
self?.item?.action()
return true
}
}
public func asyncLayout() -> (_ item: WebBrowserItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeIconLayout = self.iconNode.asyncLayout()
let currentItem = self.item
return { item, params, neighbors in
let leftInset: CGFloat = params.leftInset + 16.0 + 43.0
let iconSize = CGSize(width: 29.0, height: 29.0)
let arguments = TransformImageArguments(corners: ImageCorners(radius: 5.0), imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.presentationData.theme.list.mediaPlaceholderColor)
let imageApply = makeIconLayout(arguments)
var updatedIconSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
if currentItem == nil {
switch item.application {
case .none:
let icons = item.context.sharedContext.applicationBindings.getAvailableAlternateIcons()
let current = item.context.sharedContext.applicationBindings.getAlternateIconName()
let currentIcon = icons.first(where: { $0.name == current })?.imageName ?? "BlueIcon"
if let image = UIImage(named: currentIcon, in: getAppBundle(), compatibleWith: nil) {
updatedIconSignal = openInAppIcon(engine: item.context.engine, appIcon: .image(image: image))
}
case .safari:
if let image = UIImage(bundleImageName: "Open In/Safari") {
updatedIconSignal = openInAppIcon(engine: item.context.engine, appIcon: .image(image: image))
}
case .maps:
if let image = UIImage(bundleImageName: "Open In/Maps") {
updatedIconSignal = openInAppIcon(engine: item.context.engine, appIcon: .image(image: image))
}
case let .other(_, identifier, _, store):
updatedIconSignal = openInAppIcon(engine: item.context.engine, appIcon: .resource(resource: OpenInAppIconResource(appStoreId: identifier, store: store)))
}
}
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
verticalInset = 15.0
case .legacy:
verticalInset = 11.0
}
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
var updateCheckImage: UIImage?
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
updateCheckImage = PresentationResourcesItemList.checkIconImage(item.presentationData.theme)
}
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.accessibilityLabel = item.title
if item.checked {
strongSelf.activateArea.accessibilityValue = "Selected"
} else {
strongSelf.activateArea.accessibilityValue = ""
}
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
if let updateCheckImage = updateCheckImage {
strongSelf.checkIconNode.image = updateCheckImage
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let _ = titleApply()
let _ = imageApply()
if let updatedImageSignal = updatedIconSignal {
strongSelf.iconNode.setSignal(updatedImageSignal)
}
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - iconSize.width) / 2.0), y: floor((layout.contentSize.height - iconSize.height) / 2.0)), size: iconSize)
if let image = strongSelf.checkIconNode.image {
strongSelf.checkIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - image.size.width - floor((44.0 - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)
}
strongSelf.checkIconNode.isHidden = !item.checked
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0) + 1.0), size: titleLayout.size)
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel))
}
})
}
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,512 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import PresentationDataUtils
import ItemListUI
import AccountContext
import OpenInExternalAppUI
import ItemListPeerActionItem
import UndoUI
import WebKit
import LinkPresentation
import CoreServices
import PersistentStringHash
import UrlHandling
private final class WebBrowserSettingsControllerArguments {
let context: AccountContext
let updateDefaultBrowser: (String?) -> Void
let clearCookies: () -> Void
let clearCache: () -> Void
let addException: () -> Void
let removeException: (String) -> Void
let clearExceptions: () -> Void
init(
context: AccountContext,
updateDefaultBrowser: @escaping (String?) -> Void,
clearCookies: @escaping () -> Void,
clearCache: @escaping () -> Void,
addException: @escaping () -> Void,
removeException: @escaping (String) -> Void,
clearExceptions: @escaping () -> Void
) {
self.context = context
self.updateDefaultBrowser = updateDefaultBrowser
self.clearCookies = clearCookies
self.clearCache = clearCache
self.addException = addException
self.removeException = removeException
self.clearExceptions = clearExceptions
}
}
private enum WebBrowserSettingsSection: Int32 {
case browsers
case clearCookies
case exceptions
}
private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry {
case browserHeader(PresentationTheme, String)
case browser(PresentationTheme, String, OpenInApplication?, String?, Bool, Int32)
case clearCookies(PresentationTheme, String)
case clearCache(PresentationTheme, String)
case clearCookiesInfo(PresentationTheme, String)
case exceptionsHeader(PresentationTheme, String)
case exceptionsAdd(PresentationTheme, String)
case exception(Int32, PresentationTheme, WebBrowserException)
case exceptionsClear(PresentationTheme, String)
case exceptionsInfo(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .browserHeader, .browser:
return WebBrowserSettingsSection.browsers.rawValue
case .clearCookies, .clearCache, .clearCookiesInfo:
return WebBrowserSettingsSection.clearCookies.rawValue
case .exceptionsHeader, .exceptionsAdd, .exception, .exceptionsClear, .exceptionsInfo:
return WebBrowserSettingsSection.exceptions.rawValue
}
}
var stableId: UInt64 {
switch self {
case .browserHeader:
return 0
case let .browser(_, _, _, _, _, index):
return UInt64(1 + index)
case .clearCookies:
return 102
case .clearCache:
return 103
case .clearCookiesInfo:
return 104
case .exceptionsHeader:
return 105
case .exceptionsAdd:
return 106
case let .exception(_, _, exception):
return 2000 + exception.domain.persistentHashValue
case .exceptionsClear:
return 1000
case .exceptionsInfo:
return 1001
}
}
var sortId: Int32 {
switch self {
case .browserHeader:
return 0
case let .browser(_, _, _, _, _, index):
return 1 + index
case .clearCookies:
return 102
case .clearCache:
return 103
case .clearCookiesInfo:
return 104
case .exceptionsHeader:
return 105
case .exceptionsAdd:
return 106
case let .exception(index, _, _):
return 107 + index
case .exceptionsClear:
return 1000
case .exceptionsInfo:
return 1001
}
}
static func ==(lhs: WebBrowserSettingsControllerEntry, rhs: WebBrowserSettingsControllerEntry) -> Bool {
switch lhs {
case let .browserHeader(lhsTheme, lhsText):
if case let .browserHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .browser(lhsTheme, lhsTitle, lhsApplication, lhsIdentifier, lhsSelected, lhsIndex):
if case let .browser(rhsTheme, rhsTitle, rhsApplication, rhsIdentifier, rhsSelected, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsApplication == rhsApplication, lhsIdentifier == rhsIdentifier, lhsSelected == rhsSelected, lhsIndex == rhsIndex {
return true
} else {
return false
}
case let .clearCookies(lhsTheme, lhsText):
if case let .clearCookies(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .clearCache(lhsTheme, lhsText):
if case let .clearCache(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .clearCookiesInfo(lhsTheme, lhsText):
if case let .clearCookiesInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .exceptionsHeader(lhsTheme, lhsText):
if case let .exceptionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .exception(lhsIndex, lhsTheme, lhsException):
if case let .exception(rhsIndex, rhsTheme, rhsException) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsException == rhsException {
return true
} else {
return false
}
case let .exceptionsAdd(lhsTheme, lhsText):
if case let .exceptionsAdd(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .exceptionsClear(lhsTheme, lhsText):
if case let .exceptionsClear(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .exceptionsInfo(lhsTheme, lhsText):
if case let .exceptionsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: WebBrowserSettingsControllerEntry, rhs: WebBrowserSettingsControllerEntry) -> Bool {
return lhs.sortId < rhs.sortId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! WebBrowserSettingsControllerArguments
switch self {
case let .browserHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .browser(_, title, application, identifier, selected, _):
return WebBrowserItem(context: arguments.context, presentationData: presentationData, systemStyle: .glass, title: title, application: application, checked: selected, sectionId: self.section) {
arguments.updateDefaultBrowser(identifier)
}
case let .clearCookies(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.accentDeleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
arguments.clearCookies()
})
case let .clearCache(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.accentDeleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
arguments.clearCache()
})
case let .clearCookiesInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .exceptionsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .exception(_, _, exception):
return WebBrowserDomainExceptionItem(presentationData: presentationData, systemStyle: .glass, context: arguments.context, title: exception.title, label: exception.domain, icon: exception.icon, sectionId: self.section, style: .blocks, deleted: {
arguments.removeException(exception.domain)
})
case let .exceptionsAdd(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
arguments.addException()
})
case let .exceptionsClear(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.deleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: {
arguments.clearExceptions()
})
case let .exceptionsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
}
}
private func webBrowserSettingsControllerEntries(context: AccountContext, presentationData: PresentationData, settings: WebBrowserSettings) -> [WebBrowserSettingsControllerEntry] {
var entries: [WebBrowserSettingsControllerEntry] = []
let options = availableOpenInOptions(context: context, item: .url(url: "http://telegram.org"))
entries.append(.browserHeader(presentationData.theme, presentationData.strings.WebBrowser_OpenLinksIn_Title))
entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_Telegram, nil, nil, settings.defaultWebBrowser == nil, 0))
var index: Int32 = 1
for option in options {
entries.append(.browser(presentationData.theme, option.title, option.application, option.identifier, option.identifier == settings.defaultWebBrowser, index))
index += 1
}
if settings.defaultWebBrowser == nil {
entries.append(.clearCookies(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies))
// entries.append(.clearCache(presentationData.theme, presentationData.strings.WebBrowser_ClearCache))
entries.append(.clearCookiesInfo(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies_Info))
entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Title))
entries.append(.exceptionsAdd(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_AddException))
var exceptionIndex: Int32 = 0
for exception in settings.exceptions.reversed() {
entries.append(.exception(exceptionIndex, presentationData.theme, exception))
exceptionIndex += 1
}
if !settings.exceptions.isEmpty {
entries.append(.exceptionsClear(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Clear))
}
entries.append(.exceptionsInfo(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Info))
}
return entries
}
public func webBrowserSettingsController(context: AccountContext) -> ViewController {
var clearCookiesImpl: (() -> Void)?
var clearCacheImpl: (() -> Void)?
var addExceptionImpl: (() -> Void)?
var removeExceptionImpl: ((String) -> Void)?
var clearExceptionsImpl: (() -> Void)?
let arguments = WebBrowserSettingsControllerArguments(
context: context,
updateDefaultBrowser: { identifier in
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, {
$0.withUpdatedDefaultWebBrowser(identifier)
}).start()
},
clearCookies: {
clearCookiesImpl?()
},
clearCache: {
clearCacheImpl?()
},
addException: {
addExceptionImpl?()
},
removeException: { domain in
removeExceptionImpl?(domain)
},
clearExceptions: {
clearExceptionsImpl?()
}
)
let previousSettings = Atomic<WebBrowserSettings?>(value: nil)
let signal = combineLatest(
context.sharedContext.presentationData,
context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings])
)
|> deliverOnMainQueue
|> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) ?? WebBrowserSettings.defaultSettings
let previousSettings = previousSettings.swap(settings)
var animateChanges = false
if let previousSettings {
if previousSettings.defaultWebBrowser != settings.defaultWebBrowser {
animateChanges = true
}
if previousSettings.exceptions.count != settings.exceptions.count {
animateChanges = true
}
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.WebBrowser_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: webBrowserSettingsControllerEntries(context: context, presentationData: presentationData, settings: settings), style: .blocks, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
clearCookiesImpl = { [weak controller] in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let alertController = textAlertController(
context: context,
updatedPresentationData: nil,
title: nil,
text: presentationData.strings.WebBrowser_ClearCookies_ClearConfirmation_Text,
actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_ClearCookies_ClearConfirmation_Clear, action: {
WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeCookies, WKWebsiteDataTypeLocalStorage, WKWebsiteDataTypeSessionStorage], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{})
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
controller?.present(UndoOverlayController(
presentationData: presentationData,
content: .info(
title: nil,
text: presentationData.strings.WebBrowser_ClearCookies_Succeed,
timeout: nil,
customUndoText: nil
),
elevatedLayout: false,
position: .bottom,
action: { _ in return false }), in: .current
)
})
]
)
controller?.present(alertController, in: .window(.root))
}
clearCacheImpl = { [weak controller] in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let alertController = textAlertController(
context: context,
updatedPresentationData: nil,
title: nil,
text: presentationData.strings.WebBrowser_ClearCache_ClearConfirmation_Text,
actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_ClearCache_ClearConfirmation_Clear, action: {
WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{})
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
controller?.present(UndoOverlayController(
presentationData: presentationData,
content: .info(
title: nil,
text: presentationData.strings.WebBrowser_ClearCache_Succeed,
timeout: nil,
customUndoText: nil
),
elevatedLayout: false,
position: .bottom,
action: { _ in return false }), in: .current
)
})
]
)
controller?.present(alertController, in: .window(.root))
}
addExceptionImpl = { [weak controller] in
var dismissImpl: (() -> Void)?
let linkController = webBrowserDomainController(context: context, apply: { url in
if let url {
let _ = (fetchDomainExceptionInfo(context: context, url: url)
|> deliverOnMainQueue).startStandalone(next: { newException in
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in
var currentExceptions = currentSettings.exceptions
for exception in currentExceptions {
if exception.domain == newException.domain {
return currentSettings
}
}
currentExceptions.append(newException)
return currentSettings.withUpdatedExceptions(currentExceptions)
}).start()
dismissImpl?()
})
}
})
dismissImpl = { [weak linkController] in
linkController?.view.endEditing(true)
linkController?.dismissAnimated()
}
controller?.present(linkController, in: .window(.root))
}
removeExceptionImpl = { domain in
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in
let updatedExceptions = currentSettings.exceptions.filter { $0.domain != domain }
return currentSettings.withUpdatedExceptions(updatedExceptions)
}).start()
}
clearExceptionsImpl = { [weak controller] in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let alertController = textAlertController(
context: context,
updatedPresentationData: nil,
title: nil,
text: presentationData.strings.WebBrowser_Exceptions_ClearConfirmation_Text,
actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_Exceptions_ClearConfirmation_Clear, action: {
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in
return currentSettings.withUpdatedExceptions([])
}).start()
})
]
)
controller?.present(alertController, in: .window(.root))
}
return controller
}
private func fetchDomainExceptionInfo(context: AccountContext, url: String) -> Signal<WebBrowserException, NoError> {
let (domain, domainUrl) = cleanDomain(url: url)
if #available(iOS 13.0, *), let url = URL(string: domainUrl) {
return Signal { subscriber in
let metadataProvider = LPMetadataProvider()
metadataProvider.shouldFetchSubresources = true
metadataProvider.startFetchingMetadata(for: url, completionHandler: { metadata, _ in
let completeWithImage: (Data?) -> Void = { imageData in
var image: TelegramMediaImage?
if let imageData, let parsedImage = UIImage(data: imageData) {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: imageData)
image = TelegramMediaImage(
imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)),
representations: [
TelegramMediaImageRepresentation(
dimensions: PixelDimensions(width: Int32(parsedImage.size.width), height: Int32(parsedImage.size.height)),
resource: resource,
progressiveSizes: [],
immediateThumbnailData: nil,
hasVideo: false,
isPersonal: false
)
],
immediateThumbnailData: nil,
reference: nil,
partialReference: nil,
flags: []
)
}
let title = metadata?.value(forKey: "_siteName") as? String ?? metadata?.title
subscriber.putNext(WebBrowserException(domain: domain, title: title ?? domain, icon: image))
subscriber.putCompletion()
}
if let imageProvider = metadata?.iconProvider {
imageProvider.loadFileRepresentation(forTypeIdentifier: kUTTypeImage as String, completionHandler: { imageUrl, _ in
guard let imageUrl, let imageData = try? Data(contentsOf: imageUrl) else {
completeWithImage(nil)
return
}
completeWithImage(imageData)
})
} else {
completeWithImage(nil)
}
})
return ActionDisposable {
metadataProvider.cancel()
}
}
} else {
return .single(WebBrowserException(domain: domain, title: domain, icon: nil))
}
}
@@ -0,0 +1,597 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import LegacyComponents
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import AlertUI
import PresentationDataUtils
import UrlHandling
import InviteLinksUI
import CountrySelectionUI
import PhoneInputNode
import UndoUI
private struct DeleteAccountDataArguments {
let context: AccountContext
let openLink: (String) -> Void
let selectCountryCode: () -> Void
let updatePassword: (String) -> Void
let proceed: () -> Void
}
private enum DeleteAccountDataSection: Int32 {
case header
case main
}
private enum DeleteAccountEntryTag: Equatable, ItemListItemTag {
case password
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? DeleteAccountEntryTag {
return self == other
} else {
return false
}
}
}
private enum DeleteAccountDataEntry: ItemListNodeEntry, Equatable {
case header(PresentationTheme, String, String, String, Bool)
case peers(PresentationTheme, [EnginePeer])
case phone(PresentationTheme, PresentationStrings)
case password(PresentationTheme, String)
case info(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .header:
return DeleteAccountDataSection.header.rawValue
case .peers, .info, .phone, .password:
return DeleteAccountDataSection.main.rawValue
}
}
var stableId: Int32 {
switch self {
case .header:
return 0
case .peers:
return 1
case .info:
return 2
case .phone:
return 3
case .password:
return 4
}
}
static func == (lhs: DeleteAccountDataEntry, rhs: DeleteAccountDataEntry) -> Bool {
switch lhs {
case let .header(lhsTheme, lhsAnimation, lhsTitle, lhsText, lhsHideOnSmallScreens):
if case let .header(rhsTheme, rhsAnimation, rhsTitle, rhsText, rhsHideOnSmallScreens) = rhs, lhsTheme === rhsTheme, lhsAnimation == rhsAnimation, lhsTitle == rhsTitle, lhsText == rhsText, lhsHideOnSmallScreens == rhsHideOnSmallScreens {
return true
} else {
return false
}
case let .peers(lhsTheme, lhsPeers):
if case let .peers(rhsTheme, rhsPeers) = rhs, lhsTheme === rhsTheme, lhsPeers == rhsPeers {
return true
} else {
return false
}
case let .info(lhsTheme, lhsText):
if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .phone(lhsTheme, lhsStrings):
if case let .phone(rhsTheme, rhsStrings) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings {
return true
} else {
return false
}
case let .password(lhsTheme, lhsPlaceholder):
if case let .password(rhsTheme, rhsPlaceholder) = rhs, lhsTheme === rhsTheme, lhsPlaceholder == rhsPlaceholder {
return true
} else {
return false
}
}
}
static func <(lhs: DeleteAccountDataEntry, rhs: DeleteAccountDataEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! DeleteAccountDataArguments
switch self {
case let .header(theme, animation, title, text, hideOnSmallScreens):
return InviteLinkHeaderItem(context: arguments.context, theme: theme, title: title, text: NSAttributedString(string: text), animationName: animation, hideOnSmallScreens: hideOnSmallScreens, sectionId: self.section, linkAction: nil)
case let .peers(_, peers):
return DeleteAccountPeersItem(context: arguments.context, theme: presentationData.theme, strings: presentationData.strings, peers: peers, sectionId: self.section)
case let .info(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case .phone:
return DeleteAccountPhoneItem(theme: presentationData.theme, strings: presentationData.strings, value: (nil, nil, ""), sectionId: self.section, selectCountryCode: {
arguments.selectCountryCode()
}, updated: { _ in
})
case let .password(_, placeholder):
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: "", placeholder: placeholder, type: .password, returnKeyType: .done, tag: DeleteAccountEntryTag.password, sectionId: self.section, textUpdated: { value in
arguments.updatePassword(value)
}, action: {
arguments.proceed()
})
}
}
}
private func deleteAccountDataEntries(presentationData: PresentationData, mode: DeleteAccountDataMode, peers: [EnginePeer]) -> [DeleteAccountDataEntry] {
var entries: [DeleteAccountDataEntry] = []
let headerTitle: String
let headerText: String
let headerAnimation: String
var hideOnSmallScreen = false
switch mode {
case .peers:
headerAnimation = "Delete1"
headerTitle = presentationData.strings.DeleteAccount_CloudStorageTitle
headerText = presentationData.strings.DeleteAccount_CloudStorageText
case .groups:
headerAnimation = "Delete2"
headerTitle = presentationData.strings.DeleteAccount_GroupsAndChannelsTitle
headerText = presentationData.strings.DeleteAccount_GroupsAndChannelsText
case .messages:
headerAnimation = "Delete3"
headerTitle = presentationData.strings.DeleteAccount_MessageHistoryTitle
headerText = presentationData.strings.DeleteAccount_MessageHistoryText
case .phone:
headerAnimation = "Delete4"
headerTitle = presentationData.strings.DeleteAccount_EnterPhoneNumber
headerText = ""
hideOnSmallScreen = true
case .password:
headerAnimation = "Delete5"
headerTitle = presentationData.strings.DeleteAccount_EnterPassword
headerText = ""
hideOnSmallScreen = true
}
entries.append(.header(presentationData.theme, headerAnimation, headerTitle, headerText, hideOnSmallScreen))
switch mode {
case .peers:
if !peers.isEmpty {
entries.append(.peers(presentationData.theme, peers))
}
case .groups:
if !peers.isEmpty {
entries.append(.peers(presentationData.theme, peers))
entries.append(.info(presentationData.theme, presentationData.strings.DeleteAccount_GroupsAndChannelsInfo))
}
case .messages:
break
case .phone:
entries.append(.phone(presentationData.theme, presentationData.strings))
case .password:
entries.append(.password(presentationData.theme, presentationData.strings.LoginPassword_PasswordPlaceholder))
}
return entries
}
enum DeleteAccountDataMode {
case peers
case groups([EnginePeer])
case messages
case phone
case password
}
private struct DeleteAccountDataState: Equatable {
var password: String
var isLoading: Bool
static func == (lhs: DeleteAccountDataState, rhs: DeleteAccountDataState) -> Bool {
return lhs.password == rhs.password && lhs.isLoading == rhs.isLoading
}
}
func deleteAccountDataController(context: AccountContext, mode: DeleteAccountDataMode, twoStepAuthData: TwoStepVerificationAccessConfiguration?) -> ViewController {
let initialState = DeleteAccountDataState(password: "", isLoading: false)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((DeleteAccountDataState) -> DeleteAccountDataState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var presentControllerImpl: ((ViewController) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var replaceTopControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
var updateCodeImpl: (() -> Void)?
var activateInputImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)?
if case .phone = mode {
loadServerCountryCodes(accountManager: context.sharedContext.accountManager, engine: context.engine, completion: {
updateCodeImpl?()
})
}
var updateCountryCodeImpl: ((Int32, String) -> Void)?
var proceedImpl: (() -> Void)?
let arguments = DeleteAccountDataArguments(context: context, openLink: { _ in
}, selectCountryCode: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = AuthorizationSequenceCountrySelectionController(strings: presentationData.strings, theme: presentationData.theme)
controller.completeWithCountryCode = { code, name in
updateCountryCodeImpl?(Int32(code), name)
activateInputImpl?()
}
dismissInputImpl?()
pushControllerImpl?(controller)
}, updatePassword: { password in
updateState { current in
var updated = current
updated.password = password
return updated
}
}, proceed: {
proceedImpl?()
})
let preloadedGroupPeers = Promise<[EnginePeer]>([])
let peers: Signal<[EnginePeer], NoError>
switch mode {
case .peers:
peers = combineLatest(
context.engine.peers.recentPeers()
|> map { recentPeers -> [EnginePeer] in
if case let .peers(peers) = recentPeers {
return peers.map { EnginePeer($0) }
} else {
return []
}
},
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
) |> map { recentPeers, accountPeer -> [EnginePeer] in
var peers: [EnginePeer] = []
if let accountPeer = accountPeer {
peers.append(accountPeer)
}
peers.append(contentsOf: recentPeers.prefix(9))
return peers
}
preloadedGroupPeers.set(context.engine.peers.adminedPublicChannels(scope: .all) |> map { result in
return result.map(\.peer)
})
case let .groups(preloadedPeers):
peers = .single(preloadedPeers.shuffled())
default:
peers = .single([])
}
let cancelImpl = {
dismissImpl?()
switch mode {
case .peers:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_cloud_cancel")
case .groups:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_groups_cancel")
case .messages:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_messages_cancel")
case .phone:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_phone_cancel")
case .password:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_2fa_cancel")
}
}
var secondaryActionDisabled = false
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
peers,
statePromise.get()
)
|> map { presentationData, peers, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
cancelImpl()
})
var focusItemTag: DeleteAccountEntryTag?
var buttonTitle: String
switch mode {
case .phone:
buttonTitle = ""
case .password:
buttonTitle = ""
focusItemTag = .password
default:
buttonTitle = presentationData.strings.DeleteAccount_ComeBackLater
}
let rightNavigationButton: ItemListNavigationButton?
if state.isLoading {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
} else {
rightNavigationButton = nil
}
let footerItem = DeleteAccountFooterItem(theme: presentationData.theme, title: buttonTitle, secondaryTitle: presentationData.strings.DeleteAccount_Continue, action: {
cancelImpl()
}, secondaryAction: {
if !secondaryActionDisabled {
secondaryActionDisabled = true
proceedImpl?()
}
})
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.DeleteAccount_DeleteMyAccountTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: deleteAccountDataEntries(presentationData: presentationData, mode: mode, peers: peers), style: .blocks, focusItemTag: focusItemTag, footerItem: footerItem)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal, tabBarItem: nil)
presentControllerImpl = { [weak controller] c in
controller?.present(c, in: .window(.root))
}
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
replaceTopControllerImpl = { [weak controller] c in
if let navigationController = controller?.navigationController as? NavigationController {
navigationController.pushViewController(c, completion: { [weak navigationController, weak controller, weak c] in
if let navigationController = navigationController {
let controllers = navigationController.viewControllers.filter { $0 !== controller }
c?.navigationPresentation = .modal
navigationController.setViewControllers(controllers, animated: false)
}
})
}
}
dismissImpl = { [weak controller] in
let _ = controller?.dismiss()
}
updateCodeImpl = { [weak controller] in
controller?.forEachItemNode { itemNode in
if let itemNode = itemNode as? DeleteAccountPhoneItemNode {
itemNode.updateCountryCode()
}
}
}
activateInputImpl = { [weak controller] in
controller?.forEachItemNode { itemNode in
if let itemNode = itemNode as? DeleteAccountPhoneItemNode {
itemNode.activateInput()
}
}
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
controller.didAppear = { firstTime in
if !firstTime {
return
}
activateInputImpl?()
}
updateCountryCodeImpl = { [weak controller] code, name in
controller?.forEachItemNode { itemNode in
if let itemNode = itemNode as? DeleteAccountPhoneItemNode {
itemNode.updateCountryCode(code: code, name: name)
}
}
}
proceedImpl = { [weak controller] in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let action: ([EnginePeer], String?) -> Void = { preloadedPeers, password in
let nextMode: DeleteAccountDataMode?
switch mode {
case .peers:
if !preloadedPeers.isEmpty {
nextMode = .groups(preloadedPeers)
} else {
nextMode = .messages
}
case .groups:
nextMode = .messages
case .messages:
nextMode = .phone
case .phone:
if let twoStepAuthData = twoStepAuthData, case .set = twoStepAuthData {
nextMode = .password
} else {
nextMode = nil
}
case .password:
nextMode = nil
}
if let nextMode = nextMode {
let controller = deleteAccountDataController(context: context, mode: nextMode, twoStepAuthData: twoStepAuthData)
replaceTopControllerImpl?(controller)
} else {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_confirmation_show")
presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.DeleteAccount_ConfirmationAlertTitle, text: presentationData.strings.DeleteAccount_ConfirmationAlertText, actions: [TextAlertAction(type: .destructiveAction, title: presentationData.strings.DeleteAccount_ConfirmationAlertDelete, action: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.final")
invokeAppLogEventsSynchronization(postbox: context.account.postbox)
updateState { current in
var updated = current
updated.isLoading = true
return updated
}
let accountId = context.account.id
let accountManager = context.sharedContext.accountManager
let _ = (context.engine.auth.deleteAccount(reason: "Manual", password: password)
|> deliverOnMainQueue).start(error: { _ in
updateState { current in
var updated = current
updated.isLoading = false
return updated
}
secondaryActionDisabled = false
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]))
}, completed: {
dismissImpl?()
let presentGlobalController = context.sharedContext.presentGlobalController
let _ = logoutFromAccount(id: accountId, accountManager: accountManager, alreadyLoggedOutRemotely: false).start(completed: {
Queue.mainQueue().after(0.1) {
presentGlobalController(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.DeleteAccount_Success, timeout: nil, customUndoText: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil)
}
})
})
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_confirmation_cancel")
dismissImpl?()
})], actionLayout: .vertical))
}
}
switch mode {
case .peers:
let _ = (preloadedGroupPeers.get()
|> take(1)
|> deliverOnMainQueue).start(next: { peers in
action(peers, nil)
})
case .phone:
var code: String?
var number: String?
controller?.forEachItemNode { itemNode in
if let itemNode = itemNode as? DeleteAccountPhoneItemNode {
let value = itemNode.codeNumberAndFullNumber
if value.0 == "+93" && value.1.hasPrefix("9998") {
code = "+"
number = value.1
} else {
code = value.0
number = value.1
}
}
}
if let code, var number, (code + number).count > 4 {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue)
.start(next: { accountPeer in
if let accountPeer = accountPeer, case let .user(user) = accountPeer, var phone = user.phone {
if !phone.hasPrefix("+") {
phone = "+\(phone)"
}
var matches = false
if phone == (code + number) {
matches = true
} else {
while number.hasPrefix("0") {
number.removeFirst()
if phone == (code + number) {
matches = true
}
}
}
if !matches {
secondaryActionDisabled = false
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.DeleteAccount_InvalidPhoneNumberError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]))
return
}
action([], nil)
}
})
}
case .password:
let state = stateValue.with { $0 }
if !state.password.isEmpty {
updateState { current in
var updated = current
updated.isLoading = true
return updated
}
let _ = (context.engine.auth.requestTwoStepVerifiationSettings(password: state.password)
|> deliverOnMainQueue).start(error: { error in
secondaryActionDisabled = false
updateState { current in
var updated = current
updated.isLoading = false
return updated
}
let text: String
switch error {
case .limitExceeded:
text = presentationData.strings.LoginPassword_FloodError
case .invalidPassword:
text = presentationData.strings.DeleteAccount_InvalidPasswordError
default:
text = presentationData.strings.Login_UnknownError
}
presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]))
}, completed: {
updateState { current in
var updated = current
updated.isLoading = false
return updated
}
action([], state.password)
})
return
}
default:
action([], nil)
}
}
switch mode {
case .peers:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_cloud_show")
case .groups:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_groups_show")
case .messages:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_messages_show")
case .phone:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_phone_show")
case .password:
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.step_2fa_show")
}
return controller
}
@@ -0,0 +1,179 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import SolidRoundedButtonNode
import AppBundle
final class DeleteAccountFooterItem: ItemListControllerFooterItem {
let theme: PresentationTheme
let title: String
let secondaryTitle: String
let action: () -> Void
let secondaryAction: () -> Void
init(theme: PresentationTheme, title: String, secondaryTitle: String, action: @escaping () -> Void, secondaryAction: @escaping () -> Void) {
self.theme = theme
self.title = title
self.secondaryTitle = secondaryTitle
self.action = action
self.secondaryAction = secondaryAction
}
func isEqual(to: ItemListControllerFooterItem) -> Bool {
if let item = to as? DeleteAccountFooterItem {
return self.theme === item.theme && self.title == item.title && self.secondaryTitle == item.secondaryTitle
} else {
return false
}
}
func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode {
if let current = current as? DeleteAccountFooterItemNode {
current.item = self
return current
} else {
return DeleteAccountFooterItemNode(item: self)
}
}
}
final class DeleteAccountFooterItemNode: ItemListControllerFooterItemNode {
private let backgroundNode: NavigationBackgroundNode
private let separatorNode: ASDisplayNode
private let clipNode: ASDisplayNode
private let buttonNode: SolidRoundedButtonNode
private let secondaryButtonNode: HighlightableButtonNode
private var validLayout: ContainerViewLayout?
var item: DeleteAccountFooterItem {
didSet {
self.updateItem()
if let layout = self.validLayout {
let _ = self.updateLayout(layout: layout, transition: .immediate)
}
}
}
init(item: DeleteAccountFooterItem) {
self.item = item
self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.tabBar.backgroundColor)
self.separatorNode = ASDisplayNode()
self.clipNode = ASDisplayNode()
self.clipNode.clipsToBounds = true
self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), height: 50.0, cornerRadius: 11.0, isShimmering: true)
self.secondaryButtonNode = HighlightableButtonNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.clipNode)
self.clipNode.addSubnode(self.buttonNode)
self.clipNode.addSubnode(self.secondaryButtonNode)
self.secondaryButtonNode.addTarget(self, action: #selector(self.secondaryButtonPressed), forControlEvents: .touchUpInside)
self.updateItem()
}
@objc private func secondaryButtonPressed() {
self.item.secondaryAction()
}
private func updateItem() {
self.backgroundNode.updateColor(color: self.item.theme.rootController.tabBar.backgroundColor, transition: .immediate)
self.separatorNode.backgroundColor = self.item.theme.rootController.tabBar.separatorColor
let backgroundColor = self.item.theme.list.itemCheckColors.fillColor
let textColor = self.item.theme.list.itemCheckColors.foregroundColor
self.buttonNode.updateTheme(SolidRoundedButtonTheme(backgroundColor: backgroundColor, foregroundColor: textColor), animated: false)
self.buttonNode.title = self.item.title
self.buttonNode.pressed = { [weak self] in
self?.item.action()
}
self.secondaryButtonNode.setTitle(self.item.secondaryTitle, with: Font.regular(17.0), with: self.item.theme.list.itemAccentColor, for: .normal)
}
override func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self.backgroundNode, alpha: alpha)
transition.updateAlpha(node: self.separatorNode, alpha: alpha)
}
override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayout = layout
let buttonInset: CGFloat = 16.0
let buttonWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - buttonInset * 2.0
let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition)
let topInset: CGFloat = 9.0
let bottomInset: CGFloat
let spacing: CGFloat
if layout.size.width > 320.0 {
bottomInset = 23.0
spacing = 23.0
} else {
bottomInset = 16.0
spacing = 16.0
}
let insets = layout.insets(options: [.input])
let secondaryButtonSize = self.secondaryButtonNode.measure(CGSize(width: buttonWidth, height: CGFloat.greatestFiniteMagnitude))
var panelHeight: CGFloat = buttonHeight + topInset + spacing + secondaryButtonSize.height + bottomInset
var buttonOffset: CGFloat = 0.0
let totalPanelHeight: CGFloat
if (self.buttonNode.title?.isEmpty ?? false) {
buttonOffset = -buttonHeight - topInset
self.buttonNode.alpha = 0.0
} else {
self.buttonNode.alpha = 1.0
}
if let inputHeight = layout.inputHeight, inputHeight > 0.0 {
panelHeight += buttonOffset
totalPanelHeight = panelHeight + insets.bottom
} else {
panelHeight += insets.bottom
totalPanelHeight = panelHeight
}
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - totalPanelHeight), size: CGSize(width: layout.size.width, height: panelHeight))
transition.updateFrame(node: self.backgroundNode, frame: panelFrame)
self.backgroundNode.update(size: panelFrame.size, transition: transition)
transition.updateFrame(node: self.clipNode, frame: panelFrame)
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonInset, y: topInset + buttonOffset), size: CGSize(width: buttonWidth, height: buttonHeight)))
transition.updateFrame(node: self.secondaryButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - secondaryButtonSize.width) / 2.0), y: topInset + buttonHeight + spacing + buttonOffset), size: secondaryButtonSize))
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: panelFrame.origin, size: CGSize(width: panelFrame.width, height: UIScreenPixel)))
return panelHeight
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if self.backgroundNode.frame.contains(point) {
return true
} else {
return false
}
}
}
@@ -0,0 +1,466 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import LegacyComponents
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import AccountContext
import AlertUI
import PresentationDataUtils
import UrlHandling
import AccountUtils
import PremiumUI
import PasswordSetupUI
import StorageUsageScreen
private struct DeleteAccountOptionsArguments {
let changePhoneNumber: () -> Void
let addAccount: () -> Void
let setupPrivacy: () -> Void
let setupTwoStepAuth: () -> Void
let setPasscode: () -> Void
let clearCache: () -> Void
let clearSyncedContacts: () -> Void
let deleteChats: () -> Void
let contactSupport: () -> Void
let deleteAccount: () -> Void
}
private enum DeleteAccountOptionsSection: Int32 {
case add
case privacy
case remove
case support
case delete
}
private enum DeleteAccountOptionsEntry: ItemListNodeEntry, Equatable {
case changePhoneNumber(PresentationTheme, String, String)
case addAccount(PresentationTheme, String, String)
case changePrivacy(PresentationTheme, String, String)
case setTwoStepAuth(PresentationTheme, String, String)
case setPasscode(PresentationTheme, String, String)
case clearCache(PresentationTheme, String, String)
case clearSyncedContacts(PresentationTheme, String, String)
case deleteChats(PresentationTheme, String, String)
case contactSupport(PresentationTheme, String, String)
case deleteAccount(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .changePhoneNumber, .addAccount:
return DeleteAccountOptionsSection.add.rawValue
case .changePrivacy, .setTwoStepAuth, .setPasscode:
return DeleteAccountOptionsSection.privacy.rawValue
case .clearCache, .clearSyncedContacts, .deleteChats:
return DeleteAccountOptionsSection.remove.rawValue
case .contactSupport:
return DeleteAccountOptionsSection.support.rawValue
case .deleteAccount:
return DeleteAccountOptionsSection.delete.rawValue
}
}
var stableId: Int32 {
switch self {
case .changePhoneNumber:
return 0
case .addAccount:
return 1
case .changePrivacy:
return 2
case .setTwoStepAuth:
return 3
case .setPasscode:
return 4
case .clearCache:
return 5
case .clearSyncedContacts:
return 6
case .deleteChats:
return 7
case .contactSupport:
return 8
case .deleteAccount:
return 9
}
}
static func <(lhs: DeleteAccountOptionsEntry, rhs: DeleteAccountOptionsEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! DeleteAccountOptionsArguments
switch self {
case let .changePhoneNumber(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.changePhoneNumber, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.changePhoneNumber()
})
case let .addAccount(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteAddAccount, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.addAccount()
})
case let .changePrivacy(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.security, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.setupPrivacy()
})
case let .setTwoStepAuth(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteSetTwoStepAuth, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.setupTwoStepAuth()
})
case let .setPasscode(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteSetPasscode, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.setPasscode()
})
case let .clearCache(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.dataAndStorage, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.clearCache()
})
case let .clearSyncedContacts(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.clearSynced, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.clearSyncedContacts()
})
case let .deleteChats(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteChats, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.deleteChats()
})
case let .contactSupport(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.support, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.contactSupport()
})
case let .deleteAccount(_, title):
return ItemListActionItem(presentationData: presentationData, title: title, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.deleteAccount()
})
}
}
}
private func deleteAccountOptionsEntries(presentationData: PresentationData, canAddAccounts: Bool, hasTwoStepAuth: Bool, hasPasscode: Bool) -> [DeleteAccountOptionsEntry] {
var entries: [DeleteAccountOptionsEntry] = []
entries.append(.changePhoneNumber(presentationData.theme, presentationData.strings.DeleteAccount_Options_ChangePhoneNumberTitle, presentationData.strings.DeleteAccount_Options_ChangePhoneNumberText))
if canAddAccounts {
entries.append(.addAccount(presentationData.theme, presentationData.strings.DeleteAccount_Options_AddAccountTitle, presentationData.strings.DeleteAccount_Options_AddAccountText))
}
entries.append(.changePrivacy(presentationData.theme, presentationData.strings.DeleteAccount_Options_ChangePrivacyTitle, presentationData.strings.DeleteAccount_Options_ChangePrivacyText))
if !hasTwoStepAuth {
entries.append(.setTwoStepAuth(presentationData.theme, presentationData.strings.DeleteAccount_Options_SetTwoStepAuthTitle, presentationData.strings.DeleteAccount_Options_SetTwoStepAuthText))
}
if !hasPasscode {
entries.append(.setPasscode(presentationData.theme, presentationData.strings.DeleteAccount_Options_SetPasscodeTitle, presentationData.strings.DeleteAccount_Options_SetPasscodeText))
}
entries.append(.clearCache(presentationData.theme, presentationData.strings.DeleteAccount_Options_ClearCacheTitle, presentationData.strings.DeleteAccount_Options_ClearCacheText))
entries.append(.clearSyncedContacts(presentationData.theme, presentationData.strings.DeleteAccount_Options_ClearSyncedContactsTitle, presentationData.strings.DeleteAccount_Options_ClearSyncedContactsText))
entries.append(.deleteChats(presentationData.theme, presentationData.strings.DeleteAccount_Options_DeleteChatsTitle, presentationData.strings.DeleteAccount_Options_DeleteChatsText))
entries.append(.contactSupport(presentationData.theme, presentationData.strings.DeleteAccount_Options_ContactSupportTitle, presentationData.strings.DeleteAccount_Options_ContactSupportText))
entries.append(.deleteAccount(presentationData.theme, presentationData.strings.DeleteAccount_DeleteMyAccount))
return entries
}
public func deleteAccountOptionsController(context: AccountContext, navigationController: NavigationController, hasTwoStepAuth: Bool, twoStepAuthData: TwoStepVerificationAccessConfiguration?) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var replaceTopControllerImpl: ((ViewController, Bool) -> Void)?
var dismissImpl: (() -> Void)?
let supportPeerDisposable = MetaDisposable()
let arguments = DeleteAccountOptionsArguments(changePhoneNumber: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_phone_change_tap")
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.engine.account.peerId))
|> deliverOnMainQueue).start(next: { accountPeer in
guard let accountPeer = accountPeer, case let .user(user) = accountPeer else {
return
}
let introController = PrivacyIntroController(context: context, mode: .changePhoneNumber(user.phone ?? ""), proceedAction: {
replaceTopControllerImpl?(ChangePhoneNumberController(context: context), false)
})
pushControllerImpl?(introController)
dismissImpl?()
})
}, addAccount: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_add_account_tap")
let _ = (activeAccountsAndPeers(context: context)
|> take(1)
|> deliverOnMainQueue
).start(next: { accountAndPeer, accountsAndPeers in
var maximumAvailableAccounts: Int = 3
if accountAndPeer?.1.isPremium == true && !context.account.testingEnvironment {
maximumAvailableAccounts = 4
}
var count: Int = 1
for (accountContext, peer, _) in accountsAndPeers {
if !accountContext.account.testingEnvironment {
if peer.isPremium {
maximumAvailableAccounts = 4
}
count += 1
}
}
if count >= maximumAvailableAccounts {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .accounts, count: Int32(count), action: {
let controller = PremiumIntroScreen(context: context, source: .accounts)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
} else {
context.sharedContext.beginNewAuth(testingEnvironment: context.account.testingEnvironment)
dismissImpl?()
}
})
}, setupPrivacy: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_privacy_tap")
replaceTopControllerImpl?(makePrivacyAndSecurityController(context: context), false)
}, setupTwoStepAuth: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_2fa_tap")
if let data = twoStepAuthData {
switch data {
case .set:
break
case let .notSet(pendingEmail):
if pendingEmail == nil {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = TwoFactorAuthSplashScreen(sharedContext: context.sharedContext, engine: .authorized(context.engine), mode: .intro(.init(
title: presentationData.strings.TwoFactorSetup_Intro_Title,
text: presentationData.strings.TwoFactorSetup_Intro_Text,
actionText: presentationData.strings.TwoFactorSetup_Intro_Action,
doneText: presentationData.strings.TwoFactorSetup_Done_Action,
phoneNumber: nil
)))
replaceTopControllerImpl?(controller, false)
return
}
}
}
let controller = twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: false, data: twoStepAuthData.flatMap({ Signal<TwoStepVerificationUnlockSettingsControllerData, NoError>.single(.access(configuration: $0)) })))
replaceTopControllerImpl?(controller, false)
}, setPasscode: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_passcode_tap")
let _ = passcodeOptionsAccessController(context: context, pushController: { controller in
replaceTopControllerImpl?(controller, false)
}, completion: { _ in
replaceTopControllerImpl?(passcodeOptionsController(context: context), false)
}).start(next: { controller in
if let controller = controller {
pushControllerImpl?(controller)
}
})
dismissImpl?()
}, clearCache: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_clear_cache_tap")
pushControllerImpl?(StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in
return storageUsageExceptionsScreen(context: context, category: category)
}))
dismissImpl?()
}, clearSyncedContacts: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_clear_contacts_tap")
replaceTopControllerImpl?(dataPrivacyController(context: context), false)
}, deleteChats: {
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_delete_chats_tap")
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var faqUrl = presentationData.strings.DeleteAccount_DeleteMessagesURL
if faqUrl == "DeleteAccount.DeleteMessagesURL" || faqUrl.isEmpty {
faqUrl = "https://telegram.org/faq#q-can-i-delete-my-messages"
}
let resolvedUrl = resolveInstantViewUrl(account: context.account, url: faqUrl)
|> mapToSignal { result -> Signal<ResolvedUrl, NoError> in
guard case let .result(result) = result else {
return .complete()
}
return .single(result)
}
let resolvedUrlPromise = Promise<ResolvedUrl>()
resolvedUrlPromise.set(resolvedUrl)
let openFaq: (Promise<ResolvedUrl>) -> Void = { resolvedUrl in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
presentControllerImpl?(controller, nil)
let _ = (resolvedUrl.get()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] resolvedUrl in
controller?.dismiss()
dismissImpl?()
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in
}, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in
pushControllerImpl?(controller)
}, dismissInput: {}, contentContext: nil, progress: nil, completion: nil)
})
}
openFaq(resolvedUrlPromise)
}, contactSupport: { [weak navigationController] in
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_support_tap")
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let supportPeer = Promise<EnginePeer.Id?>()
supportPeer.set(context.engine.peers.supportPeerId())
var faqUrl = presentationData.strings.Settings_FAQ_URL
if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty {
faqUrl = "https://telegram.org/faq#general"
}
let resolvedUrl = resolveInstantViewUrl(account: context.account, url: faqUrl)
|> mapToSignal { result -> Signal<ResolvedUrl, NoError> in
guard case let .result(result) = result else {
return .complete()
}
return .single(result)
}
let resolvedUrlPromise = Promise<ResolvedUrl>()
resolvedUrlPromise.set(resolvedUrl)
let openFaq: (Promise<ResolvedUrl>) -> Void = { resolvedUrl in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
presentControllerImpl?(controller, nil)
let _ = (resolvedUrl.get()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] resolvedUrl in
controller?.dismiss()
dismissImpl?()
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in
}, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in
pushControllerImpl?(controller)
}, dismissInput: {}, contentContext: nil, progress: nil, completion: nil)
})
}
let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.Settings_FAQ_Intro, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_FAQ_Button, action: {
openFaq(resolvedUrlPromise)
dismissImpl?()
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
supportPeerDisposable.set((supportPeer.get()
|> take(1)
|> deliverOnMainQueue).start(next: { peerId in
guard let peerId = peerId else {
return
}
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer else {
return
}
if let navigationController = navigationController {
dismissImpl?()
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer)))
}
})
}))
})
])
alertController.dismissed = { _ in
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_support_cancel")
}
presentControllerImpl?(alertController, nil)
}, deleteAccount: {
let controller = deleteAccountDataController(context: context, mode: .peers, twoStepAuthData: twoStepAuthData)
replaceTopControllerImpl?(controller, true)
})
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
context.sharedContext.accountManager.accessChallengeData(),
activeAccountsAndPeers(context: context)
)
|> map { presentationData, accessChallengeData, accountsAndPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
var hasPasscode = false
switch accessChallengeData.data {
case .numericalPassword, .plaintextPassword:
hasPasscode = true
default:
break
}
let canAddAccounts = accountsAndPeers.1.count + 1 < maximumNumberOfAccounts
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.DeleteAccount_AlternativeOptionsTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: deleteAccountOptionsEntries(presentationData: presentationData, canAddAccounts: canAddAccounts, hasTwoStepAuth: hasTwoStepAuth, hasPasscode: hasPasscode), style: .blocks)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal, tabBarItem: nil)
controller.navigationPresentation = .modal
pushControllerImpl = { [weak navigationController] value in
navigationController?.pushViewController(value, animated: false)
}
presentControllerImpl = { [weak controller] value, arguments in
controller?.present(value, in: .window(.root), with: arguments ?? ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
replaceTopControllerImpl = { [weak navigationController] c, complex in
if complex {
navigationController?.pushViewController(c, completion: { [weak navigationController, weak controller, weak c] in
if let navigationController = navigationController {
let controllers = navigationController.viewControllers.filter { $0 !== controller }
c?.navigationPresentation = .modal
navigationController.setViewControllers(controllers, animated: false)
}
})
} else {
if c is PrivacyAndSecurityControllerImpl {
if let navigationController = navigationController {
if let existing = navigationController.viewControllers.first(where: { $0 is PrivacyAndSecurityControllerImpl }) as? ViewController {
existing.scrollToTop?()
dismissImpl?()
} else {
navigationController.replaceTopController(c, animated: true)
}
}
} else {
navigationController?.replaceTopController(c, animated: true)
}
}
}
dismissImpl = { [weak controller] in
let _ = controller?.dismiss()
}
addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_show")
return controller
}
@@ -0,0 +1,307 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import HorizontalPeerItem
import AccountContext
import MergeLists
private struct PeersEntry: Comparable, Identifiable {
let index: Int
let peer: EnginePeer
let theme: PresentationTheme
let strings: PresentationStrings
var stableId: EnginePeer.Id {
return self.peer.id
}
static func ==(lhs: PeersEntry, rhs: PeersEntry) -> Bool {
if lhs.index != rhs.index {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
return true
}
static func <(lhs: PeersEntry, rhs: PeersEntry) -> Bool {
return lhs.index < rhs.index
}
func item(context: AccountContext) -> ListViewItem {
return HorizontalPeerItem(
theme: self.theme,
strings: self.strings,
mode: .list(compact: true),
accountPeerId: context.account.peerId,
postbox: context.account.postbox,
network: context.account.network,
energyUsageSettings: context.sharedContext.energyUsageSettings,
contentSettings: context.currentContentSettings.with { $0 },
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
resolveInlineStickers: context.engine.stickers.resolveInlineStickers,
peer: self.peer,
presence: nil,
unreadBadge: nil,
action: { _ in },
contextAction: nil,
isPeerSelected: { _ in return false },
customWidth: nil
)
}
}
private struct DeleteAccountPeersItemNodeTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let firstTime: Bool
let animated: Bool
}
private func preparedPeersTransition(context: AccountContext, from fromEntries: [PeersEntry], to toEntries: [PeersEntry], firstTime: Bool, animated: Bool) -> DeleteAccountPeersItemNodeTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context), directionHint: .Down) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context), directionHint: nil) }
return DeleteAccountPeersItemNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, animated: animated)
}
class DeleteAccountPeersItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let peers: [EnginePeer]
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peers: [EnginePeer], sectionId: ItemListSectionId) {
self.context = context
self.theme = theme
self.strings = strings
self.peers = peers
self.sectionId = sectionId
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = DeleteAccountPeersItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? DeleteAccountPeersItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
class DeleteAccountPeersItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let dataPromise = Promise<(AccountContext, [EnginePeer], PresentationTheme, PresentationStrings)>()
private var disposable: Disposable?
private var item: DeleteAccountPeersItem?
private var layoutParams: ListViewItemLayoutParams?
private let listView: ListView
private var queuedTransitions: [DeleteAccountPeersItemNodeTransition] = []
var tag: ItemListItemTag? {
return self.item?.tag
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.listView = ListView()
self.listView.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.listView)
let previous: Atomic<[PeersEntry]> = Atomic(value: [])
let firstTime:Atomic<Bool> = Atomic(value: true)
self.disposable = (self.dataPromise.get() |> deliverOnMainQueue).start(next: { [weak self] data in
if let strongSelf = self {
let (context, peers, theme, strings) = data
var entries: [PeersEntry] = []
for peer in peers {
entries.append(PeersEntry(index: entries.count, peer: peer, theme: theme, strings: strings))
}
let animated = !firstTime.swap(false)
let transition = preparedPeersTransition(context: context, from: previous.swap(entries), to: entries, firstTime: !animated, animated: animated)
strongSelf.enqueueTransition(transition)
}
})
}
deinit {
self.disposable?.dispose()
}
private func enqueueTransition(_ transition: DeleteAccountPeersItemNodeTransition) {
self.queuedTransitions.append(transition)
self.dequeueTransitions()
}
private func dequeueTransitions() {
while !self.queuedTransitions.isEmpty {
let transition = self.queuedTransitions.removeFirst()
var options = ListViewDeleteAndInsertOptions()
if transition.firstTime {
options.insert(.PreferSynchronousResourceLoading)
options.insert(.PreferSynchronousDrawing)
options.insert(.Synchronous)
options.insert(.LowLatency)
} else if transition.animated {
options.insert(.AnimateInsertion)
}
self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { _ in })
}
}
func asyncLayout() -> (_ item: DeleteAccountPeersItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, neighbors in
let contentSize: CGSize
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
contentSize = CGSize(width: params.width, height: 109.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.dataPromise.set(.single((item.context, item.peers, item.theme, item.strings)))
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.addSubnode(strongSelf.maskNode)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
let listInsets = UIEdgeInsets(top: params.leftInset, left: 0.0, bottom: params.rightInset, right: 0.0)
strongSelf.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 92.0, height: params.width)
strongSelf.listView.position = CGPoint(x: params.width / 2.0, y: contentSize.height / 2.0)
strongSelf.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: 92.0, height: params.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
strongSelf.dequeueTransitions()
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,407 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import PhoneInputNode
import CountrySelectionUI
private func generateCountryButtonBackground(color: UIColor, strokeColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 56, height: 44.0 + 6.0), rotatedContext: { size, context in
let arrowSize: CGFloat = 6.0
let lineWidth = UIScreenPixel
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height - arrowSize)))
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize))
context.closePath()
context.fillPath()
context.setStrokeColor(strokeColor.cgColor)
context.setLineWidth(lineWidth)
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize - lineWidth / 2.0))
context.addLine(to: CGPoint(x: 15.0, y: size.height - arrowSize - lineWidth / 2.0))
context.strokePath()
context.move(to: CGPoint(x: 0.0, y: lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width, y: lineWidth / 2.0))
context.strokePath()
})?.stretchableImage(withLeftCapWidth: 55, topCapHeight: 1)
}
private func generateCountryButtonHighlightedBackground(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 56.0, height: 44.0 + 6.0), rotatedContext: { size, context in
let arrowSize: CGFloat = 6.0
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height - arrowSize)))
context.move(to: CGPoint(x: size.width, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize, y: size.height))
context.addLine(to: CGPoint(x: size.width - 1.0 - arrowSize - arrowSize, y: size.height - arrowSize))
context.closePath()
context.fillPath()
})?.stretchableImage(withLeftCapWidth: 55, topCapHeight: 2)
}
private func generatePhoneInputBackground(color: UIColor, strokeColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 82.0, height: 44.0), rotatedContext: { size, context in
let lineWidth = UIScreenPixel
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(strokeColor.cgColor)
context.setLineWidth(lineWidth)
context.move(to: CGPoint(x: 0.0, y: size.height - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width, y: size.height - lineWidth / 2.0))
context.strokePath()
context.move(to: CGPoint(x: size.width - 2.0 + lineWidth / 2.0, y: size.height - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - 2.0 + lineWidth / 2.0, y: 0.0))
context.strokePath()
})?.stretchableImage(withLeftCapWidth: 81, topCapHeight: 2)
}
final class DeleteAccountPhoneItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let value: (Int32?, String?, String)
let sectionId: ItemListSectionId
let selectCountryCode: () -> Void
let updated: (Int) -> Void
public init(theme: PresentationTheme, strings: PresentationStrings, value: (Int32?, String?, String), sectionId: ItemListSectionId, selectCountryCode: @escaping () -> Void, updated: @escaping (Int) -> Void) {
self.theme = theme
self.strings = strings
self.value = value
self.sectionId = sectionId
self.selectCountryCode = selectCountryCode
self.updated = updated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = DeleteAccountPhoneItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? DeleteAccountPhoneItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
final class DeleteAccountPhoneItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let countryButton: ASButtonNode
private let phoneBackground: ASImageNode
private let phoneInputNode: PhoneInputNode
private var item: DeleteAccountPhoneItem?
private var layoutParams: ListViewItemLayoutParams?
var preferredCountryIdForCode: [String: String] = [:]
var tag: ItemListItemTag? {
return self.item?.tag
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.countryButton = ASButtonNode()
self.phoneBackground = ASImageNode()
self.phoneBackground.displaysAsynchronously = false
self.phoneBackground.displayWithoutProcessing = true
self.phoneBackground.isLayerBacked = true
self.phoneInputNode = PhoneInputNode(fontSize: 17.0)
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.phoneBackground)
self.addSubnode(self.countryButton)
self.addSubnode(self.phoneInputNode)
self.countryButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 15.0, bottom: 4.0, right: 0.0)
self.countryButton.contentHorizontalAlignment = .left
self.countryButton.addTarget(self, action: #selector(self.countryPressed), forControlEvents: .touchUpInside)
let processNumberChange: (String) -> Bool = { [weak self] number in
guard let strongSelf = self, let item = strongSelf.item else {
return false
}
if let (country, _) = AuthorizationSequenceCountrySelectionController.lookupCountryIdByNumber(number, preferredCountries: strongSelf.preferredCountryIdForCode) {
let flagString = emojiFlagForISOCountryCode(country.id)
let localizedName: String = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(country.id, strings: item.strings) ?? country.name
strongSelf.countryButton.setTitle("\(flagString) \(localizedName)", with: Font.regular(17.0), with: item.theme.list.itemPrimaryTextColor, for: [])
let maskFont = Font.with(size: 17.0, design: .regular, traits: [.monospacedNumbers])
if let mask = AuthorizationSequenceCountrySelectionController.lookupPatternByNumber(number, preferredCountries: strongSelf.preferredCountryIdForCode).flatMap({ NSAttributedString(string: $0, font: maskFont, textColor: item.theme.list.itemPlaceholderTextColor) }) {
strongSelf.phoneInputNode.numberField.textField.attributedPlaceholder = nil
strongSelf.phoneInputNode.mask = mask
} else {
strongSelf.phoneInputNode.mask = nil
strongSelf.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: item.strings.Login_PhonePlaceholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor)
}
return true
} else {
return false
}
}
self.phoneInputNode.numberTextUpdated = { [weak self] number in
if let strongSelf = self {
let _ = processNumberChange(strongSelf.phoneInputNode.number)
}
}
self.phoneInputNode.countryCodeUpdated = { [weak self] code, name in
if let strongSelf = self, let item = strongSelf.item {
if let name = name {
strongSelf.preferredCountryIdForCode[code] = name
}
if processNumberChange(strongSelf.phoneInputNode.number) {
} else if let code = Int(code), let name = name, let countryName = countryCodeAndIdToName[CountryCodeAndId(code: code, id: name)] {
let localizedName: String = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(name, strings: item.strings) ?? countryName
strongSelf.countryButton.setTitle(localizedName, with: Font.regular(17.0), with: item.theme.list.itemPrimaryTextColor, for: [])
} else if let code = Int(code), let (_, countryName) = countryCodeToIdAndName[code] {
strongSelf.countryButton.setTitle(countryName, with: Font.regular(17.0), with: item.theme.list.itemPrimaryTextColor, for: [])
} else {
strongSelf.countryButton.setTitle(item.strings.Login_CountryCode, with: Font.regular(17.0), with: item.theme.list.itemPrimaryTextColor, for: [])
}
}
}
self.phoneInputNode.customFormatter = { number in
if let (_, code) = AuthorizationSequenceCountrySelectionController.lookupCountryIdByNumber(number, preferredCountries: [:]) {
return code.code
} else {
return nil
}
}
let countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String
var countryCodeAndId: (Int32, String) = (1, "US")
if let countryId = countryId {
let normalizedId = countryId.uppercased()
for (code, idAndName) in countryCodeToIdAndName {
if idAndName.0 == normalizedId {
countryCodeAndId = (Int32(code), idAndName.0.uppercased())
break
}
}
}
self.phoneInputNode.number = "+\(countryCodeAndId.0)"
}
@objc private func countryPressed() {
if let item = self.item {
item.selectCountryCode()
}
}
var phoneNumber: String {
return self.phoneInputNode.number
}
var codeNumberAndFullNumber: (String, String, String) {
return self.phoneInputNode.codeNumberAndFullNumber
}
func updateCountryCode() {
self.phoneInputNode.codeAndNumber = self.phoneInputNode.codeAndNumber
}
func updateCountryCode(code: Int32, name: String) {
self.phoneInputNode.codeAndNumber = (code, name, self.phoneInputNode.codeAndNumber.2)
}
func activateInput() {
self.phoneInputNode.numberField.textField.becomeFirstResponder()
}
func animateError() {
self.phoneInputNode.countryCodeField.layer.addShakeAnimation()
self.phoneInputNode.numberField.layer.addShakeAnimation()
}
func asyncLayout() -> (_ item: DeleteAccountPhoneItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { item, params, neighbors in
var updatedCountryButtonBackground: UIImage?
var updatedCountryButtonHighlightedBackground: UIImage?
var updatedPhoneBackground: UIImage?
if currentItem?.theme !== item.theme {
updatedCountryButtonBackground = generateCountryButtonBackground(color: item.theme.list.itemBlocksBackgroundColor, strokeColor: item.theme.list.itemBlocksSeparatorColor)
updatedCountryButtonHighlightedBackground = generateCountryButtonHighlightedBackground(color: item.theme.list.itemHighlightedBackgroundColor)
updatedPhoneBackground = generatePhoneInputBackground(color: item.theme.list.itemBlocksBackgroundColor, strokeColor: item.theme.list.itemBlocksSeparatorColor)
}
let contentSize: CGSize
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let countryButtonHeight: CGFloat = 44.0
let inputFieldsHeight: CGFloat = 44.0
contentSize = CGSize(width: params.width, height: countryButtonHeight + inputFieldsHeight)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.addSubnode(strongSelf.maskNode)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
if let updatedCountryButtonBackground = updatedCountryButtonBackground {
strongSelf.countryButton.setBackgroundImage(updatedCountryButtonBackground, for: [])
}
if let updatedCountryButtonHighlightedBackground = updatedCountryButtonHighlightedBackground {
strongSelf.countryButton.setBackgroundImage(updatedCountryButtonHighlightedBackground, for: .highlighted)
}
if let updatedPhoneBackground = updatedPhoneBackground {
strongSelf.phoneBackground.image = updatedPhoneBackground
}
strongSelf.phoneInputNode.countryCodeField.textField.textColor = item.theme.list.itemPrimaryTextColor
strongSelf.phoneInputNode.countryCodeField.textField.keyboardAppearance = item.theme.rootController.keyboardColor.keyboardAppearance
strongSelf.phoneInputNode.countryCodeField.textField.tintColor = item.theme.list.itemAccentColor
strongSelf.phoneInputNode.numberField.textField.textColor = item.theme.list.itemPrimaryTextColor
strongSelf.phoneInputNode.numberField.textField.keyboardAppearance = item.theme.rootController.keyboardColor.keyboardAppearance
strongSelf.phoneInputNode.numberField.textField.tintColor = item.theme.list.itemAccentColor
strongSelf.countryButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: params.leftInset + 15.0, bottom: 4.0, right: 0.0)
strongSelf.countryButton.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: 44.0 + 6.0))
strongSelf.phoneBackground.frame = CGRect(origin: CGPoint(x: 0.0, y: 44.0), size: CGSize(width: params.width, height: 44.0))
let countryCodeFrame = CGRect(origin: CGPoint(x: 11.0, y: 44.0), size: CGSize(width: 67.0, height: 44.0))
let numberFrame = CGRect(origin: CGPoint(x: 92.0, y: 44.0), size: CGSize(width: layout.size.width - 70.0 - 8.0, height: 44.0))
let placeholderFrame = numberFrame.offsetBy(dx: 0.0, dy: 8.0)
let phoneInputFrame = countryCodeFrame.union(numberFrame)
strongSelf.phoneInputNode.frame = phoneInputFrame
strongSelf.phoneInputNode.countryCodeField.frame = countryCodeFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY)
strongSelf.phoneInputNode.numberField.frame = numberFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY)
strongSelf.phoneInputNode.placeholderNode.frame = placeholderFrame.offsetBy(dx: -phoneInputFrame.minX, dy: -phoneInputFrame.minY + 4.0 + UIScreenPixel)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,200 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AccountContext
import SearchUI
public class LocalizationListController: ViewController {
private let context: AccountContext
private var controllerNode: LocalizationListControllerNode {
return self.displayNode as! LocalizationListControllerNode
}
private var _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var editItem: UIBarButtonItem!
private var doneItem: UIBarButtonItem!
private var searchContentNode: NavigationBarSearchContentNode?
private var previousContentOffset: ListViewVisibleContentOffset?
public init(context: AccountContext) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
self.editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.editPressed))
self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.title = self.presentationData.strings.Settings_AppLanguage
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.scrollToTop = { [weak self] in
if let strongSelf = self {
if let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateExpansionProgress(1.0, animated: true)
}
strongSelf.controllerNode.scrollToTop()
}
}
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings()
}
}
})
self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, inline: true, activate: { [weak self] in
self?.activateSearch()
})
self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
private func updateThemeAndStrings() {
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search)
self.title = self.presentationData.strings.Settings_AppLanguage
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.controllerNode.updatePresentationData(self.presentationData)
let editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.editPressed))
let doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
if self.navigationItem.rightBarButtonItem === self.editItem {
self.navigationItem.rightBarButtonItem = editItem
} else if self.navigationItem.rightBarButtonItem === self.doneItem {
self.navigationItem.rightBarButtonItem = doneItem
}
self.editItem = editItem
self.doneItem = doneItem
}
override public func loadDisplayNode() {
self.displayNode = LocalizationListControllerNode(context: self.context, presentationData: self.presentationData, navigationBar: self.navigationBar!, requestActivateSearch: { [weak self] in
self?.activateSearch()
}, requestDeactivateSearch: { [weak self] in
self?.deactivateSearch()
}, updateCanStartEditing: { [weak self] value in
guard let strongSelf = self else {
return
}
let item: UIBarButtonItem?
if let value = value {
item = value ? strongSelf.editItem : strongSelf.doneItem
} else {
item = nil
}
if strongSelf.navigationItem.rightBarButtonItem !== item {
strongSelf.navigationItem.setRightBarButton(item, animated: true)
}
}, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}, push: { [weak self] c in
self?.push(c)
})
self.controllerNode.listNode.visibleContentOffsetChanged = { [weak self] offset in
if let strongSelf = self {
if let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateListVisibleContentOffset(offset)
}
var previousContentOffsetValue: CGFloat?
if let previousContentOffset = strongSelf.previousContentOffset, case let .known(value) = previousContentOffset {
previousContentOffsetValue = value
}
switch offset {
case let .known(value):
let transition: ContainedViewLayoutTransition
if let previousContentOffsetValue = previousContentOffsetValue, value <= 0.0, previousContentOffsetValue > 30.0 {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
strongSelf.navigationBar?.updateBackgroundAlpha(min(30.0, max(0.0, value - 54.0)) / 30.0, transition: transition)
case .unknown, .none:
strongSelf.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate)
}
strongSelf.previousContentOffset = offset
}
}
self.controllerNode.listNode.didEndScrolling = { [weak self] _ in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
let _ = fixNavigationSearchableListNodeScrolling(strongSelf.controllerNode.listNode, searchNode: searchContentNode)
}
}
self._ready.set(self.controllerNode._ready.get())
self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, transition: transition)
}
@objc private func editPressed() {
self.controllerNode.toggleEditing()
}
private func activateSearch() {
if self.displayNavigationBar {
if let scrollToTop = self.scrollToTop {
scrollToTop()
}
if let searchContentNode = self.searchContentNode {
self.controllerNode.activateSearch(placeholderNode: searchContentNode.placeholderNode)
}
self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring))
}
}
private func deactivateSearch() {
if !self.displayNavigationBar {
self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring))
if let searchContentNode = self.searchContentNode {
self.controllerNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode)
}
}
}
}
@@ -0,0 +1,836 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import MergeLists
import ItemListUI
import PresentationDataUtils
import AccountContext
import ShareController
import SearchBarNode
import SearchUI
import UndoUI
import TelegramUIPreferences
import TranslateUI
import PremiumUI
private enum LanguageListSection: ItemListSectionId {
case translate
case official
case unofficial
}
private enum LanguageListEntryId: Hashable {
case search
case translate(Int)
case localizationTitle
case localization(String)
}
private enum LanguageListEntryType {
case official
case unofficial
}
private enum LanguageListEntry: Comparable, Identifiable {
case translateTitle(text: String)
case translate(text: String, value: Bool)
case translateEntire(text: String, value: Bool, locked: Bool)
case doNotTranslate(text: String, value: String)
case translateInfo(text: String)
case localizationTitle(text: String, section: ItemListSectionId)
case localization(index: Int, info: LocalizationInfo?, type: LanguageListEntryType, selected: Bool, activity: Bool, revealed: Bool, editing: Bool)
var stableId: LanguageListEntryId {
switch self {
case .translateTitle:
return .translate(0)
case .translate:
return .translate(1)
case .translateEntire:
return .translate(2)
case .doNotTranslate:
return .translate(3)
case .translateInfo:
return .translate(4)
case .localizationTitle:
return .localizationTitle
case let .localization(index, info, _, _, _, _, _):
return .localization(info?.languageCode ?? "\(index)")
}
}
private func index() -> Int {
switch self {
case .translateTitle:
return 0
case .translate:
return 1
case .translateEntire:
return 2
case .doNotTranslate:
return 3
case .translateInfo:
return 4
case .localizationTitle:
return 1000
case let .localization(index, _, _, _, _, _, _):
return 1001 + index
}
}
static func <(lhs: LanguageListEntry, rhs: LanguageListEntry) -> Bool {
return lhs.index() < rhs.index()
}
func item(presentationData: PresentationData, searchMode: Bool, openSearch: @escaping () -> Void, toggleShowTranslate: @escaping (Bool) -> Void, toggleTranslateChats: @escaping (Bool) -> Void, openDoNotTranslate: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void, showPremiumInfo: @escaping () -> Void) -> ListViewItem {
switch self {
case let .translateTitle(text):
return ItemListSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), text: text, sectionId: LanguageListSection.translate.rawValue)
case let .translate(text, value):
return ItemListSwitchItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, title: text, value: value, sectionId: LanguageListSection.translate.rawValue, style: .blocks, updated: { value in
toggleShowTranslate(value)
})
case let .translateEntire(text, value, locked):
return ItemListSwitchItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, title: text, value: value, enableInteractiveChanges: !locked, displayLocked: locked, sectionId: LanguageListSection.translate.rawValue, style: .blocks, updated: { value in
if !locked {
toggleTranslateChats(value)
}
}, activatedWhileDisabled: {
showPremiumInfo()
})
case let .doNotTranslate(text, value):
return ItemListDisclosureItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, title: text, label: value, sectionId: LanguageListSection.translate.rawValue, style: .blocks, action: {
openDoNotTranslate()
})
case let .translateInfo(text):
return ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(text), sectionId: LanguageListSection.translate.rawValue)
case let .localizationTitle(text, section):
return ItemListSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), text: text, sectionId: section)
case let .localization(_, info, type, selected, activity, revealed, editing):
return LocalizationListItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, id: info?.languageCode ?? "", title: info?.title ?? " ", subtitle: info?.localizedTitle ?? " ", checked: selected, activity: activity, loading: info == nil, editing: LocalizationListItemEditing(editable: !selected && !searchMode && !(info?.isOfficial ?? true), editing: editing, revealed: !selected && revealed, reorderable: false), sectionId: type == .official ? LanguageListSection.official.rawValue : LanguageListSection.unofficial.rawValue, alwaysPlain: searchMode, action: {
if let info = info {
selectLocalization(info)
}
}, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem)
}
}
}
private struct LocalizationListSearchContainerTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let isSearching: Bool
}
private func preparedLanguageListSearchContainerTransition(presentationData: PresentationData, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], selectLocalization: @escaping (LocalizationInfo) -> Void, isSearching: Bool, forceUpdate: Bool) -> LocalizationListSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, toggleShowTranslate: { _ in }, toggleTranslateChats: { _ in }, openDoNotTranslate: {}, selectLocalization: selectLocalization, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }, showPremiumInfo: {}), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, toggleShowTranslate: { _ in }, toggleTranslateChats: { _ in }, openDoNotTranslate: {}, selectLocalization: selectLocalization, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }, showPremiumInfo: {}), directionHint: nil) }
return LocalizationListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching)
}
private final class LocalizationListSearchContainerNode: SearchDisplayControllerContentNode {
private let dimNode: ASDisplayNode
private let listNode: ListView
private var enqueuedTransitions: [LocalizationListSearchContainerTransition] = []
private var hasValidLayout = false
private let searchQuery = Promise<String?>()
private let searchDisposable = MetaDisposable()
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<PresentationData>
public override var hasDim: Bool {
return true
}
init(context: AccountContext, listState: LocalizationListState, selectLocalization: @escaping (LocalizationInfo) -> Void, applyingCode: Signal<String?, NoError>) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
self.presentationDataPromise = Promise(self.presentationData)
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5)
self.listNode = ListView()
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
super.init()
self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.listNode.isHidden = true
self.addSubnode(self.dimNode)
self.addSubnode(self.listNode)
let foundItems = self.searchQuery.get()
|> mapToSignal { query -> Signal<[LocalizationInfo]?, NoError> in
if let query = query, !query.isEmpty {
let normalizedQuery = query.folding(options: [.diacriticInsensitive, .widthInsensitive, .caseInsensitive], locale: nil)
var result: [LocalizationInfo] = []
var uniqueIds = Set<String>()
for info in listState.availableSavedLocalizations + listState.availableOfficialLocalizations {
let title = info.title.folding(options: [.diacriticInsensitive, .widthInsensitive, .caseInsensitive], locale: nil)
let localizedTitle = info.localizedTitle.folding(options: [.diacriticInsensitive, .widthInsensitive, .caseInsensitive], locale: nil)
if title.hasPrefix(normalizedQuery) || localizedTitle.hasPrefix(normalizedQuery) {
if uniqueIds.contains(info.languageCode) {
continue
}
uniqueIds.insert(info.languageCode)
result.append(info)
}
}
return .single(result)
} else {
return .single(nil)
}
}
let previousEntriesHolder = Atomic<([LanguageListEntry], PresentationTheme, PresentationStrings)?>(value: nil)
self.searchDisposable.set(combineLatest(queue: .mainQueue(), foundItems, self.presentationDataPromise.get(), applyingCode).start(next: { [weak self] items, presentationData, applyingCode in
guard let strongSelf = self else {
return
}
var entries: [LanguageListEntry] = []
if let items = items {
for item in items {
entries.append(.localization(index: entries.count, info: item, type: .official, selected: presentationData.strings.primaryComponent.languageCode == item.languageCode, activity: applyingCode == item.languageCode, revealed: false, editing: false))
}
}
let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings))
let transition = preparedLanguageListSearchContainerTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, selectLocalization: selectLocalization, isSearching: items != nil, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings)
strongSelf.enqueueTransition(transition)
}))
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
strongSelf.presentationDataPromise.set(.single(presentationData))
}
}
})
self.listNode.beganInteractiveDragging = { [weak self] _ in
self?.dismissInput?()
}
}
deinit {
self.searchDisposable.dispose()
self.presentationDataDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.listNode.backgroundColor = theme.chatList.backgroundColor
}
override func searchTextUpdated(text: String) {
if text.isEmpty {
self.searchQuery.set(.single(nil))
} else {
self.searchQuery.set(.single(text))
}
}
private func enqueueTransition(_ transition: LocalizationListSearchContainerTransition) {
self.enqueuedTransitions.append(transition)
if self.hasValidLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransitions()
}
}
}
private func dequeueTransitions() {
if let transition = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.PreferSynchronousDrawing)
let isSearching = transition.isSearching
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
self?.listNode.isHidden = !isSearching
self?.dimNode.isHidden = isSearching
})
}
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let topInset = navigationBarHeight
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset)))
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if !self.hasValidLayout {
self.hasValidLayout = true
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransitions()
}
}
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancel?()
}
}
}
private struct LanguageListNodeTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let firstTime: Bool
let isLoading: Bool
let animated: Bool
let crossfade: Bool
}
private func preparedLanguageListNodeTransition(presentationData: PresentationData, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], openSearch: @escaping () -> Void, toggleShowTranslate: @escaping (Bool) -> Void, toggleTranslateChats: @escaping (Bool) -> Void, openDoNotTranslate: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void, showPremiumInfo: @escaping () -> Void, firstTime: Bool, isLoading: Bool, forceUpdate: Bool, animated: Bool, crossfade: Bool) -> LanguageListNodeTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, toggleShowTranslate: toggleShowTranslate, toggleTranslateChats: toggleTranslateChats, openDoNotTranslate: openDoNotTranslate, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, showPremiumInfo: showPremiumInfo), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, toggleShowTranslate: toggleShowTranslate, toggleTranslateChats: toggleTranslateChats, openDoNotTranslate: openDoNotTranslate, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, showPremiumInfo: showPremiumInfo), directionHint: nil) }
return LanguageListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, isLoading: isLoading, animated: animated, crossfade: crossfade)
}
final class LocalizationListControllerNode: ViewControllerTracingNode {
private let context: AccountContext
private var presentationData: PresentationData
private weak var navigationBar: NavigationBar?
private let requestActivateSearch: () -> Void
private let requestDeactivateSearch: () -> Void
private let present: (ViewController, Any?) -> Void
private let push: (ViewController) -> Void
private var didSetReady = false
let _ready = ValuePromise<Bool>()
private var containerLayout: (ContainerViewLayout, CGFloat)?
let listNode: ListView
private let leftOverlayNode: ASDisplayNode
private let rightOverlayNode: ASDisplayNode
private var queuedTransitions: [LanguageListNodeTransition] = []
private var searchDisplayController: SearchDisplayController?
private let presentationDataValue = Promise<PresentationData>()
private var updatedDisposable: Disposable?
private var listDisposable: Disposable?
private let applyDisposable = MetaDisposable()
private var currentListState: LocalizationListState?
private let applyingCode = Promise<String?>(nil)
private let isEditing = ValuePromise<Bool>(false)
private var isEditingValue: Bool = false {
didSet {
self.isEditing.set(self.isEditingValue)
}
}
init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, updateCanStartEditing: @escaping (Bool?) -> Void, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void) {
self.context = context
self.presentationData = presentationData
self.presentationDataValue.set(.single(presentationData))
self.navigationBar = navigationBar
self.requestActivateSearch = requestActivateSearch
self.requestDeactivateSearch = requestDeactivateSearch
self.present = present
self.push = push
self.listNode = ListView()
self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.list.blocksBackgroundColor, direction: true)
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
self.leftOverlayNode = ASDisplayNode()
self.leftOverlayNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
self.rightOverlayNode = ASDisplayNode()
self.rightOverlayNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
super.init()
self.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.addSubnode(self.listNode)
let openSearch: () -> Void = {
requestActivateSearch()
}
let revealedCode = Promise<String?>(nil)
var revealedCodeValue: String?
let setItemWithRevealedOptions: (String?, String?) -> Void = { id, fromId in
if (id == nil && fromId == revealedCodeValue) || (id != nil && fromId == nil) {
revealedCodeValue = id
revealedCode.set(.single(id))
}
}
let removeItem: (String) -> Void = { id in
let _ = context.engine.localization.removeSavedLocalization(languageCode: id).start()
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.LocalizationList())
|> mapToSignal { state -> Signal<LocalizationInfo?, NoError> in
return context.sharedContext.accountManager.transaction { transaction -> LocalizationInfo? in
if let settings = transaction.getSharedData(SharedDataKeys.localizationSettings)?.get(LocalizationSettings.self) {
if settings.primaryComponent.languageCode == id {
for item in state.availableOfficialLocalizations {
if item.languageCode == "en" {
return item
}
}
}
}
return nil
}
}
|> deliverOnMainQueue).start(next: { [weak self] info in
if revealedCodeValue == id {
revealedCodeValue = nil
revealedCode.set(.single(nil))
}
if let info = info {
self?.selectLocalization(info)
}
})
}
let translationConfiguration = TranslationConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
var translateButtonAvailable = false
var chatTranslationAvailable = false
switch translationConfiguration.manual {
case .enabled, .alternative:
translateButtonAvailable = true
case .system:
if #available(iOS 18.0, *) {
translateButtonAvailable = true
}
default:
break
}
switch translationConfiguration.auto {
case .enabled:
chatTranslationAvailable = true
case .system:
if #available(iOS 18.0, *) {
chatTranslationAvailable = true
}
default:
break
}
let previousState = Atomic<LocalizationListState?>(value: nil)
let previousEntriesHolder = Atomic<([LanguageListEntry], PresentationTheme, PresentationStrings)?>(value: nil)
self.listDisposable = combineLatest(
queue: .mainQueue(),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.LocalizationList()),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)),
context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.localizationSettings, ApplicationSpecificSharedDataKeys.translationSettings]),
self.presentationDataValue.get(),
self.applyingCode.get(),
revealedCode.get(),
self.isEditing.get()
).start(next: { [weak self] localizationListState, peer, sharedData, presentationData, applyingCode, revealedCode, isEditing in
guard let strongSelf = self else {
return
}
let isPremium = peer?.isPremium ?? false
var entries: [LanguageListEntry] = []
var activeLanguageCode: String?
if let localizationSettings = sharedData.entries[SharedDataKeys.localizationSettings]?.get(LocalizationSettings.self) {
activeLanguageCode = localizationSettings.primaryComponent.languageCode
}
var existingIds = Set<String>()
var showTranslate = false
var translateChats = false
var ignoredLanguages: [String] = []
if let translationSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) {
showTranslate = translationSettings.showTranslate
translateChats = isPremium ? translationSettings.translateChats : false
if let languages = translationSettings.ignoredLanguages {
ignoredLanguages = languages
} else {
if var activeLanguage = activeLanguageCode {
let rawSuffix = "-raw"
if activeLanguage.hasSuffix(rawSuffix) {
activeLanguage = String(activeLanguage.dropLast(rawSuffix.count))
}
if supportedTranslationLanguages.contains(activeLanguage) {
ignoredLanguages = [activeLanguage]
}
}
let systemLanguages = systemLanguageCodes()
for systemLanguage in systemLanguages {
if !ignoredLanguages.contains(systemLanguage) {
ignoredLanguages.append(systemLanguage)
}
}
}
} else {
translateChats = isPremium
if let activeLanguage = activeLanguageCode, supportedTranslationLanguages.contains(activeLanguage) {
ignoredLanguages = [activeLanguage]
}
for systemLanguageCode in systemLanguageCodes() {
if !ignoredLanguages.contains(systemLanguageCode) {
ignoredLanguages.append(systemLanguageCode)
}
}
}
if !localizationListState.availableOfficialLocalizations.isEmpty {
strongSelf.currentListState = localizationListState
if translateButtonAvailable || chatTranslationAvailable {
entries.append(.translateTitle(text: presentationData.strings.Localization_TranslateMessages.uppercased()))
if translateButtonAvailable {
entries.append(.translate(text: presentationData.strings.Localization_ShowTranslate, value: showTranslate))
}
if chatTranslationAvailable {
entries.append(.translateEntire(text: presentationData.strings.Localization_TranslateEntireChat, value: translateChats, locked: !isPremium))
}
var value = ""
if ignoredLanguages.count > 1 {
var ignoredLanguagesSet = Set<String>()
var filteredIgnoredLanguages: [String] = []
for language in ignoredLanguages {
if ignoredLanguagesSet.contains(language) {
continue
}
ignoredLanguagesSet.insert(language)
filteredIgnoredLanguages.append(language)
}
value = filteredIgnoredLanguages.joined(separator: ", ")
} else if let code = ignoredLanguages.first {
let enLocale = Locale(identifier: "en")
if let title = enLocale.localizedString(forLanguageCode: code) {
value = title
}
}
if showTranslate || translateChats {
entries.append(.doNotTranslate(text: presentationData.strings.Localization_DoNotTranslate, value: value))
}
if showTranslate {
entries.append(.translateInfo(text: ignoredLanguages.count > 1 ? presentationData.strings.Localization_DoNotTranslateManyInfo : presentationData.strings.Localization_DoNotTranslateInfo))
} else {
entries.append(.translateInfo(text: presentationData.strings.Localization_ShowTranslateInfoExtended))
}
}
let availableSavedLocalizations = localizationListState.availableSavedLocalizations.filter({ info in !localizationListState.availableOfficialLocalizations.contains(where: { $0.languageCode == info.languageCode }) })
if availableSavedLocalizations.isEmpty {
updateCanStartEditing(nil)
} else {
updateCanStartEditing(isEditing)
}
if !availableSavedLocalizations.isEmpty {
entries.append(.localizationTitle(text: presentationData.strings.Localization_InterfaceLanguage.uppercased(), section: LanguageListSection.unofficial.rawValue))
for info in availableSavedLocalizations {
if existingIds.contains(info.languageCode) {
continue
}
existingIds.insert(info.languageCode)
entries.append(.localization(index: entries.count, info: info, type: .unofficial, selected: info.languageCode == activeLanguageCode, activity: applyingCode == info.languageCode, revealed: revealedCode == info.languageCode, editing: isEditing))
}
} else {
entries.append(.localizationTitle(text: presentationData.strings.Localization_InterfaceLanguage.uppercased(), section: LanguageListSection.official.rawValue))
}
for info in localizationListState.availableOfficialLocalizations {
if existingIds.contains(info.languageCode) {
continue
}
existingIds.insert(info.languageCode)
entries.append(.localization(index: entries.count, info: info, type: .official, selected: info.languageCode == activeLanguageCode, activity: applyingCode == info.languageCode, revealed: revealedCode == info.languageCode, editing: false))
}
} else {
for _ in 0 ..< 15 {
entries.append(.localization(index: entries.count, info: nil, type: .official, selected: false, activity: false, revealed: false, editing: false))
}
}
let previousState = previousState.swap(localizationListState)
let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings))
let transition = preparedLanguageListNodeTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, openSearch: openSearch, toggleShowTranslate: { value in
let _ = updateTranslationSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
var updated = current.withUpdatedShowTranslate(value)
if !updated.showTranslate && !updated.translateChats {
updated = updated.withUpdatedIgnoredLanguages(nil)
}
return updated
}).start()
}, toggleTranslateChats: { value in
let _ = updateTranslationSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
var updated = current.withUpdatedTranslateChats(value)
if !updated.showTranslate && !updated.translateChats {
updated = updated.withUpdatedIgnoredLanguages(nil)
}
return updated
}).start()
}, openDoNotTranslate: { [weak self] in
if let strongSelf = self {
strongSelf.push(translationSettingsController(context: strongSelf.context))
}
}, selectLocalization: { [weak self] info in self?.selectLocalization(info) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, showPremiumInfo: {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumDemoScreen(context: context, subject: .translation, action: {
let controller = PremiumIntroScreen(context: context, source: .translation)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
strongSelf.push(controller)
}, firstTime: previousEntriesAndPresentationData == nil, isLoading: entries.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: (previousEntriesAndPresentationData?.0.count ?? 0) != entries.count, crossfade: (previousState == nil || previousState!.availableOfficialLocalizations.isEmpty) != localizationListState.availableOfficialLocalizations.isEmpty)
strongSelf.enqueueTransition(transition)
})
self.updatedDisposable = context.engine.localization.synchronizedLocalizationListState().start()
self.listNode.itemNodeHitTest = { [weak self] point in
if let strongSelf = self {
return point.x > strongSelf.leftOverlayNode.frame.maxX && point.x < strongSelf.rightOverlayNode.frame.minX
} else {
return true
}
}
}
deinit {
self.listDisposable?.dispose()
self.updatedDisposable?.dispose()
self.applyDisposable.dispose()
}
func updatePresentationData(_ presentationData: PresentationData) {
let stringsUpdated = self.presentationData.strings !== presentationData.strings
self.presentationData = presentationData
if stringsUpdated {
if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) {
self.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
}
self.presentationDataValue.set(.single(presentationData))
self.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.list.blocksBackgroundColor, direction: true)
self.searchDisplayController?.updatePresentationData(presentationData)
self.leftOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.rightOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let hadValidLayout = self.containerLayout != nil
self.containerLayout = (layout, navigationBarHeight)
var listInsets = layout.insets(options: [.input])
listInsets.top += navigationBarHeight
if layout.size.width >= 375.0 {
let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
listInsets.left += inset
listInsets.right += inset
} else {
listInsets.left += layout.safeInsets.left
listInsets.right += layout.safeInsets.right
}
self.leftOverlayNode.frame = CGRect(x: 0.0, y: 0.0, width: listInsets.left, height: layout.size.height)
self.rightOverlayNode.frame = CGRect(x: layout.size.width - listInsets.right, y: 0.0, width: listInsets.right, height: layout.size.height)
if self.leftOverlayNode.supernode == nil {
self.insertSubnode(self.leftOverlayNode, aboveSubnode: self.listNode)
}
if self.rightOverlayNode.supernode == nil {
self.insertSubnode(self.rightOverlayNode, aboveSubnode: self.listNode)
}
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: listInsets, duration: duration, curve: curve)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if !hadValidLayout {
self.dequeueTransitions()
}
}
private func enqueueTransition(_ transition: LanguageListNodeTransition) {
self.queuedTransitions.append(transition)
if self.containerLayout != nil {
self.dequeueTransitions()
}
}
private func dequeueTransitions() {
guard let _ = self.containerLayout else {
return
}
while !self.queuedTransitions.isEmpty {
let transition = self.queuedTransitions.removeFirst()
var options = ListViewDeleteAndInsertOptions()
if transition.firstTime {
options.insert(.Synchronous)
options.insert(.LowLatency)
} else if transition.crossfade {
options.insert(.AnimateCrossfade)
} else if transition.animated {
options.insert(.AnimateInsertion)
}
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in
if let strongSelf = self {
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(true)
}
}
})
}
}
private func selectLocalization(_ info: LocalizationInfo) -> Void {
let applyImpl: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.applyingCode.set(.single(info.languageCode))
strongSelf.applyDisposable.set((strongSelf.context.engine.localization.downloadAndApplyLocalization(accountManager: strongSelf.context.sharedContext.accountManager, languageCode: info.languageCode)
|> deliverOnMainQueue).start(completed: { [weak self] in
self?.applyingCode.set(.single(nil))
self?.context.engine.messages.refreshAttachMenuBots()
}))
}
if info.isOfficial {
applyImpl()
return
}
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: info.localizedTitle))
if self.presentationData.strings.primaryComponent.languageCode != info.languageCode {
items.append(ActionSheetButtonItem(title: presentationData.strings.ApplyLanguage_ChangeLanguageAction, action: {
dismissAction()
applyImpl()
}))
}
items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_ContextMenuShare, action: { [weak self] in
dismissAction()
guard let strongSelf = self else {
return
}
let shareController = ShareController(context: strongSelf.context, subject: .url("https://t.me/setlanguage/\(info.languageCode)"))
shareController.actionCompleted = { [weak self] in
if let strongSelf = self {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}
}
strongSelf.present(shareController, nil)
}))
controller.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
self.view.window?.endEditing(true)
self.present(controller, nil)
}
func toggleEditing() {
self.isEditingValue = !self.isEditingValue
}
func activateSearch(placeholderNode: SearchBarPlaceholderNode) {
guard let (containerLayout, navigationBarHeight) = self.containerLayout, self.searchDisplayController == nil else {
return
}
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: LocalizationListSearchContainerNode(context: self.context, listState: self.currentListState ?? LocalizationListState.defaultSettings, selectLocalization: { [weak self] info in self?.selectLocalization(info) }, applyingCode: self.applyingCode.get()), inline: true, cancel: { [weak self] in
self?.requestDeactivateSearch()
})
self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate)
self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in
if let strongSelf = self, let strongPlaceholderNode = placeholderNode {
if isSearchBar {
strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode)
} else if let navigationBar = strongSelf.navigationBar {
strongSelf.insertSubnode(subnode, belowSubnode: navigationBar)
}
}
}, placeholder: placeholderNode)
}
func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) {
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.deactivate(placeholder: placeholderNode)
self.searchDisplayController = nil
}
}
func scrollToTop() {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
}
@@ -0,0 +1,218 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import TelegramStringFormatting
import AccountContext
import TranslateUI
private final class TranslationSettingsControllerArguments {
let context: AccountContext
let updateLanguageSelected: (String, Bool) -> Void
init(context: AccountContext, updateLanguageSelected: @escaping (String, Bool) -> Void) {
self.context = context
self.updateLanguageSelected = updateLanguageSelected
}
}
private enum TranslationSettingsControllerSection: Int32 {
case languages
}
private enum TranslationSettingsControllerEntry: ItemListNodeEntry {
case language(Int32, PresentationTheme, String, String, Bool, String)
var section: ItemListSectionId {
switch self {
case .language:
return TranslationSettingsControllerSection.languages.rawValue
}
}
var stableId: Int32 {
switch self {
case let .language(index, _, _, _, _, _):
return index
}
}
static func ==(lhs: TranslationSettingsControllerEntry, rhs: TranslationSettingsControllerEntry) -> Bool {
switch lhs {
case let .language(lhsIndex, lhsTheme, lhsTitle, lhsSubtitle, lhsValue, lhsCode):
if case let .language(rhsIndex, rhsTheme, rhsTitle, rhsSubtitle, rhsValue, rhsCode) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsValue == rhsValue, lhsCode == rhsCode {
return true
} else {
return false
}
}
}
static func <(lhs: TranslationSettingsControllerEntry, rhs: TranslationSettingsControllerEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! TranslationSettingsControllerArguments
switch self {
case let .language(_, _, title, subtitle, value, code):
return LocalizationListItem(presentationData: presentationData, systemStyle: .glass, id: code, title: title, subtitle: subtitle, checked: value, activity: false, loading: false, editing: LocalizationListItemEditing(editable: false, editing: false, revealed: false, reorderable: false), sectionId: self.section, alwaysPlain: false, action: {
arguments.updateLanguageSelected(code, !value)
}, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in })
}
}
}
private func translationSettingsControllerEntries(theme: PresentationTheme, strings: PresentationStrings, initiallySelectedLanguages: Set<String>, settings: TranslationSettings, languages: [(String, String, String)]) -> [TranslationSettingsControllerEntry] {
var entries: [TranslationSettingsControllerEntry] = []
var index: Int32 = 0
var selectedLanguages: Set<String>
if let ignoredLanguages = settings.ignoredLanguages {
selectedLanguages = Set(ignoredLanguages)
} else {
var activeLanguage = strings.baseLanguageCode
let rawSuffix = "-raw"
if activeLanguage.hasSuffix(rawSuffix) {
activeLanguage = String(activeLanguage.dropLast(rawSuffix.count))
}
activeLanguage = normalizeTranslationLanguage(activeLanguage)
selectedLanguages = Set([activeLanguage])
for language in systemLanguageCodes() {
selectedLanguages.insert(language)
}
}
var addedLanguages = Set<String>()
for (code, title, subtitle) in languages {
if !addedLanguages.contains(code), initiallySelectedLanguages.contains(code) {
addedLanguages.insert(code)
entries.append(.language(index, theme, title, subtitle, selectedLanguages.contains(code), code))
index += 1
}
}
for (code, title, subtitle) in languages {
if !addedLanguages.contains(code) {
addedLanguages.insert(code)
entries.append(.language(index, theme, title, subtitle, selectedLanguages.contains(code), code))
index += 1
}
}
return entries
}
public func translationSettingsController(context: AccountContext) -> ViewController {
let actionsDisposable = DisposableSet()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var interfaceLanguageCode = presentationData.strings.baseLanguageCode
let rawSuffix = "-raw"
if interfaceLanguageCode.hasSuffix(rawSuffix) {
interfaceLanguageCode = String(interfaceLanguageCode.dropLast(rawSuffix.count))
}
let arguments = TranslationSettingsControllerArguments(context: context, updateLanguageSelected: { code, value in
let _ = updateTranslationSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
var updated = current
var updatedIgnoredLanguages = updated.ignoredLanguages ?? []
if current.ignoredLanguages == nil {
updatedIgnoredLanguages.append(interfaceLanguageCode)
for language in systemLanguageCodes() {
if !updatedIgnoredLanguages.contains(language) {
updatedIgnoredLanguages.append(language)
}
}
}
if value {
if !updatedIgnoredLanguages.contains(code) {
updatedIgnoredLanguages.append(code)
}
} else {
updatedIgnoredLanguages.removeAll(where: { $0 == code })
}
updated = updated.withUpdatedIgnoredLanguages(updatedIgnoredLanguages)
return updated
}).start()
})
let enLocale = Locale(identifier: "en")
var languages: [(String, String, String)] = []
var addedLanguages = Set<String>()
for code in popularTranslationLanguages {
if let title = enLocale.localizedString(forLanguageCode: code) {
let languageLocale = Locale(identifier: code)
let subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title
let value = (code, title.capitalized, subtitle.capitalized)
if code == interfaceLanguageCode {
languages.insert(value, at: 0)
} else {
languages.append(value)
}
addedLanguages.insert(code)
}
}
for code in supportedTranslationLanguages {
if !addedLanguages.contains(code), let title = enLocale.localizedString(forLanguageCode: code) {
let languageLocale = Locale(identifier: code)
let subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title
let value = (code, title.capitalized, subtitle.capitalized)
if code == interfaceLanguageCode {
languages.insert(value, at: 0)
} else {
languages.append(value)
}
}
}
let initiallySelectedLanguages = Atomic<Set<String>?>(value: nil)
let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings])
let signal = combineLatest(queue: Queue.mainQueue(), context.sharedContext.presentationData, sharedData)
|> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) ?? TranslationSettings.defaultSettings
let initiallySelectedLanguages = initiallySelectedLanguages.modify({ current in
if let current {
return current
} else {
var selectedLanguages: Set<String>
if let ignoredLanguages = settings.ignoredLanguages {
selectedLanguages = Set(ignoredLanguages)
} else {
var activeLanguage = presentationData.strings.baseLanguageCode
let rawSuffix = "-raw"
if activeLanguage.hasSuffix(rawSuffix) {
activeLanguage = String(activeLanguage.dropLast(rawSuffix.count))
}
selectedLanguages = Set([activeLanguage])
for language in systemLanguageCodes() {
selectedLanguages.insert(language)
}
}
return selectedLanguages
}
})
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.DoNotTranslate_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: translationSettingsControllerEntries(theme: presentationData.theme, strings: presentationData.strings, initiallySelectedLanguages: initiallySelectedLanguages ?? Set(), settings: settings, languages: languages), style: .blocks, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
controller.alwaysSynchronous = true
return controller
}
@@ -0,0 +1,310 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import LegacyComponents
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import AccountContext
import AlertUI
import PresentationDataUtils
import UrlHandling
import AccountUtils
import PremiumUI
import StorageUsageScreen
private struct LogoutOptionsItemArguments {
let addAccount: () -> Void
let setPasscode: () -> Void
let clearCache: () -> Void
let changePhoneNumber: () -> Void
let contactSupport: () -> Void
let logout: () -> Void
}
private enum LogoutOptionsSection: Int32 {
case options
case logOut
}
private enum LogoutOptionsEntry: ItemListNodeEntry, Equatable {
case alternativeHeader(PresentationTheme, String)
case addAccount(PresentationTheme, String, String)
case setPasscode(PresentationTheme, String, String)
case clearCache(PresentationTheme, String, String)
case changePhoneNumber(PresentationTheme, String, String)
case contactSupport(PresentationTheme, String, String)
case logout(PresentationTheme, String)
case logoutInfo(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .alternativeHeader, .addAccount, .setPasscode, .clearCache, .changePhoneNumber, .contactSupport:
return LogoutOptionsSection.options.rawValue
case .logout, .logoutInfo:
return LogoutOptionsSection.logOut.rawValue
}
}
var stableId: Int32 {
switch self {
case .alternativeHeader:
return 0
case .addAccount:
return 1
case .setPasscode:
return 2
case .clearCache:
return 3
case .changePhoneNumber:
return 4
case .contactSupport:
return 5
case .logout:
return 6
case .logoutInfo:
return 7
}
}
static func <(lhs: LogoutOptionsEntry, rhs: LogoutOptionsEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! LogoutOptionsItemArguments
switch self {
case let .alternativeHeader(_, title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .addAccount(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.addAccount, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.addAccount()
})
case let .setPasscode(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.setPasscode, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.setPasscode()
})
case let .clearCache(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.clearCache, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.clearCache()
})
case let .changePhoneNumber(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.changePhoneNumber, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.changePhoneNumber()
})
case let .contactSupport(_, title, text):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.support, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.contactSupport()
})
case let .logout(_, title):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.logout()
})
case let .logoutInfo(_, title):
return ItemListTextItem(presentationData: presentationData, text: .plain(title), sectionId: self.section)
}
}
}
private func logoutOptionsEntries(presentationData: PresentationData, canAddAccounts: Bool, hasPasscode: Bool) -> [LogoutOptionsEntry] {
var entries: [LogoutOptionsEntry] = []
entries.append(.alternativeHeader(presentationData.theme, presentationData.strings.LogoutOptions_AlternativeOptionsSection))
if canAddAccounts {
entries.append(.addAccount(presentationData.theme, presentationData.strings.LogoutOptions_AddAccountTitle, presentationData.strings.LogoutOptions_AddAccountText))
}
if !hasPasscode {
entries.append(.setPasscode(presentationData.theme, presentationData.strings.LogoutOptions_SetPasscodeTitle, presentationData.strings.LogoutOptions_SetPasscodeText))
}
entries.append(.clearCache(presentationData.theme, presentationData.strings.LogoutOptions_ClearCacheTitle, presentationData.strings.LogoutOptions_ClearCacheText))
entries.append(.changePhoneNumber(presentationData.theme, presentationData.strings.LogoutOptions_ChangePhoneNumberTitle, presentationData.strings.LogoutOptions_ChangePhoneNumberText))
entries.append(.contactSupport(presentationData.theme, presentationData.strings.LogoutOptions_ContactSupportTitle, presentationData.strings.LogoutOptions_ContactSupportText))
entries.append(.logout(presentationData.theme, presentationData.strings.LogoutOptions_LogOut))
entries.append(.logoutInfo(presentationData.theme, presentationData.strings.LogoutOptions_LogOutInfo))
return entries
}
public func logoutOptionsController(context: AccountContext, navigationController: NavigationController, canAddAccounts: Bool, phoneNumber: String) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var replaceTopControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
let supportPeerDisposable = MetaDisposable()
let arguments = LogoutOptionsItemArguments(addAccount: {
let _ = (activeAccountsAndPeers(context: context)
|> take(1)
|> deliverOnMainQueue
).start(next: { accountAndPeer, accountsAndPeers in
var maximumAvailableAccounts: Int = 3
if accountAndPeer?.1.isPremium == true && !context.account.testingEnvironment {
maximumAvailableAccounts = 4
}
var count: Int = 1
for (accountContext, peer, _) in accountsAndPeers {
if !accountContext.account.testingEnvironment {
if peer.isPremium {
maximumAvailableAccounts = 4
}
count += 1
}
}
if count >= maximumAvailableAccounts {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .accounts, count: Int32(count), action: {
let controller = PremiumIntroScreen(context: context, source: .accounts)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
} else {
context.sharedContext.beginNewAuth(testingEnvironment: context.account.testingEnvironment)
dismissImpl?()
}
})
}, setPasscode: {
let _ = passcodeOptionsAccessController(context: context, pushController: { controller in
replaceTopControllerImpl?(controller)
}, completion: { _ in
replaceTopControllerImpl?(passcodeOptionsController(context: context))
}).start(next: { controller in
if let controller = controller {
pushControllerImpl?(controller)
}
})
dismissImpl?()
}, clearCache: {
pushControllerImpl?(StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in
return storageUsageExceptionsScreen(context: context, category: category)
}))
dismissImpl?()
}, changePhoneNumber: {
let introController = PrivacyIntroController(context: context, mode: .changePhoneNumber(phoneNumber), proceedAction: {
replaceTopControllerImpl?(ChangePhoneNumberController(context: context))
})
pushControllerImpl?(introController)
dismissImpl?()
}, contactSupport: { [weak navigationController] in
let supportPeer = Promise<EnginePeer.Id?>()
supportPeer.set(context.engine.peers.supportPeerId())
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var faqUrl = presentationData.strings.Settings_FAQ_URL
if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty {
faqUrl = "https://telegram.org/faq#general"
}
let resolvedUrl = resolveInstantViewUrl(account: context.account, url: faqUrl)
|> mapToSignal { result -> Signal<ResolvedUrl, NoError> in
guard case let .result(result) = result else {
return .complete()
}
return .single(result)
}
let resolvedUrlPromise = Promise<ResolvedUrl>()
resolvedUrlPromise.set(resolvedUrl)
let openFaq: (Promise<ResolvedUrl>) -> Void = { resolvedUrl in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
presentControllerImpl?(controller, nil)
let _ = (resolvedUrl.get()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] resolvedUrl in
controller?.dismiss()
dismissImpl?()
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in
}, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in
pushControllerImpl?(controller)
}, dismissInput: {}, contentContext: nil, progress: nil, completion: nil)
})
}
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Settings_FAQ_Intro, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_FAQ_Button, action: {
openFaq(resolvedUrlPromise)
dismissImpl?()
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
supportPeerDisposable.set((supportPeer.get()
|> take(1)
|> deliverOnMainQueue).start(next: { peerId in
guard let peerId = peerId else {
return
}
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer else {
return
}
if let navigationController = navigationController {
dismissImpl?()
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer)))
}
})
}))
})
]), nil)
}, logout: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let alertController = textAlertController(context: context, title: presentationData.strings.Settings_LogoutConfirmationTitle, text: presentationData.strings.Settings_LogoutConfirmationText, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
let _ = logoutFromAccount(id: context.account.id, accountManager: context.sharedContext.accountManager, alreadyLoggedOutRemotely: false).start()
dismissImpl?()
})
])
presentControllerImpl?(alertController, nil)
})
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
context.sharedContext.accountManager.accessChallengeData()
)
|> map { presentationData, accessChallengeData -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
var hasPasscode = false
switch accessChallengeData.data {
case .numericalPassword, .plaintextPassword:
hasPasscode = true
default:
break
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.LogoutOptions_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: logoutOptionsEntries(presentationData: presentationData, canAddAccounts: canAddAccounts, hasPasscode: hasPasscode), style: .blocks)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal, tabBarItem: nil)
controller.navigationPresentation = .modal
pushControllerImpl = { [weak navigationController] value in
navigationController?.pushViewController(value, animated: false)
}
presentControllerImpl = { [weak controller] value, arguments in
controller?.present(value, in: .window(.root), with: arguments ?? ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
replaceTopControllerImpl = { [weak navigationController] c in
navigationController?.replaceTopController(c, animated: true)
}
dismissImpl = { [weak controller] in
let _ = controller?.dismiss()
}
return controller
}
@@ -0,0 +1,191 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import AccountContext
import SearchUI
import NotificationPeerExceptionController
public class NotificationExceptionsController: ViewController {
private let context: AccountContext
private var controllerNode: NotificationExceptionsControllerNode {
return self.displayNode as! NotificationExceptionsControllerNode
}
private var _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var removeAllItem: UIBarButtonItem!
private var editItem: UIBarButtonItem!
private var doneItem: UIBarButtonItem!
private let mode: NotificationExceptionMode
private let updatedMode: (NotificationExceptionMode) -> Void
private var searchContentNode: NavigationBarSearchContentNode?
public init(context: AccountContext, mode: NotificationExceptionMode, updatedMode: @escaping(NotificationExceptionMode) -> Void) {
self.context = context
self.mode = mode
self.updatedMode = updatedMode
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
self.removeAllItem = UIBarButtonItem(title: self.presentationData.strings.Notification_Exceptions_DeleteAll, style: .plain, target: self, action: #selector(self.removeAllPressed))
self.editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.editPressed))
self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.title = self.presentationData.strings.Notifications_ExceptionsTitle
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.scrollToTop = { [weak self] in
if let strongSelf = self {
if let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateExpansionProgress(1.0, animated: true)
}
strongSelf.controllerNode.scrollToTop()
}
}
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings()
}
}
})
self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, activate: { [weak self] in
self?.activateSearch()
})
self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
private func updateThemeAndStrings() {
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search)
self.title = self.presentationData.strings.Notifications_ExceptionsTitle
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.controllerNode.updatePresentationData(self.presentationData)
let removeAllItem = UIBarButtonItem(title: self.presentationData.strings.Notification_Exceptions_DeleteAll, style: .plain, target: self, action: #selector(self.removeAllPressed))
let editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.editPressed))
let doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
if self.navigationItem.rightBarButtonItem === self.editItem {
self.navigationItem.rightBarButtonItem = editItem
} else if self.navigationItem.rightBarButtonItem === self.doneItem {
self.navigationItem.rightBarButtonItem = doneItem
self.navigationItem.leftBarButtonItem = removeAllItem
}
self.removeAllItem = removeAllItem
self.editItem = editItem
self.doneItem = doneItem
}
override public func loadDisplayNode() {
self.displayNode = NotificationExceptionsControllerNode(context: self.context, presentationData: self.presentationData, navigationBar: self.navigationBar!, mode: self.mode, updatedMode: self.updatedMode, requestActivateSearch: { [weak self] in
self?.activateSearch()
}, requestDeactivateSearch: { [weak self] animated in
self?.deactivateSearch(animated: animated)
}, updateCanStartEditing: { [weak self] value in
guard let strongSelf = self else {
return
}
var leftItem: UIBarButtonItem?
var item: UIBarButtonItem?
if let value = value {
item = value ? strongSelf.editItem : strongSelf.doneItem
leftItem = value ? strongSelf.removeAllItem : nil
}
if strongSelf.navigationItem.leftBarButtonItem !== leftItem {
strongSelf.navigationItem.setLeftBarButton(leftItem, animated: true)
}
if strongSelf.navigationItem.rightBarButtonItem !== item {
strongSelf.navigationItem.setRightBarButton(item, animated: true)
}
}, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}, pushController: { [weak self] c in
(self?.navigationController as? NavigationController)?.pushViewController(c)
})
self.controllerNode.listNode.visibleContentOffsetChanged = { [weak self] offset in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateListVisibleContentOffset(offset)
}
}
self.controllerNode.listNode.didEndScrolling = { [weak self] _ in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
let _ = fixNavigationSearchableListNodeScrolling(strongSelf.controllerNode.listNode, searchNode: searchContentNode)
}
}
self._ready.set(self.controllerNode._ready.get())
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, actualNavigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@objc private func removeAllPressed() {
self.controllerNode.removeAll()
}
@objc private func editPressed() {
self.controllerNode.toggleEditing()
}
private func activateSearch() {
if self.displayNavigationBar {
if let scrollToTop = self.scrollToTop {
scrollToTop()
}
if let searchContentNode = self.searchContentNode {
self.controllerNode.activateSearch(placeholderNode: searchContentNode.placeholderNode)
}
self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring))
}
}
private func deactivateSearch(animated: Bool) {
if !self.displayNavigationBar {
self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring))
if let searchContentNode = self.searchContentNode {
self.controllerNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode, animated: animated)
}
}
}
}
@@ -0,0 +1,139 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import SearchBarNode
private let searchBarFont = Font.regular(14.0)
class NotificationSearchItem: ListViewItem, ItemListItem {
let selectable: Bool = false
var sectionId: ItemListSectionId {
return 0
}
var tag: ItemListItemTag? {
return nil
}
var requestsNoInset: Bool {
return true
}
let theme: PresentationTheme
private let placeholder: String
private let activate: () -> Void
init(theme: PresentationTheme, placeholder: String, activate: @escaping () -> Void) {
self.theme = theme
self.placeholder = placeholder
self.activate = activate
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = NotificationSearchItemNode()
node.placeholder = self.placeholder
let makeLayout = node.asyncLayout()
let (layout, apply) = makeLayout(self, params)
node.contentSize = layout.contentSize
node.insets = layout.insets
node.activate = self.activate
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
apply(false)
})
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? NotificationSearchItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply(animation.isAnimated)
})
}
}
}
}
}
}
class NotificationSearchItemNode: ListViewItemNode {
let searchBarNode: SearchBarPlaceholderNode
var placeholder: String?
fileprivate var activate: (() -> Void)? {
didSet {
self.searchBarNode.activate = self.activate
}
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
required init() {
self.searchBarNode = SearchBarPlaceholderNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.searchBarNode)
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let makeLayout = self.asyncLayout()
let (layout, apply) = makeLayout(item as! NotificationSearchItem, params)
apply(false)
self.contentSize = layout.contentSize
self.insets = layout.insets
}
func asyncLayout() -> (_ item: NotificationSearchItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let searchBarNodeLayout = self.searchBarNode.asyncLayout()
let placeholder = self.placeholder
return { item, params in
let baseWidth = params.width - params.leftInset - params.rightInset
let backgroundColor = item.theme.chatList.itemBackgroundColor
let placeholderString = NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: UIColor(rgb: 0x8e8e93))
let (_, searchBarApply) = searchBarNodeLayout(placeholderString, placeholderString, CGSize(width: baseWidth - 16.0, height: 28.0), 1.0, UIColor(rgb: 0x8e8e93), item.theme.chatList.regularSearchBarColor, backgroundColor, .immediate)
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 44.0), insets: UIEdgeInsets())
return (layout, { [weak self] animated in
if let strongSelf = self {
let transition: ContainedViewLayoutTransition
if animated {
transition = .animated(duration: 0.3, curve: .easeInOut)
} else {
transition = .immediate
}
strongSelf.searchBarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 8.0, y: 8.0), size: CGSize(width: baseWidth - 16.0, height: 28.0))
searchBarApply()
strongSelf.searchBarNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: baseWidth - 16.0, height: 28.0))
transition.updateBackgroundColor(node: strongSelf, color: backgroundColor)
}
})
}
}
}
@@ -0,0 +1,901 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import DeviceAccess
import ItemListUI
import PresentationDataUtils
import AccountContext
import AlertUI
import PresentationDataUtils
import TelegramNotices
import NotificationSoundSelectionUI
import TelegramStringFormatting
import NotificationPeerExceptionController
private struct CounterTagSettings: OptionSet {
var rawValue: Int32
init(rawValue: Int32) {
self.rawValue = rawValue
}
init(summaryTags: PeerSummaryCounterTags) {
var result = CounterTagSettings()
if summaryTags.contains(.contact) {
result.insert(.regularChatsAndGroups)
}
if summaryTags.contains(.channel) {
result.insert(.channels)
}
self = result
}
func toSumaryTags() -> PeerSummaryCounterTags {
var result = PeerSummaryCounterTags()
if self.contains(.regularChatsAndGroups) {
result.insert(.contact)
result.insert(.nonContact)
result.insert(.bot)
result.insert(.group)
}
if self.contains(.channels) {
result.insert(.channel)
}
return result
}
static let regularChatsAndGroups = CounterTagSettings(rawValue: 1 << 0)
static let channels = CounterTagSettings(rawValue: 1 << 1)
}
private final class NotificationsAndSoundsArguments {
let context: AccountContext
let presentController: (ViewController, ViewControllerPresentationArguments?) -> Void
let pushController: (ViewController) -> Void
let soundSelectionDisposable: MetaDisposable
let authorizeNotifications: () -> Void
let suppressWarning: () -> Void
let openPeerCategory: (NotificationsPeerCategory) -> Void
let openReactions: () -> Void
let updateInAppSounds: (Bool) -> Void
let updateInAppVibration: (Bool) -> Void
let updateInAppPreviews: (Bool) -> Void
let updateDisplayNameOnLockscreen: (Bool) -> Void
let updateIncludeTag: (CounterTagSettings, Bool) -> Void
let updateTotalUnreadCountCategory: (Bool) -> Void
let updateJoinedNotifications: (Bool) -> Void
let resetNotifications: () -> Void
let openAppSettings: () -> Void
let updateNotificationsFromAllAccounts: (Bool) -> Void
init(context: AccountContext, presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping(ViewController)->Void, soundSelectionDisposable: MetaDisposable, authorizeNotifications: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeerCategory: @escaping (NotificationsPeerCategory) -> Void, openReactions: @escaping () -> Void, updateInAppSounds: @escaping (Bool) -> Void, updateInAppVibration: @escaping (Bool) -> Void, updateInAppPreviews: @escaping (Bool) -> Void, updateDisplayNameOnLockscreen: @escaping (Bool) -> Void, updateIncludeTag: @escaping (CounterTagSettings, Bool) -> Void, updateTotalUnreadCountCategory: @escaping (Bool) -> Void, resetNotifications: @escaping () -> Void, openAppSettings: @escaping () -> Void, updateJoinedNotifications: @escaping (Bool) -> Void, updateNotificationsFromAllAccounts: @escaping (Bool) -> Void) {
self.context = context
self.presentController = presentController
self.pushController = pushController
self.soundSelectionDisposable = soundSelectionDisposable
self.authorizeNotifications = authorizeNotifications
self.suppressWarning = suppressWarning
self.openPeerCategory = openPeerCategory
self.openReactions = openReactions
self.updateInAppSounds = updateInAppSounds
self.updateInAppVibration = updateInAppVibration
self.updateInAppPreviews = updateInAppPreviews
self.updateDisplayNameOnLockscreen = updateDisplayNameOnLockscreen
self.updateIncludeTag = updateIncludeTag
self.updateTotalUnreadCountCategory = updateTotalUnreadCountCategory
self.resetNotifications = resetNotifications
self.openAppSettings = openAppSettings
self.updateJoinedNotifications = updateJoinedNotifications
self.updateNotificationsFromAllAccounts = updateNotificationsFromAllAccounts
}
}
private enum NotificationsAndSoundsSection: Int32 {
case accounts
case permission
case categories
case inApp
case displayNamesOnLockscreen
case badge
case joinedNotifications
case reset
}
public enum NotificationsAndSoundsEntryTag: ItemListItemTag {
case allAccounts
case inAppSounds
case inAppVibrate
case inAppPreviews
case displayNamesOnLockscreen
case includeChannels
case unreadCountCategory
case joinedNotifications
case reset
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? NotificationsAndSoundsEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
case accountsHeader(PresentationTheme, String)
case allAccounts(PresentationTheme, String, Bool)
case accountsInfo(PresentationTheme, String)
case permissionInfo(PresentationTheme, String, String, Bool)
case permissionEnable(PresentationTheme, String)
case categoriesHeader(PresentationTheme, String)
case privateChats(PresentationTheme, String, String, String)
case groupChats(PresentationTheme, String, String, String)
case channels(PresentationTheme, String, String, String)
case stories(PresentationTheme, String, String, String)
case reactions(PresentationTheme, String, String, String)
case inAppHeader(PresentationTheme, String)
case inAppSounds(PresentationTheme, String, Bool)
case inAppVibrate(PresentationTheme, String, Bool)
case inAppPreviews(PresentationTheme, String, Bool)
case displayNamesOnLockscreen(PresentationTheme, String, Bool)
case displayNamesOnLockscreenInfo(PresentationTheme, String)
case badgeHeader(PresentationTheme, String)
case includeChannels(PresentationTheme, String, Bool)
case unreadCountCategory(PresentationTheme, String, Bool)
case unreadCountCategoryInfo(PresentationTheme, String)
case joinedNotifications(PresentationTheme, String, Bool)
case joinedNotificationsInfo(PresentationTheme, String)
case reset(PresentationTheme, String)
case resetNotice(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .accountsHeader, .allAccounts, .accountsInfo:
return NotificationsAndSoundsSection.accounts.rawValue
case .permissionInfo, .permissionEnable:
return NotificationsAndSoundsSection.permission.rawValue
case .categoriesHeader, .privateChats, .groupChats, .channels, .stories, .reactions:
return NotificationsAndSoundsSection.categories.rawValue
case .inAppHeader, .inAppSounds, .inAppVibrate, .inAppPreviews:
return NotificationsAndSoundsSection.inApp.rawValue
case .displayNamesOnLockscreen, .displayNamesOnLockscreenInfo:
return NotificationsAndSoundsSection.displayNamesOnLockscreen.rawValue
case .badgeHeader, .includeChannels, .unreadCountCategory, .unreadCountCategoryInfo:
return NotificationsAndSoundsSection.badge.rawValue
case .joinedNotifications, .joinedNotificationsInfo:
return NotificationsAndSoundsSection.joinedNotifications.rawValue
case .reset, .resetNotice:
return NotificationsAndSoundsSection.reset.rawValue
}
}
var stableId: Int32 {
switch self {
case .accountsHeader:
return 0
case .allAccounts:
return 1
case .accountsInfo:
return 2
case .permissionInfo:
return 3
case .permissionEnable:
return 4
case .categoriesHeader:
return 5
case .privateChats:
return 6
case .groupChats:
return 7
case .channels:
return 8
case .stories:
return 9
case .reactions:
return 10
case .inAppHeader:
return 14
case .inAppSounds:
return 15
case .inAppVibrate:
return 16
case .inAppPreviews:
return 17
case .displayNamesOnLockscreen:
return 18
case .displayNamesOnLockscreenInfo:
return 19
case .badgeHeader:
return 20
case .includeChannels:
return 21
case .unreadCountCategory:
return 22
case .unreadCountCategoryInfo:
return 23
case .joinedNotifications:
return 24
case .joinedNotificationsInfo:
return 25
case .reset:
return 26
case .resetNotice:
return 27
}
}
var tag: ItemListItemTag? {
switch self {
case .allAccounts:
return NotificationsAndSoundsEntryTag.allAccounts
case .inAppSounds:
return NotificationsAndSoundsEntryTag.inAppSounds
case .inAppVibrate:
return NotificationsAndSoundsEntryTag.inAppVibrate
case .inAppPreviews:
return NotificationsAndSoundsEntryTag.inAppPreviews
case .displayNamesOnLockscreen:
return NotificationsAndSoundsEntryTag.displayNamesOnLockscreen
case .includeChannels:
return NotificationsAndSoundsEntryTag.includeChannels
case .unreadCountCategory:
return NotificationsAndSoundsEntryTag.unreadCountCategory
case .joinedNotifications:
return NotificationsAndSoundsEntryTag.joinedNotifications
case .reset:
return NotificationsAndSoundsEntryTag.reset
default:
return nil
}
}
static func ==(lhs: NotificationsAndSoundsEntry, rhs: NotificationsAndSoundsEntry) -> Bool {
switch lhs {
case let .accountsHeader(lhsTheme, lhsText):
if case let .accountsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .allAccounts(lhsTheme, lhsText, lhsValue):
if case let .allAccounts(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .accountsInfo(lhsTheme, lhsText):
if case let .accountsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .permissionInfo(lhsTheme, lhsTitle, lhsText, lhsSuppressed):
if case let .permissionInfo(rhsTheme, rhsTitle, rhsText, rhsSuppressed) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText, lhsSuppressed == rhsSuppressed {
return true
} else {
return false
}
case let .permissionEnable(lhsTheme, lhsText):
if case let .permissionEnable(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .categoriesHeader(lhsTheme, lhsText):
if case let .categoriesHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .privateChats(lhsTheme, lhsTitle, lhsSubtitle, lhsLabel):
if case let .privateChats(rhsTheme, rhsTitle, rhsSubtitle, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsLabel == rhsLabel {
return true
} else {
return false
}
case let .groupChats(lhsTheme, lhsTitle, lhsSubtitle, lhsLabel):
if case let .groupChats(rhsTheme, rhsTitle, rhsSubtitle, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsLabel == rhsLabel {
return true
} else {
return false
}
case let .channels(lhsTheme, lhsTitle, lhsSubtitle, lhsLabel):
if case let .channels(rhsTheme, rhsTitle, rhsSubtitle, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsLabel == rhsLabel {
return true
} else {
return false
}
case let .stories(lhsTheme, lhsTitle, lhsSubtitle, lhsLabel):
if case let .stories(rhsTheme, rhsTitle, rhsSubtitle, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsLabel == rhsLabel {
return true
} else {
return false
}
case let .reactions(lhsTheme, lhsTitle, lhsSubtitle, lhsLabel):
if case let .reactions(rhsTheme, rhsTitle, rhsSubtitle, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsLabel == rhsLabel {
return true
} else {
return false
}
case let .inAppHeader(lhsTheme, lhsText):
if case let .inAppHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .inAppSounds(lhsTheme, lhsText, lhsValue):
if case let .inAppSounds(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .inAppVibrate(lhsTheme, lhsText, lhsValue):
if case let .inAppVibrate(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .inAppPreviews(lhsTheme, lhsText, lhsValue):
if case let .inAppPreviews(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .displayNamesOnLockscreen(lhsTheme, lhsText, lhsValue):
if case let .displayNamesOnLockscreen(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .displayNamesOnLockscreenInfo(lhsTheme, lhsText):
if case let .displayNamesOnLockscreenInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .badgeHeader(lhsTheme, lhsText):
if case let .badgeHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .includeChannels(lhsTheme, lhsText, lhsValue):
if case let .includeChannels(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .unreadCountCategory(lhsTheme, lhsText, lhsValue):
if case let .unreadCountCategory(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .unreadCountCategoryInfo(lhsTheme, lhsText):
if case let .unreadCountCategoryInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .joinedNotifications(lhsTheme, lhsText, lhsValue):
if case let .joinedNotifications(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .joinedNotificationsInfo(lhsTheme, lhsText):
if case let .joinedNotificationsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .reset(lhsTheme, lhsText):
if case let .reset(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .resetNotice(lhsTheme, lhsText):
if case let .resetNotice(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: NotificationsAndSoundsEntry, rhs: NotificationsAndSoundsEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! NotificationsAndSoundsArguments
switch self {
case let .accountsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .allAccounts(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateNotificationsFromAllAccounts(updatedValue)
}, tag: self.tag)
case let .accountsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .permissionInfo(_, title, text, suppressed):
return ItemListInfoItem(presentationData: presentationData, systemStyle: .glass, title: title, text: .plain(text), style: .blocks, sectionId: self.section, closeAction: suppressed ? nil : {
arguments.suppressWarning()
})
case let .permissionEnable(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.authorizeNotifications()
})
case let .categoriesHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .privateChats(_, title, subtitle, label):
return NotificationsCategoryItemListItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/EditProfile"), title: title, subtitle: subtitle, label: label, sectionId: self.section, style: .blocks, action: {
arguments.openPeerCategory(.privateChat)
})
case let .groupChats(_, title, subtitle, label):
return NotificationsCategoryItemListItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/GroupChats"), title: title, subtitle: subtitle, label: label, sectionId: self.section, style: .blocks, action: {
arguments.openPeerCategory(.group)
})
case let .channels(_, title, subtitle, label):
return NotificationsCategoryItemListItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Channels"), title: title, subtitle: subtitle, label: label, sectionId: self.section, style: .blocks, action: {
arguments.openPeerCategory(.channel)
})
case let .stories(_, title, subtitle, label):
return NotificationsCategoryItemListItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.stories, title: title, subtitle: subtitle, label: label, sectionId: self.section, style: .blocks, action: {
arguments.openPeerCategory(.stories)
})
case let .reactions(_, title, subtitle, label):
return NotificationsCategoryItemListItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.reactions, title: title, subtitle: subtitle, label: label, sectionId: self.section, style: .blocks, action: {
arguments.openReactions()
})
case let .inAppHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .inAppSounds(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateInAppSounds(updatedValue)
}, tag: self.tag)
case let .inAppVibrate(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateInAppVibration(updatedValue)
}, tag: self.tag)
case let .inAppPreviews(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateInAppPreviews(updatedValue)
}, tag: self.tag)
case let .displayNamesOnLockscreen(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateDisplayNameOnLockscreen(updatedValue)
}, tag: self.tag)
case let .displayNamesOnLockscreenInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text.replacingOccurrences(of: "]", with: "]()")), sectionId: self.section, linkAction: { _ in
arguments.openAppSettings()
})
case let .badgeHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .includeChannels(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateIncludeTag(.channels, updatedValue)
}, tag: self.tag)
case let .unreadCountCategory(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateTotalUnreadCountCategory(updatedValue)
}, tag: self.tag)
case let .unreadCountCategoryInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .joinedNotifications(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateJoinedNotifications(updatedValue)
}, tag: self.tag)
case let .joinedNotificationsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .reset(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.resetNotifications()
}, tag: self.tag)
case let .resetNotice(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
}
}
private func filteredGlobalSound(_ sound: PeerMessageSound) -> PeerMessageSound {
if case .default = sound {
return defaultCloudPeerNotificationSound
} else {
return sound
}
}
private func notificationsAndSoundsEntries(authorizationStatus: AccessType, warningSuppressed: Bool, globalSettings: GlobalNotificationSettingsSet, inAppSettings: InAppNotificationSettings, exceptions: (users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode, stories: NotificationExceptionMode), presentationData: PresentationData, hasMoreThanOneAccount: Bool) -> [NotificationsAndSoundsEntry] {
var entries: [NotificationsAndSoundsEntry] = []
if hasMoreThanOneAccount {
entries.append(.accountsHeader(presentationData.theme, presentationData.strings.NotificationSettings_ShowNotificationsFromAccountsSection))
entries.append(.allAccounts(presentationData.theme, presentationData.strings.NotificationSettings_ShowNotificationsAllAccounts, inAppSettings.displayNotificationsFromAllAccounts))
entries.append(.accountsInfo(presentationData.theme, inAppSettings.displayNotificationsFromAllAccounts ? presentationData.strings.NotificationSettings_ShowNotificationsAllAccountsInfoOn : presentationData.strings.NotificationSettings_ShowNotificationsAllAccountsInfoOff))
}
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
let title: String
let text: String
if case .unreachable = authorizationStatus {
title = presentationData.strings.Notifications_PermissionsUnreachableTitle
text = presentationData.strings.Notifications_PermissionsUnreachableText
} else {
title = presentationData.strings.Notifications_PermissionsTitle
text = presentationData.strings.Notifications_PermissionsText
}
switch (authorizationStatus, warningSuppressed) {
case (.denied, _):
entries.append(.permissionInfo(presentationData.theme, title, text, true))
entries.append(.permissionEnable(presentationData.theme, presentationData.strings.Notifications_PermissionsAllowInSettings))
case (.unreachable, false):
entries.append(.permissionInfo(presentationData.theme, title, text, false))
entries.append(.permissionEnable(presentationData.theme, presentationData.strings.Notifications_PermissionsOpenSettings))
case (.notDetermined, _):
entries.append(.permissionInfo(presentationData.theme, title, text, true))
entries.append(.permissionEnable(presentationData.theme, presentationData.strings.Notifications_PermissionsAllow))
default:
break
}
}
entries.append(.categoriesHeader(presentationData.theme, presentationData.strings.Notifications_MessageNotifications.uppercased()))
entries.append(.privateChats(presentationData.theme, presentationData.strings.Notifications_PrivateChats, !exceptions.users.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.users.peerIds.count)) : "", globalSettings.privateChats.enabled ? presentationData.strings.Notifications_On : presentationData.strings.Notifications_Off))
entries.append(.groupChats(presentationData.theme, presentationData.strings.Notifications_GroupChats, !exceptions.groups.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.groups.peerIds.count)) : "", globalSettings.groupChats.enabled ? presentationData.strings.Notifications_On : presentationData.strings.Notifications_Off))
entries.append(.channels(presentationData.theme, presentationData.strings.Notifications_Channels, !exceptions.channels.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.channels.peerIds.count)) : "", globalSettings.channels.enabled ? presentationData.strings.Notifications_On : presentationData.strings.Notifications_Off))
let storiesValue: String
switch globalSettings.privateChats.storySettings.mute {
case .default:
storiesValue = presentationData.strings.Notifications_TopChats
case .muted:
storiesValue = presentationData.strings.Notifications_Off
case .unmuted:
storiesValue = presentationData.strings.Notifications_On
}
entries.append(.stories(presentationData.theme, presentationData.strings.Notifications_Stories, !exceptions.stories.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.stories.peerIds.count)) : "", storiesValue))
var reactionsValue: String = ""
var hasReactionNotifications = false
switch globalSettings.reactionSettings.messages {
case .nobody:
break
default:
if !reactionsValue.isEmpty {
reactionsValue.append(", ")
}
hasReactionNotifications = true
reactionsValue.append(presentationData.strings.Notifications_Reactions_SubtitleMessages)
}
switch globalSettings.reactionSettings.stories {
case .nobody:
break
default:
if !reactionsValue.isEmpty {
reactionsValue.append(", ")
}
hasReactionNotifications = true
reactionsValue.append(presentationData.strings.Notifications_Reactions_SubtitleStories)
}
entries.append(.reactions(presentationData.theme, presentationData.strings.Notifications_Reactions, reactionsValue, hasReactionNotifications ? presentationData.strings.Notifications_On : presentationData.strings.Notifications_Off))
entries.append(.inAppHeader(presentationData.theme, presentationData.strings.Notifications_InAppNotifications.uppercased()))
entries.append(.inAppSounds(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsSounds, inAppSettings.playSounds))
entries.append(.inAppVibrate(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsVibrate, inAppSettings.vibrate))
entries.append(.inAppPreviews(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsPreview, inAppSettings.displayPreviews))
entries.append(.displayNamesOnLockscreen(presentationData.theme, presentationData.strings.Notifications_DisplayNamesOnLockScreen, inAppSettings.displayNameOnLockscreen))
entries.append(.displayNamesOnLockscreenInfo(presentationData.theme, presentationData.strings.Notifications_DisplayNamesOnLockScreenInfoWithLink))
entries.append(.badgeHeader(presentationData.theme, presentationData.strings.Notifications_Badge.uppercased()))
let counterTagSettings = CounterTagSettings(summaryTags: inAppSettings.totalUnreadCountIncludeTags)
entries.append(.includeChannels(presentationData.theme, presentationData.strings.Notifications_Badge_IncludeChannels, counterTagSettings.contains(.channels)))
entries.append(.unreadCountCategory(presentationData.theme, presentationData.strings.Notifications_Badge_CountUnreadMessages, inAppSettings.totalUnreadCountDisplayCategory == .messages))
entries.append(.unreadCountCategoryInfo(presentationData.theme, inAppSettings.totalUnreadCountDisplayCategory == .chats ? presentationData.strings.Notifications_Badge_CountUnreadMessages_InfoOff : presentationData.strings.Notifications_Badge_CountUnreadMessages_InfoOn))
entries.append(.joinedNotifications(presentationData.theme, presentationData.strings.NotificationSettings_ContactJoined, globalSettings.contactsJoined))
entries.append(.joinedNotificationsInfo(presentationData.theme, presentationData.strings.NotificationSettings_ContactJoinedInfo))
entries.append(.reset(presentationData.theme, presentationData.strings.Notifications_ResetAllNotifications))
entries.append(.resetNotice(presentationData.theme, presentationData.strings.Notifications_ResetAllNotificationsHelp))
return entries
}
public func notificationsAndSoundsController(context: AccountContext, exceptionsList: NotificationExceptionsList?, focusOnItemTag: NotificationsAndSoundsEntryTag? = nil) -> ViewController {
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
let notificationExceptions: Promise<(users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode, stories: NotificationExceptionMode)> = Promise()
let updateNotificationExceptions:((users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode, stories: NotificationExceptionMode)) -> Void = { value in
notificationExceptions.set(.single(value))
}
let arguments = NotificationsAndSoundsArguments(context: context, presentController: { controller, arguments in
presentControllerImpl?(controller, arguments)
}, pushController: { controller in
pushControllerImpl?(controller)
}, soundSelectionDisposable: MetaDisposable(), authorizeNotifications: {
let _ = (DeviceAccess.authorizationStatus(applicationInForeground: context.sharedContext.applicationBindings.applicationInForeground, subject: .notifications)
|> take(1)
|> deliverOnMainQueue).start(next: { status in
switch status {
case .notDetermined:
DeviceAccess.authorizeAccess(to: .notifications, registerForNotifications: { result in
context.sharedContext.applicationBindings.registerForNotifications(result)
})
case .denied, .restricted:
context.sharedContext.applicationBindings.openSettings()
case .unreachable:
ApplicationSpecificNotice.setPermissionWarning(accountManager: context.sharedContext.accountManager, permission: .notifications, value: Int32(Date().timeIntervalSince1970))
context.sharedContext.applicationBindings.openSettings()
default:
break
}
})
}, suppressWarning: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Notifications_PermissionsSuppressWarningTitle, text: presentationData.strings.Notifications_PermissionsSuppressWarningText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Notifications_PermissionsKeepDisabled, action: {
ApplicationSpecificNotice.setPermissionWarning(accountManager: context.sharedContext.accountManager, permission: .notifications, value: Int32(Date().timeIntervalSince1970))
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Notifications_PermissionsEnable, action: {
context.sharedContext.applicationBindings.openSettings()
})]), nil)
}, openPeerCategory: { category in
_ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels, stories) in
let mode: NotificationExceptionMode
switch category {
case .privateChat:
mode = users
case .group:
mode = groups
case .channel:
mode = channels
case .stories:
mode = stories
}
pushControllerImpl?(notificationsPeerCategoryController(context: context, category: category, mode: mode, updatedMode: { mode in
_ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels, stories) in
switch mode {
case .users:
updateNotificationExceptions((mode, groups, channels, stories))
case .groups:
updateNotificationExceptions((users, mode, channels, stories))
case .channels:
updateNotificationExceptions((users, groups, mode, stories))
case .stories:
updateNotificationExceptions((users, groups, channels, mode))
}
})
}, focusOnItemTag: nil))
})
}, openReactions: {
pushControllerImpl?(reactionNotificationSettingsController(
context: context
))
}, updateInAppSounds: { value in
let _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings
settings.playSounds = value
return settings
}).start()
}, updateInAppVibration: { value in
let _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings
settings.vibrate = value
return settings
}).start()
}, updateInAppPreviews: { value in
let _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings
settings.displayPreviews = value
return settings
}).start()
}, updateDisplayNameOnLockscreen: { value in
let _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings
settings.displayNameOnLockscreen = value
return settings
}).start()
}, updateIncludeTag: { tag, value in
let _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var currentSettings = CounterTagSettings(summaryTags: settings.totalUnreadCountIncludeTags)
if !value {
currentSettings.remove(tag)
} else {
currentSettings.insert(tag)
}
var settings = settings
settings.totalUnreadCountIncludeTags = currentSettings.toSumaryTags()
return settings
}).start()
}, updateTotalUnreadCountCategory: { value in
let _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings
settings.totalUnreadCountDisplayCategory = value ? .messages : .chats
return settings
}).start()
}, resetNotifications: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.Notifications_ResetAllNotificationsText),
ActionSheetButtonItem(title: presentationData.strings.Notifications_Reset, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let modifyPeers = context.engine.peers.resetAllPeerNotificationSettings()
let updateGlobal = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { _ in
return GlobalNotificationSettingsSet.defaultSettings
})
let reset = resetPeerNotificationSettings(network: context.account.network)
let signal = combineLatest(modifyPeers, updateGlobal, reset)
let _ = signal.start()
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, nil)
}, openAppSettings: {
context.sharedContext.applicationBindings.openSettings()
}, updateJoinedNotifications: { value in
let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
settings.contactsJoined = value
return settings
}).start()
}, updateNotificationsFromAllAccounts: { value in
let _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings
settings.displayNotificationsFromAllAccounts = value
return settings
}).start()
})
let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings])
let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications])
let exceptionsSignal = Signal<NotificationExceptionsList?, NoError>.single(exceptionsList) |> then(context.engine.peers.notificationExceptionsList() |> map(Optional.init))
let defaultStorySettings = PeerStoryNotificationSettings.default
notificationExceptions.set(exceptionsSignal |> map { list -> (NotificationExceptionMode, NotificationExceptionMode, NotificationExceptionMode, NotificationExceptionMode) in
var users:[PeerId : NotificationExceptionWrapper] = [:]
var groups: [PeerId : NotificationExceptionWrapper] = [:]
var channels: [PeerId : NotificationExceptionWrapper] = [:]
var stories: [PeerId : NotificationExceptionWrapper] = [:]
if let list = list {
for (key, value) in list.settings {
if let peer = list.peers[key], !peer.debugDisplayTitle.isEmpty, peer.id != context.account.peerId {
if value.storySettings != defaultStorySettings {
stories[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer))
}
switch value.muteState {
case .default:
switch value.messageSound {
case .default:
break
default:
switch key.namespace {
case Namespaces.Peer.CloudUser:
users[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer))
default:
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
channels[key] = NotificationExceptionWrapper(settings: value, peer: .channel(peer))
} else {
groups[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer))
}
}
}
default:
switch key.namespace {
case Namespaces.Peer.CloudUser:
users[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer))
default:
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
channels[key] = NotificationExceptionWrapper(settings: value, peer: .channel(peer))
} else {
groups[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer))
}
}
}
}
}
}
return (.users(users), .groups(groups), .channels(channels), .stories(stories))
})
let notificationsWarningSuppressed = Promise<Bool>(true)
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
notificationsWarningSuppressed.set(.single(true)
|> then(
context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.permissionWarningKey(permission: .notifications)!)
|> map { noticeView -> Bool in
let timestamp = noticeView.value.flatMap({ ApplicationSpecificNotice.getTimestampValue($0) })
if let timestamp = timestamp, timestamp > 0 {
return true
} else {
return false
}
}))
}
let hasMoreThanOneAccount = context.sharedContext.activeAccountContexts
|> map { _, contexts, _ -> Bool in
return contexts.count > 1
}
|> distinctUntilChanged
let signal = combineLatest(context.sharedContext.presentationData, sharedData, preferences, notificationExceptions.get(), DeviceAccess.authorizationStatus(applicationInForeground: context.sharedContext.applicationBindings.applicationInForeground, subject: .notifications), notificationsWarningSuppressed.get(), hasMoreThanOneAccount)
|> map { presentationData, sharedData, view, exceptions, authorizationStatus, warningSuppressed, hasMoreThanOneAccount -> (ItemListControllerState, (ItemListNodeState, Any)) in
let viewSettings: GlobalNotificationSettingsSet
if let settings = view.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) {
viewSettings = settings.effective
} else {
viewSettings = GlobalNotificationSettingsSet.defaultSettings
}
let inAppSettings: InAppNotificationSettings
if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings]?.get(InAppNotificationSettings.self) {
inAppSettings = settings
} else {
inAppSettings = InAppNotificationSettings.defaultSettings
}
let entries = notificationsAndSoundsEntries(authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, globalSettings: viewSettings, inAppSettings: inAppSettings, exceptions: exceptions, presentationData: presentationData, hasMoreThanOneAccount: hasMoreThanOneAccount)
var index = 0
var scrollToItem: ListViewScrollToItem?
if let focusOnItemTag = focusOnItemTag {
for entry in entries {
if entry.tag?.isEqual(to: focusOnItemTag) ?? false {
scrollToItem = ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
}
index += 1
}
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Notifications_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, initialScrollToItem: scrollToItem)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
pushControllerImpl = { [weak controller] c in
(controller?.navigationController as? NavigationController)?.pushViewController(c)
}
return controller
}
@@ -0,0 +1,454 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ShimmerEffect
import ItemListUI
public class NotificationsCategoryItemListItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let icon: UIImage?
let title: String
let subtitle: String
let enabled: Bool
let label: String
public let sectionId: ItemListSectionId
let style: ItemListStyle
let action: (() -> Void)?
public let tag: ItemListItemTag?
public let shimmeringIndex: Int?
public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .glass, icon: UIImage? = nil, title: String, subtitle: String, enabled: Bool = true, label: String, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) {
self.presentationData = presentationData
self.systemStyle = systemStyle
self.icon = icon
self.title = title
self.subtitle = subtitle
self.enabled = enabled
self.label = label
self.sectionId = sectionId
self.style = style
self.action = action
self.tag = tag
self.shimmeringIndex = shimmeringIndex
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = NotificationsCategoryItemListItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? NotificationsCategoryItemListItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
public var selectable: Bool = true
public func selected(listView: ListView){
listView.clearHighlightAnimated(true)
if self.enabled {
self.action?()
}
}
}
public class NotificationsCategoryItemListItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
let iconNode: ASImageNode
let titleNode: TextNode
let subtitleNode: TextNode
let labelNode: TextNode
let arrowNode: ASImageNode
private let activateArea: AccessibilityAreaNode
private var item: NotificationsCategoryItemListItem?
override public var canBeSelected: Bool {
if let item = self.item, let _ = item.action {
return true
} else {
return false
}
}
public var tag: ItemListItemTag? {
return self.item?.tag
}
private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.maskNode = ASImageNode()
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.subtitleNode = TextNode()
self.subtitleNode.isUserInteractionEnabled = false
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.arrowNode = ASImageNode()
self.arrowNode.displayWithoutProcessing = true
self.arrowNode.displaysAsynchronously = false
self.arrowNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.activateArea)
}
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
var rect = rect
rect.origin.y += self.insets.top
self.absoluteLocation = (rect, containerSize)
if let shimmerNode = self.placeholderNode {
shimmerNode.updateAbsoluteRect(rect, within: containerSize)
}
}
public func asyncLayout() -> (_ item: NotificationsCategoryItemListItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let currentItem = self.item
return { item, params, neighbors in
let rightInset = 34.0 + params.rightInset
var updateArrowImage: UIImage?
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
}
var updateIcon = false
if currentItem?.icon != item.icon {
updateIcon = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
var leftInset = 16.0 + params.leftInset
if let _ = item.icon {
leftInset += 43.0
}
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let titleColor = item.presentationData.theme.list.itemPrimaryTextColor
let detailFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
let detailColor = item.presentationData.theme.list.itemSecondaryTextColor
let labelFont = titleFont
let labelColor = item.presentationData.theme.list.itemSecondaryTextColor
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: labelColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 60.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let additionalTextRightInset: CGFloat = labelLayout.size.width
let textConstrain = params.width - params.rightInset - 20.0 - leftInset - additionalTextRightInset
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.subtitle, font: detailFont, textColor: detailColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
verticalInset = 15.0
case .legacy:
verticalInset = 11.0
}
let titleSpacing: CGFloat = 1.0
let height: CGFloat
if !item.subtitle.isEmpty {
height = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height
} else {
height = verticalInset * 2.0 + titleLayout.size.height
}
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = item.title
strongSelf.activateArea.accessibilityValue = item.label
if item.enabled {
strongSelf.activateArea.accessibilityTraits = []
} else {
strongSelf.activateArea.accessibilityTraits = .notEnabled
}
if let icon = item.icon {
if strongSelf.iconNode.supernode == nil {
strongSelf.addSubnode(strongSelf.iconNode)
}
if updateIcon {
strongSelf.iconNode.image = icon
}
let iconY = floorToScreenPixels((layout.contentSize.height - icon.size.height) / 2.0)
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - icon.size.width) / 2.0), y: iconY), size: icon.size)
} else if strongSelf.iconNode.supernode != nil {
strongSelf.iconNode.image = nil
strongSelf.iconNode.removeFromSupernode()
}
if let updateArrowImage = updateArrowImage {
strongSelf.arrowNode.image = updateArrowImage
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let _ = titleApply()
let _ = subtitleApply()
let _ = labelApply()
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))
}
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: subtitleLayout.size)
strongSelf.subtitleNode.frame = subtitleFrame
let labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: floorToScreenPixels((height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
if let arrowImage = strongSelf.arrowNode.image {
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width, y: floorToScreenPixels((height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: height + UIScreenPixel))
if let shimmeringIndex = item.shimmeringIndex {
let shimmerNode: ShimmerEffectNode
if let current = strongSelf.placeholderNode {
shimmerNode = current
} else {
shimmerNode = ShimmerEffectNode()
strongSelf.placeholderNode = shimmerNode
if strongSelf.backgroundNode.supernode != nil {
strongSelf.insertSubnode(shimmerNode, aboveSubnode: strongSelf.backgroundNode)
} else {
strongSelf.addSubnode(shimmerNode)
}
}
shimmerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
if let (rect, size) = strongSelf.absoluteLocation {
shimmerNode.updateAbsoluteRect(rect, within: size)
}
var shapes: [ShimmerEffectNode.Shape] = []
let titleLineWidth: CGFloat = (shimmeringIndex % 2 == 0) ? 120.0 : 80.0
let lineDiameter: CGFloat = 8.0
let titleFrame = strongSelf.titleNode.frame
shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter))
shimmerNode.update(backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: contentSize)
} else if let shimmerNode = strongSelf.placeholderNode {
strongSelf.placeholderNode = nil
shimmerNode.removeFromSupernode()
}
}
})
}
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted && (self.item?.enabled ?? false) {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,320 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import ItemListPeerItem
import ItemListPeerActionItem
private final class BlockedPeersControllerArguments {
let context: AccountContext
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
let addPeer: () -> Void
let removePeer: (PeerId) -> Void
let openPeer: (Peer) -> Void
init(context: AccountContext, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (Peer) -> Void) {
self.context = context
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.addPeer = addPeer
self.removePeer = removePeer
self.openPeer = openPeer
}
}
private enum BlockedPeersSection: Int32 {
case actions
case peers
}
private enum BlockedPeersEntryStableId: Hashable {
case add
case peer(PeerId)
}
private enum BlockedPeersEntry: ItemListNodeEntry {
case add(PresentationTheme, String)
case peerItem(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, ItemListPeerItemEditing, Bool)
var section: ItemListSectionId {
switch self {
case .add:
return BlockedPeersSection.actions.rawValue
case .peerItem:
return BlockedPeersSection.peers.rawValue
}
}
var stableId: BlockedPeersEntryStableId {
switch self {
case .add:
return .add
case let .peerItem(_, _, _, _, _, peer, _, _):
return .peer(peer.id)
}
}
static func ==(lhs: BlockedPeersEntry, rhs: BlockedPeersEntry) -> Bool {
switch lhs {
case let .add(lhsTheme, lhsText):
if case let .add(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsPeer, lhsEditing, lhsEnabled):
if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsPeer, rhsEditing, rhsEnabled) = rhs {
if lhsIndex != rhsIndex {
return false
}
if lhsTheme !== rhsTheme {
return false
}
if lhsStrings !== rhsStrings {
return false
}
if lhsDateTimeFormat != rhsDateTimeFormat {
return false
}
if lhsNameOrder != rhsNameOrder {
return false
}
if !lhsPeer.isEqual(rhsPeer) {
return false
}
if lhsEditing != rhsEditing {
return false
}
if lhsEnabled != rhsEnabled {
return false
}
return true
} else {
return false
}
}
}
static func <(lhs: BlockedPeersEntry, rhs: BlockedPeersEntry) -> Bool {
switch lhs {
case .add:
if case .add = rhs {
return false
} else {
return true
}
case let .peerItem(index, _, _, _, _, _, _, _):
switch rhs {
case .add:
return false
case let .peerItem(rhsIndex, _, _, _, _, _, _, _):
return index < rhsIndex
}
}
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! BlockedPeersControllerArguments
switch self {
case let .add(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.blockAccentIcon(theme), title: text, sectionId: self.section, height: .generic, editing: false, action: {
arguments.addPeer()
})
case let .peerItem(_, _, strings, dateTimeFormat, nameDisplayOrder, peer, editing, enabled):
let revealOptions = ItemListPeerItemRevealOptions(options: [ItemListPeerItemRevealOption(type: .destructive, title: strings.BlockedUsers_Unblock, action: {
arguments.removePeer(peer.id)
})])
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(peer), presence: nil, text: .none, label: .none, editing: editing, revealOptions: revealOptions, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: {
arguments.openPeer(peer)
}, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
}, removePeer: { peerId in
arguments.removePeer(peerId)
})
}
}
}
private struct BlockedPeersControllerState: Equatable {
let editing: Bool
let peerIdWithRevealedOptions: PeerId?
let removingPeerId: PeerId?
init() {
self.editing = false
self.peerIdWithRevealedOptions = nil
self.removingPeerId = nil
}
init(editing: Bool, peerIdWithRevealedOptions: PeerId?, removingPeerId: PeerId?) {
self.editing = editing
self.peerIdWithRevealedOptions = peerIdWithRevealedOptions
self.removingPeerId = removingPeerId
}
static func ==(lhs: BlockedPeersControllerState, rhs: BlockedPeersControllerState) -> Bool {
if lhs.editing != rhs.editing {
return false
}
if lhs.peerIdWithRevealedOptions != rhs.peerIdWithRevealedOptions {
return false
}
if lhs.removingPeerId != rhs.removingPeerId {
return false
}
return true
}
func withUpdatedEditing(_ editing: Bool) -> BlockedPeersControllerState {
return BlockedPeersControllerState(editing: editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId)
}
func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> BlockedPeersControllerState {
return BlockedPeersControllerState(editing: self.editing, peerIdWithRevealedOptions: peerIdWithRevealedOptions, removingPeerId: self.removingPeerId)
}
func withUpdatedRemovingPeerId(_ removingPeerId: PeerId?) -> BlockedPeersControllerState {
return BlockedPeersControllerState(editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: removingPeerId)
}
}
private func blockedPeersControllerEntries(presentationData: PresentationData, state: BlockedPeersControllerState, blockedPeersState: BlockedPeersContextState) -> [BlockedPeersEntry] {
var entries: [BlockedPeersEntry] = []
if !blockedPeersState.peers.isEmpty || !blockedPeersState.canLoadMore {
entries.append(.add(presentationData.theme, presentationData.strings.BlockedUsers_BlockUser))
var index: Int32 = 0
for peer in blockedPeersState.peers {
entries.append(.peerItem(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, peer.peer!, ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: peer.peerId == state.peerIdWithRevealedOptions), state.removingPeerId != peer.peerId))
index += 1
}
}
return entries
}
public func blockedPeersController(context: AccountContext, blockedPeersContext: BlockedPeersContext) -> ViewController {
let statePromise = ValuePromise(BlockedPeersControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: BlockedPeersControllerState())
let updateState: ((BlockedPeersControllerState) -> BlockedPeersControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var pushControllerImpl: ((ViewController) -> Void)?
let actionsDisposable = DisposableSet()
let removePeerDisposable = MetaDisposable()
actionsDisposable.add(removePeerDisposable)
let arguments = BlockedPeersControllerArguments(context: context, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
updateState { state in
if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
return state.withUpdatedPeerIdWithRevealedOptions(peerId)
} else {
return state
}
}
}, addPeer: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyPrivateChats, .excludeSavedMessages, .removeSearchHeader, .excludeRecent, .doNotSearchMessages], title: presentationData.strings.BlockedUsers_SelectUserTitle))
controller.peerSelected = { [weak controller] peer, _ in
let peerId = peer.id
guard let strongController = controller else {
return
}
strongController.inProgress = true
removePeerDisposable.set((blockedPeersContext.add(peerId: peerId)
|> deliverOnMainQueue).start(completed: {
guard let strongController = controller else {
return
}
strongController.inProgress = false
strongController.dismiss()
}))
}
pushControllerImpl?(controller)
}, removePeer: { memberId in
updateState {
return $0.withUpdatedRemovingPeerId(memberId)
}
removePeerDisposable.set((blockedPeersContext.remove(peerId: memberId)
|> deliverOnMainQueue).start(error: { _ in
updateState {
return $0.withUpdatedRemovingPeerId(nil)
}
}, completed: {
updateState {
return $0.withUpdatedRemovingPeerId(nil)
}
}))
}, openPeer: { peer in
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
pushControllerImpl?(controller)
}
})
var previousState: BlockedPeersContextState?
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), blockedPeersContext.state)
|> deliverOnMainQueue
|> map { presentationData, state, blockedPeersState -> (ItemListControllerState, (ItemListNodeState, Any)) in
var rightNavigationButton: ItemListNavigationButton?
if !blockedPeersState.peers.isEmpty {
if state.editing {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
updateState { state in
return state.withUpdatedEditing(false)
}
})
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: {
updateState { state in
return state.withUpdatedEditing(true)
}
})
}
}
var emptyStateItem: ItemListControllerEmptyStateItem?
if blockedPeersState.peers.isEmpty && !blockedPeersState.canLoadMore {
emptyStateItem = ItemListTextEmptyStateItem(text: presentationData.strings.BlockedUsers_Info)
}
let previousStateValue = previousState
previousState = blockedPeersState
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.BlockedUsers_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: blockedPeersControllerEntries(presentationData: presentationData, state: state, blockedPeersState: blockedPeersState), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previousStateValue != nil && previousStateValue!.peers.count >= blockedPeersState.peers.count, scrollEnabled: emptyStateItem == nil)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
pushControllerImpl = { [weak controller] c in
if let controller = controller {
(controller.navigationController as? NavigationController)?.pushViewController(c)
}
}
controller.visibleBottomContentOffsetChanged = { offset in
if case let .known(value) = offset, value < 40.0 {
blockedPeersContext.loadMore()
}
}
return controller
}
@@ -0,0 +1,340 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import AlertUI
import PresentationDataUtils
import AuthorizationUtils
import PhoneNumberFormat
private final class ConfirmPhoneNumberCodeControllerArguments {
let updateEntryText: (String) -> Void
let next: () -> Void
init(updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void) {
self.updateEntryText = updateEntryText
self.next = next
}
}
private enum ConfirmPhoneNumberCodeSection: Int32 {
case code
}
private enum ConfirmPhoneNumberCodeTag: ItemListItemTag {
case input
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? ConfirmPhoneNumberCodeTag {
switch self {
case .input:
if case .input = other {
return true
} else {
return false
}
}
} else {
return false
}
}
}
private enum ConfirmPhoneNumberCodeEntry: ItemListNodeEntry {
case codeEntry(PresentationTheme, PresentationStrings, String, String)
case codeInfo(PresentationTheme, PresentationStrings, String, String)
var section: ItemListSectionId {
return ConfirmPhoneNumberCodeSection.code.rawValue
}
var stableId: Int32 {
switch self {
case .codeEntry:
return 1
case .codeInfo:
return 2
}
}
static func ==(lhs: ConfirmPhoneNumberCodeEntry, rhs: ConfirmPhoneNumberCodeEntry) -> Bool {
switch lhs {
case let .codeEntry(lhsTheme, lhsStrings, lhsTitle, lhsText):
if case let .codeEntry(rhsTheme, rhsStrings, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsTitle == rhsTitle, lhsText == rhsText {
return true
} else {
return false
}
case let .codeInfo(lhsTheme, lhsStrings, lhsPhoneNumber, lhsText):
if case let .codeInfo(rhsTheme, rhsStrings, rhsPhoneNumber, rhsText) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPhoneNumber == rhsPhoneNumber, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: ConfirmPhoneNumberCodeEntry, rhs: ConfirmPhoneNumberCodeEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ConfirmPhoneNumberCodeControllerArguments
switch self {
case let .codeEntry(_, _, title, text):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: title, textColor: .black), text: text, placeholder: "", type: .number, spacing: 10.0, tag: ConfirmPhoneNumberCodeTag.input, sectionId: self.section, textUpdated: { updatedText in
arguments.updateEntryText(updatedText)
}, action: {
arguments.next()
})
case let .codeInfo(_, strings, formattedPhoneNumber, nextOptionText):
let stringAndRanges = strings.CancelResetAccount_TextSMS(formattedPhoneNumber)
var result = ""
result += stringAndRanges.string
if let range = result.range(of: formattedPhoneNumber) {
result.insert("*", at: range.upperBound)
result.insert("*", at: range.upperBound)
result.insert("*", at: range.lowerBound)
result.insert("*", at: range.lowerBound)
}
if !nextOptionText.isEmpty {
result += "\n\n" + nextOptionText
}
return ItemListTextItem(presentationData: presentationData, text: .markdown(result), sectionId: self.section)
}
}
}
private struct ConfirmPhoneNumberCodeControllerState: Equatable {
var codeText: String
var checking: Bool
init(codeText: String, checking: Bool) {
self.codeText = codeText
self.checking = checking
}
}
private func confirmPhoneNumberCodeControllerEntries(presentationData: PresentationData, state: ConfirmPhoneNumberCodeControllerState, formattedPhoneNumber: String, codeData: CancelAccountResetData, timeout: Int32?, strings: PresentationStrings, theme: PresentationTheme) -> [ConfirmPhoneNumberCodeEntry] {
var entries: [ConfirmPhoneNumberCodeEntry] = []
entries.append(.codeEntry(presentationData.theme, presentationData.strings, presentationData.strings.ChangePhoneNumberCode_CodePlaceholder, state.codeText))
var text = ""
if let nextType = codeData.nextType {
text += authorizationNextOptionText(currentType: codeData.type, nextType: nextType, timeout: timeout, strings: presentationData.strings, primaryColor: .black, accentColor: .black).0.string
}
entries.append(.codeInfo(presentationData.theme, presentationData.strings, formattedPhoneNumber, text))
return entries
}
private func timeoutSignal(codeData: CancelAccountResetData) -> Signal<Int32?, NoError> {
if let _ = codeData.nextType, let timeout = codeData.timeout {
return Signal { subscriber in
let value = Atomic<Int32>(value: timeout)
subscriber.putNext(timeout)
let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: {
subscriber.putNext(value.modify { value in
return max(0, value - 1)
})
}, queue: Queue.mainQueue())
timer.start()
return ActionDisposable {
timer.invalidate()
}
}
} else {
return .single(nil)
}
}
protocol ConfirmPhoneNumberCodeController: AnyObject {
func applyCode(_ code: Int)
}
private final class ConfirmPhoneNumberCodeControllerImpl: ItemListController, ConfirmPhoneNumberCodeController {
private let applyCodeImpl: (Int) -> Void
init(context: AccountContext, state: Signal<(ItemListControllerState, (ItemListNodeState, Any)), NoError>, applyCodeImpl: @escaping (Int) -> Void) {
self.applyCodeImpl = applyCodeImpl
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(presentationData: ItemListPresentationData(presentationData), updatedPresentationData: context.sharedContext.presentationData |> map(ItemListPresentationData.init(_:)), state: state, tabBarItem: nil)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func applyCode(_ code: Int) {
self.applyCodeImpl(code)
}
}
public func confirmPhoneNumberCodeController(context: AccountContext, phoneNumber: String, codeData: CancelAccountResetData) -> ViewController {
let initialState = ConfirmPhoneNumberCodeControllerState(codeText: "", checking: false)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((ConfirmPhoneNumberCodeControllerState) -> ConfirmPhoneNumberCodeControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
let actionsDisposable = DisposableSet()
let confirmPhoneDisposable = MetaDisposable()
actionsDisposable.add(confirmPhoneDisposable)
let nextTypeDisposable = MetaDisposable()
actionsDisposable.add(nextTypeDisposable)
let currentDataPromise = Promise<CancelAccountResetData>()
currentDataPromise.set(.single(codeData))
let timeout = Promise<Int32?>()
timeout.set(currentDataPromise.get()
|> mapToSignal(timeoutSignal))
let resendCode = currentDataPromise.get()
|> mapToSignal { [weak currentDataPromise] data -> Signal<Void, NoError> in
if let _ = data.nextType {
return timeout.get()
|> filter { $0 == 0 }
|> take(1)
|> mapToSignal { _ -> Signal<Void, NoError> in
return Signal { subscriber in
return context.engine.auth.requestNextCancelAccountResetOption(phoneNumber: phoneNumber, phoneCodeHash: data.hash).start(next: { next in
currentDataPromise?.set(.single(next))
}, error: { error in
})
}
}
} else {
return .complete()
}
}
nextTypeDisposable.set(resendCode.start())
let checkCode: () -> Void = {
var code: String?
updateState { state in
var state = state
if state.checking || state.codeText.isEmpty {
return state
} else {
code = state.codeText
state.checking = true
return state
}
}
if let code = code {
confirmPhoneDisposable.set((context.engine.auth.requestCancelAccountReset(phoneCodeHash: codeData.hash, phoneCode: code)
|> deliverOnMainQueue).start(error: { error in
updateState { state in
var state = state
state.checking = false
return state
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let alertText: String
switch error {
case .generic:
alertText = presentationData.strings.Login_UnknownError
case .invalidCode:
alertText = presentationData.strings.Login_InvalidCodeError
case .codeExpired:
alertText = presentationData.strings.Login_CodeExpiredError
case .limitExceeded:
alertText = presentationData.strings.Login_CodeFloodError
}
presentControllerImpl?(textAlertController(context: context, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, completed: {
updateState { state in
var state = state
state.checking = false
return state
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.CancelResetAccount_Success(formatPhoneNumber(context: context, number: phoneNumber)).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
dismissImpl?()
}))
}
}
let arguments = ConfirmPhoneNumberCodeControllerArguments(updateEntryText: { updatedText in
var initiateCheck = false
updateState { state in
var state = state
if state.codeText.count < 5 && updatedText.count == 5 {
initiateCheck = true
}
state.codeText = updatedText
return state
}
if initiateCheck {
checkCode()
}
}, next: {
checkCode()
})
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get() |> deliverOnMainQueue, currentDataPromise.get() |> deliverOnMainQueue, timeout.get() |> deliverOnMainQueue)
|> deliverOnMainQueue
|> map { presentationData, state, data, timeout -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
var rightNavigationButton: ItemListNavigationButton?
if state.checking {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
} else {
var nextEnabled = true
if state.codeText.isEmpty {
nextEnabled = false
}
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Next), style: .bold, enabled: nextEnabled, action: {
checkCode()
})
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.CancelResetAccount_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: confirmPhoneNumberCodeControllerEntries(presentationData: presentationData, state: state, formattedPhoneNumber: formatPhoneNumber(context: context, number: phoneNumber), codeData: data, timeout: timeout, strings: presentationData.strings, theme: presentationData.theme), style: .blocks, focusItemTag: ConfirmPhoneNumberCodeTag.input, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ConfirmPhoneNumberCodeControllerImpl(context: context, state: signal, applyCodeImpl: { code in
updateState { state in
var state = state
state.codeText = "\(code)"
return state
}
checkCode()
})
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
controller?.dismiss()
}
return controller
}
@@ -0,0 +1,474 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import AlertUI
import PresentationDataUtils
private enum CreatePasswordField {
case password
case passwordConfirmation
case hint
case email
}
private final class CreatePasswordControllerArguments {
let context: AccountContext
let updateFieldText: (CreatePasswordField, String) -> Void
let selectNextInputItem: (CreatePasswordEntryTag) -> Void
let save: () -> Void
let cancelEmailConfirmation: () -> Void
init(context: AccountContext, updateFieldText: @escaping (CreatePasswordField, String) -> Void, selectNextInputItem: @escaping (CreatePasswordEntryTag) -> Void, save: @escaping () -> Void, cancelEmailConfirmation: @escaping () -> Void) {
self.context = context
self.updateFieldText = updateFieldText
self.selectNextInputItem = selectNextInputItem
self.save = save
self.cancelEmailConfirmation = cancelEmailConfirmation
}
}
private enum CreatePasswordSection: Int32 {
case password
case hint
case email
case emailCancel
}
private enum CreatePasswordEntryTag: ItemListItemTag {
case password
case passwordConfirmation
case hint
case email
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? CreatePasswordEntryTag {
return self == other
} else {
return false
}
}
}
private enum CreatePasswordEntry: ItemListNodeEntry, Equatable {
case passwordHeader(PresentationTheme, String)
case password(PresentationTheme, PresentationStrings, String, String)
case passwordConfirmation(PresentationTheme, PresentationStrings, String, String)
case passwordInfo(PresentationTheme, String)
case hintHeader(PresentationTheme, String)
case hint(PresentationTheme, PresentationStrings, String, String, Bool)
case hintInfo(PresentationTheme, String)
case emailHeader(PresentationTheme, String)
case email(PresentationTheme, PresentationStrings, String, String)
case emailInfo(PresentationTheme, String)
case emailConfirmation(PresentationTheme, String)
case emailCancel(PresentationTheme, String, Bool)
var section: ItemListSectionId {
switch self {
case .passwordHeader, .password, .passwordConfirmation, .passwordInfo:
return CreatePasswordSection.password.rawValue
case .hintHeader, .hint, .hintInfo:
return CreatePasswordSection.hint.rawValue
case .emailHeader, .email, .emailInfo, .emailConfirmation:
return CreatePasswordSection.email.rawValue
case .emailCancel:
return CreatePasswordSection.emailCancel.rawValue
}
}
var stableId: Int32 {
switch self {
case .passwordHeader:
return 0
case .password:
return 1
case .passwordConfirmation:
return 2
case .passwordInfo:
return 3
case .hintHeader:
return 4
case .hint:
return 5
case .hintInfo:
return 6
case .emailHeader:
return 7
case .email:
return 8
case .emailInfo:
return 9
case .emailConfirmation:
return 10
case .emailCancel:
return 11
}
}
static func <(lhs: CreatePasswordEntry, rhs: CreatePasswordEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! CreatePasswordControllerArguments
switch self {
case let .passwordHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .password(_, _, text, value):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(), text: value, placeholder: text, type: .password, returnKeyType: .next, spacing: 0.0, tag: CreatePasswordEntryTag.password, sectionId: self.section, textUpdated: { updatedText in
arguments.updateFieldText(.password, updatedText)
}, action: {
arguments.selectNextInputItem(CreatePasswordEntryTag.password)
})
case let .passwordConfirmation(_, _, text, value):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(), text: value, placeholder: text, type: .password, returnKeyType: .next, spacing: 0.0, tag: CreatePasswordEntryTag.passwordConfirmation, sectionId: self.section, textUpdated: { updatedText in
arguments.updateFieldText(.passwordConfirmation, updatedText)
}, action: {
arguments.selectNextInputItem(CreatePasswordEntryTag.passwordConfirmation)
})
case let .passwordInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .hintHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .hint(_, _, text, value, last):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(), text: value, placeholder: text, type: .regular(capitalization: true, autocorrection: false), returnKeyType: last ? .done : .next, spacing: 0.0, tag: CreatePasswordEntryTag.hint, sectionId: self.section, textUpdated: { updatedText in
arguments.updateFieldText(.hint, updatedText)
}, action: {
if last {
arguments.save()
} else {
arguments.selectNextInputItem(CreatePasswordEntryTag.hint)
}
})
case let .hintInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .emailHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .email(_, _, text, value):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(), text: value, placeholder: text, type: .email, returnKeyType: .done, spacing: 0.0, tag: CreatePasswordEntryTag.email, sectionId: self.section, textUpdated: { updatedText in
arguments.updateFieldText(.email, updatedText)
}, action: {
arguments.save()
})
case let .emailInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .emailConfirmation(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .emailCancel(_, text, enabled):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.cancelEmailConfirmation()
})
}
}
}
private struct CreatePasswordControllerState: Equatable {
var state: CreatePasswordState
var passwordText: String = ""
var passwordConfirmationText: String = ""
var hintText: String = ""
var emailText: String = ""
var saving: Bool = false
init(state: CreatePasswordState) {
self.state = state
}
}
private func createPasswordControllerEntries(presentationData: PresentationData, context: CreatePasswordContext, state: CreatePasswordControllerState) -> [CreatePasswordEntry] {
var entries: [CreatePasswordEntry] = []
switch state.state {
case let .setup(currentPassword):
entries.append(.passwordHeader(presentationData.theme, presentationData.strings.FastTwoStepSetup_PasswordSection))
entries.append(.password(presentationData.theme, presentationData.strings, presentationData.strings.FastTwoStepSetup_PasswordPlaceholder, state.passwordText))
entries.append(.passwordConfirmation(presentationData.theme, presentationData.strings, presentationData.strings.FastTwoStepSetup_PasswordConfirmationPlaceholder, state.passwordConfirmationText))
if case .paymentInfo = context {
entries.append(.passwordInfo(presentationData.theme, presentationData.strings.FastTwoStepSetup_PasswordHelp))
}
let showEmail = currentPassword == nil
entries.append(.hintHeader(presentationData.theme, presentationData.strings.FastTwoStepSetup_HintSection))
entries.append(.hint(presentationData.theme, presentationData.strings, presentationData.strings.FastTwoStepSetup_HintPlaceholder, state.hintText, !showEmail))
entries.append(.hintInfo(presentationData.theme, presentationData.strings.FastTwoStepSetup_HintHelp))
if showEmail {
entries.append(.emailHeader(presentationData.theme, presentationData.strings.FastTwoStepSetup_EmailSection))
entries.append(.email(presentationData.theme, presentationData.strings, presentationData.strings.FastTwoStepSetup_EmailPlaceholder, state.emailText))
entries.append(.emailInfo(presentationData.theme, presentationData.strings.FastTwoStepSetup_EmailHelp))
}
case let .pendingVerification(emailPattern):
entries.append(.emailConfirmation(presentationData.theme, presentationData.strings.TwoStepAuth_ConfirmationText + "\n\(emailPattern)"))
entries.append(.emailCancel(presentationData.theme, presentationData.strings.TwoStepAuth_ConfirmationAbort, !state.saving))
}
return entries
}
enum CreatePasswordContext {
case account
case secureId
case paymentInfo
}
enum CreatePasswordState: Equatable {
case setup(currentPassword: String?)
case pendingVerification(emailPattern: String)
}
func createPasswordController(context: AccountContext, createPasswordContext: CreatePasswordContext, state: CreatePasswordState, completion: @escaping (String, String, Bool) -> Void, updatePasswordEmailConfirmation: @escaping ((String, String)?) -> Void, processPasswordEmailConfirmation: Bool = true) -> ViewController {
let statePromise = ValuePromise(CreatePasswordControllerState(state: state), ignoreRepeated: true)
let stateValue = Atomic(value: CreatePasswordControllerState(state: state))
let updateState: ((CreatePasswordControllerState) -> CreatePasswordControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
let actionsDisposable = DisposableSet()
let saveDisposable = MetaDisposable()
actionsDisposable.add(saveDisposable)
var initialFocusImpl: (() -> Void)?
var selectNextInputItemImpl: ((CreatePasswordEntryTag) -> Void)?
let saveImpl = {
var state: CreatePasswordControllerState?
updateState { s in
state = s
return s
}
if let state = state {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
if state.passwordText.isEmpty {
} else if state.passwordText != state.passwordConfirmationText {
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
} else {
let saveImpl: () -> Void = {
var currentPassword: String?
var email: String?
updateState { state in
var state = state
if case let .setup(password) = state.state {
currentPassword = password
if password != nil {
email = nil
} else {
email = state.emailText
}
}
state.saving = true
return state
}
saveDisposable.set((context.engine.auth.updateTwoStepVerificationPassword(currentPassword: currentPassword, updatedPassword: .password(password: state.passwordText, hint: state.hintText, email: email))
|> deliverOnMainQueue).start(next: { update in
switch update {
case .none:
break
case let .password(password, pendingEmail):
if let pendingEmail = pendingEmail, let email = email {
if processPasswordEmailConfirmation {
updateState { state in
var state = state
state.saving = false
state.state = .pendingVerification(emailPattern: pendingEmail.pattern)
return state
}
}
updatePasswordEmailConfirmation((email, pendingEmail.pattern))
} else {
completion(password, state.hintText, !state.emailText.isEmpty)
}
}
}, error: { _ in
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
}))
}
var emailAlert = false
switch state.state {
case let .setup(currentPassword):
if currentPassword != nil {
emailAlert = false
} else {
emailAlert = state.emailText.isEmpty
}
case .pendingVerification:
break
}
if emailAlert {
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: presentationData.strings.TwoStepAuth_EmailSkip, action: {
saveImpl()
})]), nil)
} else {
saveImpl()
}
}
}
}
let arguments = CreatePasswordControllerArguments(context: context, updateFieldText: { field, updatedText in
updateState { state in
var state = state
switch field {
case .password:
state.passwordText = updatedText
case .passwordConfirmation:
state.passwordConfirmationText = updatedText
case .hint:
state.hintText = updatedText
case .email:
state.emailText = updatedText
}
return state
}
}, selectNextInputItem: { tag in
selectNextInputItemImpl?(tag)
}, save: {
saveImpl()
}, cancelEmailConfirmation: {
var currentPassword: String?
updateState { state in
var state = state
switch state.state {
case let .setup(password):
currentPassword = password
case .pendingVerification:
currentPassword = nil
}
state.saving = true
return state
}
saveDisposable.set((context.engine.auth.updateTwoStepVerificationPassword(currentPassword: currentPassword, updatedPassword: .none)
|> deliverOnMainQueue).start(next: { _ in
updateState { state in
var state = state
state.saving = false
state.state = .setup(currentPassword: nil)
return state
}
updatePasswordEmailConfirmation(nil)
}, error: { _ in
updateState { state in
var state = state
state.saving = false
return state
}
}))
})
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get())
|> deliverOnMainQueue
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
var rightNavigationButton: ItemListNavigationButton?
if state.saving {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
} else {
switch state.state {
case .setup:
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: !state.passwordText.isEmpty, action: {
saveImpl()
})
case .pendingVerification:
break
}
}
let title: String
switch state.state {
case let .setup(currentPassword):
if currentPassword != nil {
title = presentationData.strings.TwoStepAuth_ChangePassword
} else {
title = presentationData.strings.FastTwoStepSetup_Title
}
case .pendingVerification:
title = presentationData.strings.FastTwoStepSetup_Title
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createPasswordControllerEntries(presentationData: presentationData, context: createPasswordContext, state: state), style: .blocks, focusItemTag: CreatePasswordEntryTag.password, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
controller?.dismiss()
}
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
initialFocusImpl = { [weak controller] in
guard let controller = controller, controller.didAppearOnce else {
return
}
var resultItemNode: ItemListSingleLineInputItemNode?
let _ = controller.frameForItemNode({ itemNode in
if let itemNode = itemNode as? ItemListSingleLineInputItemNode, let tag = itemNode.tag, tag.isEqual(to: CreatePasswordEntryTag.password) {
resultItemNode = itemNode
return true
}
return false
})
if let resultItemNode = resultItemNode {
resultItemNode.focus()
}
}
selectNextInputItemImpl = { [weak controller] currentTag in
guard let controller = controller else {
return
}
var resultItemNode: ItemListSingleLineInputItemNode?
var focusOnNext = false
let _ = controller.frameForItemNode({ itemNode in
if let itemNode = itemNode as? ItemListSingleLineInputItemNode, let tag = itemNode.tag {
if focusOnNext && resultItemNode == nil {
resultItemNode = itemNode
return true
} else if currentTag.isEqual(to: tag) {
focusOnNext = true
}
}
return false
})
if let resultItemNode = resultItemNode {
resultItemNode.focus()
}
}
controller.didAppear = { firstTime in
if !firstTime {
return
}
initialFocusImpl?()
}
return controller
}
@@ -0,0 +1,576 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import AlertUI
import PresentationDataUtils
import TelegramNotices
import UndoUI
private final class DataPrivacyControllerArguments {
let account: Account
let clearPaymentInfo: () -> Void
let updateSecretChatLinkPreviews: (Bool) -> Void
let deleteContacts: () -> Void
let updateSyncContacts: (Bool) -> Void
let updateSuggestFrequentContacts: (Bool) -> Void
let deleteCloudDrafts: () -> Void
let openBotListSettings: () -> Void
init(account: Account, clearPaymentInfo: @escaping () -> Void, updateSecretChatLinkPreviews: @escaping (Bool) -> Void, deleteContacts: @escaping () -> Void, updateSyncContacts: @escaping (Bool) -> Void, updateSuggestFrequentContacts: @escaping (Bool) -> Void, deleteCloudDrafts: @escaping () -> Void, openBotListSettings: @escaping () -> Void) {
self.account = account
self.clearPaymentInfo = clearPaymentInfo
self.updateSecretChatLinkPreviews = updateSecretChatLinkPreviews
self.deleteContacts = deleteContacts
self.updateSyncContacts = updateSyncContacts
self.updateSuggestFrequentContacts = updateSuggestFrequentContacts
self.deleteCloudDrafts = deleteCloudDrafts
self.openBotListSettings = openBotListSettings
}
}
private enum PrivacyAndSecuritySection: Int32 {
case contacts
case frequentContacts
case chats
case payments
case secretChats
case bots
}
private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
case contactsHeader(PresentationTheme, String)
case deleteContacts(PresentationTheme, String, Bool)
case syncContacts(PresentationTheme, String, Bool)
case syncContactsInfo(PresentationTheme, String)
case frequentContacts(PresentationTheme, String, Bool)
case frequentContactsInfo(PresentationTheme, String)
case chatsHeader(PresentationTheme, String)
case deleteCloudDrafts(PresentationTheme, String, Bool)
case paymentHeader(PresentationTheme, String)
case clearPaymentInfo(PresentationTheme, String, Bool)
case paymentInfo(PresentationTheme, String)
case secretChatLinkPreviewsHeader(PresentationTheme, String)
case secretChatLinkPreviews(PresentationTheme, String, Bool)
case secretChatLinkPreviewsInfo(PresentationTheme, String)
case botList
var section: ItemListSectionId {
switch self {
case .contactsHeader, .deleteContacts, .syncContacts, .syncContactsInfo:
return PrivacyAndSecuritySection.contacts.rawValue
case .frequentContacts, .frequentContactsInfo:
return PrivacyAndSecuritySection.frequentContacts.rawValue
case .chatsHeader, .deleteCloudDrafts:
return PrivacyAndSecuritySection.chats.rawValue
case .paymentHeader, .clearPaymentInfo, .paymentInfo:
return PrivacyAndSecuritySection.payments.rawValue
case .secretChatLinkPreviewsHeader, .secretChatLinkPreviews, .secretChatLinkPreviewsInfo:
return PrivacyAndSecuritySection.secretChats.rawValue
case .botList:
return PrivacyAndSecuritySection.bots.rawValue
}
}
var stableId: Int32 {
switch self {
case .contactsHeader:
return 0
case .deleteContacts:
return 1
case .syncContacts:
return 2
case .syncContactsInfo:
return 3
case .frequentContacts:
return 4
case .frequentContactsInfo:
return 5
case .chatsHeader:
return 6
case .deleteCloudDrafts:
return 7
case .paymentHeader:
return 8
case .clearPaymentInfo:
return 9
case .paymentInfo:
return 10
case .secretChatLinkPreviewsHeader:
return 11
case .secretChatLinkPreviews:
return 12
case .secretChatLinkPreviewsInfo:
return 13
case .botList:
return 14
}
}
static func ==(lhs: PrivacyAndSecurityEntry, rhs: PrivacyAndSecurityEntry) -> Bool {
switch lhs {
case let .contactsHeader(lhsTheme, lhsText):
if case let .contactsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .deleteContacts(lhsTheme, lhsText, lhsEnabled):
if case let .deleteContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .syncContacts(lhsTheme, lhsText, lhsEnabled):
if case let .syncContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .syncContactsInfo(lhsTheme, lhsText):
if case let .syncContactsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .frequentContacts(lhsTheme, lhsText, lhsEnabled):
if case let .frequentContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .frequentContactsInfo(lhsTheme, lhsText):
if case let .frequentContactsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .chatsHeader(lhsTheme, lhsText):
if case let .chatsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .deleteCloudDrafts(lhsTheme, lhsText, lhsEnabled):
if case let .deleteCloudDrafts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .paymentHeader(lhsTheme, lhsText):
if case let .paymentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .clearPaymentInfo(lhsTheme, lhsText, lhsEnabled):
if case let .clearPaymentInfo(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .paymentInfo(lhsTheme, lhsText):
if case let .paymentInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .secretChatLinkPreviewsHeader(lhsTheme, lhsText):
if case let .secretChatLinkPreviewsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .secretChatLinkPreviews(lhsTheme, lhsText, lhsEnabled):
if case let .secretChatLinkPreviews(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .secretChatLinkPreviewsInfo(lhsTheme, lhsText):
if case let .secretChatLinkPreviewsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case .botList:
if case .botList = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: PrivacyAndSecurityEntry, rhs: PrivacyAndSecurityEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! DataPrivacyControllerArguments
switch self {
case let .contactsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .deleteContacts(_, text, value):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.deleteContacts()
})
case let .syncContacts(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateSyncContacts(updatedValue)
})
case let .syncContactsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .frequentContacts(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: !value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateSuggestFrequentContacts(updatedValue)
})
case let .frequentContactsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .chatsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .deleteCloudDrafts(_, text, value):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.deleteCloudDrafts()
})
case let .paymentHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .clearPaymentInfo(_, text, enabled):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.clearPaymentInfo()
})
case let .paymentInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .secretChatLinkPreviewsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .secretChatLinkPreviews(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateSecretChatLinkPreviews(updatedValue)
})
case let .secretChatLinkPreviewsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case .botList:
return ItemListDisclosureItem(
presentationData: presentationData,
systemStyle: .glass,
title: presentationData.strings.Settings_BotListSettings,
label: "",
sectionId: self.section,
style: .blocks,
action: {
arguments.openBotListSettings()
}
)
}
}
}
private struct DataPrivacyControllerState: Equatable {
var clearingPaymentInfo: Bool = false
var deletingContacts: Bool = false
var updatedSuggestFrequentContacts: Bool? = nil
var deletingCloudDrafts: Bool = false
}
private func dataPrivacyControllerEntries(presentationData: PresentationData, state: DataPrivacyControllerState, secretChatLinkPreviews: Bool?, synchronizeDeviceContacts: Bool, frequentContacts: Bool, hasBotSettings: Bool) -> [PrivacyAndSecurityEntry] {
var entries: [PrivacyAndSecurityEntry] = []
entries.append(.contactsHeader(presentationData.theme, presentationData.strings.Privacy_ContactsTitle))
entries.append(.deleteContacts(presentationData.theme, presentationData.strings.Privacy_ContactsReset, !state.deletingContacts))
entries.append(.syncContacts(presentationData.theme, presentationData.strings.Privacy_ContactsSync, synchronizeDeviceContacts))
entries.append(.syncContactsInfo(presentationData.theme, presentationData.strings.Privacy_ContactsSyncHelp))
entries.append(.frequentContacts(presentationData.theme, presentationData.strings.Privacy_TopPeers, frequentContacts))
entries.append(.frequentContactsInfo(presentationData.theme, presentationData.strings.Privacy_TopPeersHelp))
entries.append(.chatsHeader(presentationData.theme, presentationData.strings.Privacy_ChatsTitle))
entries.append(.deleteCloudDrafts(presentationData.theme, presentationData.strings.Privacy_DeleteDrafts, !state.deletingCloudDrafts))
entries.append(.paymentHeader(presentationData.theme, presentationData.strings.Privacy_PaymentsTitle))
entries.append(.clearPaymentInfo(presentationData.theme, presentationData.strings.Privacy_PaymentsClearInfo, !state.clearingPaymentInfo))
entries.append(.paymentInfo(presentationData.theme, presentationData.strings.Privacy_PaymentsClearInfoHelp))
entries.append(.secretChatLinkPreviewsHeader(presentationData.theme, presentationData.strings.Privacy_SecretChatsTitle))
entries.append(.secretChatLinkPreviews(presentationData.theme, presentationData.strings.Privacy_SecretChatsLinkPreviews, secretChatLinkPreviews ?? true))
entries.append(.secretChatLinkPreviewsInfo(presentationData.theme, presentationData.strings.Privacy_SecretChatsLinkPreviewsHelp))
if hasBotSettings {
entries.append(.botList)
}
return entries
}
public func dataPrivacyController(context: AccountContext) -> ViewController {
let statePromise = ValuePromise(DataPrivacyControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: DataPrivacyControllerState())
let updateState: ((DataPrivacyControllerState) -> DataPrivacyControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var presentControllerImpl: ((ViewController) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
let actionsDisposable = DisposableSet()
let currentInfoDisposable = MetaDisposable()
actionsDisposable.add(currentInfoDisposable)
let clearPaymentInfoDisposable = MetaDisposable()
actionsDisposable.add(clearPaymentInfoDisposable)
let arguments = DataPrivacyControllerArguments(account: context.account, clearPaymentInfo: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
var values = [true, true]
let toggleCheck: (Int) -> Void = { [weak controller] itemIndex in
controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in
if let item = item as? ActionSheetCheckboxItem {
values[itemIndex] = !item.value
return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action)
}
return item
})
controller?.updateItem(groupIndex: 0, itemIndex: 2, { item in
if let item = item as? ActionSheetButtonItem {
let disabled = !values[0] && !values[1]
return ActionSheetButtonItem(title: item.title, color: disabled ? .disabled : .accent, enabled: !disabled, action: item.action)
}
return item
})
}
var items: [ActionSheetItem] = []
items.append(ActionSheetCheckboxItem(title: presentationData.strings.Privacy_PaymentsClear_PaymentInfo, label: "", value: true, action: { value in
toggleCheck(0)
}))
items.append(ActionSheetCheckboxItem(title: presentationData.strings.Privacy_PaymentsClear_ShippingInfo, label: "", value: true, action: { value in
toggleCheck(1)
}))
items.append(ActionSheetButtonItem(title: presentationData.strings.Cache_ClearNone, action: {
var clear = false
updateState { state in
var state = state
if !state.clearingPaymentInfo {
clear = true
state.clearingPaymentInfo = true
}
return state
}
if clear {
var info = BotPaymentInfo()
if values[0] {
info.insert(.paymentInfo)
}
if values[1] {
info.insert(.shippingInfo)
}
clearPaymentInfoDisposable.set((context.engine.payments.clearBotPaymentInfo(info: info)
|> deliverOnMainQueue).start(completed: {
updateState { state in
var state = state
state.clearingPaymentInfo = false
return state
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text: String?
if info.contains([.paymentInfo, .shippingInfo]) {
text = presentationData.strings.Privacy_PaymentsClear_AllInfoCleared
} else if info.contains(.paymentInfo) {
text = presentationData.strings.Privacy_PaymentsClear_PaymentInfoCleared
} else if info.contains(.shippingInfo) {
text = presentationData.strings.Privacy_PaymentsClear_ShippingInfoCleared
} else {
text = nil
}
if let text = text {
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: text, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }))
}
}))
}
dismissAction()
}))
controller.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller)
}, updateSecretChatLinkPreviews: { value in
let _ = ApplicationSpecificNotice.setSecretChatLinkPreviews(accountManager: context.sharedContext.accountManager, value: value).start()
}, deleteContacts: {
var canBegin = false
updateState { state in
if !state.deletingContacts {
canBegin = true
}
return state
}
if canBegin {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_ContactsResetConfirmation, actions: [TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
var begin = false
updateState { state in
var state = state
if !state.deletingContacts {
state.deletingContacts = true
begin = true
}
return state
}
if !begin {
return
}
let _ = context.engine.contacts.updateIsContactSynchronizationEnabled(isContactSynchronizationEnabled: false).start()
actionsDisposable.add((context.engine.contacts.deleteAllContacts()
|> deliverOnMainQueue).start(completed: {
updateState { state in
var state = state
state.deletingContacts = false
return state
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_ContactsReset_ContactsDeleted, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }))
}))
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})]))
}
}, updateSyncContacts: { value in
let _ = context.engine.contacts.updateIsContactSynchronizationEnabled(isContactSynchronizationEnabled: value).start()
}, updateSuggestFrequentContacts: { value in
let apply: () -> Void = {
updateState { state in
var state = state
state.updatedSuggestFrequentContacts = value
return state
}
let _ = context.engine.peers.updateRecentPeersEnabled(enabled: value).start()
}
if !value {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_TopPeersWarning, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
apply()
})]))
} else {
apply()
}
}, deleteCloudDrafts: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Privacy_DeleteDrafts, color: .destructive, action: {
var clear = false
updateState { state in
var state = state
if !state.deletingCloudDrafts {
clear = true
state.deletingCloudDrafts = true
}
return state
}
if clear {
clearPaymentInfoDisposable.set((context.engine.messages.clearCloudDraftsInteractively()
|> deliverOnMainQueue).start(completed: {
updateState { state in
var state = state
state.deletingCloudDrafts = false
return state
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_DeleteDrafts_DraftsDeleted, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }))
}))
}
dismissAction()
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller)
}, openBotListSettings: {
pushControllerImpl?(context.sharedContext.makeBotSettingsScreen(context: context, peerId: nil))
})
actionsDisposable.add(context.engine.peers.managedUpdatedRecentPeers().start())
let hasBotSettings = context.engine.peers.botsWithBiometricState()
|> map { peerIds -> Bool in
return !peerIds.isEmpty
}
|> distinctUntilChanged
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.secretChatLinkPreviewsKey()), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]), context.account.postbox.preferencesView(keys: [PreferencesKeys.contactsSettings]), context.engine.peers.recentPeers(), hasBotSettings)
|> map { presentationData, state, noticeView, sharedData, preferences, recentPeers, hasBotSettings -> (ItemListControllerState, (ItemListNodeState, Any)) in
let secretChatLinkPreviews = noticeView.value.flatMap({ ApplicationSpecificNotice.getSecretChatLinkPreviews($0) })
let settings: ContactsSettings = preferences.values[PreferencesKeys.contactsSettings]?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings
let synchronizeDeviceContacts: Bool = settings.synchronizeContacts
let suggestRecentPeers: Bool
if let updatedSuggestFrequentContacts = state.updatedSuggestFrequentContacts {
suggestRecentPeers = updatedSuggestFrequentContacts
} else {
switch recentPeers {
case .peers:
suggestRecentPeers = true
case .disabled:
suggestRecentPeers = false
}
}
let rightNavigationButton: ItemListNavigationButton? = nil
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PrivateDataSettings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let animateChanges = false
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataPrivacyControllerEntries(presentationData: presentationData, state: state, secretChatLinkPreviews: secretChatLinkPreviews, synchronizeDeviceContacts: synchronizeDeviceContacts, frequentContacts: suggestRecentPeers, hasBotSettings: hasBotSettings), style: .blocks, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
presentControllerImpl = { [weak controller] c in
controller?.present(c, in: .window(.root))
}
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
return controller
}
@@ -0,0 +1,328 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import Postbox
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import WallpaperBackgroundNode
class ForwardPrivacyChatPreviewItem: ListViewItem, ItemListItem {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let systemStyle: ItemListSystemStyle
let sectionId: ItemListSectionId
let fontSize: PresentationFontSize
let chatBubbleCorners: PresentationChatBubbleCorners
let wallpaper: TelegramWallpaper
let dateTimeFormat: PresentationDateTimeFormat
let nameDisplayOrder: PresentationPersonNameOrder
let peerName: String
let linkEnabled: Bool
let tooltipText: String
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle = .legacy, sectionId: ItemListSectionId, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, peerName: String, linkEnabled: Bool, tooltipText: String) {
self.context = context
self.theme = theme
self.strings = strings
self.systemStyle = systemStyle
self.sectionId = sectionId
self.fontSize = fontSize
self.chatBubbleCorners = chatBubbleCorners
self.wallpaper = wallpaper
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
self.peerName = peerName
self.linkEnabled = linkEnabled
self.tooltipText = tooltipText
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ForwardPrivacyChatPreviewItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ForwardPrivacyChatPreviewItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
class ForwardPrivacyChatPreviewItemNode: ListViewItemNode {
private var backgroundNode: WallpaperBackgroundNode?
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let containerNode: ASDisplayNode
private var messageNode: ListViewItemNode?
private let tooltipContainerNode: ContextMenuContainerNode
private let textNode: ImmediateTextNode
private let measureTextNode: TextNode
private var item: ForwardPrivacyChatPreviewItem?
init() {
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.containerNode = ASDisplayNode()
self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
self.tooltipContainerNode = ContextMenuContainerNode(isBlurred: false, isDark: true)
self.tooltipContainerNode.containerNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8)
self.textNode = ImmediateTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.textNode.maximumNumberOfLines = 0
self.measureTextNode = TextNode()
super.init(layerBacked: false, dynamicBounce: false)
self.clipsToBounds = true
self.addSubnode(self.containerNode)
self.tooltipContainerNode.containerNode.addSubnode(self.textNode)
self.addSubnode(self.tooltipContainerNode)
}
func asyncLayout() -> (_ item: ForwardPrivacyChatPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentNode = self.messageNode
let makeTextLayout = TextNode.asyncLayout(self.measureTextNode)
var currentBackgroundNode = self.backgroundNode
return { item, params, neighbors in
if currentBackgroundNode == nil {
currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false)
currentBackgroundNode?.update(wallpaper: item.wallpaper, animated: false)
currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners)
}
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1))
var peers = SimpleDictionary<PeerId, Peer>()
let messages = SimpleDictionary<MessageId, Message>()
peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: item.peerName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .preset(.blue), backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
let forwardInfo = MessageForwardInfo(author: item.linkEnabled ? peers[peerId] : nil, source: nil, sourceMessageId: nil, date: 0, authorSignature: item.linkEnabled ? nil : item.peerName, psaType: nil, flags: [])
let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: forwardInfo, author: nil, text: item.strings.Privacy_Forwards_PreviewMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)
var node: ListViewItemNode?
if let current = currentNode {
node = current
messageItem.updateNode(async: { $0() }, node: { return current }, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: current.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height))
current.contentSize = layout.contentSize
current.insets = layout.insets
current.frame = nodeFrame
apply(ListViewItemApply(isOnScreen: true))
})
} else {
messageItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { messageNode, apply in
node = messageNode
apply().1(ListViewItemApply(isOnScreen: true))
})
}
var contentSize = CGSize(width: params.width, height: 8.0 + 8.0)
if let node = node {
contentSize.height += node.frame.size.height
}
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
let attributedMeasureText = NSAttributedString(string: item.peerName, font: Font.regular(13.0), textColor: .black)
let (authorNameLayout, _) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedMeasureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let authorNameWidth = authorNameLayout.size.width
let authorNameCenter = authorNameWidth / 2.0 + 3.0
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
if let currentBackgroundNode {
currentBackgroundNode.update(wallpaper: item.wallpaper, animated: false)
currentBackgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners)
}
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
var topOffset: CGFloat = 8.0
if let node = node {
strongSelf.messageNode = node
if node.supernode == nil {
strongSelf.containerNode.addSubnode(node)
}
node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: node.frame.size), within: layout.contentSize)
topOffset += node.frame.size.height
}
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode {
strongSelf.backgroundNode = currentBackgroundNode
strongSelf.insertSubnode(currentBackgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
let displayMode: WallpaperDisplayMode
if abs(params.availableHeight - params.width) < 100.0, params.availableHeight > 700.0 {
displayMode = .halfAspectFill
} else {
if backgroundFrame.width > backgroundFrame.height * 4.0 {
if params.availableHeight < 700.0 {
displayMode = .halfAspectFill
} else {
displayMode = .aspectFill
}
} else {
displayMode = .aspectFill
}
}
if let backgroundNode = strongSelf.backgroundNode {
backgroundNode.frame = backgroundFrame.insetBy(dx: 0.0, dy: -100.0)
backgroundNode.update(wallpaper: item.wallpaper, animated: false)
backgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners)
backgroundNode.updateLayout(size: backgroundNode.bounds.size, displayMode: displayMode, transition: .immediate)
}
strongSelf.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
strongSelf.textNode.attributedText = NSAttributedString(string: item.tooltipText, font: Font.regular(14.0), textColor: .white, paragraphAlignment: .center)
var textSize = strongSelf.textNode.updateLayout(CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude))
textSize.width = ceil(textSize.width / 2.0) * 2.0
textSize.height = ceil(textSize.height / 2.0) * 2.0
let contentSize = CGSize(width: textSize.width + 12.0, height: textSize.height + 34.0)
var sourceRect: CGRect
if let messageNode = strongSelf.messageNode as? ChatMessagePreviewItemNode, let forwardInfoNode = messageNode.forwardInfoReferenceNode {
sourceRect = forwardInfoNode.convert(forwardInfoNode.bounds, to: strongSelf)
sourceRect.origin = CGPoint(x: sourceRect.minX + authorNameCenter, y: sourceRect.minY)
sourceRect.size.width = 0.0
} else {
sourceRect = CGRect(origin: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0), size: CGSize())
}
let verticalOrigin: CGFloat
var arrowOnBottom = true
if sourceRect.minY - 54.0 > 0.0 {
verticalOrigin = sourceRect.minY - contentSize.height
} else {
verticalOrigin = min(layout.size.height - contentSize.height, sourceRect.maxY)
arrowOnBottom = false
}
let horizontalOrigin: CGFloat = floor(min(max(params.leftInset + 8.0, sourceRect.midX - contentSize.width / 2.0), layout.size.width - contentSize.width - 8.0))
strongSelf.tooltipContainerNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin, y: verticalOrigin), size: contentSize)
strongSelf.tooltipContainerNode.relativeArrowPosition = (sourceRect.midX - horizontalOrigin, arrowOnBottom)
strongSelf.tooltipContainerNode.updateLayout(transition: .immediate)
let textFrame = CGRect(origin: CGPoint(x: 6.0, y: 17.0), size: textSize)
strongSelf.textNode.frame = textFrame
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,109 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import AccountContext
class GlobalAutoremoveHeaderItem: ListViewItem, ItemListItem {
let context: AccountContext
let theme: PresentationTheme
let sectionId: ItemListSectionId
init(context: AccountContext, theme: PresentationTheme, sectionId: ItemListSectionId) {
self.context = context
self.theme = theme
self.sectionId = sectionId
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = GlobalAutoremoveHeaderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? GlobalAutoremoveHeaderItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
private let titleFont = Font.regular(13.0)
class GlobalAutoremoveHeaderItemNode: ListViewItemNode {
private var animationNode: AnimatedStickerNode
private var item: GlobalAutoremoveHeaderItem?
init() {
self.animationNode = DefaultAnimatedStickerNodeImpl()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.animationNode)
}
func asyncLayout() -> (_ item: GlobalAutoremoveHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, neighbors in
//let leftInset: CGFloat = 32.0 + params.leftInset
let topInset: CGFloat = 110.0
let contentSize = CGSize(width: params.width, height: topInset)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
if strongSelf.item == nil {
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "GlobalAutoRemove"), width: 220, height: 220, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
strongSelf.animationNode.visibility = true
}
strongSelf.item = item
let iconSize = CGSize(width: 110.0, height: 110.0)
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
strongSelf.animationNode.updateLayout(size: iconSize)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,465 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import ItemListPeerActionItem
import DeviceAccess
import QrCodeUI
import ChatTimerScreen
import UndoUI
private final class GlobalAutoremoveScreenArguments {
let context: AccountContext
let updateValue: (Int32) -> Void
let openCustomValue: () -> Void
let infoLinkAction: () -> Void
init(
context: AccountContext,
updateValue: @escaping (Int32) -> Void,
openCustomValue: @escaping () -> Void,
infoLinkAction: @escaping () -> Void
) {
self.context = context
self.updateValue = updateValue
self.openCustomValue = openCustomValue
self.infoLinkAction = infoLinkAction
}
}
private enum GlobalAutoremoveSection: Int32 {
case header
case general
}
private enum GlobalAutoremoveEntry: ItemListNodeEntry {
case header
case sectionHeader(String)
case timerOption(value: Int32, text: String, isSelected: Bool)
case customAction(String)
case info(String)
var section: ItemListSectionId {
switch self {
case .header:
return GlobalAutoremoveSection.header.rawValue
case .sectionHeader, .timerOption, .customAction, .info:
return GlobalAutoremoveSection.general.rawValue
}
}
var stableId: Int {
return self.sortIndex
}
var sortIndex: Int {
switch self {
case .header:
return 0
case .sectionHeader:
return 1
case let .timerOption(value, _, _):
return 1000 + Int(value)
case .customAction:
return Int.max - 1000 + 0
case .info:
return Int.max - 1000 + 1
}
}
static func ==(lhs: GlobalAutoremoveEntry, rhs: GlobalAutoremoveEntry) -> Bool {
switch lhs {
case .header:
if case .header = rhs {
return true
} else {
return false
}
case let .sectionHeader(text):
if case .sectionHeader(text) = rhs {
return true
} else {
return false
}
case let .timerOption(value, text, isSelected):
if case .timerOption(value, text, isSelected) = rhs {
return true
} else {
return false
}
case let .customAction(text):
if case .customAction(text) = rhs {
return true
} else {
return false
}
case let .info(text):
if case .info(text) = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: GlobalAutoremoveEntry, rhs: GlobalAutoremoveEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! GlobalAutoremoveScreenArguments
switch self {
case .header:
return GlobalAutoremoveHeaderItem(context: arguments.context, theme: presentationData.theme, sectionId: self.section)
case let .sectionHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .timerOption(value, text, isSelected):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .right, checked: isSelected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateValue(value)
})
case let .customAction(text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openCustomValue()
})
case let .info(text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in
arguments.infoLinkAction()
})
}
}
}
private struct GlobalAutoremoveScreenState: Equatable {
var additionalValues: Set<Int32>
var updatedValue: Int32
}
private func globalAutoremoveScreenEntries(presentationData: PresentationData, state: GlobalAutoremoveScreenState) -> [GlobalAutoremoveEntry] {
var entries: [GlobalAutoremoveEntry] = []
entries.append(.header)
let effectiveCurrentValue = state.updatedValue
entries.append(.sectionHeader(presentationData.strings.GlobalAutodeleteSettings_OptionsHeader))
var values: [Int32] = [
0,
1 * 24 * 60 * 60,
7 * 24 * 60 * 60,
31 * 24 * 60 * 60
]
if !values.contains(effectiveCurrentValue) {
values.append(effectiveCurrentValue)
}
for value in state.additionalValues {
if !values.contains(value) {
values.append(value)
}
}
values.sort()
for value in values {
let text: String
if value == 0 {
text = presentationData.strings.Autoremove_OptionOff
} else {
text = presentationData.strings.GlobalAutodeleteSettings_OptionTitle(timeIntervalString(strings: presentationData.strings, value: value, usage: .afterTime)).string
}
entries.append(.timerOption(value: value, text: text, isSelected: effectiveCurrentValue == value))
}
entries.append(.customAction(presentationData.strings.GlobalAutodeleteSettings_SetCustomTime))
if effectiveCurrentValue == 0 {
entries.append(.info(presentationData.strings.GlobalAutodeleteSettings_InfoDisabled))
} else {
entries.append(.info(presentationData.strings.GlobalAutodeleteSettings_InfoEnabled(timeIntervalString(strings: presentationData.strings, value: effectiveCurrentValue, usage: .afterTime)).string))
}
return entries
}
public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32, updated: @escaping (Int32) -> Void) -> ViewController {
let initialState = GlobalAutoremoveScreenState(
additionalValues: Set([initialValue]),
updatedValue: initialValue
)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((GlobalAutoremoveScreenState) -> GlobalAutoremoveScreenState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var presentInCurrentControllerImpl: ((ViewController) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
var getController: (() -> ViewController?)?
let _ = dismissImpl
let _ = pushControllerImpl
let _ = presentControllerImpl
let _ = updateState
let actionsDisposable = DisposableSet()
let updateTimeoutDisposable = MetaDisposable()
actionsDisposable.add(updateTimeoutDisposable)
let updateValue: (Int32) -> Void = { timeout in
let apply: (Int32) -> Void = { timeout in
updateState { state in
var state = state
state.updatedValue = timeout
if timeout != 0 {
state.additionalValues.removeAll()
state.additionalValues.insert(timeout)
}
return state
}
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
var isOn: Bool = true
var text: String?
if timeout != 0 {
text = presentationData.strings.GlobalAutodeleteSettings_SetConfirmToastEnabled(timeIntervalString(strings: presentationData.strings, value: timeout, usage: .afterTime)).string
} else {
isOn = false
text = presentationData.strings.GlobalAutodeleteSettings_SetConfirmToastDisabled
}
if let text = text {
var animateAsReplacement = false
if let window = getController?()?.window {
window.forEachController { other in
if let other = other as? UndoOverlayController {
animateAsReplacement = true
other.dismiss()
}
}
}
if let current = getController?() {
current.forEachController { other in
if let other = other as? UndoOverlayController {
animateAsReplacement = true
other.dismiss()
}
return true
}
}
presentInCurrentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: animateAsReplacement, action: { _ in return false }))
}
updateTimeoutDisposable.set((context.engine.privacy.updateGlobalMessageRemovalTimeout(timeout: timeout == 0 ? nil : timeout)
|> deliverOnMainQueue).start(completed: {
updated(timeout)
}))
}
if timeout == 0 || stateValue.with({ $0 }).updatedValue != 0 {
apply(timeout)
} else {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let valueText = timeIntervalString(strings: presentationData.strings, value: timeout, usage: .afterTime)
presentControllerImpl?(standardTextAlertController(
theme: AlertControllerTheme(presentationData: presentationData),
title: presentationData.strings.GlobalAutodeleteSettings_SetConfirmTitle,
text: presentationData.strings.GlobalAutodeleteSettings_SetConfirmText(valueText).string,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.GlobalAutodeleteSettings_SetConfirmAction, action: {
apply(timeout)
}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {})
],
actionLayout: .vertical
), nil)
}
}
let arguments = GlobalAutoremoveScreenArguments(
context: context,
updateValue: { value in
updateValue(value)
},
openCustomValue: {
let currentValue = stateValue.with({ $0 }).updatedValue
let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .autoremove, currentTime: currentValue == 0 ? nil : currentValue, dismissByTapOutside: true, completion: { value in
updateValue(value)
})
presentControllerImpl?(controller, nil)
},
infoLinkAction: {
let value = stateValue.with({ $0 }).updatedValue
if value == 0 {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let valueText = timeIntervalString(strings: presentationData.strings, value: value, usage: .timer)
let selectionController = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(
context: context,
mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: presentationData.strings.GlobalAutodeleteSettings_ApplyChatsTitle,
searchPlaceholder: presentationData.strings.GlobalAutodeleteSettings_ApplyChatsPlaceholder(valueText).string,
selectedChats: Set(),
additionalCategories: nil,
chatListFilters: nil,
displayAutoremoveTimeout: true
)),
filters: [.excludeSelf],
isPeerEnabled: { peer in
var canManage = false
if case let .user(user) = peer {
if user.botInfo == nil {
canManage = true
}
if user.id.isRepliesOrSavedMessages(accountPeerId: context.account.peerId) {
return false
}
} else if case .secretChat = peer {
canManage = true
} else if case let .legacyGroup(group) = peer {
canManage = !group.hasBannedPermission(.banChangeInfo)
} else if case let .channel(channel) = peer {
canManage = channel.hasPermission(.changeInfo)
}
return canManage
},
attemptDisabledItemSelection: { peer, _ in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text: String
if case let .channel(channel) = peer {
if case .group = channel.info {
text = presentationData.strings.GlobalAutodeleteSettings_AttemptDisabledGroupSelection
} else {
text = presentationData.strings.GlobalAutodeleteSettings_AttemptDisabledChannelSelection
}
} else if case .legacyGroup = peer {
text = presentationData.strings.GlobalAutodeleteSettings_AttemptDisabledGroupSelection
} else {
text = presentationData.strings.GlobalAutodeleteSettings_AttemptDisabledGenericSelection
}
presentControllerImpl?(standardTextAlertController(
theme: AlertControllerTheme(presentationData: presentationData),
title: nil,
text: text,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
]
), nil)
}
))
selectionController.navigationPresentation = .modal
let _ = (selectionController.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak selectionController] result in
var contacts: [ContactListPeerId] = []
if case let .result(peerIdsValue, _) = result {
contacts = peerIdsValue
}
let peerIds = contacts.compactMap { item -> EnginePeer.Id? in
switch item {
case let .peer(id):
return id
case .deviceContact:
return nil
}
}
if peerIds.isEmpty {
selectionController?.dismiss()
} else {
selectionController?.displayProgress = true
let _ = (context.engine.peers.setChatMessageAutoremoveTimeouts(peerIds: peerIds, timeout: value)
|> deliverOnMainQueue).start(completed: {
selectionController?.dismiss()
let isOn: Bool = true
let text = presentationData.strings.GlobalAutodeleteSettings_ApplyChatsToast(timeIntervalString(strings: presentationData.strings, value: value, usage: .timer), presentationData.strings.GlobalAutodeleteSettings_ApplyChatsSubject(Int32(peerIds.count))).string
var animateAsReplacement = false
if let window = getController?()?.window {
window.forEachController { other in
if let other = other as? UndoOverlayController {
animateAsReplacement = true
other.dismiss()
}
}
}
if let current = getController?() {
current.forEachController { other in
if let other = other as? UndoOverlayController {
animateAsReplacement = true
other.dismiss()
}
return true
}
}
presentInCurrentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: animateAsReplacement, action: { _ in return false }))
})
}
})
pushControllerImpl?(selectionController)
}
)
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
statePromise.get()
)
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let rightNavigationButton: ItemListNavigationButton? = nil
let title: ItemListControllerTitle = .text(presentationData.strings.GlobalAutodeleteSettings_Title)
let entries: [GlobalAutoremoveEntry] = globalAutoremoveScreenEntries(presentationData: presentationData, state: state)
let animateChanges = false
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges, scrollEnabled: true)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
getController = { [weak controller] in
return controller
}
presentControllerImpl = { [weak controller] c, p in
guard let controller else {
return
}
controller.present(c, in: .window(.root), with: p)
}
presentInCurrentControllerImpl = { [weak controller] c in
guard let controller else {
return
}
controller.present(c, in: .current, with: nil)
}
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
return controller
}
@@ -0,0 +1,472 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting
import ItemListUI
import PresentationDataUtils
import AccountContext
import UndoUI
import PremiumUI
import MessagePriceItem
private final class IncomingMessagePrivacyScreenArguments {
let context: AccountContext
let updateValue: (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void
let disabledValuePressed: () -> Void
let infoLinkAction: () -> Void
let openExceptions: () -> Void
let openPremiumInfo: () -> Void
let openSetCustomStarsAmount: () -> Void
init(
context: AccountContext,
updateValue: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void,
disabledValuePressed: @escaping () -> Void,
infoLinkAction: @escaping () -> Void,
openExceptions: @escaping () -> Void,
openPremiumInfo: @escaping () -> Void,
openSetCustomStarsAmount: @escaping () -> Void
) {
self.context = context
self.updateValue = updateValue
self.disabledValuePressed = disabledValuePressed
self.infoLinkAction = infoLinkAction
self.openExceptions = openExceptions
self.openPremiumInfo = openPremiumInfo
self.openSetCustomStarsAmount = openSetCustomStarsAmount
}
}
private enum IncomingMessagePrivacySection: Int32 {
case header
case info
case price
case exceptions
}
private enum GlobalAutoremoveEntry: ItemListNodeEntry {
case header
case optionEverybody(value: GlobalPrivacySettings.NonContactChatsPrivacy)
case optionPremium(value: GlobalPrivacySettings.NonContactChatsPrivacy, isEnabled: Bool)
case optionChargeForMessages(value: GlobalPrivacySettings.NonContactChatsPrivacy, isEnabled: Bool)
case footer(value: GlobalPrivacySettings.NonContactChatsPrivacy)
case priceHeader
case price(value: Int64, maxValue: Int64, price: String, isEnabled: Bool)
case priceInfo(commission: Int32, value: String)
case exceptionsHeader
case exceptions(count: Int)
case exceptionsInfo
case info
var section: ItemListSectionId {
switch self {
case .header, .optionEverybody, .optionPremium, .optionChargeForMessages, .footer:
return IncomingMessagePrivacySection.header.rawValue
case .info:
return IncomingMessagePrivacySection.info.rawValue
case .priceHeader, .price, .priceInfo:
return IncomingMessagePrivacySection.price.rawValue
case .exceptionsHeader, .exceptions, .exceptionsInfo:
return IncomingMessagePrivacySection.exceptions.rawValue
}
}
var stableId: Int {
return self.sortIndex
}
var sortIndex: Int {
switch self {
case .header:
return 0
case .optionEverybody:
return 1
case .optionPremium:
return 2
case .optionChargeForMessages:
return 3
case .footer:
return 4
case .info:
return 5
case .priceHeader:
return 6
case .price:
return 7
case .priceInfo:
return 8
case .exceptionsHeader:
return 9
case .exceptions:
return 10
case .exceptionsInfo:
return 11
}
}
static func <(lhs: GlobalAutoremoveEntry, rhs: GlobalAutoremoveEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! IncomingMessagePrivacyScreenArguments
switch self {
case .header:
return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.Privacy_Messages_SectionTitle, sectionId: self.section)
case let .optionEverybody(value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: presentationData.strings.Privacy_Messages_ValueEveryone, style: .left, checked: value == .everybody, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateValue(.everybody)
})
case let .optionPremium(value, isEnabled):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, icon: isEnabled ? nil : generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/Lock"), color: presentationData.theme.list.itemSecondaryTextColor), iconPlacement: .check, title: presentationData.strings.Privacy_Messages_ValueContactsAndPremium, style: .left, checked: isEnabled && value == .requirePremium, zeroSeparatorInsets: false, sectionId: self.section, action: {
if isEnabled {
arguments.updateValue(.requirePremium)
} else {
arguments.disabledValuePressed()
}
})
case let .optionChargeForMessages(value, isEnabled):
var isChecked = false
if case .paidMessages = value {
isChecked = true
}
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, icon: isEnabled || isChecked ? nil : generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/Lock"), color: presentationData.theme.list.itemSecondaryTextColor), iconPlacement: .check, title: presentationData.strings.Privacy_Messages_ChargeForMessages, style: .left, checked: isChecked, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateValue(.paidMessages(StarsAmount(value: 400, nanos: 0)))
})
case let .footer(value):
let text: String
if case .paidMessages = value {
text = presentationData.strings.Privacy_Messages_ChargeForMessagesInfo
} else {
text = presentationData.strings.Privacy_Messages_SectionFooter
}
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case .info:
return ItemListTextItem(presentationData: presentationData, text: .markdown(presentationData.strings.Privacy_Messages_PremiumInfoFooter), sectionId: self.section, linkAction: { _ in
arguments.infoLinkAction()
})
case .priceHeader:
return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.Privacy_Messages_MessagePrice, sectionId: self.section)
case let .price(value, maxValue, price, isEnabled):
return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, systemStyle: .glass, isEnabled: isEnabled, minValue: 1, maxValue: maxValue, value: value, price: price, sectionId: self.section, updated: { value, _ in
arguments.updateValue(.paidMessages(StarsAmount(value: value, nanos: 0)))
}, openSetCustom: {
arguments.openSetCustomStarsAmount()
}, openPremiumInfo: {
arguments.openPremiumInfo()
})
case let .priceInfo(commission, value):
return ItemListTextItem(presentationData: presentationData, text: .markdown(presentationData.strings.Privacy_Messages_MessagePriceInfo("\(commission)", value).string), sectionId: self.section)
case .exceptionsHeader:
return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.Privacy_Messages_RemoveFeeHeader, sectionId: self.section)
case let .exceptions(count):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: presentationData.strings.Privacy_Messages_RemoveFee, label: count > 0 ? "\(count)" : "", sectionId: self.section, style: .blocks, action: {
arguments.openExceptions()
})
case .exceptionsInfo:
return ItemListTextItem(presentationData: presentationData, text: .markdown(presentationData.strings.Privacy_Messages_RemoveFeeInfo), sectionId: self.section)
}
}
}
private struct IncomingMessagePrivacyScreenState: Equatable {
var updatedValue: GlobalPrivacySettings.NonContactChatsPrivacy
var disableFor: [EnginePeer.Id: SelectivePrivacyPeer]
}
private func incomingMessagePrivacyScreenEntries(presentationData: PresentationData, state: IncomingMessagePrivacyScreenState, enableSetting: Bool, isPremium: Bool, configuration: StarsSubscriptionConfiguration) -> [GlobalAutoremoveEntry] {
var entries: [GlobalAutoremoveEntry] = []
entries.append(.header)
entries.append(.optionEverybody(value: state.updatedValue))
entries.append(.optionPremium(value: state.updatedValue, isEnabled: enableSetting))
if configuration.paidMessagesAvailable {
entries.append(.optionChargeForMessages(value: state.updatedValue, isEnabled: isPremium))
}
if case let .paidMessages(amount) = state.updatedValue {
entries.append(.footer(value: state.updatedValue))
entries.append(.priceHeader)
let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0
let price = "~\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
entries.append(.price(value: amount.value, maxValue: configuration.paidMessageMaxAmount, price: price, isEnabled: isPremium))
entries.append(.priceInfo(commission: configuration.paidMessageCommissionPermille / 10, value: price))
if isPremium {
entries.append(.exceptionsHeader)
entries.append(.exceptions(count: state.disableFor.count))
entries.append(.exceptionsInfo)
}
} else {
entries.append(.footer(value: state.updatedValue))
entries.append(.info)
}
return entries
}
public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void) -> ViewController {
var disableFor: [EnginePeer.Id: SelectivePrivacyPeer] = [:]
if case let .enableContacts(value, _, _, _) = exceptions {
disableFor = value
}
let initialState = IncomingMessagePrivacyScreenState(
updatedValue: value,
disableFor: disableFor
)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((IncomingMessagePrivacyScreenState) -> IncomingMessagePrivacyScreenState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let configuration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var presentInCurrentControllerImpl: ((ViewController) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
let _ = dismissImpl
let _ = pushControllerImpl
let _ = presentControllerImpl
let actionsDisposable = DisposableSet()
let addPeerDisposable = MetaDisposable()
actionsDisposable.add(addPeerDisposable)
let updateTimeoutDisposable = MetaDisposable()
actionsDisposable.add(updateTimeoutDisposable)
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
let arguments = IncomingMessagePrivacyScreenArguments(
context: context,
updateValue: { value in
updateState { state in
var state = state
state.updatedValue = value
return state
}
},
disabledValuePressed: {
presentInCurrentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: presentationData.strings.Privacy_Messages_PremiumToast_Title, text: presentationData.strings.Privacy_Messages_PremiumToast_Text, customUndoText: presentationData.strings.Privacy_Messages_PremiumToast_Action, timeout: nil, linkAction: { _ in
}), elevatedLayout: false, action: { action in
if case .undo = action {
let controller = PremiumIntroScreen(context: context, source: .settings)
pushControllerImpl?(controller)
}
return false
}))
},
infoLinkAction: {
let controller = PremiumIntroScreen(context: context, source: .settings)
pushControllerImpl?(controller)
},
openExceptions: {
var peerIds: [EnginePeer.Id: SelectivePrivacyPeer] = [:]
updateState { state in
peerIds = state.disableFor
return state
}
if peerIds.isEmpty {
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: presentationData.strings.PrivacySettings_SearchUsersTitle,
searchPlaceholder: presentationData.strings.PrivacySettings_SearchUsersPlaceholder,
selectedChats: Set(),
additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: [], selectedCategories: Set()),
chatListFilters: nil,
onlyUsers: false,
disableChannels: true,
disableBots: true,
disableContacts: true
))))
addPeerDisposable.set((controller.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] result in
var peerIds: [ContactListPeerId] = []
if case let .result(peerIdsValue, _) = result {
peerIds = peerIdsValue
}
if peerIds.isEmpty {
controller?.dismiss()
return
}
let filteredIds = peerIds.compactMap { peerId -> EnginePeer.Id? in
if case let .peer(value) = peerId {
return value
} else {
return nil
}
}
let _ = (context.engine.data.get(
EngineDataMap(filteredIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)),
EngineDataMap(filteredIds.map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
)
|> map { peerMap, participantCountMap -> [EnginePeer.Id: SelectivePrivacyPeer] in
var updatedPeers: [EnginePeer.Id: SelectivePrivacyPeer] = [:]
var existingIds = Set(updatedPeers.values.map { $0.peer.id })
for peerId in peerIds {
guard case let .peer(peerId) = peerId else {
continue
}
if let maybePeer = peerMap[peerId], let peer = maybePeer, !existingIds.contains(peerId) {
existingIds.insert(peerId)
var participantCount: Int32?
if case let .channel(channel) = peer, case .group = channel.info {
if let maybeParticipantCount = participantCountMap[peerId], let participantCountValue = maybeParticipantCount {
participantCount = Int32(participantCountValue)
}
}
updatedPeers[peer.id] = SelectivePrivacyPeer(peer: peer._asPeer(), participantCount: participantCount)
}
}
return updatedPeers
}
|> deliverOnMainQueue).start(next: { updatedPeerIds in
controller?.dismiss()
updateState { state in
var updatedState = state
updatedState.disableFor = updatedPeerIds
return updatedState
}
let settings: SelectivePrivacySettings = .enableContacts(enableFor: updatedPeerIds, disableFor: [:], enableForPremium: false, enableForBots: false)
let _ = context.engine.privacy.updateSelectiveAccountPrivacySettings(type: .noPaidMessages, settings: settings).start()
})
}))
controller.navigationPresentation = .modal
pushControllerImpl?(controller)
} else {
let controller = selectivePrivacyPeersController(context: context, title: presentationData.strings.Privacy_Messages_Exceptions_Title, footer: presentationData.strings.Privacy_Messages_RemoveFeeInfo, hideContacts: true, initialPeers: peerIds, initialEnableForPremium: false, displayPremiumCategory: false, initialEnableForBots: false, displayBotsCategory: false, updated: { updatedPeerIds, _, _ in
updateState { state in
var updatedState = state
updatedState.disableFor = updatedPeerIds
return updatedState
}
let settings: SelectivePrivacySettings = .enableContacts(enableFor: updatedPeerIds, disableFor: [:], enableForPremium: false, enableForBots: false)
let _ = context.engine.privacy.updateSelectiveAccountPrivacySettings(type: .noPaidMessages, settings: settings).start()
})
pushControllerImpl?(controller)
}
},
openPremiumInfo: {
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .messagePrivacy, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .messageEffects, forceDark: false, dismissed: nil)
replaceImpl?(controller)
}, dismissed: nil)
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
},
openSetCustomStarsAmount: {
var currentAmount: StarsAmount = StarsAmount(value: 1, nanos: 0)
if case let .paidMessages(value) = stateValue.with({ $0 }).updatedValue {
currentAmount = value
}
let starsScreen = context.sharedContext.makeStarsWithdrawalScreen(context: context, subject: .enterAmount(
current: currentAmount,
minValue: StarsAmount(value: 1, nanos: 0),
fractionAfterCommission: 80, kind: .privacy,
completion: { amount in
updateState { state in
var state = state
state.updatedValue = .paidMessages(StarsAmount(value: amount, nanos: 0))
return state
}
}
))
pushControllerImpl?(starsScreen)
}
)
let enableSetting: Signal<Bool, NoError> = context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
TelegramEngine.EngineData.Item.Configuration.App()
)
|> map { accountPeer, appConfig -> Bool in
if let accountPeer, accountPeer.isPremium {
return true
}
if let data = appConfig.data, let setting = data["new_noncontact_peers_require_premium_without_ownpremium"] as? Bool {
if setting {
return true
}
}
return false
}
|> distinctUntilChanged
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
statePromise.get(),
enableSetting
)
|> map { presentationData, state, enableSetting -> (ItemListControllerState, (ItemListNodeState, Any)) in
let rightNavigationButton: ItemListNavigationButton? = nil
let title: ItemListControllerTitle = .text(presentationData.strings.Privacy_Messages_Title)
let entries: [GlobalAutoremoveEntry] = incomingMessagePrivacyScreenEntries(presentationData: presentationData, state: state, enableSetting: enableSetting, isPremium: context.isPremium, configuration: configuration)
let animateChanges = false
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges, scrollEnabled: true)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
presentControllerImpl = { [weak controller] c, p in
guard let controller else {
return
}
controller.present(c, in: .window(.root), with: p)
}
presentInCurrentControllerImpl = { [weak controller] c in
guard let controller else {
return
}
controller.forEachController { c in
if let c = c as? UndoOverlayController {
c.dismiss()
}
return true
}
controller.present(c, in: .current, with: nil)
}
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
controller.attemptNavigation = { _ in
let updatedValue = stateValue.with({ $0 }).updatedValue
if !context.isPremium, case .paidMessages = updatedValue {
} else {
update(updatedValue)
}
return true
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
return controller
}
@@ -0,0 +1,219 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramPresentationData
import AuthorizationUI
import AuthenticationServices
import UndoUI
final class LoginEmailSetupDelegate: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
var authorizationCompletion: ((Any) -> Void)?
private var context: AccountContext
init(context: AccountContext) {
self.context = context
}
@available(iOS 13.0, *)
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
self.authorizationCompletion?(authorization.credential)
}
@available(iOS 13.0, *)
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
Logger.shared.log("AppleSignIn", "Failed with error: \(error.localizedDescription)")
}
@available(iOS 13.0, *)
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return self.context.sharedContext.mainWindow!.viewController!.view.window!
}
}
public func loginEmailSetupController(context: AccountContext, blocking: Bool, emailPattern: String?, canAutoDismissIfNeeded: Bool = false, navigationController: NavigationController?, completion: @escaping () -> Void, dismiss: @escaping () -> Void) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var dismissEmailControllerImpl: (() -> Void)?
var presentControllerImpl: ((ViewController) -> Void)?
let delegate = LoginEmailSetupDelegate(context: context)
let emailChangeCompletion: (AuthorizationSequenceCodeEntryController?) -> Void = { codeController in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
codeController?.animateSuccess()
completion()
Queue.mainQueue().after(0.75) {
if let navigationController {
let controllers = navigationController.viewControllers.filter { controller in
if controller is AuthorizationSequenceEmailEntryController || controller is AuthorizationSequenceCodeEntryController {
return false
} else {
return true
}
}
navigationController.setViewControllers(controllers, animated: true)
Queue.mainQueue().after(0.5, {
navigationController.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: presentationData.strings.LoginEmail_Success_Title, text: presentationData.strings.LoginEmail_Success_Text, cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in
return false
}))
})
}
}
}
let emailController = AuthorizationSequenceEmailEntryController(context: canAutoDismissIfNeeded ? context : nil, presentationData: presentationData, mode: emailPattern != nil ? .change : .setup, blocking: blocking, back: {
dismissEmailControllerImpl?()
})
emailController.proceedWithEmail = { [weak emailController] email in
emailController?.inProgress = true
let _ = (sendLoginEmailChangeCode(account: context.account, email: email)
|> deliverOnMainQueue).start(next: { data in
var dismissCodeControllerImpl: (() -> Void)?
var presentControllerImpl: ((ViewController) -> Void)?
let codeController = AuthorizationSequenceCodeEntryController(presentationData: presentationData, back: {
dismissCodeControllerImpl?()
dismiss()
})
presentControllerImpl = { [weak codeController] c in
codeController?.present(c, in: .window(.root), with: nil)
}
codeController.loginWithCode = { [weak codeController] code in
let _ = (verifyLoginEmailChange(account: context.account, code: .emailCode(code))
|> deliverOnMainQueue).start(error: { error in
Queue.mainQueue().async {
codeController?.inProgress = false
if case .invalidCode = error {
codeController?.animateError(text: presentationData.strings.Login_WrongCodeError)
} else {
var resetCode = false
let text: String
switch error {
case .limitExceeded:
resetCode = true
text = presentationData.strings.Login_CodeFloodError
case .invalidCode:
resetCode = true
text = presentationData.strings.Login_InvalidCodeError
case .generic:
text = presentationData.strings.Login_UnknownError
case .codeExpired:
text = presentationData.strings.Login_CodeExpired
case .timeout:
text = presentationData.strings.Login_NetworkError
case .invalidEmailToken:
text = presentationData.strings.Login_InvalidEmailTokenError
case .emailNotAllowed:
text = presentationData.strings.Login_EmailNotAllowedError
}
if resetCode {
codeController?.resetCode()
}
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]))
}
}
}, completed: { [weak codeController] in
emailChangeCompletion(codeController)
})
}
codeController.updateData(number: "", email: email, codeType: .email(emailPattern: "", length: data.length, resetAvailablePeriod: nil, resetPendingDate: nil, appleSignInAllowed: false, setup: true), nextType: nil, timeout: nil, termsOfService: nil, previousCodeType: nil, isPrevious: false)
navigationController?.pushViewController(codeController)
dismissCodeControllerImpl = { [weak codeController] in
codeController?.dismiss()
}
}, error: { [weak emailController] error in
emailController?.inProgress = false
let text: String
switch error {
case .limitExceeded:
text = presentationData.strings.Login_CodeFloodError
case .generic, .codeExpired:
text = presentationData.strings.Login_UnknownError
case .timeout:
text = presentationData.strings.Login_NetworkError
case .invalidEmail:
text = presentationData.strings.Login_InvalidEmailError
case .emailNotAllowed:
text = presentationData.strings.Login_EmailNotAllowedError
}
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]))
}, completed: { [weak emailController] in
emailController?.inProgress = false
})
}
emailController.signInWithApple = { [weak emailController] in
if #available(iOS 13.0, *) {
let appleIdProvider = ASAuthorizationAppleIDProvider()
let request = appleIdProvider.createRequest()
request.requestedScopes = [.email]
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = delegate
authorizationController.presentationContextProvider = delegate
authorizationController.performRequests()
emailController?.authorization = authorizationController
emailController?.authorizationDelegate = delegate
delegate.authorizationCompletion = { [weak emailController] credential in
guard let credential = credential as? ASAuthorizationCredential else {
return
}
switch credential {
case let appleIdCredential as ASAuthorizationAppleIDCredential:
guard let tokenData = appleIdCredential.identityToken, let token = String(data: tokenData, encoding: .utf8) else {
emailController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
let _ = (verifyLoginEmailChange(account: context.account, code: .appleToken(token))
|> deliverOnMainQueue).start(error: { error in
let text: String
switch error {
case .limitExceeded:
text = presentationData.strings.Login_CodeFloodError
case .generic, .codeExpired:
text = presentationData.strings.Login_UnknownError
case .invalidCode:
text = presentationData.strings.Login_InvalidCodeError
case .timeout:
text = presentationData.strings.Login_NetworkError
case .invalidEmailToken:
text = presentationData.strings.Login_InvalidEmailTokenError
case .emailNotAllowed:
text = presentationData.strings.Login_EmailNotAllowedError
}
emailController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, completed: { [weak emailController] in
emailController?.authorization = nil
emailController?.authorizationDelegate = nil
emailChangeCompletion(nil)
})
default:
break
}
}
}
}
emailController.updateData(appleSignInAllowed: true)
presentControllerImpl = { [weak emailController] c in
emailController?.present(c, in: .window(.root), with: nil)
}
dismissEmailControllerImpl = { [weak emailController] in
dismiss()
emailController?.dismiss()
}
return emailController
}
@@ -0,0 +1,512 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import LegacyComponents
import LocalAuthentication
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import LocalAuth
import PasscodeUI
import TelegramStringFormatting
import TelegramIntents
private final class PasscodeOptionsControllerArguments {
let turnPasscodeOff: () -> Void
let changePasscode: () -> Void
let changePasscodeTimeout: () -> Void
let changeTouchId: (Bool) -> Void
init(turnPasscodeOff: @escaping () -> Void, changePasscode: @escaping () -> Void, changePasscodeTimeout: @escaping () -> Void, changeTouchId: @escaping (Bool) -> Void) {
self.turnPasscodeOff = turnPasscodeOff
self.changePasscode = changePasscode
self.changePasscodeTimeout = changePasscodeTimeout
self.changeTouchId = changeTouchId
}
}
private enum PasscodeOptionsSection: Int32 {
case setting
case options
}
private enum PasscodeOptionsEntry: ItemListNodeEntry {
case togglePasscode(PresentationTheme, String, Bool)
case changePasscode(PresentationTheme, String)
case settingInfo(PresentationTheme, String)
case autoLock(PresentationTheme, String, String)
case touchId(PresentationTheme, String, Bool)
var section: ItemListSectionId {
switch self {
case .togglePasscode, .changePasscode, .settingInfo:
return PasscodeOptionsSection.setting.rawValue
case .autoLock, .touchId:
return PasscodeOptionsSection.options.rawValue
}
}
var stableId: Int32 {
switch self {
case .togglePasscode:
return 0
case .changePasscode:
return 1
case .settingInfo:
return 2
case .autoLock:
return 3
case .touchId:
return 4
}
}
static func ==(lhs: PasscodeOptionsEntry, rhs: PasscodeOptionsEntry) -> Bool {
switch lhs {
case let .togglePasscode(lhsTheme, lhsText, lhsValue):
if case let .togglePasscode(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .changePasscode(lhsTheme, lhsText):
if case let .changePasscode(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .settingInfo(lhsTheme, lhsText):
if case let .settingInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .autoLock(lhsTheme, lhsText, lhsValue):
if case let .autoLock(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .touchId(lhsTheme, lhsText, lhsValue):
if case let .touchId(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
}
}
static func <(lhs: PasscodeOptionsEntry, rhs: PasscodeOptionsEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! PasscodeOptionsControllerArguments
switch self {
case let .togglePasscode(_, title, value):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
if value {
arguments.turnPasscodeOff()
}
})
case let .changePasscode(_, title):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.changePasscode()
})
case let .settingInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .autoLock(_, title, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, label: value, sectionId: self.section, style: .blocks, action: {
arguments.changePasscodeTimeout()
})
case let .touchId(_, title, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.changeTouchId(value)
})
}
}
}
private struct PasscodeOptionsControllerState: Equatable {
static func ==(lhs: PasscodeOptionsControllerState, rhs: PasscodeOptionsControllerState) -> Bool {
return true
}
}
private struct PasscodeOptionsData: Equatable {
let accessChallenge: PostboxAccessChallengeData
let presentationSettings: PresentationPasscodeSettings
init(accessChallenge: PostboxAccessChallengeData, presentationSettings: PresentationPasscodeSettings) {
self.accessChallenge = accessChallenge
self.presentationSettings = presentationSettings
}
static func ==(lhs: PasscodeOptionsData, rhs: PasscodeOptionsData) -> Bool {
return lhs.accessChallenge == rhs.accessChallenge && lhs.presentationSettings == rhs.presentationSettings
}
func withUpdatedAccessChallenge(_ accessChallenge: PostboxAccessChallengeData) -> PasscodeOptionsData {
return PasscodeOptionsData(accessChallenge: accessChallenge, presentationSettings: self.presentationSettings)
}
func withUpdatedPresentationSettings(_ presentationSettings: PresentationPasscodeSettings) -> PasscodeOptionsData {
return PasscodeOptionsData(accessChallenge: self.accessChallenge, presentationSettings: presentationSettings)
}
}
private func autolockStringForTimeout(strings: PresentationStrings, timeout: Int32?) -> String {
if let timeout = timeout {
if timeout == 10 {
return "If away for 10 seconds"
} else if timeout == 1 * 60 {
return strings.PasscodeSettings_AutoLock_IfAwayFor_1minute
} else if timeout == 5 * 60 {
return strings.PasscodeSettings_AutoLock_IfAwayFor_5minutes
} else if timeout == 1 * 60 * 60 {
return strings.PasscodeSettings_AutoLock_IfAwayFor_1hour
} else if timeout == 5 * 60 * 60 {
return strings.PasscodeSettings_AutoLock_IfAwayFor_5hours
} else {
return ""
}
} else {
return strings.PasscodeSettings_AutoLock_Disabled
}
}
private func passcodeOptionsControllerEntries(presentationData: PresentationData, state: PasscodeOptionsControllerState, passcodeOptionsData: PasscodeOptionsData) -> [PasscodeOptionsEntry] {
var entries: [PasscodeOptionsEntry] = []
switch passcodeOptionsData.accessChallenge {
case .none:
entries.append(.togglePasscode(presentationData.theme, presentationData.strings.PasscodeSettings_TurnPasscodeOn, false))
entries.append(.settingInfo(presentationData.theme, presentationData.strings.PasscodeSettings_Help))
case .numericalPassword, .plaintextPassword:
entries.append(.togglePasscode(presentationData.theme, presentationData.strings.PasscodeSettings_TurnPasscodeOff, true))
entries.append(.changePasscode(presentationData.theme, presentationData.strings.PasscodeSettings_ChangePasscode))
entries.append(.settingInfo(presentationData.theme, presentationData.strings.PasscodeSettings_Help))
entries.append(.autoLock(presentationData.theme, presentationData.strings.PasscodeSettings_AutoLock, autolockStringForTimeout(strings: presentationData.strings, timeout: passcodeOptionsData.presentationSettings.autolockTimeout)))
if let biometricAuthentication = LocalAuth.biometricAuthentication {
switch biometricAuthentication {
case .touchId:
entries.append(.touchId(presentationData.theme, presentationData.strings.PasscodeSettings_UnlockWithTouchId, passcodeOptionsData.presentationSettings.enableBiometrics))
case .faceId:
entries.append(.touchId(presentationData.theme, presentationData.strings.PasscodeSettings_UnlockWithFaceId, passcodeOptionsData.presentationSettings.enableBiometrics))
}
}
}
return entries
}
func passcodeOptionsController(context: AccountContext) -> ViewController {
let initialState = PasscodeOptionsControllerState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var popControllerImpl: (() -> Void)?
var replaceTopControllerImpl: ((ViewController, Bool) -> Void)?
let actionsDisposable = DisposableSet()
let passcodeOptionsDataPromise = Promise<PasscodeOptionsData>()
passcodeOptionsDataPromise.set(context.sharedContext.accountManager.transaction { transaction -> (PostboxAccessChallengeData, PresentationPasscodeSettings) in
let passcodeSettings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.presentationPasscodeSettings)?.get(PresentationPasscodeSettings.self) ?? PresentationPasscodeSettings.defaultSettings
return (transaction.getAccessChallengeData(), passcodeSettings)
}
|> map { accessChallenge, passcodeSettings -> PasscodeOptionsData in
return PasscodeOptionsData(accessChallenge: accessChallenge, presentationSettings: passcodeSettings)
})
let arguments = PasscodeOptionsControllerArguments(turnPasscodeOff: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.PasscodeSettings_TurnPasscodeOff, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let challenge = PostboxAccessChallengeData.none
let _ = context.sharedContext.accountManager.transaction({ transaction -> Void in
transaction.setAccessChallengeData(challenge)
}).start()
let _ = (passcodeOptionsDataPromise.get() |> take(1)).start(next: { [weak passcodeOptionsDataPromise] data in
passcodeOptionsDataPromise?.set(.single(data.withUpdatedAccessChallenge(challenge)))
})
var innerReplaceTopControllerImpl: ((ViewController, Bool) -> Void)?
let controller = PrivacyIntroController(context: context, mode: .passcode, proceedAction: {
let setupController = PasscodeSetupController(context: context, mode: .setup(change: false, .digits6))
setupController.complete = { passcode, numerical in
let _ = (context.sharedContext.accountManager.transaction({ transaction -> Void in
var data = transaction.getAccessChallengeData()
if numerical {
data = PostboxAccessChallengeData.numericalPassword(value: passcode)
} else {
data = PostboxAccessChallengeData.plaintextPassword(value: passcode)
}
transaction.setAccessChallengeData(data)
updatePresentationPasscodeSettingsInternal(transaction: transaction, { $0.withUpdatedAutolockTimeout(1 * 60 * 60).withUpdatedBiometricsDomainState(LocalAuth.evaluatedPolicyDomainState) })
}) |> deliverOnMainQueue).start(next: { _ in
}, error: { _ in
}, completed: {
innerReplaceTopControllerImpl?(passcodeOptionsController(context: context), true)
})
}
innerReplaceTopControllerImpl?(setupController, true)
innerReplaceTopControllerImpl = { [weak setupController] c, animated in
(setupController?.navigationController as? NavigationController)?.replaceTopController(c, animated: animated)
}
})
replaceTopControllerImpl?(controller, false)
innerReplaceTopControllerImpl = { [weak controller] c, animated in
(controller?.navigationController as? NavigationController)?.replaceTopController(c, animated: animated)
}
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, changePasscode: {
let _ = (context.sharedContext.accountManager.transaction({ transaction -> Bool in
switch transaction.getAccessChallengeData() {
case .none, .numericalPassword:
return true
case .plaintextPassword:
return false
}
})
|> deliverOnMainQueue).start(next: { isSimple in
let setupController = PasscodeSetupController(context: context, mode: .setup(change: true, .digits6))
setupController.complete = { passcode, numerical in
let _ = (context.sharedContext.accountManager.transaction({ transaction -> Void in
var data = transaction.getAccessChallengeData()
if numerical {
data = PostboxAccessChallengeData.numericalPassword(value: passcode)
} else {
data = PostboxAccessChallengeData.plaintextPassword(value: passcode)
}
transaction.setAccessChallengeData(data)
}) |> deliverOnMainQueue).start(next: { _ in
}, error: { _ in
}, completed: {
popControllerImpl?()
})
}
pushControllerImpl?(setupController)
})
}, changePasscodeTimeout: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
var items: [ActionSheetItem] = []
let setAction: (Int32?) -> Void = { value in
let _ = (passcodeOptionsDataPromise.get()
|> take(1)).start(next: { [weak passcodeOptionsDataPromise] data in
passcodeOptionsDataPromise?.set(.single(data.withUpdatedPresentationSettings(data.presentationSettings.withUpdatedAutolockTimeout(value))))
let _ = updatePresentationPasscodeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
return current.withUpdatedAutolockTimeout(value)
}).start()
})
}
var values: [Int32] = [0, 1 * 60, 5 * 60, 1 * 60 * 60, 5 * 60 * 60]
#if DEBUG
values.append(10)
values.sort()
#endif
for value in values {
var t: Int32?
if value != 0 {
t = value
}
items.append(ActionSheetButtonItem(title: autolockStringForTimeout(strings: presentationData.strings, timeout: t), color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
setAction(t)
}))
}
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, changeTouchId: { value in
let _ = (passcodeOptionsDataPromise.get() |> take(1)).start(next: { [weak passcodeOptionsDataPromise] data in
passcodeOptionsDataPromise?.set(.single(data.withUpdatedPresentationSettings(data.presentationSettings.withUpdatedEnableBiometrics(value))))
let _ = updatePresentationPasscodeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
return current.withUpdatedEnableBiometrics(value)
}).start()
})
})
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), passcodeOptionsDataPromise.get()) |> deliverOnMainQueue
|> map { presentationData, state, passcodeOptionsData -> (ItemListControllerState, (ItemListNodeState, Any)) in
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PasscodeSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: passcodeOptionsControllerEntries(presentationData: presentationData, state: state, passcodeOptionsData: passcodeOptionsData), style: .blocks, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
pushControllerImpl = { [weak controller] c in
(controller?.navigationController as? NavigationController)?.pushViewController(c)
}
popControllerImpl = { [weak controller] in
let _ = (controller?.navigationController as? NavigationController)?.popViewController(animated: true)
}
replaceTopControllerImpl = { [weak controller] c, animated in
(controller?.navigationController as? NavigationController)?.replaceTopController(c, animated: animated)
}
return controller
}
public func passcodeOptionsAccessController(context: AccountContext, animateIn: Bool = true, pushController: ((ViewController) -> Void)?, completion: @escaping (Bool) -> Void) -> Signal<ViewController?, NoError> {
return context.sharedContext.accountManager.transaction { transaction -> PostboxAccessChallengeData in
return transaction.getAccessChallengeData()
}
|> deliverOnMainQueue
|> map { challenge -> ViewController? in
if case .none = challenge {
let controller = PrivacyIntroController(context: context, mode: .passcode, proceedAction: {
let setupController = PasscodeSetupController(context: context, mode: .setup(change: false, .digits6))
setupController.complete = { passcode, numerical in
let _ = (context.sharedContext.accountManager.transaction({ transaction -> Void in
var data = transaction.getAccessChallengeData()
if numerical {
data = PostboxAccessChallengeData.numericalPassword(value: passcode)
} else {
data = PostboxAccessChallengeData.plaintextPassword(value: passcode)
}
transaction.setAccessChallengeData(data)
updatePresentationPasscodeSettingsInternal(transaction: transaction, { $0.withUpdatedAutolockTimeout(1 * 60 * 60).withUpdatedBiometricsDomainState(LocalAuth.evaluatedPolicyDomainState) })
}) |> deliverOnMainQueue).start(next: { _ in
}, error: { _ in
}, completed: {
completion(true)
deleteAllSendMessageIntents()
})
}
pushController?(setupController)
})
return controller
} else {
let controller = PasscodeSetupController(context: context, mode: .entry(challenge))
controller.check = { passcode in
var succeed = false
switch challenge {
case .none:
succeed = true
case let .numericalPassword(code):
succeed = passcode == normalizeArabicNumeralString(code, type: .western)
case let .plaintextPassword(code):
succeed = passcode == code
}
if succeed {
completion(true)
}
return succeed
}
return controller
}
}
}
public func passcodeEntryController(
context: AccountContext,
animateIn: Bool = true,
modalPresentation: Bool = false,
completion: @escaping (Bool) -> Void
) -> Signal<ViewController?, NoError> {
return passcodeEntryController(
accountManager: context.sharedContext.accountManager,
applicationBindings: context.sharedContext.applicationBindings,
presentationData: context.sharedContext.currentPresentationData.with { $0 },
updatedPresentationData: context.sharedContext.presentationData,
statusBarHost: context.sharedContext.mainWindow?.statusBarHost,
appLockContext: context.sharedContext.appLockContext,
animateIn: animateIn,
modalPresentation: modalPresentation,
completion: completion
)
}
public func passcodeEntryController(
accountManager: AccountManager<TelegramAccountManagerTypes>,
applicationBindings: TelegramApplicationBindings,
presentationData: PresentationData,
updatedPresentationData: Signal<PresentationData, NoError>,
statusBarHost: StatusBarHost?,
appLockContext: AppLockContext,
animateIn: Bool = true,
modalPresentation: Bool = false,
completion: @escaping (Bool) -> Void
) -> Signal<ViewController?, NoError> {
return accountManager.transaction { transaction -> PostboxAccessChallengeData in
return transaction.getAccessChallengeData()
}
|> mapToSignal { accessChallengeData -> Signal<(PostboxAccessChallengeData, PresentationPasscodeSettings?), NoError> in
return accountManager.transaction { transaction -> (PostboxAccessChallengeData, PresentationPasscodeSettings?) in
let passcodeSettings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.presentationPasscodeSettings)?.get(PresentationPasscodeSettings.self)
return (accessChallengeData, passcodeSettings)
}
}
|> deliverOnMainQueue
|> map { (challenge, passcodeSettings) -> ViewController? in
if case .none = challenge {
completion(true)
return nil
} else {
let biometrics: PasscodeEntryControllerBiometricsMode
#if targetEnvironment(simulator)
biometrics = .enabled(nil)
#else
if let passcodeSettings = passcodeSettings, passcodeSettings.enableBiometrics {
biometrics = .enabled(applicationBindings.isMainApp ? passcodeSettings.biometricsDomainState : passcodeSettings.shareBiometricsDomainState)
} else {
biometrics = .none
}
#endif
let controller = PasscodeEntryController(applicationBindings: applicationBindings, accountManager: accountManager, appLockContext: appLockContext, presentationData: presentationData, presentationDataSignal: updatedPresentationData, statusBarHost: statusBarHost, challengeData: challenge, biometrics: biometrics, arguments: PasscodeEntryControllerPresentationArguments(animated: false, fadeIn: true, cancel: {
completion(false)
}, modalPresentation: modalPresentation))
controller.presentationCompleted = { [weak controller] in
Queue.mainQueue().after(0.5, { [weak controller] in
controller?.requestBiometrics()
})
}
controller.completed = { [weak controller] in
controller?.dismiss(completion: {
completion(true)
})
}
return controller
}
}
}
@@ -0,0 +1,205 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AccountContext
import AppBundle
import PhoneNumberFormat
public enum PrivacyIntroControllerMode {
case passcode
case twoStepVerification
case changePhoneNumber(String)
var animationName: String? {
switch self {
case .passcode:
return "Passcode"
case .changePhoneNumber:
return "ChangePhoneNumber"
case .twoStepVerification:
return nil
}
}
func icon(theme: PresentationTheme) -> UIImage? {
switch self {
case .passcode, .changePhoneNumber, .twoStepVerification:
return generateTintedImage(image: UIImage(bundleImageName: "Settings/PasscodeIntroIcon"), color: theme.list.freeTextColor)
}
}
func controllerTitle(strings: PresentationStrings) -> String {
switch self {
case .passcode:
return strings.PasscodeSettings_Title
case .twoStepVerification:
return strings.PrivacySettings_TwoStepAuth
case .changePhoneNumber:
return strings.ChangePhoneNumberNumber_Title
}
}
func title(context: AccountContext, strings: PresentationStrings) -> String {
switch self {
case .passcode:
return strings.PasscodeSettings_Title
case .twoStepVerification:
return strings.TwoStepAuth_AdditionalPassword
case let .changePhoneNumber(phoneNumber):
return formatPhoneNumber(context: context, number: phoneNumber)
}
}
func text(strings: PresentationStrings) -> String {
switch self {
case .passcode:
return strings.PasscodeSettings_HelpTop
case .twoStepVerification:
return strings.TwoStepAuth_SetPasswordHelp
case .changePhoneNumber:
return strings.PhoneNumberHelp_Help
}
}
func buttonTitle(strings: PresentationStrings) -> String {
switch self {
case .passcode:
return strings.PasscodeSettings_TurnPasscodeOn
case .twoStepVerification:
return strings.TwoStepAuth_SetPassword
case .changePhoneNumber:
return strings.PhoneNumberHelp_ChangeNumber
}
}
func notice(strings: PresentationStrings) -> String {
switch self {
case .passcode:
return strings.PasscodeSettings_HelpBottom
case .twoStepVerification, .changePhoneNumber:
return ""
}
}
}
public final class PrivacyIntroControllerPresentationArguments {
let fadeIn: Bool
let animateIn: Bool
public init(fadeIn: Bool = false, animateIn: Bool = false) {
self.fadeIn = fadeIn
self.animateIn = animateIn
}
}
public final class PrivacyIntroController: ViewController {
private let context: AccountContext
private let mode: PrivacyIntroControllerMode
private let arguments: PrivacyIntroControllerPresentationArguments
private let proceedAction: () -> Void
private var controllerNode: PrivacyIntroControllerNode {
return self.displayNode as! PrivacyIntroControllerNode
}
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var isDismissed: Bool = false
public init(context: AccountContext, mode: PrivacyIntroControllerMode, arguments: PrivacyIntroControllerPresentationArguments = PrivacyIntroControllerPresentationArguments(), proceedAction: @escaping () -> Void) {
self.context = context
self.mode = mode
self.arguments = arguments
self.proceedAction = proceedAction
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.title = self.mode.controllerTitle(strings: self.presentationData.strings)
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
if arguments.animateIn {
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false)
}
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings()
}
}
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
@objc private func cancelPressed() {
self.dismiss()
}
private func updateThemeAndStrings() {
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.controllerNode.updatePresentationData(self.presentationData)
}
override public func loadDisplayNode() {
self.displayNode = PrivacyIntroControllerNode(context: self.context, mode: self.mode, proceedAction: self.proceedAction)
self.displayNodeDidLoad()
self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if self.arguments.animateIn {
self.controllerNode.animateIn(slide: true)
} else if self.arguments.fadeIn {
self.controllerNode.animateIn(slide: false)
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
if self.arguments.animateIn {
self.controllerNode.animateOut(completion: { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
completion?()
})
} else {
self.presentingViewController?.dismiss(animated: false, completion: nil)
completion?()
}
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}
@@ -0,0 +1,217 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import AuthorizationUtils
import AnimatedStickerNode
import TelegramAnimatedStickerNode
private func generateButtonImage(backgroundColor: UIColor, highlightColor: UIColor?) -> UIImage? {
return generateImage(CGSize(width: 66.0, height: 52.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 26.0, height: 26.0))
context.addPath(path.cgPath)
context.clip()
if let highlightColor = highlightColor {
context.setFillColor(highlightColor.cgColor)
context.fill(bounds)
} else {
context.setFillColor(backgroundColor.cgColor)
context.fill(bounds)
}
}, opaque: false)?.stretchableImage(withLeftCapWidth: 26, topCapHeight: 26)
}
private let titleFont = Font.bold(17.0)
private let textFont = Font.regular(14.0)
private let buttonFont = Font.regular(17.0)
final class PrivacyIntroControllerNode: ViewControllerTracingNode {
private let context: AccountContext
private let mode: PrivacyIntroControllerMode
private var presentationData: PresentationData?
private let proceedAction: () -> Void
private let iconNode: ASImageNode
private let animationNode: AnimatedStickerNode
private let titleNode: ASTextNode
private let textNode: ASTextNode
private let buttonNode: HighlightTrackingButtonNode
private let buttonBackgroundNode: ASImageNode
private let buttonHighlightedBackgroundNode: ASImageNode
private let buttonTextNode: ASTextNode
private let noticeNode: ASTextNode
private var validLayout: (ContainerViewLayout, CGFloat)?
init(context: AccountContext, mode: PrivacyIntroControllerMode, proceedAction: @escaping () -> Void) {
self.context = context
self.mode = mode
self.proceedAction = proceedAction
self.iconNode = ASImageNode()
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.titleNode = ASTextNode()
self.textNode = ASTextNode()
self.buttonNode = HighlightTrackingButtonNode()
self.buttonBackgroundNode = ASImageNode()
self.buttonBackgroundNode.contentMode = .scaleToFill
self.buttonHighlightedBackgroundNode = ASImageNode()
self.buttonHighlightedBackgroundNode.alpha = 0.0
self.buttonHighlightedBackgroundNode.contentMode = .scaleToFill
self.buttonTextNode = ASTextNode()
self.noticeNode = ASTextNode()
super.init()
if let animationName = mode.animationName {
self.iconNode.isHidden = true
self.animationNode.isHidden = false
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 380, height: 380, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
} else {
self.iconNode.isHidden = false
self.animationNode.isHidden = true
}
self.addSubnode(self.iconNode)
self.addSubnode(self.animationNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.buttonBackgroundNode)
self.addSubnode(self.buttonHighlightedBackgroundNode)
self.addSubnode(self.buttonTextNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.noticeNode)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.buttonHighlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonHighlightedBackgroundNode.alpha = 1.0
} else {
strongSelf.buttonHighlightedBackgroundNode.alpha = 0.0
strongSelf.buttonHighlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.updatePresentationData(context.sharedContext.currentPresentationData.with { $0 })
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.backgroundColor = presentationData.theme.list.blocksBackgroundColor
if self.animationNode.isHidden {
self.iconNode.image = self.mode.icon(theme: presentationData.theme)
}
self.titleNode.attributedText = NSAttributedString(string: self.mode.title(context: self.context, strings: presentationData.strings), font: titleFont, textColor: presentationData.theme.list.sectionHeaderTextColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: self.mode.text(strings: presentationData.strings), font: textFont, textColor: presentationData.theme.list.freeTextColor, paragraphAlignment: .center)
self.noticeNode.attributedText = NSAttributedString(string: self.mode.notice(strings: presentationData.strings), font: textFont, textColor: presentationData.theme.list.freeTextColor, paragraphAlignment: .center)
self.buttonTextNode.attributedText = NSAttributedString(string: self.mode.buttonTitle(strings: presentationData.strings), font: buttonFont, textColor: presentationData.theme.list.itemAccentColor, paragraphAlignment: .center)
self.buttonTextNode.isAccessibilityElement = false
self.buttonNode.accessibilityLabel = self.buttonTextNode.attributedText?.string
self.buttonBackgroundNode.image = generateButtonImage(backgroundColor: presentationData.theme.list.itemBlocksBackgroundColor, highlightColor: nil)
self.buttonHighlightedBackgroundNode.image = generateButtonImage(backgroundColor: presentationData.theme.list.itemBlocksBackgroundColor, highlightColor: presentationData.theme.list.itemHighlightedBackgroundColor)
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.statusBar])
insets.top += navigationBarHeight
var iconSize = CGSize()
var animationSize = CGSize()
if !self.animationNode.isHidden {
animationSize = CGSize(width: 180.0, height: 180.0)
self.animationNode.updateLayout(size: animationSize)
var iconAlpha: CGFloat = 1.0
if case .compact = layout.metrics.widthClass, layout.size.width > layout.size.height {
iconAlpha = 0.0
iconSize = CGSize()
}
transition.updateAlpha(node: self.animationNode, alpha: iconAlpha)
} else if let size = self.iconNode.image?.size {
iconSize = size
var iconAlpha: CGFloat = 1.0
if case .compact = layout.metrics.widthClass, layout.size.width > layout.size.height {
iconAlpha = 0.0
iconSize = CGSize()
}
transition.updateAlpha(node: self.iconNode, alpha: iconAlpha)
}
let inset: CGFloat = 30.0
let titleSize = self.titleNode.measure(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude))
let textSize = self.textNode.measure(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude))
let noticeSize = self.noticeNode.measure(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude))
let buttonInset: CGFloat
if layout.size.width >= 375.0 {
buttonInset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
} else {
buttonInset = 0.0
}
let items: [AuthorizationLayoutItem] = [
AuthorizationLayoutItem(node: self.iconNode, size: iconSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)),
AuthorizationLayoutItem(node: self.animationNode, size: animationSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)),
AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 20.0, maxValue: 30.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)),
AuthorizationLayoutItem(node: self.textNode, size: textSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 16.0, maxValue: 16.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)),
AuthorizationLayoutItem(node: self.buttonNode, size: CGSize(width: layout.size.width - buttonInset * 2.0, height: 52.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 40.0, maxValue: 40.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)),
AuthorizationLayoutItem(node: self.noticeNode, size: noticeSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 44.0, maxValue: 44.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 20.0, maxValue: 40.0))
]
let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - 10.0)), items: items, transition: transition, failIfDoesNotFit: false)
transition.updateFrame(node: self.buttonBackgroundNode, frame: self.buttonNode.frame)
transition.updateFrame(node: self.buttonHighlightedBackgroundNode, frame: self.buttonNode.frame)
let buttonTextSize = self.buttonTextNode.measure(layout.size)
let buttonTextFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonTextSize.width) / 2.0), y: floor(self.buttonNode.frame.center.y - buttonTextSize.height / 2.0)), size: buttonTextSize)
transition.updateFrame(node: self.buttonTextNode, frame: buttonTextFrame)
}
func animateIn(slide: Bool) {
if slide {
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
})
} else {
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.buttonBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.buttonTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.noticeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
}
func animateOut(completion: (() -> Void)? = nil) {
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
completion?()
})
}
@objc func buttonPressed() {
self.proceedAction()
}
}
@@ -0,0 +1,626 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import TelegramStringFormatting
struct ItemListRecentSessionItemEditing: Equatable {
let editable: Bool
let editing: Bool
let revealed: Bool
static func ==(lhs: ItemListRecentSessionItemEditing, rhs: ItemListRecentSessionItemEditing) -> Bool {
if lhs.editable != rhs.editable {
return false
}
if lhs.editing != rhs.editing {
return false
}
if lhs.revealed != rhs.revealed {
return false
}
return true
}
}
enum ItemListRecentSessionItemText {
case presence
case text(String)
case none
}
final class ItemListRecentSessionItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let dateTimeFormat: PresentationDateTimeFormat
let session: RecentAccountSession
let enabled: Bool
let editable: Bool
let editing: Bool
let revealed: Bool
let sectionId: ItemListSectionId
let setSessionIdWithRevealedOptions: (Int64?, Int64?) -> Void
let removeSession: (Int64) -> Void
let action: (() -> Void)?
init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editable: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, action: (() -> Void)?) {
self.presentationData = presentationData
self.systemStyle = systemStyle
self.dateTimeFormat = dateTimeFormat
self.session = session
self.enabled = enabled
self.editable = editable
self.editing = editing
self.revealed = revealed
self.sectionId = sectionId
self.setSessionIdWithRevealedOptions = setSessionIdWithRevealedOptions
self.removeSession = removeSession
self.action = action
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListRecentSessionItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(false) })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListRecentSessionItemNode {
let makeLayout = nodeValue.asyncLayout()
var animated = true
if case .None = animation {
animated = false
}
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply(animated)
})
}
}
}
}
}
public var selectable: Bool = true
public func selected(listView: ListView){
listView.clearHighlightAnimated(true)
if self.enabled {
self.action?()
}
}
}
func iconForSession(_ session: RecentAccountSession) -> (UIImage?, UIColor?, String?, [String]?) {
let platform = session.platform.lowercased()
let device = session.deviceModel.lowercased()
let systemVersion = session.systemVersion.lowercased()
if device.contains("xbox") {
return (UIImage(bundleImageName: "Settings/Devices/Xbox"), UIColor(rgb: 0x35c759), nil, nil)
}
if device.contains("chrome") && !device.contains("chromebook") {
return (UIImage(bundleImageName: "Settings/Devices/Chrome"), UIColor(rgb: 0x35c759), "device_chrome", ["Vector 20.Vector 20.Обводка 1", "Ellipse 18.Ellipse 18.Обводка 1"])
}
if device.contains("brave") {
return (UIImage(bundleImageName: "Settings/Devices/Brave"), UIColor(rgb: 0xff9500), nil, nil)
}
if device.contains("vivaldi") {
return (UIImage(bundleImageName: "Settings/Devices/Vivaldi"), UIColor(rgb: 0xff3c30), nil, nil)
}
if device.contains("safari") {
return (UIImage(bundleImageName: "Settings/Devices/Safari"), UIColor(rgb: 0x0079ff), "device_safari", ["Com 2.Com 2.Заливка 1"])
}
if device.contains("firefox") {
return (UIImage(bundleImageName: "Settings/Devices/Firefox"), UIColor(rgb: 0xff9500), "device_firefox", nil)
}
if device.contains("opera") {
return (UIImage(bundleImageName: "Settings/Devices/Opera"), UIColor(rgb: 0xff3c30), nil, nil)
}
if platform.contains("android") {
return (UIImage(bundleImageName: "Settings/Devices/Android"), UIColor(rgb: 0x35c759), "device_android", ["Eye L.Eye L.Заливка 1", "Eye R.Eye R.Заливка 1"])
}
if device.contains("iphone") {
return (UIImage(bundleImageName: "Settings/Devices/iPhone"), UIColor(rgb: 0x0079ff), "device_iphone", ["apple.apple.Заливка 1"])
}
if device.contains("ipad") {
return (UIImage(bundleImageName: "Settings/Devices/iPad"), UIColor(rgb: 0x0079ff), "device_ipad", ["apple.apple.Заливка 1"])
}
if (platform.contains("macos") || systemVersion.contains("macos")) && device.contains("mac") {
return (UIImage(bundleImageName: "Settings/Devices/Mac"), UIColor(rgb: 0x0079ff), "device_mac", nil)
}
if platform.contains("ios") || platform.contains("macos") || systemVersion.contains("macos") {
return (UIImage(bundleImageName: "Settings/Devices/iOS"), UIColor(rgb: 0x0079ff), nil, nil)
}
if platform.contains("ubuntu") || systemVersion.contains("ubuntu") {
return (UIImage(bundleImageName: "Settings/Devices/Ubuntu"), UIColor(rgb: 0xff9500), "device_ubuntu", ["Ellipse 25.Ellipse 24.Обводка 1", "Ellipse 24.Ellipse 24.Обводка 1", "Union.Union.Заливка 1"])
}
if platform.contains("linux") || systemVersion.contains("linux") {
return (UIImage(bundleImageName: "Settings/Devices/Linux"), UIColor(rgb: 0x8e8e93), "device_linux", nil)
}
if platform.contains("windows") || systemVersion.contains("windows") {
return (UIImage(bundleImageName: "Settings/Devices/Windows"), UIColor(rgb: 0x0079ff), "device_windows", ["Union.Union.Заливка 1"])
}
return (UIImage(bundleImageName: "Settings/Devices/Generic"), UIColor(rgb: 0x8e8e93), nil, nil)
}
private func trimmedLocationName(_ session: RecentAccountSession) -> String {
var country = session.country
country = country.replacingOccurrences(of: "United Arab Emirates", with: "UAE")
return country
}
class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private var disabledOverlayNode: ASDisplayNode?
private let maskNode: ASImageNode
let iconNode: ASImageNode
private let titleNode: TextNode
private let appNode: TextNode
private let locationNode: TextNode
private let containerNode: ASDisplayNode
override var controlsContainer: ASDisplayNode {
return self.containerNode
}
private let activateArea: AccessibilityAreaNode
private var layoutParams: (ItemListRecentSessionItem, ListViewItemLayoutParams, ItemListNeighbors)?
private var editableControlNode: ItemListEditableControlNode?
override public var canBeSelected: Bool {
if let item = self.layoutParams?.0, let _ = item.action {
return true
} else {
return false
}
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.containerNode = ASDisplayNode()
self.iconNode = ASImageNode()
self.iconNode.cornerRadius = 7.0
self.iconNode.clipsToBounds = true
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.appNode = TextNode()
self.appNode.isUserInteractionEnabled = false
self.appNode.contentMode = .left
self.appNode.contentsScale = UIScreen.main.scale
self.locationNode = TextNode()
self.locationNode.isUserInteractionEnabled = false
self.locationNode.contentMode = .left
self.locationNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.iconNode)
self.containerNode.addSubnode(self.titleNode)
self.containerNode.addSubnode(self.appNode)
self.containerNode.addSubnode(self.locationNode)
self.addSubnode(self.activateArea)
}
func asyncLayout() -> (_ item: ItemListRecentSessionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeAppLayout = TextNode.asyncLayout(self.appNode)
let makeLocationLayout = TextNode.asyncLayout(self.locationNode)
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
var currentDisabledOverlayNode = self.disabledOverlayNode
let currentItem = self.layoutParams?.0
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
let titleFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0))
let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
verticalInset = 14.0
case .legacy:
verticalInset = 10.0
}
let titleSpacing: CGFloat = 1.0
let textSpacing: CGFloat = 3.0
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
var titleAttributedString: NSAttributedString?
var appAttributedString: NSAttributedString?
var locationAttributedString: NSAttributedString?
let peerRevealOptions: [ItemListRevealOption]
if item.editable && item.enabled {
peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.AuthSessions_Terminate, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]
} else {
peerRevealOptions = []
}
let rightInset: CGFloat = params.rightInset
var appVersion = item.session.appVersion
appVersion = appVersion.replacingOccurrences(of: "APPSTORE", with: "").replacingOccurrences(of: "BETA", with: "Beta").trimmingTrailingSpaces()
if let openingRoundBraceRange = appVersion.range(of: " ("), let closingRoundBraceRange = appVersion.range(of: ")") {
appVersion = appVersion.replacingCharacters(in: openingRoundBraceRange.lowerBound ..< closingRoundBraceRange.upperBound, with: "")
}
var deviceString = ""
if !item.session.deviceModel.isEmpty {
deviceString = item.session.deviceModel
}
// if !item.session.platform.isEmpty {
// if !deviceString.isEmpty {
// deviceString += ", "
// }
// deviceString += item.session.platform
// }
var updatedIcon: UIImage?
if item.session != currentItem?.session {
updatedIcon = iconForSession(item.session).0
}
let appString = "\(item.session.appName) \(appVersion)"
titleAttributedString = NSAttributedString(string: deviceString, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
appAttributedString = NSAttributedString(string: appString, font: textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
let label: String
if item.session.isCurrent {
label = item.presentationData.strings.Presence_online
} else {
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
label = stringForRelativeActivityTimestamp(strings: item.presentationData.strings, dateTimeFormat: item.dateTimeFormat, relativeTimestamp: item.session.activityDate, relativeTo: timestamp)
}
locationAttributedString = NSAttributedString(string: "\(trimmedLocationName(item.session))\(label)", font: textFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
let leftInset: CGFloat = 59.0 + params.leftInset
var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)?
let editingOffset: CGFloat
if item.editing {
let sizeAndApply = editableControlLayout(item.presentationData.theme, false)
editableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0
} else {
editingOffset = 0.0
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset - 5.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (appLayout, appApply) = makeAppLayout(TextNodeLayoutArguments(attributedString: appAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (locationLayout, locationApply) = makeLocationLayout(TextNodeLayoutArguments(attributedString: locationAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + appLayout.size.height + textSpacing + locationLayout.size.height)
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
if !item.enabled {
if currentDisabledOverlayNode == nil {
currentDisabledOverlayNode = ASDisplayNode()
currentDisabledOverlayNode?.backgroundColor = UIColor(white: 1.0, alpha: 0.5)
}
} else {
currentDisabledOverlayNode = nil
}
return (layout, { [weak self] animated in
if let strongSelf = self {
strongSelf.layoutParams = (item, params, neighbors)
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
var label = ""
if item.session.isCurrent {
label = item.presentationData.strings.VoiceOver_AuthSessions_CurrentSession
label += ", "
}
label += titleAttributedString?.string ?? ""
strongSelf.activateArea.accessibilityLabel = label
var value = ""
if let string = appAttributedString?.string {
value += string
}
if let string = locationAttributedString?.string {
if !value.isEmpty {
value += "\n"
}
value += string
}
strongSelf.activateArea.accessibilityValue = value
if item.enabled {
strongSelf.activateArea.accessibilityTraits = []
} else {
strongSelf.activateArea.accessibilityTraits = .notEnabled
}
if let updatedIcon = updatedIcon {
strongSelf.iconNode.image = updatedIcon
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let revealOffset = strongSelf.revealOffset
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
if let currentDisabledOverlayNode = currentDisabledOverlayNode {
if currentDisabledOverlayNode != strongSelf.disabledOverlayNode {
strongSelf.disabledOverlayNode = currentDisabledOverlayNode
strongSelf.addSubnode(currentDisabledOverlayNode)
currentDisabledOverlayNode.alpha = 0.0
transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0)
currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight))
} else {
transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight)))
}
} else if let disabledOverlayNode = strongSelf.disabledOverlayNode {
transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in
disabledOverlayNode?.removeFromSupernode()
})
strongSelf.disabledOverlayNode = nil
}
if let editableControlSizeAndApply = editableControlSizeAndApply {
let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height))
if strongSelf.editableControlNode == nil {
let editableControlNode = editableControlSizeAndApply.1(layout.contentSize.height)
editableControlNode.tapped = {
if let strongSelf = self {
strongSelf.setRevealOptionsOpened(true, animated: true)
strongSelf.revealOptionsInteractivelyOpened()
}
}
strongSelf.editableControlNode = editableControlNode
strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.containerNode)
editableControlNode.frame = editableControlFrame
transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY))
editableControlNode.alpha = 0.0
transition.updateAlpha(node: editableControlNode, alpha: 1.0)
} else {
strongSelf.editableControlNode?.frame = editableControlFrame
}
strongSelf.editableControlNode?.isHidden = !item.editable
} else if let editableControlNode = strongSelf.editableControlNode {
var editableControlFrame = editableControlNode.frame
editableControlFrame.origin.x = -editableControlFrame.size.width
strongSelf.editableControlNode = nil
transition.updateAlpha(node: editableControlNode, alpha: 0.0)
transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in
editableControlNode?.removeFromSupernode()
})
}
let _ = titleApply()
let _ = appApply()
let _ = locationApply()
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.addSubnode(strongSelf.maskNode)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset + editingOffset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: strongSelf.backgroundNode.frame.size)
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 16.0, y: 12.0), size: CGSize(width: 30.0, height: 30.0)))
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset), size: titleLayout.size))
transition.updateFrame(node: strongSelf.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: appLayout.size))
transition.updateFrame(node: strongSelf.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.appNode.frame.maxY + textSpacing), size: locationLayout.size))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
strongSelf.setRevealOptions((left: [], right: peerRevealOptions))
strongSelf.setRevealOptionsOpened(item.revealed, animated: animated)
}
})
}
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted && (self.layoutParams?.0.enabled ?? false) {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
guard let params = self.layoutParams?.1 else {
return
}
let leftInset: CGFloat = 59.0 + params.leftInset
let editingOffset: CGFloat
if let editableControlNode = self.editableControlNode {
editingOffset = editableControlNode.bounds.size.width
var editableControlFrame = editableControlNode.frame
editableControlFrame.origin.x = params.leftInset + offset
transition.updateFrame(node: editableControlNode, frame: editableControlFrame)
} else {
editingOffset = 0.0
}
transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + self.revealOffset + editingOffset + 16.0, y: self.iconNode.frame.minY), size: self.iconNode.bounds.size))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size))
transition.updateFrame(node: self.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.appNode.frame.minY), size: self.appNode.bounds.size))
transition.updateFrame(node: self.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.locationNode.frame.minY), size: self.locationNode.bounds.size))
}
override func revealOptionsInteractivelyOpened() {
if let (item, _, _) = self.layoutParams {
item.setSessionIdWithRevealedOptions(item.session.hash, nil)
}
}
override func revealOptionsInteractivelyClosed() {
if let (item, _, _) = self.layoutParams {
item.setSessionIdWithRevealedOptions(nil, item.session.hash)
}
}
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
if let (item, _, _) = self.layoutParams {
item.removeSession(item.session.hash)
}
}
}
@@ -0,0 +1,552 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AvatarNode
import TelegramStringFormatting
import LocalizedPeerData
import AccountContext
struct ItemListWebsiteItemEditing: Equatable {
let editing: Bool
let revealed: Bool
static func ==(lhs: ItemListWebsiteItemEditing, rhs: ItemListWebsiteItemEditing) -> Bool {
if lhs.editing != rhs.editing {
return false
}
if lhs.revealed != rhs.revealed {
return false
}
return true
}
}
final class ItemListWebsiteItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let dateTimeFormat: PresentationDateTimeFormat
let nameDisplayOrder: PresentationPersonNameOrder
let website: WebAuthorization
let peer: Peer?
let enabled: Bool
let editing: Bool
let revealed: Bool
let sectionId: ItemListSectionId
let setSessionIdWithRevealedOptions: (Int64?, Int64?) -> Void
let removeSession: (Int64) -> Void
let action: (() -> Void)?
init(context: AccountContext, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, website: WebAuthorization, peer: Peer?, enabled: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, action: (() -> Void)?) {
self.context = context
self.presentationData = presentationData
self.systemStyle = systemStyle
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
self.website = website
self.peer = peer
self.enabled = enabled
self.editing = editing
self.revealed = revealed
self.sectionId = sectionId
self.setSessionIdWithRevealedOptions = setSessionIdWithRevealedOptions
self.removeSession = removeSession
self.action = action
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListWebsiteItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(false) })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListWebsiteItemNode {
let makeLayout = nodeValue.asyncLayout()
var animated = true
if case .None = animation {
animated = false
}
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply(animated)
})
}
}
}
}
}
public var selectable: Bool = true
public func selected(listView: ListView){
listView.clearHighlightAnimated(true)
if self.enabled {
self.action?()
}
}
}
private let avatarFont = avatarPlaceholderFont(size: 11.0)
private func trimmedLocationName(_ session: WebAuthorization) -> String {
var country = session.region
country = country.replacingOccurrences(of: "United Arab Emirates", with: "UAE")
return country
}
class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private var disabledOverlayNode: ASDisplayNode?
private let maskNode: ASImageNode
let avatarNode: AvatarNode
private let titleNode: TextNode
private let appNode: TextNode
private let locationNode: TextNode
private let containerNode: ASDisplayNode
override var controlsContainer: ASDisplayNode {
return self.containerNode
}
private let activateArea: AccessibilityAreaNode
private var layoutParams: (ItemListWebsiteItem, ListViewItemLayoutParams, ItemListNeighbors)?
private var editableControlNode: ItemListEditableControlNode?
override public var canBeSelected: Bool {
if let item = self.layoutParams?.0, let _ = item.action {
return true
} else {
return false
}
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.containerNode = ASDisplayNode()
self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.cornerRadius = 7.0
self.avatarNode.clipsToBounds = true
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.appNode = TextNode()
self.appNode.isUserInteractionEnabled = false
self.appNode.contentMode = .left
self.appNode.contentsScale = UIScreen.main.scale
self.locationNode = TextNode()
self.locationNode.isUserInteractionEnabled = false
self.locationNode.contentMode = .left
self.locationNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.avatarNode)
self.containerNode.addSubnode(self.titleNode)
self.containerNode.addSubnode(self.appNode)
self.containerNode.addSubnode(self.locationNode)
self.addSubnode(self.activateArea)
}
func asyncLayout() -> (_ item: ItemListWebsiteItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeAppLayout = TextNode.asyncLayout(self.appNode)
let makeLocationLayout = TextNode.asyncLayout(self.locationNode)
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
var currentDisabledOverlayNode = self.disabledOverlayNode
let currentItem = self.layoutParams?.0
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
let titleFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0))
let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
if currentItem?.presentationData !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
var titleAttributedString: NSAttributedString?
var appAttributedString: NSAttributedString?
var locationAttributedString: NSAttributedString?
let peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.AuthSessions_LogOut, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]
let rightInset: CGFloat = params.rightInset
if let user = item.peer as? TelegramUser {
titleAttributedString = NSAttributedString(string: EnginePeer(user).displayTitle(strings: item.presentationData.strings, displayOrder: item.nameDisplayOrder), font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
}
var appString = ""
if !item.website.domain.isEmpty {
appString = item.website.domain
}
if !item.website.browser.isEmpty {
if !appString.isEmpty {
appString += ", "
}
appString += item.website.browser
}
if !item.website.platform.isEmpty {
if !appString.isEmpty {
appString += ", "
}
appString += item.website.platform
}
appAttributedString = NSAttributedString(string: appString, font: textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let label = stringForRelativeActivityTimestamp(strings: item.presentationData.strings, dateTimeFormat: item.dateTimeFormat, relativeTimestamp: item.website.dateActive, relativeTo: timestamp)
locationAttributedString = NSAttributedString(string: "\(trimmedLocationName(item.website))\(label)", font: textFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
let leftInset: CGFloat = 59.0 + params.leftInset
var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)?
let editingOffset: CGFloat
if item.editing {
let sizeAndApply = editableControlLayout(item.presentationData.theme, false)
editableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0
} else {
editingOffset = 0.0
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - 5.0 - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (appLayout, appApply) = makeAppLayout(TextNodeLayoutArguments(attributedString: appAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (locationLayout, locationApply) = makeLocationLayout(TextNodeLayoutArguments(attributedString: locationAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var verticalInset: CGFloat = 0.0
if case .glass = item.systemStyle {
verticalInset = 4.0
}
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let contentSize = CGSize(width: params.width, height: 75.0 + verticalInset * 2.0)
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
if !item.enabled {
if currentDisabledOverlayNode == nil {
currentDisabledOverlayNode = ASDisplayNode()
currentDisabledOverlayNode?.backgroundColor = UIColor(white: 1.0, alpha: 0.5)
}
} else {
currentDisabledOverlayNode = nil
}
return (layout, { [weak self] animated in
if let strongSelf = self {
strongSelf.layoutParams = (item, params, neighbors)
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = titleAttributedString?.string ?? ""
var value = ""
if let string = appAttributedString?.string {
value += string
}
if let string = locationAttributedString?.string {
if !value.isEmpty {
value += "\n"
}
value += string
}
strongSelf.activateArea.accessibilityValue = value
if item.enabled {
strongSelf.activateArea.accessibilityTraits = []
} else {
strongSelf.activateArea.accessibilityTraits = .notEnabled
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
if let peer = item.peer {
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: EnginePeer(peer), authorOfMessage: nil, overrideImage: nil, emptyColor: nil, clipStyle: .none, synchronousLoad: false)
}
let revealOffset = strongSelf.revealOffset
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
if let currentDisabledOverlayNode = currentDisabledOverlayNode {
if currentDisabledOverlayNode != strongSelf.disabledOverlayNode {
strongSelf.disabledOverlayNode = currentDisabledOverlayNode
strongSelf.addSubnode(currentDisabledOverlayNode)
currentDisabledOverlayNode.alpha = 0.0
transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0)
currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight))
} else {
transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight)))
}
} else if let disabledOverlayNode = strongSelf.disabledOverlayNode {
transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in
disabledOverlayNode?.removeFromSupernode()
})
strongSelf.disabledOverlayNode = nil
}
if let editableControlSizeAndApply = editableControlSizeAndApply {
let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height))
if strongSelf.editableControlNode == nil {
let editableControlNode = editableControlSizeAndApply.1(layout.contentSize.height)
editableControlNode.tapped = {
if let strongSelf = self {
strongSelf.setRevealOptionsOpened(true, animated: true)
strongSelf.revealOptionsInteractivelyOpened()
}
}
strongSelf.editableControlNode = editableControlNode
strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.containerNode)
editableControlNode.frame = editableControlFrame
transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY))
editableControlNode.alpha = 0.0
transition.updateAlpha(node: editableControlNode, alpha: 1.0)
} else {
strongSelf.editableControlNode?.frame = editableControlFrame
}
strongSelf.editableControlNode?.isHidden = false
} else if let editableControlNode = strongSelf.editableControlNode {
var editableControlFrame = editableControlNode.frame
editableControlFrame.origin.x = -editableControlFrame.size.width
strongSelf.editableControlNode = nil
transition.updateAlpha(node: editableControlNode, alpha: 0.0)
transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in
editableControlNode?.removeFromSupernode()
})
}
let _ = titleApply()
let _ = appApply()
let _ = locationApply()
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.addSubnode(strongSelf.maskNode)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset + editingOffset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: strongSelf.backgroundNode.frame.size)
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 16.0, y: verticalInset + 12.0), size: CGSize(width: 30.0, height: 30.0)))
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + 10.0), size: titleLayout.size))
transition.updateFrame(node: strongSelf.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + 30.0), size: appLayout.size))
transition.updateFrame(node: strongSelf.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + 50.0), size: locationLayout.size))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
strongSelf.setRevealOptions((left: [], right: peerRevealOptions))
strongSelf.setRevealOptionsOpened(item.revealed, animated: animated)
}
})
}
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted && (self.layoutParams?.0.enabled ?? false) {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
guard let params = self.layoutParams?.1 else {
return
}
let leftInset: CGFloat = 59.0 + params.leftInset
let editingOffset: CGFloat
if let editableControlNode = self.editableControlNode {
editingOffset = editableControlNode.bounds.size.width
var editableControlFrame = editableControlNode.frame
editableControlFrame.origin.x = params.leftInset + offset
transition.updateFrame(node: editableControlNode, frame: editableControlFrame)
} else {
editingOffset = 0.0
}
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + self.revealOffset + editingOffset + 16.0, y: self.avatarNode.frame.minY), size: self.avatarNode.bounds.size))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + self.revealOffset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size))
transition.updateFrame(node: self.appNode, frame: CGRect(origin: CGPoint(x: leftInset + self.revealOffset + editingOffset, y: self.appNode.frame.minY), size: self.appNode.bounds.size))
transition.updateFrame(node: self.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + self.revealOffset + editingOffset, y: self.locationNode.frame.minY), size: self.locationNode.bounds.size))
}
override func revealOptionsInteractivelyOpened() {
if let (item, _, _) = self.layoutParams {
item.setSessionIdWithRevealedOptions(item.website.hash, nil)
}
}
override func revealOptionsInteractivelyClosed() {
if let (item, _, _) = self.layoutParams {
item.setSessionIdWithRevealedOptions(nil, item.website.hash)
}
}
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
if let (item, _, _) = self.layoutParams {
item.removeSession(item.website.hash)
}
}
}
@@ -0,0 +1,895 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import ItemListPeerActionItem
import DeviceAccess
import QrCodeUI
private final class RecentSessionsControllerArguments {
let context: AccountContext
let setSessionIdWithRevealedOptions: (Int64?, Int64?) -> Void
let removeSession: (Int64) -> Void
let terminateOtherSessions: () -> Void
let openSession: (RecentAccountSession) -> Void
let openWebSession: (WebAuthorization, Peer?) -> Void
let removeWebSession: (Int64) -> Void
let terminateAllWebSessions: () -> Void
let addDevice: () -> Void
let openOtherAppsUrl: () -> Void
let setupAuthorizationTTL: () -> Void
let openDesktopLink: () -> Void
let openWebLink: () -> Void
init(context: AccountContext, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, terminateOtherSessions: @escaping () -> Void, openSession: @escaping (RecentAccountSession) -> Void, openWebSession: @escaping (WebAuthorization, Peer?) -> Void, removeWebSession: @escaping (Int64) -> Void, terminateAllWebSessions: @escaping () -> Void, addDevice: @escaping () -> Void, openOtherAppsUrl: @escaping () -> Void, setupAuthorizationTTL: @escaping () -> Void, openDesktopLink: @escaping () -> Void, openWebLink: @escaping () -> Void) {
self.context = context
self.setSessionIdWithRevealedOptions = setSessionIdWithRevealedOptions
self.removeSession = removeSession
self.terminateOtherSessions = terminateOtherSessions
self.openSession = openSession
self.openWebSession = openWebSession
self.removeWebSession = removeWebSession
self.terminateAllWebSessions = terminateAllWebSessions
self.addDevice = addDevice
self.openOtherAppsUrl = openOtherAppsUrl
self.setupAuthorizationTTL = setupAuthorizationTTL
self.openDesktopLink = openDesktopLink
self.openWebLink = openWebLink
}
}
private enum RecentSessionsMode: Int {
case sessions
case websites
}
private enum RecentSessionsSection: Int32 {
case header
case currentSession
case pendingSessions
case otherSessions
case ttl
}
private enum RecentSessionsEntryStableId: Hashable {
case session(Int64)
case index(Int32)
case devicesInfo
case ttl(Int32)
}
private struct SortIndex: Comparable {
var section: Int
var item: Int
static func <(lhs: SortIndex, rhs: SortIndex) -> Bool {
if lhs.section != rhs.section {
return lhs.section < rhs.section
}
return lhs.item < rhs.item
}
}
private enum RecentSessionsEntry: ItemListNodeEntry {
case header(SortIndex, String)
case currentSessionHeader(SortIndex, String)
case currentSession(SortIndex, PresentationStrings, PresentationDateTimeFormat, RecentAccountSession)
case terminateOtherSessions(SortIndex, String)
case terminateAllWebSessions(SortIndex, String)
case currentAddDevice(SortIndex, String)
case currentSessionInfo(SortIndex, String)
case pendingSessionsHeader(SortIndex, String)
case pendingSession(index: Int32, sortIndex: SortIndex, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool)
case pendingSessionsInfo(SortIndex, String)
case otherSessionsHeader(SortIndex, String)
case addDevice(SortIndex, String)
case session(index: Int32, sortIndex: SortIndex, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool)
case website(index: Int32, sortIndex: SortIndex, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, website: WebAuthorization, peer: Peer?, enabled: Bool, editing: Bool, revealed: Bool)
case devicesInfo(SortIndex, String)
case ttlHeader(SortIndex, String)
case ttlTimeout(SortIndex, String, String)
var section: ItemListSectionId {
switch self {
case .header:
return RecentSessionsSection.header.rawValue
case .currentSessionHeader, .currentSession, .terminateOtherSessions, .terminateAllWebSessions, .currentAddDevice, .currentSessionInfo:
return RecentSessionsSection.currentSession.rawValue
case .pendingSessionsHeader, .pendingSession, .pendingSessionsInfo:
return RecentSessionsSection.pendingSessions.rawValue
case .otherSessionsHeader, .addDevice, .session, .website, .devicesInfo:
return RecentSessionsSection.otherSessions.rawValue
case .ttlHeader, .ttlTimeout:
return RecentSessionsSection.ttl.rawValue
}
}
var stableId: RecentSessionsEntryStableId {
switch self {
case .header:
return .index(0)
case .currentSessionHeader:
return .index(1)
case .currentSession:
return .index(2)
case .terminateOtherSessions:
return .index(3)
case .terminateAllWebSessions:
return .index(4)
case .currentAddDevice:
return .index(5)
case .currentSessionInfo:
return .index(6)
case .pendingSessionsHeader:
return .index(7)
case let .pendingSession(_, _, _, _, session, _, _, _):
return .session(session.hash)
case .pendingSessionsInfo:
return .index(8)
case .otherSessionsHeader:
return .index(9)
case .addDevice:
return .index(10)
case let .session(_, _, _, _, session, _, _, _):
return .session(session.hash)
case let .website(_, _, _, _, _, website, _, _, _, _):
return .session(website.hash)
case .devicesInfo:
return .devicesInfo
case .ttlHeader:
return .index(11)
case .ttlTimeout:
return .index(12)
}
}
var sortIndex: SortIndex {
switch self {
case let .header(index, _):
return index
case let .currentSessionHeader(index, _):
return index
case let .currentSession(index, _, _, _):
return index
case let .terminateOtherSessions(index, _):
return index
case let .terminateAllWebSessions(index, _):
return index
case let .currentAddDevice(index, _):
return index
case let .currentSessionInfo(index, _):
return index
case let .pendingSessionsHeader(index, _):
return index
case let .pendingSession(_, index, _, _, _, _, _, _):
return index
case let .pendingSessionsInfo(index, _):
return index
case let .otherSessionsHeader(index, _):
return index
case let .addDevice(index, _):
return index
case let .session(_, index, _, _, _, _, _, _):
return index
case let .website(_, index, _, _, _, _, _, _, _, _):
return index
case let .devicesInfo(index, _):
return index
case let .ttlHeader(index, _):
return index
case let .ttlTimeout(index, _, _):
return index
}
}
static func ==(lhs: RecentSessionsEntry, rhs: RecentSessionsEntry) -> Bool {
switch lhs {
case let .header(lhsSortIndex, lhsText):
if case let .header(rhsSortIndex, rhsText) = rhs, lhsSortIndex == rhsSortIndex, lhsText == rhsText {
return true
} else {
return false
}
case let .currentSessionHeader(lhsSortIndex, lhsText):
if case let .currentSessionHeader(rhsSortIndex, rhsText) = rhs, lhsSortIndex == rhsSortIndex, lhsText == rhsText {
return true
} else {
return false
}
case let .terminateOtherSessions(lhsSortIndex, lhsText):
if case let .terminateOtherSessions(rhsSortIndex, rhsText) = rhs, lhsSortIndex == rhsSortIndex, lhsText == rhsText {
return true
} else {
return false
}
case let .terminateAllWebSessions(lhsSortIndex, lhsText):
if case let .terminateAllWebSessions(rhsSortIndex, rhsText) = rhs, lhsSortIndex == rhsSortIndex, lhsText == rhsText {
return true
} else {
return false
}
case let .currentAddDevice(lhsSortIndex, lhsText):
if case let .currentAddDevice(rhsSortIndex, rhsText) = rhs, lhsSortIndex == rhsSortIndex, lhsText == rhsText {
return true
} else {
return false
}
case let .currentSessionInfo(lhsSortIndex, lhsText):
if case let .currentSessionInfo(rhsSortIndex, rhsText) = rhs, lhsSortIndex == rhsSortIndex, lhsText == rhsText {
return true
} else {
return false
}
case let .pendingSessionsHeader(lhsSortIndex, lhsText):
if case let .pendingSessionsHeader(rhsSortIndex, rhsText) = rhs, lhsSortIndex == rhsSortIndex, lhsText == rhsText {
return true
} else {
return false
}
case let .pendingSession(lhsIndex, lhsSortIndex, lhsStrings, lhsDateTimeFormat, lhsSession, lhsEnabled, lhsEditing, lhsRevealed):
if case let .pendingSession(rhsIndex, rhsSortIndex, rhsStrings, rhsDateTimeFormat, rhsSession, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsSortIndex == rhsSortIndex, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsSession == rhsSession, lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed {
return true
} else {
return false
}
case let .pendingSessionsInfo(lhsSortIndex, lhsText):
if case let .pendingSessionsInfo(rhsSortIndex, rhsText) = rhs, lhsSortIndex == rhsSortIndex, lhsText == rhsText {
return true
} else {
return false
}
case let .otherSessionsHeader(lhsSortIndex, lhsText):
if case let .otherSessionsHeader(rhsSortIndex, rhsText) = rhs, lhsSortIndex == rhsSortIndex, lhsText == rhsText {
return true
} else {
return false
}
case let .addDevice(lhsSortIndex, lhsText):
if case let .addDevice(rhsSortIndex, rhsText) = rhs, lhsSortIndex == rhsSortIndex, lhsText == rhsText {
return true
} else {
return false
}
case let .currentSession(lhsSortIndex, lhsStrings, lhsDateTimeFormat, lhsSession):
if case let .currentSession(rhsSortIndex, rhsStrings, rhsDateTimeFormat, rhsSession) = rhs, lhsSortIndex == rhsSortIndex, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsSession == rhsSession {
return true
} else {
return false
}
case let .session(lhsIndex, lhsSortIndex, lhsStrings, lhsDateTimeFormat, lhsSession, lhsEnabled, lhsEditing, lhsRevealed):
if case let .session(rhsIndex, rhsSortIndex, rhsStrings, rhsDateTimeFormat, rhsSession, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsSortIndex == rhsSortIndex, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsSession == rhsSession, lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed {
return true
} else {
return false
}
case let .website(lhsIndex, lhsSortIndex, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsWebsite, lhsPeer, lhsEnabled, lhsEditing, lhsRevealed):
if case let .website(rhsIndex, rhsSortIndex, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsWebsite, rhsPeer, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsSortIndex == rhsSortIndex, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameOrder == rhsNameOrder, lhsWebsite == rhsWebsite, arePeersEqual(lhsPeer, rhsPeer), lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed {
return true
} else {
return false
}
case let .devicesInfo(lhsSortIndex, lhsText):
if case let .devicesInfo(rhsSortIndex, rhsText) = rhs, lhsSortIndex == rhsSortIndex, lhsText == rhsText {
return true
} else {
return false
}
case let .ttlHeader(lhsSortIndex, lhsText):
if case let .ttlHeader(rhsSortIndex, rhsText) = rhs, lhsSortIndex == rhsSortIndex, lhsText == rhsText {
return true
} else {
return false
}
case let .ttlTimeout(lhsSortIndex, lhsText, lhsValue):
if case let .ttlTimeout(rhsSortIndex, rhsText, rhsValue) = rhs, lhsSortIndex == rhsSortIndex, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
}
}
static func <(lhs: RecentSessionsEntry, rhs: RecentSessionsEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! RecentSessionsControllerArguments
switch self {
case let .header(_, text):
return RecentSessionsHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animationName: "Devices", sectionId: self.section, buttonAction: {
arguments.addDevice()
}, linkAction: { action in
if case let .tap(link) = action {
switch link {
case "desktop":
arguments.openDesktopLink()
case "web":
arguments.openWebLink()
default:
break
}
}
})
case let .currentSessionHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .currentSession(_, _, dateTimeFormat, session):
return ItemListRecentSessionItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, session: session, enabled: true, editable: false, editing: false, revealed: false, sectionId: self.section, setSessionIdWithRevealedOptions: { _, _ in
}, removeSession: { _ in
}, action: {
arguments.openSession(session)
})
case let .terminateOtherSessions(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.blockDestructiveIcon(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: {
arguments.terminateOtherSessions()
})
case let .terminateAllWebSessions(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.blockDestructiveIcon(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: {
arguments.terminateAllWebSessions()
})
case let .currentAddDevice(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.addDeviceIcon(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
arguments.addDevice()
})
case let .currentSessionInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { action in
switch action {
case .tap:
arguments.openOtherAppsUrl()
}
})
case let .pendingSessionsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .pendingSession(_, _, _, dateTimeFormat, session, enabled, editing, revealed):
return ItemListRecentSessionItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in
arguments.setSessionIdWithRevealedOptions(previousId, id)
}, removeSession: { id in
arguments.removeSession(id)
}, action: {
arguments.openSession(session)
})
case let .pendingSessionsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .otherSessionsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .addDevice(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.addDeviceIcon(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
arguments.addDevice()
})
case let .session(_, _, _, dateTimeFormat, session, enabled, editing, revealed):
return ItemListRecentSessionItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in
arguments.setSessionIdWithRevealedOptions(previousId, id)
}, removeSession: { id in
arguments.removeSession(id)
}, action: {
arguments.openSession(session)
})
case let .website(_, _, _, dateTimeFormat, nameDisplayOrder, website, peer, enabled, editing, revealed):
return ItemListWebsiteItem(context: arguments.context, presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, website: website, peer: peer, enabled: enabled, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in
arguments.setSessionIdWithRevealedOptions(previousId, id)
}, removeSession: { id in
arguments.removeWebSession(id)
}, action: {
arguments.openWebSession(website, peer)
})
case let .devicesInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { action in
switch action {
case .tap:
arguments.openOtherAppsUrl()
}
})
case let .ttlHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .ttlTimeout(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, action: {
arguments.setupAuthorizationTTL()
}, tag: PrivacyAndSecurityEntryTag.accountTimeout)
}
}
}
private struct RecentSessionsControllerState: Equatable {
let editing: Bool
let sessionIdWithRevealedOptions: Int64?
let removingSessionId: Int64?
let terminatingOtherSessions: Bool
init() {
self.editing = false
self.sessionIdWithRevealedOptions = nil
self.removingSessionId = nil
self.terminatingOtherSessions = false
}
init(editing: Bool, sessionIdWithRevealedOptions: Int64?, removingSessionId: Int64?, terminatingOtherSessions: Bool) {
self.editing = editing
self.sessionIdWithRevealedOptions = sessionIdWithRevealedOptions
self.removingSessionId = removingSessionId
self.terminatingOtherSessions = terminatingOtherSessions
}
static func ==(lhs: RecentSessionsControllerState, rhs: RecentSessionsControllerState) -> Bool {
if lhs.editing != rhs.editing {
return false
}
if lhs.sessionIdWithRevealedOptions != rhs.sessionIdWithRevealedOptions {
return false
}
if lhs.removingSessionId != rhs.removingSessionId {
return false
}
if lhs.terminatingOtherSessions != rhs.terminatingOtherSessions {
return false
}
return true
}
func withUpdatedEditing(_ editing: Bool) -> RecentSessionsControllerState {
return RecentSessionsControllerState(editing: editing, sessionIdWithRevealedOptions: self.sessionIdWithRevealedOptions, removingSessionId: self.removingSessionId, terminatingOtherSessions: self.terminatingOtherSessions)
}
func withUpdatedSessionIdWithRevealedOptions(_ sessionIdWithRevealedOptions: Int64?) -> RecentSessionsControllerState {
return RecentSessionsControllerState(editing: self.editing, sessionIdWithRevealedOptions: sessionIdWithRevealedOptions, removingSessionId: self.removingSessionId, terminatingOtherSessions: self.terminatingOtherSessions)
}
func withUpdatedRemovingSessionId(_ removingSessionId: Int64?) -> RecentSessionsControllerState {
return RecentSessionsControllerState(editing: self.editing, sessionIdWithRevealedOptions: self.sessionIdWithRevealedOptions, removingSessionId: removingSessionId, terminatingOtherSessions: self.terminatingOtherSessions)
}
func withUpdatedTerminatingOtherSessions(_ terminatingOtherSessions: Bool) -> RecentSessionsControllerState {
return RecentSessionsControllerState(editing: self.editing, sessionIdWithRevealedOptions: self.sessionIdWithRevealedOptions, removingSessionId: self.removingSessionId, terminatingOtherSessions: terminatingOtherSessions)
}
}
private func recentSessionsControllerEntries(presentationData: PresentationData, state: RecentSessionsControllerState, sessionsState: ActiveSessionsContextState, enableQRLogin: Bool) -> [RecentSessionsEntry] {
var entries: [RecentSessionsEntry] = []
entries.append(.header(SortIndex(section: 0, item: 0), presentationData.strings.AuthSessions_HeaderInfo))
if !sessionsState.sessions.isEmpty {
var existingSessionIds = Set<Int64>()
entries.append(.currentSessionHeader(SortIndex(section: 1, item: 0), presentationData.strings.AuthSessions_CurrentSession))
if let index = sessionsState.sessions.firstIndex(where: { $0.hash == 0 }) {
existingSessionIds.insert(sessionsState.sessions[index].hash)
entries.append(.currentSession(SortIndex(section: 1, item: 1), presentationData.strings, presentationData.dateTimeFormat, sessionsState.sessions[index]))
}
var hasAddDevice = false
if sessionsState.sessions.count > 1 || enableQRLogin {
if sessionsState.sessions.count > 1 {
entries.append(.terminateOtherSessions(SortIndex(section: 1, item: 2), presentationData.strings.AuthSessions_TerminateOtherSessions))
entries.append(.currentSessionInfo(SortIndex(section: 1, item: 3), presentationData.strings.AuthSessions_TerminateOtherSessionsHelp))
} else if enableQRLogin {
hasAddDevice = true
// entries.append(.currentAddDevice(SortIndex(section: 1, item: 4), presentationData.strings.AuthSessions_AddDevice))
entries.append(.currentSessionInfo(SortIndex(section: 1, item: 5), presentationData.strings.AuthSessions_OtherDevices))
}
let filteredPendingSessions: [RecentAccountSession] = sessionsState.sessions.filter({ $0.flags.contains(.passwordPending) })
if !filteredPendingSessions.isEmpty {
entries.append(.pendingSessionsHeader(SortIndex(section: 1, item: 6), presentationData.strings.AuthSessions_IncompleteAttempts))
for i in 0 ..< filteredPendingSessions.count {
if !existingSessionIds.contains(filteredPendingSessions[i].hash) {
existingSessionIds.insert(filteredPendingSessions[i].hash)
entries.append(.pendingSession(index: Int32(i), sortIndex: SortIndex(section: 2, item: i), strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, session: filteredPendingSessions[i], enabled: state.removingSessionId != filteredPendingSessions[i].hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == filteredPendingSessions[i].hash))
}
}
entries.append(.pendingSessionsInfo(SortIndex(section: 3, item: 0), presentationData.strings.AuthSessions_IncompleteAttemptsInfo))
}
if sessionsState.sessions.count > 1 {
entries.append(.otherSessionsHeader(SortIndex(section: 4, item: 0), presentationData.strings.AuthSessions_OtherSessions))
}
// if enableQRLogin && !hasAddDevice {
// entries.append(.addDevice(SortIndex(section: 4, item: 1), presentationData.strings.AuthSessions_AddDevice))
// }
let filteredSessions: [RecentAccountSession] = sessionsState.sessions.sorted(by: { lhs, rhs in
return lhs.activityDate > rhs.activityDate
})
for i in 0 ..< filteredSessions.count {
if !existingSessionIds.contains(filteredSessions[i].hash) {
existingSessionIds.insert(filteredSessions[i].hash)
entries.append(.session(index: Int32(i), sortIndex: SortIndex(section: 5, item: i), strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, session: filteredSessions[i], enabled: state.removingSessionId != filteredSessions[i].hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == filteredSessions[i].hash))
}
}
if enableQRLogin && !hasAddDevice {
entries.append(.devicesInfo(SortIndex(section: 6, item: 0), presentationData.strings.AuthSessions_OtherDevices))
}
}
entries.append(.ttlHeader(SortIndex(section: 7, item: 0), presentationData.strings.AuthSessions_TerminateIfAwayTitle.uppercased()))
entries.append(.ttlTimeout(SortIndex(section: 7, item: 1), presentationData.strings.AuthSessions_TerminateIfAwayFor, timeIntervalString(strings: presentationData.strings, value: sessionsState.ttlDays * 24 * 60 * 60)))
}
return entries
}
private func recentSessionsControllerEntries(presentationData: PresentationData, state: RecentSessionsControllerState, websites: [WebAuthorization]?, peers: [PeerId : Peer]?) -> [RecentSessionsEntry] {
var entries: [RecentSessionsEntry] = []
if let websites = websites, let peers = peers {
var existingSessionIds = Set<Int64>()
if websites.count > 0 {
entries.append(.terminateAllWebSessions(SortIndex(section: 0, item: 0), presentationData.strings.AuthSessions_LogOutApplications))
entries.append(.currentSessionInfo(SortIndex(section: 0, item: 1), presentationData.strings.AuthSessions_LogOutApplicationsHelp))
entries.append(.otherSessionsHeader(SortIndex(section: 0, item: 2), presentationData.strings.AuthSessions_LoggedInWithTelegram))
let filteredWebsites: [WebAuthorization] = websites.sorted(by: { lhs, rhs in
return lhs.dateActive > rhs.dateActive
})
for i in 0 ..< filteredWebsites.count {
let website = websites[i]
if !existingSessionIds.contains(website.hash) {
existingSessionIds.insert(website.hash)
entries.append(.website(index: Int32(i), sortIndex: SortIndex(section: 1, item: i), strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, website: website, peer: peers[website.botId], enabled: state.removingSessionId != website.hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == website.hash))
}
}
}
}
return entries
}
private final class RecentSessionsControllerImpl: ItemListController, RecentSessionsController {
}
public func recentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext, webSessionsContext: WebSessionsContext, websitesOnly: Bool) -> ViewController & RecentSessionsController {
let statePromise = ValuePromise(RecentSessionsControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: RecentSessionsControllerState())
let updateState: ((RecentSessionsControllerState) -> RecentSessionsControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
activeSessionsContext.loadMore()
webSessionsContext.loadMore()
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
let removeSessionDisposable = MetaDisposable()
actionsDisposable.add(removeSessionDisposable)
let terminateOtherSessionsDisposable = MetaDisposable()
actionsDisposable.add(terminateOtherSessionsDisposable)
let didAppearValue = ValuePromise<Bool>(false)
if websitesOnly {
let autoDismissDisposable = (webSessionsContext.state
|> filter { !$0.isLoadingMore && $0.sessions.isEmpty }
|> take(1)
|> mapToSignal { _ in
return didAppearValue.get()
|> filter { $0 }
|> take(1)
}
|> deliverOnMainQueue).start(next: { _ in
dismissImpl?()
})
actionsDisposable.add(autoDismissDisposable)
}
let mode = ValuePromise<RecentSessionsMode>(websitesOnly ? .websites : .sessions)
let removeSessionImpl: (Int64, @escaping () -> Void) -> Void = { sessionId, completion in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.AuthSessions_TerminateSessionText),
ActionSheetButtonItem(title: presentationData.strings.AuthSessions_TerminateSession, color: .destructive, action: {
dismissAction()
completion()
updateState {
return $0.withUpdatedRemovingSessionId(sessionId)
}
removeSessionDisposable.set((activeSessionsContext.remove(hash: sessionId)
|> deliverOnMainQueue).start(error: { _ in
updateState {
return $0.withUpdatedRemovingSessionId(nil)
}
}, completed: {
updateState {
return $0.withUpdatedRemovingSessionId(nil)
}
context.sharedContext.updateNotificationTokensRegistration()
}))
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
let removeWebSessionImpl: (Int64) -> Void = { sessionId in
updateState {
return $0.withUpdatedRemovingSessionId(sessionId)
}
removeSessionDisposable.set(((webSessionsContext.remove(hash: sessionId)
|> mapToSignal { _ -> Signal<Void, NoError> in
})
|> deliverOnMainQueue).start(error: { _ in
}, completed: {
updateState {
return $0.withUpdatedRemovingSessionId(nil)
}
}))
}
let updateAuthorizationTTLDisposable = MetaDisposable()
actionsDisposable.add(updateAuthorizationTTLDisposable)
let updateSessionDisposable = MetaDisposable()
actionsDisposable.add(updateSessionDisposable)
let arguments = RecentSessionsControllerArguments(context: context, setSessionIdWithRevealedOptions: { sessionId, fromSessionId in
updateState { state in
if (sessionId == nil && fromSessionId == state.sessionIdWithRevealedOptions) || (sessionId != nil && fromSessionId == nil) {
return state.withUpdatedSessionIdWithRevealedOptions(sessionId)
} else {
return state
}
}
}, removeSession: { sessionId in
removeSessionImpl(sessionId, {})
}, terminateOtherSessions: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.AuthSessions_TerminateOtherSessionsText),
ActionSheetButtonItem(title: presentationData.strings.AuthSessions_TerminateOtherSessions, color: .destructive, action: {
dismissAction()
updateState {
return $0.withUpdatedTerminatingOtherSessions(true)
}
terminateOtherSessionsDisposable.set((activeSessionsContext.removeOther()
|> deliverOnMainQueue).start(error: { _ in
updateState {
return $0.withUpdatedTerminatingOtherSessions(false)
}
}, completed: {
updateState {
return $0.withUpdatedTerminatingOtherSessions(false)
}
context.sharedContext.updateNotificationTokensRegistration()
}))
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, openSession: { session in
let controller = RecentSessionScreen(context: context, subject: .session(session), updateAcceptSecretChats: { value in
updateSessionDisposable.set(activeSessionsContext.updateSessionAcceptsSecretChats(session, accepts: value).start())
}, updateAcceptIncomingCalls: { value in
updateSessionDisposable.set(activeSessionsContext.updateSessionAcceptsIncomingCalls(session, accepts: value).start())
}, remove: { completion in
removeSessionImpl(session.hash, {
completion()
})
})
presentControllerImpl?(controller, nil)
}, openWebSession: { session, peer in
let controller = RecentSessionScreen(context: context, subject: .website(session, peer.flatMap(EnginePeer.init)), updateAcceptSecretChats: { _ in }, updateAcceptIncomingCalls: { _ in }, remove: { completion in
removeWebSessionImpl(session.hash)
completion()
})
presentControllerImpl?(controller, nil)
}, removeWebSession: { sessionId in
removeWebSessionImpl(sessionId)
}, terminateAllWebSessions: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.AuthSessions_LogOutApplications, color: .destructive, action: {
dismissAction()
updateState {
return $0.withUpdatedTerminatingOtherSessions(true)
}
terminateOtherSessionsDisposable.set((webSessionsContext.removeAll()
|> deliverOnMainQueue).start(error: { _ in
}, completed: {
updateState {
return $0.withUpdatedTerminatingOtherSessions(false)
}
mode.set(.sessions)
}))
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, addDevice: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
DeviceAccess.authorizeAccess(to: .camera(.qrCode), presentationData: presentationData, present: { c, a in
c.presentationArguments = a
context.sharedContext.mainWindow?.present(c, on: .root)
}, openSettings: {
context.sharedContext.applicationBindings.openSettings()
}, { granted in
guard granted else {
return
}
pushControllerImpl?(QrCodeScanScreen(context: context, subject: .authTransfer(activeSessionsContext: activeSessionsContext)))
})
}, openOtherAppsUrl: {
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://telegram.org/apps", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {})
}, setupAuthorizationTTL: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
let ttlAction: (Int32) -> Void = { ttl in
updateAuthorizationTTLDisposable.set(activeSessionsContext.updateAuthorizationTTL(days: ttl).start())
}
let timeoutValues: [Int32] = [
7,
30,
90,
180
]
let timeoutItems: [ActionSheetItem] = timeoutValues.map { value in
return ActionSheetButtonItem(title: timeIntervalString(strings: presentationData.strings, value: value * 24 * 60 * 60), action: {
dismissAction()
ttlAction(value)
})
}
controller.setItemGroups([
ActionSheetItemGroup(items: timeoutItems),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, nil)
}, openDesktopLink: {
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://getdesktop.telegram.org", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {})
}, openWebLink: {
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://web.telegram.org", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {})
})
let previousMode = Atomic<RecentSessionsMode>(value: .sessions)
let enableQRLogin = context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|> map { view -> Bool in
guard let appConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) else {
return false
}
guard let data = appConfiguration.data, let enableQR = data["qr_login_camera"] as? Bool, enableQR else {
return false
}
return true
}
|> distinctUntilChanged
let signal = combineLatest(context.sharedContext.presentationData, mode.get(), statePromise.get(), activeSessionsContext.state, webSessionsContext.state, enableQRLogin)
|> deliverOnMainQueue
|> map { presentationData, mode, state, sessionsState, websitesAndPeers, enableQRLogin -> (ItemListControllerState, (ItemListNodeState, Any)) in
var rightNavigationButton: ItemListNavigationButton?
let websites = websitesAndPeers.sessions
let peers = websitesAndPeers.peers
if sessionsState.sessions.count > 1 {
if state.terminatingOtherSessions {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
} else if state.editing {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
updateState { state in
return state.withUpdatedEditing(false)
}
})
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: {
updateState { state in
return state.withUpdatedEditing(true)
}
})
}
}
let emptyStateItem: ItemListControllerEmptyStateItem? = nil
// if sessionsState.sessions.count == 1 && mode == .sessions {
// emptyStateItem = RecentSessionsEmptyStateItem(theme: presentationData.theme, strings: presentationData.strings)
// } else {
// emptyStateItem = nil
// }
let title: ItemListControllerTitle
let entries: [RecentSessionsEntry]
if websitesOnly {
title = .text(presentationData.strings.AuthSessions_LoggedIn)
} else {
title = .text(presentationData.strings.AuthSessions_DevicesTitle)
}
var animateChanges = true
switch (mode, websites, peers) {
case (.websites, let websites, let peers):
entries = recentSessionsControllerEntries(presentationData: presentationData, state: state, websites: websites, peers: peers)
default:
entries = recentSessionsControllerEntries(presentationData: presentationData, state: state, sessionsState: sessionsState, enableQRLogin: enableQRLogin)
}
let previousMode = previousMode.swap(mode)
var crossfadeState = false
if previousMode != mode {
crossfadeState = true
animateChanges = false
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: crossfadeState, animateChanges: animateChanges, scrollEnabled: emptyStateItem == nil)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = RecentSessionsControllerImpl(context: context, state: signal)
controller.titleControlValueChanged = { [weak mode] index in
mode?.set(index == 0 ? .sessions : .websites)
}
controller.didAppear = { _ in
didAppearValue.set(true)
}
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
return controller
}
@@ -0,0 +1,115 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AppBundle
final class RecentSessionsEmptyStateItem: ItemListControllerEmptyStateItem {
let theme: PresentationTheme
let strings: PresentationStrings
init(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
}
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool {
if let item = to as? RecentSessionsEmptyStateItem {
return self.theme === item.theme && self.strings === item.strings
} else {
return false
}
}
func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode {
if let current = current as? RecentSessionsEmptyStateItemNode {
current.item = self
return current
} else {
return RecentSessionsEmptyStateItemNode(item: self)
}
}
}
final class RecentSessionsEmptyStateItemNode: ItemListControllerEmptyStateItemNode {
private let imageNode: ASImageNode
private let titleNode: ASTextNode
private let textNode: ASTextNode
private var validLayout: (ContainerViewLayout, CGFloat)?
var item: RecentSessionsEmptyStateItem {
didSet {
self.updateThemeAndStrings(theme: self.item.theme, strings: self.item.strings)
if let (layout, navigationHeight) = self.validLayout {
self.updateLayout(layout: layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
}
}
init(item: RecentSessionsEmptyStateItem) {
self.item = item
self.imageNode = ASImageNode()
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false
self.textNode = ASTextNode()
self.textNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.imageNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.updateThemeAndStrings(theme: self.item.theme, strings: self.item.strings)
}
private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.imageNode.image = generateTintedImage(image: UIImage(bundleImageName: "Settings/RecentSessionsPlaceholder"), color: theme.list.freeTextColor)
self.titleNode.attributedText = NSAttributedString(string: strings.AuthSessions_EmptyTitle, font: Font.bold(17.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: strings.AuthSessions_EmptyText, font: Font.regular(14.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
}
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [])
insets.top += navigationBarHeight + 270.0
let imageSpacing: CGFloat = 8.0
let textSpacing: CGFloat = 8.0
let imageSize = self.imageNode.image?.size ?? CGSize()
let imageHeight = layout.size.width < layout.size.height ? imageSize.height + imageSpacing : 0.0
var textVisible = true
if layout.size.width == 320 {
textVisible = false
}
let titleSize = self.titleNode.measure(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right - 50.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let textSize = self.textNode.measure(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right - 50.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
var totalHeight = imageHeight + titleSize.height
if textVisible {
totalHeight += textSpacing + textSize.height
}
let topOffset = insets.top + floor((layout.size.height - insets.top - insets.bottom - totalHeight) / 2.0)
var visible = true
if case .compact = layout.metrics.widthClass, layout.size.width > layout.size.height {
visible = false
}
transition.updateAlpha(node: self.imageNode, alpha: visible ? 1.0 : 0.0)
transition.updateAlpha(node: self.titleNode, alpha: visible ? 1.0 : 0.0)
transition.updateAlpha(node: self.textNode, alpha: visible && textVisible ? 1.0 : 0.0)
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: topOffset), size: imageSize))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: topOffset + imageHeight), size: titleSize))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: self.titleNode.frame.maxY + textSpacing), size: textSize))
}
}
@@ -0,0 +1,243 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import AccountContext
import Markdown
import TextFormat
import SolidRoundedButtonNode
import ComponentFlow
import ButtonComponent
import BundleIconComponent
class RecentSessionsHeaderItem: ListViewItem, ItemListItem {
let context: AccountContext
let theme: PresentationTheme
let text: String
let animationName: String
let sectionId: ItemListSectionId
let buttonAction: () -> Void
let linkAction: ((ItemListTextItemLinkAction) -> Void)?
init(context: AccountContext, theme: PresentationTheme, text: String, animationName: String, sectionId: ItemListSectionId, buttonAction: @escaping () -> Void, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) {
self.context = context
self.theme = theme
self.text = text
self.animationName = animationName
self.sectionId = sectionId
self.buttonAction = buttonAction
self.linkAction = linkAction
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = RecentSessionsHeaderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? RecentSessionsHeaderItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
private let titleFont = Font.regular(13.0)
class RecentSessionsHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode
private var animationNode: AnimatedStickerNode
private let buttonNode: SolidRoundedButtonNode
private let button = ComponentView<Empty>()
private var item: RecentSessionsHeaderItem?
init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = true
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), fontSize: 16.0, height: 50.0, cornerRadius: 11.0)
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.animationNode)
//self.addSubnode(self.buttonNode)
}
override public func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
self.titleNode.view.addGestureRecognizer(recognizer)
self.buttonNode.pressed = { [weak self] in
if let strongSelf = self, let item = strongSelf.item {
item.buttonAction()
}
}
}
func asyncLayout() -> (_ item: RecentSessionsHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
let leftInset: CGFloat = 32.0 + params.leftInset
let topInset: CGFloat = 124.0
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
}
let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height + 69.0)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
if strongSelf.item == nil {
strongSelf.animationNode.autoplay = true
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: item.animationName), width: 256, height: 256, playbackMode: .still(.start), mode: .direct(cachePathPrefix: nil))
strongSelf.animationNode.visibility = true
Queue.mainQueue().after(0.3) {
strongSelf.animationNode.play(firstFrame: false, fromIndex: nil)
}
}
strongSelf.item = item
strongSelf.buttonNode.title = item.context.sharedContext.currentPresentationData.with { $0 }.strings.AuthSessions_LinkDesktopDevice
if let _ = updatedTheme {
strongSelf.buttonNode.icon = UIImage(bundleImageName: "Settings/QrButtonIcon")
strongSelf.buttonNode.updateTheme(SolidRoundedButtonTheme(theme: item.theme))
}
let buttonSideInset: CGFloat = 36.0
let buttonWidth = min(375, contentSize.width - buttonSideInset * 2.0)
let buttonHeight = 52.0
let buttonFrame = CGRect(x: floorToScreenPixels((params.width - buttonWidth) / 2.0), y: contentSize.height - buttonHeight + 4.0, width: buttonWidth, height: buttonHeight)
strongSelf.buttonNode.frame = buttonFrame
let _ = strongSelf.button.update(
transition: .immediate,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: item.theme.list.itemCheckColors.fillColor,
foreground: item.theme.list.itemCheckColors.foregroundColor,
pressedColor: item.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: "button",
component: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Settings/QrButtonIcon", tintColor: item.theme.list.itemCheckColors.foregroundColor))),
AnyComponentWithIdentity(id: "label", component: AnyComponent(
Text(text: item.context.sharedContext.currentPresentationData.with { $0 }.strings.AuthSessions_LinkDesktopDevice, font: Font.semibold(17.0), color: item.theme.list.itemCheckColors.foregroundColor)
))
], spacing: 6.0)
)
),
action: {
item.buttonAction()
}
)),
environment: {},
containerSize: CGSize(width: buttonWidth, height: buttonHeight)
)
if let buttonView = strongSelf.button.view {
if buttonView.superview == nil {
strongSelf.view.addSubview(buttonView)
}
buttonView.frame = buttonFrame
}
strongSelf.accessibilityLabel = attributedText.string
let iconSize = CGSize(width: 128.0, height: 128.0)
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
strongSelf.animationNode.updateLayout(size: iconSize)
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 8.0), size: titleLayout.size)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
if let item = self.item {
if let (_, attributes) = self.titleNode.attributesAtPoint(location) {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
item.linkAction?(.tap(url))
}
}
}
default:
break
}
}
default:
break
}
}
}
@@ -0,0 +1,925 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import AccountContext
import SolidRoundedButtonNode
import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting
import PresentationDataUtils
import AnimationUI
import MergeLists
import MediaResources
import StickerResources
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import AvatarNode
import UndoUI
private func closeButtonImage(theme: PresentationTheme) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: 0x808084, alpha: 0.1).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setStrokeColor(theme.actionSheet.inputClearButtonColor.cgColor)
context.move(to: CGPoint(x: 10.0, y: 10.0))
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
context.strokePath()
context.move(to: CGPoint(x: 20.0, y: 10.0))
context.addLine(to: CGPoint(x: 10.0, y: 20.0))
context.strokePath()
})
}
final class RecentSessionScreen: ViewController {
enum Subject {
case session(RecentAccountSession)
case website(WebAuthorization, EnginePeer?)
}
private var controllerNode: RecentSessionScreenNode {
return self.displayNode as! RecentSessionScreenNode
}
private var animatedIn = false
private let context: AccountContext
private let subject: RecentSessionScreen.Subject
private let remove: (@escaping () -> Void) -> Void
private let updateAcceptSecretChats: (Bool) -> Void
private let updateAcceptIncomingCalls: (Bool) -> Void
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
var dismissed: (() -> Void)?
var passthroughHitTestImpl: ((CGPoint) -> UIView?)? {
didSet {
if self.isNodeLoaded {
self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl
}
}
}
init(context: AccountContext, subject: RecentSessionScreen.Subject, updateAcceptSecretChats: @escaping (Bool) -> Void, updateAcceptIncomingCalls: @escaping (Bool) -> Void, remove: @escaping (@escaping () -> Void) -> Void) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.subject = subject
self.remove = remove
self.updateAcceptSecretChats = updateAcceptSecretChats
self.updateAcceptIncomingCalls = updateAcceptIncomingCalls
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .Ignore
self.blocksBackgroundWhenInOverlay = true
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.presentationData = presentationData
strongSelf.controllerNode.updatePresentationData(presentationData)
}
})
self.statusBar.statusBarStyle = .Ignore
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
override public func loadDisplayNode() {
self.displayNode = RecentSessionScreenNode(context: self.context, presentationData: self.presentationData, controller: self, subject: self.subject)
self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl
self.controllerNode.present = { [weak self] c in
self?.present(c, in: .current)
}
self.controllerNode.dismiss = { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
self.controllerNode.remove = { [weak self] in
self?.remove({
self?.controllerNode.animateOut()
})
}
self.controllerNode.updateAcceptSecretChats = { [weak self] value in
self?.updateAcceptSecretChats(value)
}
self.controllerNode.updateAcceptIncomingCalls = { [weak self] value in
self?.updateAcceptIncomingCalls(value)
}
}
override public func loadView() {
super.loadView()
self.view.disablesInteractiveTransitionGestureRecognizer = true
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.animatedIn {
self.animatedIn = true
self.controllerNode.animateIn()
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
self.controllerNode.animateOut(completion: completion)
self.dismissed?()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}
private class RecentSessionScreenNode: ViewControllerTracingNode, ASScrollViewDelegate {
private let context: AccountContext
private var presentationData: PresentationData
private weak var controller: RecentSessionScreen?
private let subject: RecentSessionScreen.Subject
private let dimNode: ASDisplayNode
private let wrappingScrollNode: ASScrollNode
private let contentContainerNode: ASDisplayNode
private let topContentContainerNode: SparseNode
private let backgroundNode: ASDisplayNode
private let contentBackgroundNode: ASDisplayNode
private var iconNode: ASImageNode?
private var animationBackgroundNode: ASDisplayNode?
private var animationNode: AnimationNode?
private var avatarNode: AvatarNode?
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let fieldBackgroundNode: ASDisplayNode
private let deviceTitleNode: ImmediateTextNode
private let deviceValueNode: ImmediateTextNode
private let firstSeparatorNode: ASDisplayNode
private let ipTitleNode: ImmediateTextNode
private let ipValueNode: ImmediateTextNode
private let secondSeparatorNode: ASDisplayNode
private let locationTitleNode: ImmediateTextNode
private let locationValueNode: ImmediateTextNode
private let locationInfoNode: ImmediateTextNode
private let acceptBackgroundNode: ASDisplayNode
private let acceptHeaderNode: ImmediateTextNode
private let secretChatsTitleNode: ImmediateTextNode
private let secretChatsSwitchNode: SwitchNode
private let secretChatsActivateAreaNode: AccessibilityAreaNode
private let incomingCallsTitleNode: ImmediateTextNode
private let incomingCallsSwitchNode: SwitchNode
private let incomingCallsActivateAreaNode: AccessibilityAreaNode
private let acceptSeparatorNode: ASDisplayNode
private let cancelButton: HighlightableButtonNode
private let terminateButton: SolidRoundedButtonNode
private var containerLayout: (ContainerViewLayout, CGFloat)?
var present: ((ViewController) -> Void)?
var remove: (() -> Void)?
var dismiss: (() -> Void)?
var updateAcceptSecretChats: ((Bool) -> Void)?
var updateAcceptIncomingCalls: ((Bool) -> Void)?
init(context: AccountContext, presentationData: PresentationData, controller: RecentSessionScreen, subject: RecentSessionScreen.Subject) {
self.context = context
self.controller = controller
self.presentationData = presentationData
self.subject = subject
self.wrappingScrollNode = ASScrollNode()
self.wrappingScrollNode.view.alwaysBounceVertical = true
self.wrappingScrollNode.view.delaysContentTouches = false
self.wrappingScrollNode.view.showsVerticalScrollIndicator = false
self.wrappingScrollNode.view.canCancelContentTouches = true
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.contentContainerNode = ASDisplayNode()
self.contentContainerNode.isOpaque = false
self.topContentContainerNode = SparseNode()
self.topContentContainerNode.isOpaque = false
self.backgroundNode = ASDisplayNode()
self.backgroundNode.clipsToBounds = true
self.backgroundNode.cornerRadius = 16.0
let backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
let textColor = self.presentationData.theme.list.itemPrimaryTextColor
let accentColor = self.presentationData.theme.list.itemAccentColor
let secondaryTextColor = self.presentationData.theme.list.itemSecondaryTextColor
self.contentBackgroundNode = ASDisplayNode()
self.contentBackgroundNode.backgroundColor = backgroundColor
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 2
self.titleNode.textAlignment = .center
self.textNode = ImmediateTextNode()
self.textNode.maximumNumberOfLines = 1
self.textNode.textAlignment = .center
self.fieldBackgroundNode = ASDisplayNode()
self.fieldBackgroundNode.clipsToBounds = true
self.fieldBackgroundNode.cornerRadius = 11
self.fieldBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor
self.deviceTitleNode = ImmediateTextNode()
self.deviceValueNode = ImmediateTextNode()
self.ipTitleNode = ImmediateTextNode()
self.ipValueNode = ImmediateTextNode()
self.locationTitleNode = ImmediateTextNode()
self.locationValueNode = ImmediateTextNode()
self.locationInfoNode = ImmediateTextNode()
self.acceptHeaderNode = ImmediateTextNode()
self.secretChatsTitleNode = ImmediateTextNode()
self.secretChatsSwitchNode = SwitchNode()
self.incomingCallsTitleNode = ImmediateTextNode()
self.incomingCallsSwitchNode = SwitchNode()
self.secretChatsActivateAreaNode = AccessibilityAreaNode()
self.incomingCallsActivateAreaNode = AccessibilityAreaNode()
self.cancelButton = HighlightableButtonNode()
self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal)
self.cancelButton.accessibilityLabel = presentationData.strings.Common_Close
self.cancelButton.accessibilityTraits = [.button]
self.terminateButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: self.presentationData.theme.list.itemDestructiveColor), font: .regular, height: 44.0, cornerRadius: 11.0)
var hasSecretChats = false
var hasIncomingCalls = false
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let title: String
let subtitle: String
let subtitleActive: Bool
let device: String
let deviceTitle: String
let location: String
let ip: String
switch subject {
case let .session(session):
self.terminateButton.title = self.presentationData.strings.AuthSessions_View_TerminateSession
var appVersion = session.appVersion
appVersion = appVersion.replacingOccurrences(of: "APPSTORE", with: "").replacingOccurrences(of: "BETA", with: "Beta").trimmingTrailingSpaces()
if session.isCurrent {
subtitle = presentationData.strings.Presence_online
subtitleActive = true
} else {
subtitle = stringForRelativeActivityTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, relativeTimestamp: session.activityDate, relativeTo: timestamp)
subtitleActive = false
}
deviceTitle = presentationData.strings.AuthSessions_View_Application
var deviceString = ""
if !session.deviceModel.isEmpty {
deviceString = session.deviceModel
}
title = deviceString
device = "\(session.appName) \(appVersion)"
location = session.country
ip = session.ip
let (icon, backgroundColor, animationName, colorsArray) = iconForSession(session)
if let animationName = animationName {
var colors: [String: UIColor] = [:]
if let colorsArray = colorsArray {
for color in colorsArray {
colors[color] = backgroundColor
}
}
let animationNode = AnimationNode(animation: animationName, colors: colors, scale: 1.0)
self.animationNode = animationNode
let animationBackgroundNode = ASDisplayNode()
animationBackgroundNode.cornerRadius = 20.0
animationBackgroundNode.backgroundColor = backgroundColor
self.animationBackgroundNode = animationBackgroundNode
} else if let icon = icon {
let iconNode = ASImageNode()
iconNode.displaysAsynchronously = false
iconNode.image = icon
self.iconNode = iconNode
}
self.secretChatsSwitchNode.isOn = session.flags.contains(.acceptsSecretChats)
self.incomingCallsSwitchNode.isOn = session.flags.contains(.acceptsIncomingCalls)
self.secretChatsActivateAreaNode.accessibilityValue = self.secretChatsSwitchNode.isOn ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off
self.incomingCallsActivateAreaNode.accessibilityValue = self.incomingCallsSwitchNode.isOn ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off
if !session.flags.contains(.passwordPending) && session.apiId != 22 {
hasIncomingCalls = true
if ![2040, 2496].contains(session.apiId) {
hasSecretChats = true
}
}
case let .website(website, peer):
self.terminateButton.title = self.presentationData.strings.AuthSessions_View_Logout
if let peer = peer {
title = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
} else {
title = ""
}
subtitle = website.domain
subtitleActive = false
deviceTitle = presentationData.strings.AuthSessions_View_Browser
var deviceString = ""
if !website.browser.isEmpty {
deviceString += website.browser
}
if !website.platform.isEmpty {
if !deviceString.isEmpty {
deviceString += ", "
}
deviceString += website.platform
}
device = deviceString
location = website.region
ip = website.ip
let avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 12.0))
avatarNode.clipsToBounds = true
avatarNode.cornerRadius = 17.0
if let peer {
avatarNode.setPeer(context: context, theme: presentationData.theme, peer: peer, authorOfMessage: nil, overrideImage: nil, emptyColor: nil, clipStyle: .none, synchronousLoad: false, displayDimensions: CGSize(width: 72.0, height: 72.0), storeUnrounded: false)
}
self.avatarNode = avatarNode
}
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.regular(30.0), textColor: textColor)
self.titleNode.accessibilityLabel = title
self.titleNode.isAccessibilityElement = true
self.textNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(17.0), textColor: subtitleActive ? accentColor : secondaryTextColor)
self.textNode.accessibilityLabel = subtitle
self.textNode.isAccessibilityElement = true
self.deviceTitleNode.attributedText = NSAttributedString(string: deviceTitle, font: Font.regular(17.0), textColor: textColor)
self.deviceValueNode.attributedText = NSAttributedString(string: device, font: Font.regular(17.0), textColor: secondaryTextColor)
self.deviceValueNode.accessibilityLabel = deviceTitle
self.deviceValueNode.accessibilityValue = device
self.deviceValueNode.isAccessibilityElement = true
self.firstSeparatorNode = ASDisplayNode()
self.firstSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor
self.ipTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_IP, font: Font.regular(17.0), textColor: textColor)
self.ipValueNode.attributedText = NSAttributedString(string: ip, font: Font.regular(17.0), textColor: secondaryTextColor)
self.ipValueNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_IP
self.ipValueNode.accessibilityValue = ip
self.ipValueNode.isAccessibilityElement = true
self.secondSeparatorNode = ASDisplayNode()
self.secondSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor
self.locationTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_Location, font: Font.regular(17.0), textColor: textColor)
self.locationValueNode.attributedText = NSAttributedString(string: location, font: Font.regular(17.0), textColor: secondaryTextColor)
self.locationValueNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_Location
self.locationValueNode.accessibilityValue = location
self.locationValueNode.isAccessibilityElement = true
self.locationInfoNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_LocationInfo, font: Font.regular(13.0), textColor: secondaryTextColor)
self.locationInfoNode.maximumNumberOfLines = 4
self.locationInfoNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_LocationInfo
self.locationInfoNode.isAccessibilityElement = true
self.acceptBackgroundNode = ASDisplayNode()
self.acceptBackgroundNode.clipsToBounds = true
self.acceptBackgroundNode.cornerRadius = 11
self.acceptBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor
self.acceptHeaderNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptTitle.uppercased(), font: Font.regular(17.0), textColor: textColor)
self.acceptHeaderNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_AcceptTitle
self.acceptHeaderNode.isAccessibilityElement = true
self.secretChatsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptSecretChats, font: Font.regular(17.0), textColor: textColor)
self.incomingCallsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptIncomingCalls, font: Font.regular(17.0), textColor: textColor)
self.secretChatsActivateAreaNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_AcceptSecretChats
self.secretChatsActivateAreaNode.accessibilityHint = self.presentationData.strings.VoiceOver_Common_SwitchHint
self.incomingCallsActivateAreaNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_AcceptIncomingCalls
self.incomingCallsActivateAreaNode.accessibilityHint = self.presentationData.strings.VoiceOver_Common_SwitchHint
self.acceptSeparatorNode = ASDisplayNode()
self.acceptSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor
super.init()
self.backgroundColor = nil
self.isOpaque = false
self.addSubnode(self.dimNode)
self.wrappingScrollNode.view.delegate = self.wrappedScrollViewDelegate
self.addSubnode(self.wrappingScrollNode)
self.wrappingScrollNode.addSubnode(self.backgroundNode)
self.wrappingScrollNode.addSubnode(self.contentContainerNode)
self.wrappingScrollNode.addSubnode(self.topContentContainerNode)
self.backgroundNode.addSubnode(self.contentBackgroundNode)
self.contentContainerNode.addSubnode(self.titleNode)
self.contentContainerNode.addSubnode(self.textNode)
self.contentContainerNode.addSubnode(self.fieldBackgroundNode)
self.contentContainerNode.addSubnode(self.deviceTitleNode)
self.contentContainerNode.addSubnode(self.deviceValueNode)
self.contentContainerNode.addSubnode(self.ipTitleNode)
self.contentContainerNode.addSubnode(self.ipValueNode)
self.contentContainerNode.addSubnode(self.locationTitleNode)
self.contentContainerNode.addSubnode(self.locationValueNode)
self.contentContainerNode.addSubnode(self.locationInfoNode)
self.contentContainerNode.addSubnode(self.firstSeparatorNode)
self.contentContainerNode.addSubnode(self.secondSeparatorNode)
self.contentContainerNode.addSubnode(self.terminateButton)
self.topContentContainerNode.addSubnode(self.cancelButton)
self.iconNode.flatMap { self.contentContainerNode.addSubnode($0) }
self.animationBackgroundNode.flatMap { self.contentContainerNode.addSubnode($0) }
self.animationNode.flatMap { self.contentContainerNode.addSubnode($0) }
self.avatarNode.flatMap { self.contentContainerNode.addSubnode($0) }
if hasIncomingCalls {
self.contentContainerNode.addSubnode(self.acceptBackgroundNode)
self.contentContainerNode.addSubnode(self.acceptHeaderNode)
if hasSecretChats {
self.contentContainerNode.addSubnode(self.secretChatsTitleNode)
self.contentContainerNode.addSubnode(self.secretChatsSwitchNode)
self.contentContainerNode.addSubnode(self.secretChatsActivateAreaNode)
self.secretChatsSwitchNode.valueUpdated = { [weak self] value in
if let strongSelf = self {
strongSelf.updateAcceptSecretChats?(value)
strongSelf.secretChatsActivateAreaNode.accessibilityValue = value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off
}
}
self.secretChatsActivateAreaNode.activate = { [weak self] in
guard let strongSelf = self else {
return false
}
let value = !strongSelf.secretChatsSwitchNode.isOn
strongSelf.updateAcceptSecretChats?(value)
strongSelf.secretChatsActivateAreaNode.accessibilityValue = value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off
return true
}
self.contentContainerNode.addSubnode(self.acceptSeparatorNode)
}
self.contentContainerNode.addSubnode(self.incomingCallsTitleNode)
self.contentContainerNode.addSubnode(self.incomingCallsSwitchNode)
self.contentContainerNode.addSubnode(self.incomingCallsActivateAreaNode)
self.incomingCallsSwitchNode.valueUpdated = { [weak self] value in
if let strongSelf = self {
strongSelf.updateAcceptIncomingCalls?(value)
strongSelf.incomingCallsActivateAreaNode.accessibilityValue = value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off
}
}
self.incomingCallsActivateAreaNode.activate = { [weak self] in
guard let strongSelf = self else {
return false
}
let value = !strongSelf.incomingCallsSwitchNode.isOn
strongSelf.updateAcceptIncomingCalls?(value)
strongSelf.incomingCallsActivateAreaNode.accessibilityValue = value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off
return true
}
}
self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
self.terminateButton.pressed = { [weak self] in
if let strongSelf = self {
strongSelf.remove?()
}
}
}
override func didLoad() {
super.didLoad()
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture)))
let titleGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleTitleLongPress(_:)))
self.titleNode.view.addGestureRecognizer(titleGestureRecognizer)
let deviceGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleDeviceLongPress(_:)))
self.deviceValueNode.view.addGestureRecognizer(deviceGestureRecognizer)
let locationGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLocationLongPress(_:)))
self.locationValueNode.view.addGestureRecognizer(locationGestureRecognizer)
let ipGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleIpLongPress(_:)))
self.ipValueNode.view.addGestureRecognizer(ipGestureRecognizer)
if let animationNode = self.animationNode {
animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.animationPressed)))
}
}
@objc private func handleTitleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
if gestureRecognizer.state == .began {
self.displayCopyContextMenu(self.titleNode, self.titleNode.attributedText?.string ?? "")
}
}
@objc private func handleDeviceLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
if gestureRecognizer.state == .began {
self.displayCopyContextMenu(self.deviceValueNode, self.deviceValueNode.attributedText?.string ?? "")
}
}
@objc private func handleLocationLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
if gestureRecognizer.state == .began {
self.displayCopyContextMenu(self.locationValueNode, self.locationValueNode.attributedText?.string ?? "")
}
}
@objc private func handleIpLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
if gestureRecognizer.state == .began {
self.displayCopyContextMenu(self.ipValueNode, self.ipValueNode.attributedText?.string ?? "")
}
}
private func displayCopyContextMenu(_ node: ASDisplayNode, _ string: String) {
if !string.isEmpty {
var actions: [ContextMenuAction] = []
actions.append(ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in
UIPasteboard.general.string = string
if let strongSelf = self {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}
}))
let contextMenuController = makeContextMenuController(actions: actions)
self.controller?.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
if let strongSelf = self {
return (node, node.bounds.insetBy(dx: 0.0, dy: -2.0), strongSelf, strongSelf.view.bounds)
} else {
return nil
}
}))
}
}
func updatePresentationData(_ presentationData: PresentationData) {
guard !self.animatedOut else {
return
}
let previousTheme = self.presentationData.theme
self.presentationData = presentationData
self.contentBackgroundNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.regular(30.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
let subtitleColor: UIColor
if case let .session(session) = self.subject, session.isCurrent {
subtitleColor = self.presentationData.theme.list.itemAccentColor
} else {
subtitleColor = self.presentationData.theme.list.itemSecondaryTextColor
}
self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: subtitleColor)
self.fieldBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor
self.firstSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor
self.secondSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor
self.acceptSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor
self.deviceTitleNode.attributedText = NSAttributedString(string: self.deviceTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.locationTitleNode.attributedText = NSAttributedString(string: self.locationTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.ipTitleNode.attributedText = NSAttributedString(string: self.ipTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.deviceValueNode.attributedText = NSAttributedString(string: self.deviceValueNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor)
self.locationValueNode.attributedText = NSAttributedString(string: self.locationValueNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor)
self.ipValueNode.attributedText = NSAttributedString(string: self.ipValueNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor)
self.locationInfoNode.attributedText = NSAttributedString(string: self.locationInfoNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor)
self.acceptHeaderNode.attributedText = NSAttributedString(string: self.acceptHeaderNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor)
self.secretChatsTitleNode.attributedText = NSAttributedString(string: self.secretChatsTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.incomingCallsTitleNode.attributedText = NSAttributedString(string: self.incomingCallsTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.acceptBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor
if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal)
self.terminateButton.updateTheme(SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: self.presentationData.theme.list.itemDestructiveColor))
}
@objc func animationPressed() {
if let animationNode = self.animationNode, !animationNode.isPlaying {
animationNode.playOnce()
}
}
@objc func cancelButtonPressed() {
self.animateOut()
}
@objc func dimTapGesture() {
self.cancelButtonPressed()
}
private var animatedOut = false
func animateIn() {
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
let dimPosition = self.dimNode.layer.position
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
let targetBounds = self.bounds
self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset)
self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset)
transition.animateView({
self.bounds = targetBounds
self.dimNode.position = dimPosition
})
}
func animateOut(completion: (() -> Void)? = nil) {
self.animatedOut = true
var dimCompleted = false
var offsetCompleted = false
let internalCompletion: () -> Void = { [weak self] in
if let strongSelf = self, dimCompleted && offsetCompleted {
strongSelf.dismiss?()
}
completion?()
}
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
dimCompleted = true
internalCompletion()
})
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
self.wrappingScrollNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
offsetCompleted = true
internalCompletion()
})
self.controller?.window?.forEachController { c in
if let c = c as? UndoOverlayController {
c.dismiss()
}
}
}
var passthroughHitTestImpl: ((CGPoint) -> UIView?)?
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.contains(point) {
if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) {
return self.dimNode.view
}
}
return super.hitTest(point, with: event)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let contentOffset = scrollView.contentOffset
let additionalTopHeight = max(0.0, -contentOffset.y)
if additionalTopHeight >= 30.0 {
self.cancelButtonPressed()
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let isFirstTime = self.containerLayout == nil
self.containerLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.statusBar, .input])
let cleanInsets = layout.insets(options: [.statusBar])
insets.top = max(10.0, insets.top)
let bottomInset: CGFloat = 10.0 + cleanInsets.bottom
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0)
transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let iconSize = CGSize(width: 72.0, height: 72.0)
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - iconSize.width) / 2.0), y: 36.0), size: iconSize)
if let iconNode = self.iconNode {
transition.updateFrame(node: iconNode, frame: iconFrame)
} else if let animationNode = self.animationNode, let animationBackgroundNode = self.animationBackgroundNode {
transition.updateFrame(node: animationNode, frame: iconFrame)
transition.updateFrame(node: animationBackgroundNode, frame: iconFrame)
if #available(iOS 13.0, *) {
animationBackgroundNode.layer.cornerCurve = .continuous
}
if isFirstTime {
Queue.mainQueue().after(0.5) {
animationNode.playOnce()
}
}
} else if let avatarNode = self.avatarNode {
transition.updateFrame(node: avatarNode, frame: iconFrame)
}
let inset: CGFloat = 16.0
let titleSize = self.titleNode.updateLayout(CGSize(width: width - inset * 2.0, height: 100.0))
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - titleSize.width) / 2.0), y: 120.0), size: titleSize)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
let textSize = self.textNode.updateLayout(CGSize(width: width - inset * 2.0, height: 60.0))
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - textSize.width) / 2.0), y: titleFrame.maxY), size: textSize)
transition.updateFrame(node: self.textNode, frame: textFrame)
let cancelSize = CGSize(width: 44.0, height: 44.0)
let cancelFrame = CGRect(origin: CGPoint(x: width - cancelSize.width - 3.0, y: 6.0), size: cancelSize)
transition.updateFrame(node: self.cancelButton, frame: cancelFrame)
let fieldItemHeight: CGFloat = 44.0
var fieldFrame = CGRect(x: inset, y: textFrame.maxY + 24.0, width: width - inset * 2.0, height: fieldItemHeight * 2.0)
if !(self.ipValueNode.attributedText?.string ?? "").isEmpty {
fieldFrame.size.height += fieldItemHeight
self.ipTitleNode.isHidden = false
self.ipValueNode.isHidden = false
self.secondSeparatorNode.isHidden = false
} else {
self.ipTitleNode.isHidden = true
self.ipValueNode.isHidden = true
self.secondSeparatorNode.isHidden = true
}
transition.updateFrame(node: self.fieldBackgroundNode, frame: fieldFrame)
let maxFieldTitleWidth = (width - inset * 4.0) * 0.4
let deviceTitleTextSize = self.deviceTitleNode.updateLayout(CGSize(width: maxFieldTitleWidth, height: fieldItemHeight))
let deviceTitleTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.minY + floorToScreenPixels((fieldItemHeight - deviceTitleTextSize.height) / 2.0)), size: deviceTitleTextSize)
transition.updateFrame(node: self.deviceTitleNode, frame: deviceTitleTextFrame)
let deviceValueTextSize = self.deviceValueNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0 - deviceTitleTextSize.width - 10.0, height: fieldItemHeight))
let deviceValueTextFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - deviceValueTextSize.width - inset, y: fieldFrame.minY + floorToScreenPixels((fieldItemHeight - deviceValueTextSize.height) / 2.0)), size: deviceValueTextSize)
transition.updateFrame(node: self.deviceValueNode, frame: deviceValueTextFrame)
transition.updateFrame(node: self.firstSeparatorNode, frame: CGRect(x: fieldFrame.minX + inset, y: fieldFrame.minY + fieldItemHeight, width: fieldFrame.width - inset, height: UIScreenPixel))
let ipTitleTextSize = self.ipTitleNode.updateLayout(CGSize(width: maxFieldTitleWidth, height: fieldItemHeight))
let ipTitleTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.minY + fieldItemHeight + floorToScreenPixels((fieldItemHeight - ipTitleTextSize.height) / 2.0)), size: ipTitleTextSize)
transition.updateFrame(node: self.ipTitleNode, frame: ipTitleTextFrame)
let ipValueTextSize = self.ipValueNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0 - ipTitleTextSize.width - 10.0, height: fieldItemHeight))
let ipValueTextFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - ipValueTextSize.width - inset, y: fieldFrame.minY + fieldItemHeight + floorToScreenPixels((fieldItemHeight - ipValueTextSize.height) / 2.0)), size: ipValueTextSize)
transition.updateFrame(node: self.ipValueNode, frame: ipValueTextFrame)
transition.updateFrame(node: self.secondSeparatorNode, frame: CGRect(x: fieldFrame.minX + inset, y: fieldFrame.minY + fieldItemHeight + fieldItemHeight, width: fieldFrame.width - inset, height: UIScreenPixel))
let locationTitleTextSize = self.locationTitleNode.updateLayout(CGSize(width: maxFieldTitleWidth, height: fieldItemHeight))
let locationTitleTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - locationTitleTextSize.height) / 2.0)), size: locationTitleTextSize)
transition.updateFrame(node: self.locationTitleNode, frame: locationTitleTextFrame)
let locationValueTextSize = self.locationValueNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0 - locationTitleTextSize.width - 10.0, height: fieldItemHeight))
let locationValueTextFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - locationValueTextSize.width - inset, y: fieldFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - locationValueTextSize.height) / 2.0)), size: locationValueTextSize)
transition.updateFrame(node: self.locationValueNode, frame: locationValueTextFrame)
let locationInfoTextSize = self.locationInfoNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0, height: fieldItemHeight * 2.0))
let locationInfoTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.maxY + 6.0), size: locationInfoTextSize)
transition.updateFrame(node: self.locationInfoNode, frame: locationInfoTextFrame)
var contentHeight = locationInfoTextFrame.maxY + bottomInset + 64.0
var secretFrame = CGRect(x: inset, y: locationInfoTextFrame.maxY + 59.0, width: width - inset * 2.0, height: fieldItemHeight)
if let _ = self.secretChatsTitleNode.supernode {
secretFrame.size.height += fieldItemHeight
}
transition.updateFrame(node: self.acceptBackgroundNode, frame: secretFrame)
let secretChatsHeaderTextSize = self.acceptHeaderNode.updateLayout(CGSize(width: secretFrame.width - inset * 2.0, height: fieldItemHeight))
let secretChatsHeaderTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.minY - secretChatsHeaderTextSize.height - 6.0), size: secretChatsHeaderTextSize)
transition.updateFrame(node: self.acceptHeaderNode, frame: secretChatsHeaderTextFrame)
if let _ = self.secretChatsTitleNode.supernode {
let secretChatsTitleTextSize = self.secretChatsTitleNode.updateLayout(CGSize(width: width - inset * 4.0 - 80.0, height: fieldItemHeight))
let secretChatsTitleTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.minY + floorToScreenPixels((fieldItemHeight - secretChatsTitleTextSize.height) / 2.0)), size: secretChatsTitleTextSize)
transition.updateFrame(node: self.secretChatsTitleNode, frame: secretChatsTitleTextFrame)
if let switchView = self.secretChatsSwitchNode.view as? UISwitch {
if self.secretChatsSwitchNode.bounds.size.width.isZero {
switchView.sizeToFit()
}
let switchSize = switchView.bounds.size
self.secretChatsSwitchNode.frame = CGRect(origin: CGPoint(x: fieldFrame.maxX - switchSize.width - inset, y: secretFrame.minY + floorToScreenPixels((fieldItemHeight - switchSize.height) / 2.0)), size: switchSize)
self.secretChatsActivateAreaNode.frame = CGRect(origin: CGPoint(x: secretFrame.minX, y: secretFrame.minY), size: CGSize(width: fieldFrame.width, height: fieldItemHeight))
}
}
let incomingCallsTitleTextSize = self.incomingCallsTitleNode.updateLayout(CGSize(width: width - inset * 4.0 - 80.0, height: fieldItemHeight))
let incomingCallsTitleTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - incomingCallsTitleTextSize.height) / 2.0)), size: incomingCallsTitleTextSize)
transition.updateFrame(node: self.incomingCallsTitleNode, frame: incomingCallsTitleTextFrame)
transition.updateFrame(node: self.acceptSeparatorNode, frame: CGRect(x: secretFrame.minX + inset, y: secretFrame.minY + fieldItemHeight, width: fieldFrame.width - inset, height: UIScreenPixel))
if let switchView = self.incomingCallsSwitchNode.view as? UISwitch {
if self.incomingCallsSwitchNode.bounds.size.width.isZero {
switchView.sizeToFit()
}
let switchSize = switchView.bounds.size
self.incomingCallsSwitchNode.frame = CGRect(origin: CGPoint(x: fieldFrame.maxX - switchSize.width - inset, y: secretFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - switchSize.height) / 2.0)), size: switchSize)
self.incomingCallsActivateAreaNode.frame = CGRect(origin: CGPoint(x: secretFrame.minX, y: secretFrame.maxY - fieldItemHeight), size: CGSize(width: fieldFrame.width, height: fieldItemHeight))
}
if let _ = self.acceptBackgroundNode.supernode {
contentHeight += secretFrame.maxY - locationInfoTextFrame.maxY
}
contentHeight += 40.0
let isCurrent: Bool
if case let .session(session) = self.subject, session.isCurrent {
isCurrent = true
} else {
isCurrent = false
}
if isCurrent {
contentHeight -= 68.0
self.terminateButton.isHidden = true
self.terminateButton.isAccessibilityElement = false
} else {
self.terminateButton.isHidden = false
self.terminateButton.isAccessibilityElement = true
}
let sideInset = floor((layout.size.width - width) / 2.0)
let scrollContentHeight = max(layout.size.height, contentHeight)
let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: max(layout.statusBarHeight ?? 20.0, layout.size.height - contentHeight)), size: CGSize(width: width, height: contentHeight))
let contentFrame = contentContainerFrame
self.wrappingScrollNode.view.contentSize = CGSize(width: layout.size.width, height: scrollContentHeight)
var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: width, height: contentFrame.height + 2000.0))
if backgroundFrame.minY < contentFrame.minY {
backgroundFrame.origin.y = contentFrame.minY
}
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
let doneButtonHeight = self.terminateButton.updateLayout(width: width - inset * 2.0, transition: transition)
transition.updateFrame(node: self.terminateButton, frame: CGRect(x: inset, y: contentHeight - doneButtonHeight - 40.0 - insets.bottom - 6.0, width: width, height: doneButtonHeight))
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame)
}
}
@@ -0,0 +1,658 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import ItemListPeerItem
import ItemListPeerActionItem
import AvatarNode
private final class SelectivePrivacyPeersControllerArguments {
let context: AccountContext
let setPeerIdWithRevealedOptions: (EnginePeer.Id?, EnginePeer.Id?) -> Void
let removePeer: (EnginePeer.Id) -> Void
let addPeer: () -> Void
let openPeer: (EnginePeer) -> Void
let deleteAll: () -> Void
let removePremiumUsers: () -> Void
let removeBots: () -> Void
init(context: AccountContext, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, addPeer: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void, deleteAll: @escaping () -> Void, removePremiumUsers: @escaping () -> Void, removeBots: @escaping () -> Void) {
self.context = context
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.removePeer = removePeer
self.addPeer = addPeer
self.openPeer = openPeer
self.deleteAll = deleteAll
self.removePremiumUsers = removePremiumUsers
self.removeBots = removeBots
}
}
private enum SelectivePrivacyPeersSection: Int32 {
case peers
case delete
}
private enum SelectivePrivacyPeersEntryStableId: Hashable {
case header
case add
case premiumUsers
case bots
case peer(EnginePeer.Id)
case footer
case delete
}
private let premiumAvatarIcon: UIImage? = {
return generatePremiumCategoryIcon(size: CGSize(width: 31.0, height: 31.0), cornerRadius: 8.0)
}()
private let botsIcon: UIImage? = {
return generateAvatarImage(size: CGSize(width: 31.0, height: 31.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Bot"), color: .white), cornerRadius: 8.0, color: .violet)
}()
private enum SelectivePrivacyPeersEntry: ItemListNodeEntry {
case premiumUsersItem(ItemListPeerItemEditing, Bool)
case botsItem(ItemListPeerItemEditing, Bool)
case peerItem(Int32, PresentationDateTimeFormat, PresentationPersonNameOrder, SelectivePrivacyPeer, ItemListPeerItemEditing, Bool)
case addItem(String, Bool)
case headerItem(String)
case footerItem(String)
case deleteItem(String)
var section: ItemListSectionId {
switch self {
case .addItem, .premiumUsersItem, .botsItem, .peerItem, .headerItem, .footerItem:
return SelectivePrivacyPeersSection.peers.rawValue
case .deleteItem:
return SelectivePrivacyPeersSection.delete.rawValue
}
}
var stableId: SelectivePrivacyPeersEntryStableId {
switch self {
case .premiumUsersItem:
return .premiumUsers
case .botsItem:
return .bots
case let .peerItem(_, _, _, peer, _, _):
return .peer(peer.peer.id)
case .addItem:
return .add
case .headerItem:
return .header
case .footerItem:
return .footer
case .deleteItem:
return .delete
}
}
static func ==(lhs: SelectivePrivacyPeersEntry, rhs: SelectivePrivacyPeersEntry) -> Bool {
switch lhs {
case let .premiumUsersItem(editing, isEnabled):
if case .premiumUsersItem(editing, isEnabled) = rhs {
return true
} else {
return false
}
case let .botsItem(editing, isEnabled):
if case .botsItem(editing, isEnabled) = rhs {
return true
} else {
return false
}
case let .peerItem(lhsIndex, lhsDateTimeFormat, lhsNameOrder, lhsPeer, lhsEditing, lhsEnabled):
if case let .peerItem(rhsIndex, rhsDateTimeFormat, rhsNameOrder, rhsPeer, rhsEditing, rhsEnabled) = rhs {
if lhsIndex != rhsIndex {
return false
}
if lhsPeer != rhsPeer {
return false
}
if lhsDateTimeFormat != rhsDateTimeFormat {
return false
}
if lhsNameOrder != rhsNameOrder {
return false
}
if lhsEditing != rhsEditing {
return false
}
if lhsEnabled != rhsEnabled {
return false
}
return true
} else {
return false
}
case let .addItem(lhsText, lhsEditing):
if case let .addItem(rhsText, rhsEditing) = rhs, lhsText == rhsText, lhsEditing == rhsEditing {
return true
} else {
return false
}
case let .headerItem(lhsText):
if case let .headerItem(rhsText) = rhs, lhsText == rhsText {
return true
} else {
return false
}
case let .footerItem(lhsText):
if case let .footerItem(rhsText) = rhs, lhsText == rhsText {
return true
} else {
return false
}
case let .deleteItem(lhsText):
if case let .deleteItem(rhsText) = rhs, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: SelectivePrivacyPeersEntry, rhs: SelectivePrivacyPeersEntry) -> Bool {
switch lhs {
case .deleteItem:
return false
case .footerItem:
switch rhs {
case .deleteItem:
return true
case .peerItem, .addItem, .botsItem, .headerItem, .premiumUsersItem, .footerItem:
return false
}
case let .peerItem(index, _, _, _, _, _):
switch rhs {
case .deleteItem, .footerItem:
return true
case let .peerItem(rhsIndex, _, _, _, _, _):
return index < rhsIndex
case .addItem, .headerItem, .premiumUsersItem, .botsItem:
return false
}
case .premiumUsersItem:
switch rhs {
case .peerItem, .deleteItem, .botsItem, .footerItem:
return true
case .premiumUsersItem, .addItem, .headerItem:
return false
}
case .botsItem:
switch rhs {
case .peerItem, .deleteItem, .footerItem:
return true
case .botsItem, .premiumUsersItem, .addItem, .headerItem:
return false
}
case .addItem:
switch rhs {
case .peerItem, .deleteItem, .botsItem, .premiumUsersItem, .footerItem:
return true
case .addItem, .headerItem:
return false
}
case .headerItem:
switch rhs {
case .peerItem, .deleteItem, .botsItem, .premiumUsersItem, .addItem, .footerItem:
return true
case .headerItem:
return false
}
}
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! SelectivePrivacyPeersControllerArguments
switch self {
case let .premiumUsersItem(editing, enabled):
let peer: EnginePeer = .user(TelegramUser(
id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(1)), accessHash: nil, firstName: presentationData.strings.PrivacySettings_CategoryPremiumUsers, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: .firstLast, context: arguments.context, peer: peer, customAvatarIcon: premiumAvatarIcon, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: {
}, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
}, removePeer: { peerId in
arguments.removePremiumUsers()
})
case let .botsItem(editing, enabled):
let peer: EnginePeer = .user(TelegramUser(
id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(2)), accessHash: nil, firstName: presentationData.strings.PrivacySettings_CategoryBots, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: .firstLast, context: arguments.context, peer: peer, customAvatarIcon: botsIcon, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: {
}, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
}, removePeer: { peerId in
arguments.removeBots()
})
case let .peerItem(_, dateTimeFormat, nameDisplayOrder, peer, editing, enabled):
var text: ItemListPeerItemText = .none
if let group = peer.peer as? TelegramGroup {
text = .text(presentationData.strings.Conversation_StatusMembers(Int32(group.participantCount)), .secondary)
} else if let channel = peer.peer as? TelegramChannel {
if let participantCount = peer.participantCount {
text = .text(presentationData.strings.Conversation_StatusMembers(Int32(participantCount)), .secondary)
} else {
switch channel.info {
case .group:
text = .text(presentationData.strings.Group_Status, .secondary)
case .broadcast:
text = .text(presentationData.strings.Channel_Status, .secondary)
}
}
}
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(peer.peer), presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: {
arguments.openPeer(EnginePeer(peer.peer))
}, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
}, removePeer: { peerId in
arguments.removePeer(peerId)
})
case let .addItem(text, editing):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), title: text, sectionId: self.section, height: .compactPeerList, editing: editing, action: {
arguments.addPeer()
})
case let .headerItem(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .footerItem(text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .deleteItem(text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.deleteAll()
})
}
}
}
private struct SelectivePrivacyPeersControllerState: Equatable {
var enableForPremium: Bool
var enableForBots: Bool
var editing: Bool
var peerIdWithRevealedOptions: EnginePeer.Id?
init(enableForPremium: Bool, enableForBots: Bool, editing: Bool, peerIdWithRevealedOptions: EnginePeer.Id?) {
self.enableForPremium = enableForPremium
self.enableForBots = enableForBots
self.editing = editing
self.peerIdWithRevealedOptions = peerIdWithRevealedOptions
}
}
private func selectivePrivacyPeersControllerEntries(presentationData: PresentationData, state: SelectivePrivacyPeersControllerState, peers: [SelectivePrivacyPeer], footer: String?) -> [SelectivePrivacyPeersEntry] {
var entries: [SelectivePrivacyPeersEntry] = []
let title: String
if peers.isEmpty {
title = presentationData.strings.Privacy_Exceptions
} else {
title = presentationData.strings.Privacy_ExceptionsCount(Int32(peers.count))
}
entries.append(.headerItem(title))
entries.append(.addItem(presentationData.strings.Privacy_AddNewPeer, state.editing))
if state.enableForPremium {
entries.append(.premiumUsersItem(ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: state.peerIdWithRevealedOptions?.id._internalGetInt64Value() == 1), true))
}
if state.enableForBots {
entries.append(.botsItem(ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: state.peerIdWithRevealedOptions?.id._internalGetInt64Value() == 2), true))
}
var index: Int32 = 0
for peer in peers {
entries.append(.peerItem(index, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, peer, ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: peer.peer.id == state.peerIdWithRevealedOptions), true))
index += 1
}
if let footer {
entries.append(.footerItem(footer))
}
if !peers.isEmpty || state.enableForPremium || state.enableForBots {
entries.append(.deleteItem(presentationData.strings.Privacy_Exceptions_DeleteAllExceptions))
}
return entries
}
public func selectivePrivacyPeersController(context: AccountContext, title: String, footer: String? = nil, hideContacts: Bool = false, initialPeers: [EnginePeer.Id: SelectivePrivacyPeer], initialEnableForPremium: Bool, displayPremiumCategory: Bool, initialEnableForBots: Bool, displayBotsCategory: Bool, updated: @escaping ([EnginePeer.Id: SelectivePrivacyPeer], Bool, Bool) -> Void) -> ViewController {
let initialState = SelectivePrivacyPeersControllerState(enableForPremium: initialEnableForPremium, enableForBots: initialEnableForBots, editing: false, peerIdWithRevealedOptions: nil)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((SelectivePrivacyPeersControllerState) -> SelectivePrivacyPeersControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
let actionsDisposable = DisposableSet()
let addPeerDisposable = MetaDisposable()
actionsDisposable.add(addPeerDisposable)
let removePeerDisposable = MetaDisposable()
actionsDisposable.add(removePeerDisposable)
let peersPromise = Promise<[SelectivePrivacyPeer]>()
peersPromise.set(.single(Array(initialPeers.values)))
let arguments = SelectivePrivacyPeersControllerArguments(context: context, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
updateState { state in
if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
var state = state
state.peerIdWithRevealedOptions = peerId
return state
} else {
return state
}
}
}, removePeer: { memberId in
let applyPeers: Signal<Void, NoError> = peersPromise.get()
|> take(1)
|> deliverOnMainQueue
|> mapToSignal { peers -> Signal<Void, NoError> in
var updatedPeers = peers
for i in 0 ..< updatedPeers.count {
if updatedPeers[i].peer.id == memberId {
updatedPeers.remove(at: i)
break
}
}
peersPromise.set(.single(updatedPeers))
var updatedPeerDict: [EnginePeer.Id: SelectivePrivacyPeer] = [:]
for peer in updatedPeers {
updatedPeerDict[peer.peer.id] = peer
}
updated(updatedPeerDict, stateValue.with({ $0 }).enableForPremium, stateValue.with({ $0 }).enableForBots)
if updatedPeerDict.isEmpty {
dismissImpl?()
}
return .complete()
}
removePeerDisposable.set(applyPeers.start())
}, addPeer: {
enum AdditionalCategoryId: Int {
case premiumUsers
case bots
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var additionalCategories: [ChatListNodeAdditionalCategory] = []
if displayPremiumCategory {
additionalCategories = [
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.premiumUsers.rawValue,
icon: generatePremiumCategoryIcon(size: CGSize(width: 40.0, height: 40.0), cornerRadius: 12.0),
smallIcon: generatePremiumCategoryIcon(size: CGSize(width: 22.0, height: 22.0), cornerRadius: 6.0),
title: presentationData.strings.PrivacySettings_CategoryPremiumUsers,
appearance: .option(sectionTitle: presentationData.strings.PrivacySettings_SearchUserTypesHeader)
)
]
}
if displayBotsCategory {
additionalCategories = [
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.bots.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Bot"), color: .white), cornerRadius: 12.0, color: .violet),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Bot"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .violet),
title: presentationData.strings.PrivacySettings_CategoryBots,
appearance: .option(sectionTitle: presentationData.strings.PrivacySettings_SearchUserTypesHeader)
)
]
}
var selectedCategories = Set<Int>()
if stateValue.with({ $0 }).enableForPremium {
selectedCategories.insert(AdditionalCategoryId.premiumUsers.rawValue)
}
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: presentationData.strings.PrivacySettings_SearchUsersTitle,
searchPlaceholder: presentationData.strings.PrivacySettings_SearchUsersPlaceholder,
selectedChats: Set(),
additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories),
chatListFilters: nil,
onlyUsers: false,
disableChannels: true,
disableBots: hideContacts,
disableContacts: hideContacts
)), alwaysEnabled: true))
addPeerDisposable.set((controller.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] result in
var peerIds: [ContactListPeerId] = []
var premiumSelected = false
var botsSelected = false
if case let .result(peerIdsValue, additionalOptionIds) = result {
peerIds = peerIdsValue
premiumSelected = additionalOptionIds.contains(AdditionalCategoryId.premiumUsers.rawValue)
botsSelected = additionalOptionIds.contains(AdditionalCategoryId.bots.rawValue)
} else {
return
}
let applyPeers: Signal<Void, NoError> = peersPromise.get()
|> take(1)
|> mapToSignal { peers -> Signal<[SelectivePrivacyPeer], NoError> in
let filteredPeerIds = peerIds.compactMap { peerId -> EnginePeer.Id? in
if case let .peer(value) = peerId {
return value
} else {
return nil
}
}
return context.engine.data.get(
EngineDataMap(filteredPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)),
EngineDataMap(filteredPeerIds.map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
)
|> map { peerMap, participantCountMap -> [SelectivePrivacyPeer] in
var updatedPeers = peers
var existingIds = Set(updatedPeers.map { $0.peer.id })
for peerId in peerIds {
guard case let .peer(peerId) = peerId else {
continue
}
if let maybePeer = peerMap[peerId], let peer = maybePeer, !existingIds.contains(peerId) {
existingIds.insert(peerId)
var participantCount: Int32?
if case let .channel(channel) = peer, case .group = channel.info {
if let maybeParticipantCount = participantCountMap[peerId], let participantCountValue = maybeParticipantCount {
participantCount = Int32(participantCountValue)
}
}
updatedPeers.append(SelectivePrivacyPeer(peer: peer._asPeer(), participantCount: participantCount))
}
}
return updatedPeers
}
}
|> deliverOnMainQueue
|> mapToSignal { updatedPeers -> Signal<Void, NoError> in
peersPromise.set(.single(updatedPeers))
var updatedPeerDict: [EnginePeer.Id: SelectivePrivacyPeer] = [:]
for peer in updatedPeers {
updatedPeerDict[peer.peer.id] = peer
}
updated(updatedPeerDict, premiumSelected, botsSelected)
updateState { state in
var state = state
state.enableForPremium = premiumSelected
state.enableForBots = botsSelected
return state
}
return .complete()
}
removePeerDisposable.set(applyPeers.start())
controller?.dismiss()
}))
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, openPeer: { peer in
guard let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
return
}
pushControllerImpl?(controller)
}, deleteAll: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.Privacy_Exceptions_DeleteAllConfirmation),
ActionSheetButtonItem(title: presentationData.strings.Privacy_Exceptions_DeleteAll, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let applyPeers: Signal<Void, NoError> = peersPromise.get()
|> take(1)
|> deliverOnMainQueue
|> mapToSignal { _ -> Signal<Void, NoError> in
updateState { state in
var state = state
state.enableForPremium = false
return state
}
peersPromise.set(.single([]))
updated([:], false, false)
dismissImpl?()
return .complete()
}
removePeerDisposable.set(applyPeers.start())
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, nil)
}, removePremiumUsers: {
updateState { state in
var state = state
state.enableForPremium = false
return state
}
let applyPeers: Signal<Void, NoError> = peersPromise.get()
|> take(1)
|> deliverOnMainQueue
|> mapToSignal { peers -> Signal<Void, NoError> in
let updatedPeers = peers
peersPromise.set(.single(updatedPeers))
var updatedPeerDict: [EnginePeer.Id: SelectivePrivacyPeer] = [:]
for peer in updatedPeers {
updatedPeerDict[peer.peer.id] = peer
}
updated(updatedPeerDict, stateValue.with({ $0 }).enableForPremium, stateValue.with({ $0 }).enableForBots)
if updatedPeerDict.isEmpty && !stateValue.with({ $0 }).enableForPremium && !stateValue.with({ $0 }).enableForBots {
dismissImpl?()
}
return .complete()
}
removePeerDisposable.set(applyPeers.start())
}, removeBots: {
updateState { state in
var state = state
state.enableForBots = false
return state
}
let applyPeers: Signal<Void, NoError> = peersPromise.get()
|> take(1)
|> deliverOnMainQueue
|> mapToSignal { peers -> Signal<Void, NoError> in
let updatedPeers = peers
peersPromise.set(.single(updatedPeers))
var updatedPeerDict: [EnginePeer.Id: SelectivePrivacyPeer] = [:]
for peer in updatedPeers {
updatedPeerDict[peer.peer.id] = peer
}
updated(updatedPeerDict, stateValue.with({ $0 }).enableForPremium, stateValue.with({ $0 }).enableForBots)
if updatedPeerDict.isEmpty && !stateValue.with({ $0 }).enableForPremium && !stateValue.with({ $0 }).enableForBots {
dismissImpl?()
}
return .complete()
}
removePeerDisposable.set(applyPeers.start())
})
var previousPeers: [SelectivePrivacyPeer]?
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), peersPromise.get())
|> deliverOnMainQueue
|> map { presentationData, state, peers -> (ItemListControllerState, (ItemListNodeState, Any)) in
var rightNavigationButton: ItemListNavigationButton?
if !peers.isEmpty {
if state.editing {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
updateState { state in
var state = state
state.editing = false
return state
}
})
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: {
updateState { state in
var state = state
state.editing = true
return state
}
})
}
}
let previous = previousPeers
previousPeers = peers
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: selectivePrivacyPeersControllerEntries(presentationData: presentationData, state: state, peers: peers, footer: footer), style: .blocks, emptyStateItem: nil, animateChanges: previous != nil && previous!.count >= peers.count)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
dismissImpl = { [weak controller] in
if let controller = controller, let navigationController = controller.navigationController as? NavigationController {
navigationController.filterController(controller, animated: true)
}
}
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
pushControllerImpl = { [weak controller] c in
if let navigationController = controller?.navigationController as? NavigationController {
navigationController.pushViewController(c)
}
}
return controller
}
@@ -0,0 +1,415 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import DeviceAccess
import ItemListUI
import PresentationDataUtils
import AccountContext
import AlertUI
import PresentationDataUtils
import TelegramNotices
import NotificationSoundSelectionUI
import TelegramStringFormatting
private final class ReactionNotificationSettingsControllerArguments {
let context: AccountContext
let soundSelectionDisposable: MetaDisposable
let openMessages: () -> Void
let openStories: () -> Void
let toggleMessages: (Bool) -> Void
let toggleStories: (Bool) -> Void
let updatePreviews: (Bool) -> Void
let openSound: (PeerMessageSound) -> Void
init(
context: AccountContext,
soundSelectionDisposable: MetaDisposable,
openMessages: @escaping () -> Void,
openStories: @escaping () -> Void,
toggleMessages: @escaping (Bool) -> Void,
toggleStories: @escaping (Bool) -> Void,
updatePreviews: @escaping (Bool) -> Void,
openSound: @escaping (PeerMessageSound) -> Void
) {
self.context = context
self.soundSelectionDisposable = soundSelectionDisposable
self.openMessages = openMessages
self.openStories = openStories
self.toggleMessages = toggleMessages
self.toggleStories = toggleStories
self.updatePreviews = updatePreviews
self.openSound = openSound
}
}
private enum ReactionNotificationSettingsSection: Int32 {
case categories
case options
}
private enum ReactionNotificationSettingsEntry: ItemListNodeEntry {
enum StableId: Hashable {
case categoriesHeader
case messages
case stories
case optionsHeader
case previews
case sound
}
case categoriesHeader(String)
case messages(title: String, text: String?, value: Bool)
case stories(title: String, text: String?, value: Bool)
case optionsHeader(String)
case previews(String, Bool)
case sound(String, String, PeerMessageSound)
var section: ItemListSectionId {
switch self {
case .categoriesHeader, .messages, .stories:
return ReactionNotificationSettingsSection.categories.rawValue
case .optionsHeader, .previews, .sound:
return ReactionNotificationSettingsSection.options.rawValue
}
}
var stableId: StableId {
switch self {
case .categoriesHeader:
return .categoriesHeader
case .messages:
return .messages
case .stories:
return .stories
case .optionsHeader:
return .optionsHeader
case .previews:
return .previews
case .sound:
return .sound
}
}
var sortIndex: Int32 {
switch self {
case .categoriesHeader:
return 0
case .messages:
return 1
case .stories:
return 2
case .optionsHeader:
return 3
case .previews:
return 4
case .sound:
return 5
}
}
static func ==(lhs: ReactionNotificationSettingsEntry, rhs: ReactionNotificationSettingsEntry) -> Bool {
switch lhs {
case let .categoriesHeader(lhsText):
if case let .categoriesHeader(rhsText) = rhs, lhsText == rhsText {
return true
} else {
return false
}
case let .messages(title, text, value):
if case .messages(title, text, value) = rhs {
return true
} else {
return false
}
case let .stories(title, text, value):
if case .stories(title, text, value) = rhs {
return true
} else {
return false
}
case let .optionsHeader(lhsText):
if case let .optionsHeader(rhsText) = rhs, lhsText == rhsText {
return true
} else {
return false
}
case let .previews(lhsText, lhsValue):
if case let .previews(rhsText, rhsValue) = rhs, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .sound(lhsText, lhsValue, lhsSound):
if case let .sound(rhsText, rhsValue, rhsSound) = rhs, lhsText == rhsText, lhsValue == rhsValue, lhsSound == rhsSound {
return true
} else {
return false
}
}
}
static func <(lhs: ReactionNotificationSettingsEntry, rhs: ReactionNotificationSettingsEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ReactionNotificationSettingsControllerArguments
switch self {
case let .categoriesHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .messages(title, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, text: text, textColor: .accent, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleMessages(value)
}, action: {
arguments.openMessages()
})
case let .stories(title, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, text: text, textColor: .accent, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleStories(value)
}, action: {
arguments.openStories()
})
case let .optionsHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .previews(text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updatePreviews(value)
})
case let .sound(text, value, sound):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openSound(sound)
}, tag: self.tag)
}
}
}
private func filteredGlobalSound(_ sound: PeerMessageSound) -> PeerMessageSound {
if case .default = sound {
return defaultCloudPeerNotificationSound
} else {
return sound
}
}
private func reactionNotificationSettingsEntries(
globalSettings: GlobalNotificationSettingsSet,
state: ReactionNotificationSettingsState,
presentationData: PresentationData,
notificationSoundList: NotificationSoundList?
) -> [ReactionNotificationSettingsEntry] {
var entries: [ReactionNotificationSettingsEntry] = []
entries.append(.categoriesHeader(presentationData.strings.Notifications_Reactions_SettingsHeader))
let messagesText: String?
let messagesValue: Bool
switch globalSettings.reactionSettings.messages {
case .nobody:
messagesText = nil
messagesValue = false
case .contacts:
messagesText = presentationData.strings.Notifications_Reactions_SubtitleContacts
messagesValue = true
case .everyone:
messagesText = presentationData.strings.Notifications_Reactions_SubtitleEveryone
messagesValue = true
}
let storiesText: String?
let storiesValue: Bool
switch globalSettings.reactionSettings.stories {
case .nobody:
storiesText = nil
storiesValue = false
case .contacts:
storiesText = presentationData.strings.Notifications_Reactions_SubtitleContacts
storiesValue = true
case .everyone:
storiesText = presentationData.strings.Notifications_Reactions_SubtitleEveryone
storiesValue = true
}
entries.append(.messages(title: presentationData.strings.Notifications_Reactions_ItemMessages, text: messagesText, value: messagesValue))
entries.append(.stories(title: presentationData.strings.Notifications_Reactions_ItemStories, text: storiesText, value: storiesValue))
if messagesValue || storiesValue {
entries.append(.optionsHeader(presentationData.strings.Notifications_Options.uppercased()))
entries.append(.previews(presentationData.strings.Notifications_Stories_DisplayName, globalSettings.reactionSettings.hideSender != .hide))
entries.append(.sound(presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: filteredGlobalSound(globalSettings.reactionSettings.sound)), filteredGlobalSound(globalSettings.reactionSettings.sound)))
}
return entries
}
private struct ReactionNotificationSettingsState: Equatable {
init() {
}
}
public func reactionNotificationSettingsController(
context: AccountContext
) -> ViewController {
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
let stateValue = Atomic<ReactionNotificationSettingsState>(value: ReactionNotificationSettingsState())
let statePromise: ValuePromise<ReactionNotificationSettingsState> = ValuePromise(ignoreRepeated: true)
statePromise.set(stateValue.with { $0 })
let updateState: ((ReactionNotificationSettingsState) -> ReactionNotificationSettingsState) -> Void = { f in
let result = stateValue.modify { f($0) }
statePromise.set(result)
}
let _ = updateState
let openCategory: (Bool) -> Void = { isMessages in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text: String
if isMessages {
text = presentationData.strings.Notifications_Reactions_SheetTitleMessages
} else {
text = presentationData.strings.Notifications_Reactions_SheetTitleStories
}
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: text),
ActionSheetButtonItem(title: presentationData.strings.Notifications_Reactions_SheetValueEveryone, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
if isMessages {
settings.reactionSettings.messages = .everyone
} else {
settings.reactionSettings.stories = .everyone
}
return settings
}).start()
}),
ActionSheetButtonItem(title: presentationData.strings.Notifications_Reactions_SheetValueContacts, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
if isMessages {
settings.reactionSettings.messages = .contacts
} else {
settings.reactionSettings.stories = .contacts
}
return settings
}).start()
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, nil)
}
let arguments = ReactionNotificationSettingsControllerArguments(
context: context,
soundSelectionDisposable: MetaDisposable(),
openMessages: {
openCategory(true)
},
openStories: {
openCategory(false)
},
toggleMessages: { value in
let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
if value {
settings.reactionSettings.messages = .contacts
} else {
settings.reactionSettings.messages = .nobody
}
return settings
}).start()
},
toggleStories: { value in
let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
if value {
settings.reactionSettings.stories = .contacts
} else {
settings.reactionSettings.stories = .nobody
}
return settings
}).start()
},
updatePreviews: { value in
let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
settings.reactionSettings.hideSender = value ? .show : .hide
return settings
}).start()
}, openSound: { sound in
let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: sound, defaultSound: nil, completion: { value in
let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in
var settings = settings
settings.reactionSettings.sound = value
return settings
}).start()
})
pushControllerImpl?(controller)
}
)
let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications])
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
context.engine.peers.notificationSoundList(),
preferences,
statePromise.get()
)
|> map { presentationData, notificationSoundList, preferencesView, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let viewSettings: GlobalNotificationSettingsSet
if let settings = preferencesView.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) {
viewSettings = settings.effective
} else {
viewSettings = GlobalNotificationSettingsSet.defaultSettings
}
let entries = reactionNotificationSettingsEntries(
globalSettings: viewSettings,
state: state,
presentationData: presentationData,
notificationSoundList: notificationSoundList
)
let leftNavigationButton: ItemListNavigationButton?
let rightNavigationButton: ItemListNavigationButton?
leftNavigationButton = nil
rightNavigationButton = nil
let title: String = presentationData.strings.Notifications_Reactions_Title
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
pushControllerImpl = { [weak controller] c in
(controller?.navigationController as? NavigationController)?.pushViewController(c)
}
return controller
}
@@ -0,0 +1,766 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import MergeLists
import ItemListUI
import PresentationDataUtils
import AccountContext
import SearchBarNode
import SearchUI
import ChatListSearchItemHeader
extension SettingsSearchableItemIcon {
func image() -> UIImage? {
switch self {
case .profile:
return PresentationResourcesSettings.editProfile
case .proxy:
return PresentationResourcesSettings.proxy
case .savedMessages:
return PresentationResourcesSettings.savedMessages
case .calls:
return PresentationResourcesSettings.recentCalls
case .stickers:
return PresentationResourcesSettings.stickers
case .notifications:
return PresentationResourcesSettings.notifications
case .privacy:
return PresentationResourcesSettings.security
case .data:
return PresentationResourcesSettings.dataAndStorage
case .appearance:
return PresentationResourcesSettings.appearance
case .language:
return PresentationResourcesSettings.language
case .watch:
return PresentationResourcesSettings.watch
case .passport:
return PresentationResourcesSettings.passport
case .support:
return PresentationResourcesSettings.support
case .faq:
return PresentationResourcesSettings.faq
case .chatFolders:
return PresentationResourcesSettings.chatFolders
case .deleteAccount:
return PresentationResourcesSettings.deleteAccount
case .devices:
return PresentationResourcesSettings.devices
case .premium:
return PresentationResourcesSettings.premium
case .stories:
return PresentationResourcesSettings.stories
}
}
}
final class SettingsSearchItem: ItemListControllerSearch {
let context: AccountContext
let theme: PresentationTheme
let placeholder: String
let activated: Bool
let updateActivated: (Bool) -> Void
let presentController: (ViewController, Any?) -> Void
let pushController: (ViewController) -> Void
let getNavigationController: (() -> NavigationController?)?
let resolvedFaqUrl: Signal<ResolvedUrl?, NoError>
let exceptionsList: Signal<NotificationExceptionsList?, NoError>
let archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>
let privacySettings: Signal<AccountPrivacySettings?, NoError>
let hasTwoStepAuth: Signal<Bool?, NoError>
let twoStepAuthData: Signal<TwoStepVerificationAccessConfiguration?, NoError>
let activeSessionsContext: Signal<ActiveSessionsContext?, NoError>
let webSessionsContext: Signal<WebSessionsContext?, NoError>
private var updateActivity: ((Bool) -> Void)?
private var activity: ValuePromise<Bool> = ValuePromise(ignoreRepeated: false)
private let activityDisposable = MetaDisposable()
init(context: AccountContext, theme: PresentationTheme, placeholder: String, activated: Bool, updateActivated: @escaping (Bool) -> Void, presentController: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, getNavigationController: (() -> NavigationController?)?, resolvedFaqUrl: Signal<ResolvedUrl?, NoError>, exceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, hasTwoStepAuth: Signal<Bool?, NoError>, twoStepAuthData: Signal<TwoStepVerificationAccessConfiguration?, NoError>, activeSessionsContext: Signal<ActiveSessionsContext?, NoError>, webSessionsContext: Signal<WebSessionsContext?, NoError>) {
self.context = context
self.theme = theme
self.placeholder = placeholder
self.activated = activated
self.updateActivated = updateActivated
self.presentController = presentController
self.pushController = pushController
self.getNavigationController = getNavigationController
self.resolvedFaqUrl = resolvedFaqUrl
self.exceptionsList = exceptionsList
self.archivedStickerPacks = archivedStickerPacks
self.privacySettings = privacySettings
self.hasTwoStepAuth = hasTwoStepAuth
self.twoStepAuthData = twoStepAuthData
self.activeSessionsContext = activeSessionsContext
self.webSessionsContext = webSessionsContext
self.activityDisposable.set((activity.get() |> mapToSignal { value -> Signal<Bool, NoError> in
if value {
return .single(value) |> delay(0.2, queue: Queue.mainQueue())
} else {
return .single(value)
}
}).start(next: { [weak self] value in
self?.updateActivity?(value)
}))
}
deinit {
self.activityDisposable.dispose()
}
func isEqual(to: ItemListControllerSearch) -> Bool {
if let to = to as? SettingsSearchItem {
if self.context !== to.context || self.theme !== to.theme || self.placeholder != to.placeholder || self.activated != to.activated {
return false
}
return true
} else {
return false
}
}
func titleContentNode(current: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)? {
let updateActivated: (Bool) -> Void = self.updateActivated
if let current = current as? NavigationBarSearchContentNode {
current.updateThemeAndPlaceholder(theme: self.theme, placeholder: self.placeholder)
return current
} else {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
return NavigationBarSearchContentNode(theme: presentationData.theme, placeholder: presentationData.strings.Settings_Search, activate: {
updateActivated(true)
})
}
}
func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode {
let updateActivated: (Bool) -> Void = self.updateActivated
let presentController: (ViewController, Any?) -> Void = self.presentController
let pushController: (ViewController) -> Void = self.pushController
if let current = current as? SettingsSearchItemNode, let titleContentNode = titleContentNode as? NavigationBarSearchContentNode {
current.updatePresentationData(self.context.sharedContext.currentPresentationData.with { $0 })
if current.isSearching != self.activated {
if self.activated {
current.activateSearch(placeholderNode: titleContentNode.placeholderNode)
} else {
current.deactivateSearch(placeholderNode: titleContentNode.placeholderNode)
}
}
return current
} else {
return SettingsSearchItemNode(context: self.context, cancel: {
updateActivated(false)
}, updateActivity: { [weak self] value in
self?.activity.set(value)
}, pushController: { c in
pushController(c)
}, presentController: { c, a in
presentController(c, a)
}, getNavigationController: self.getNavigationController, resolvedFaqUrl: self.resolvedFaqUrl, exceptionsList: self.exceptionsList, archivedStickerPacks: self.archivedStickerPacks, privacySettings: self.privacySettings, hasTwoStepAuth: self.hasTwoStepAuth, twoStepAuthData: self.twoStepAuthData, activeSessionsContext: self.activeSessionsContext, webSessionsContext: self.webSessionsContext)
}
}
}
final class SettingsSearchInteraction {
let openItem: (SettingsSearchableItem) -> Void
let deleteRecentItem: (SettingsSearchableItemId) -> Void
init(openItem: @escaping (SettingsSearchableItem) -> Void, deleteRecentItem: @escaping (SettingsSearchableItemId) -> Void) {
self.openItem = openItem
self.deleteRecentItem = deleteRecentItem
}
}
private enum SettingsSearchEntryStableId: Hashable {
case result(SettingsSearchableItemId)
}
private enum SettingsSearchEntry: Comparable, Identifiable {
case result(index: Int, item: SettingsSearchableItem, icon: UIImage?)
var stableId: SettingsSearchEntryStableId {
switch self {
case let .result(_, item, _):
return .result(item.id)
}
}
private func index() -> Int {
switch self {
case let .result(index, _, _):
return index
}
}
static func <(lhs: SettingsSearchEntry, rhs: SettingsSearchEntry) -> Bool {
return lhs.index() < rhs.index()
}
static func == (lhs: SettingsSearchEntry, rhs: SettingsSearchEntry) -> Bool {
if case let .result(lhsIndex, lhsItem, _) = lhs {
if case let .result(rhsIndex, rhsItem, _) = rhs, lhsIndex == rhsIndex, lhsItem.id == rhsItem.id {
return true
}
}
return false
}
func item(theme: PresentationTheme, strings: PresentationStrings, interaction: SettingsSearchInteraction) -> ListViewItem {
switch self {
case let .result(_, item, icon):
return SettingsSearchResultItem(theme: theme, strings: strings, item: item, icon: icon, interaction: interaction, sectionId: 0)
}
}
}
private struct SettingsSearchContainerTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let isSearching: Bool
}
private func preparedSettingsSearchContainerTransition(theme: PresentationTheme, strings: PresentationStrings, from fromEntries: [SettingsSearchEntry], to toEntries: [SettingsSearchEntry], interaction: SettingsSearchInteraction, isSearching: Bool, forceUpdate: Bool) -> SettingsSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(theme: theme, strings: strings, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(theme: theme, strings: strings, interaction: interaction), directionHint: nil) }
return SettingsSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching)
}
private enum SettingsSearchRecentEntryStableId: Hashable {
case recent(SettingsSearchableItemId)
}
private enum SettingsSearchRecentEntry: Comparable, Identifiable {
case recent(Int, SettingsSearchableItem, ChatListSearchItemHeader)
case faq(Int, SettingsSearchableItem, ChatListSearchItemHeader)
var stableId: SettingsSearchRecentEntryStableId {
switch self {
case let .recent(_, item, _), let .faq(_, item, _):
return .recent(item.id)
}
}
var header: ChatListSearchItemHeader {
switch self {
case let .recent(_, _, header), let .faq(_, _, header):
return header
}
}
static func ==(lhs: SettingsSearchRecentEntry, rhs: SettingsSearchRecentEntry) -> Bool {
switch lhs {
case let .recent(lhsIndex, lhsItem, lhsHeader):
if case let .recent(rhsIndex, rhsItem, rhsHeader) = rhs, lhsIndex == rhsIndex, lhsItem.id == rhsItem.id, lhsHeader.id == rhsHeader.id {
return true
} else {
return false
}
case let .faq(lhsIndex, lhsItem, lhsHeader):
if case let .faq(rhsIndex, rhsItem, rhsHeader) = rhs, lhsIndex == rhsIndex, lhsItem.id == rhsItem.id, lhsHeader.id == rhsHeader.id {
return true
} else {
return false
}
}
}
static func <(lhs: SettingsSearchRecentEntry, rhs: SettingsSearchRecentEntry) -> Bool {
switch lhs {
case let .recent(lhsIndex, _, _):
switch rhs {
case let .recent(rhsIndex, _, _):
return lhsIndex <= rhsIndex
case .faq:
return false
}
case let .faq(lhsIndex, _, _):
switch rhs {
case .recent:
return true
case let .faq(rhsIndex, _, _):
return lhsIndex <= rhsIndex
}
}
}
func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: SettingsSearchInteraction) -> ListViewItem {
switch self {
case let .recent(_, item, header):
return SettingsSearchRecentItem(account: account, theme: theme, strings: strings, title: item.title, breadcrumbs: item.breadcrumbs, isFaq: false, action: {
interaction.openItem(item)
}, deleted: {
interaction.deleteRecentItem(item.id)
}, header: header)
case let .faq(_, item, header):
return SettingsSearchRecentItem(account: account, theme: theme, strings: strings, title: item.title, breadcrumbs: item.breadcrumbs, isFaq: true, action: {
interaction.openItem(item)
}, deleted: {
interaction.deleteRecentItem(item.id)
}, header: header)
}
}
}
private struct SettingsSearchContainerRecentTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let isEmpty: Bool
}
private func preparedSettingsSearchContainerRecentTransition(from fromEntries: [SettingsSearchRecentEntry], to toEntries: [SettingsSearchRecentEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: SettingsSearchInteraction) -> SettingsSearchContainerRecentTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction), directionHint: nil) }
return SettingsSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates, isEmpty: toEntries.isEmpty)
}
public final class SettingsSearchContainerNode: SearchDisplayControllerContentNode {
private let listNode: ListView
private let recentListNode: ListView
private var enqueuedTransitions: [SettingsSearchContainerTransition] = []
private var enqueuedRecentTransitions: [(SettingsSearchContainerRecentTransition, Bool)] = []
private var hasValidLayout = false
private let searchQuery = Promise<String?>()
private let searchDisposable = MetaDisposable()
private var recentDisposable: Disposable?
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<PresentationData>
public init(context: AccountContext, openResult: @escaping (SettingsSearchableItem) -> Void, resolvedFaqUrl: Signal<ResolvedUrl?, NoError>, exceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, hasTwoStepAuth: Signal<Bool?, NoError>, twoStepAuthData: Signal<TwoStepVerificationAccessConfiguration?, NoError>, activeSessionsContext: Signal<ActiveSessionsContext?, NoError>, webSessionsContext: Signal<WebSessionsContext?, NoError>) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
self.presentationDataPromise = Promise(self.presentationData)
self.listNode = ListView()
self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.listNode.isHidden = true
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
self.recentListNode = ListView()
self.recentListNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.recentListNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
self.recentListNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
super.init()
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.addSubnode(self.recentListNode)
self.addSubnode(self.listNode)
let interaction = SettingsSearchInteraction(openItem: { result in
addRecentSettingsSearchItem(engine: context.engine, item: result.id)
openResult(result)
}, deleteRecentItem: { id in
removeRecentSettingsSearchItem(engine: context.engine, item: id)
})
let searchableItems = Promise<[SettingsSearchableItem]>()
searchableItems.set(settingsSearchableItems(context: context, notificationExceptionsList: exceptionsList, archivedStickerPacks: archivedStickerPacks, privacySettings: privacySettings, hasTwoStepAuth: hasTwoStepAuth, twoStepAuthData: twoStepAuthData, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext))
let faqItems = Promise<[SettingsSearchableItem]>()
faqItems.set(faqSearchableItems(context: context, resolvedUrl: resolvedFaqUrl, suggestAccountDeletion: false))
let queryAndFoundItems = combineLatest(searchableItems.get(), faqSearchableItems(context: context, resolvedUrl: resolvedFaqUrl, suggestAccountDeletion: true))
|> mapToSignal { searchableItems, faqSearchableItems -> Signal<(String, [SettingsSearchableItem])?, NoError> in
return self.searchQuery.get()
|> mapToSignal { query -> Signal<(String, [SettingsSearchableItem])?, NoError> in
if let query = query, !query.isEmpty {
let results = searchSettingsItems(items: searchableItems, query: query)
let faqResults = searchSettingsItems(items: faqSearchableItems, query: query)
let finalResults: [SettingsSearchableItem]
if faqResults.first?.id == .faq(1) {
finalResults = faqResults + results
} else {
finalResults = results + faqResults
}
return .single((query, finalResults))
} else {
return .single(nil)
}
}
}
self.recentListNode.isHidden = false
let previousRecentlySearchedItemOrder = Atomic<[SettingsSearchableItemId]>(value: [])
let fixedRecentlySearchedItems = settingsSearchRecentItems(engine: context.engine)
|> map { recentIds -> [SettingsSearchableItemId] in
var result: [SettingsSearchableItemId] = []
let _ = previousRecentlySearchedItemOrder.modify { current in
var updated: [SettingsSearchableItemId] = []
for id in current {
inner: for recentId in recentIds {
if recentId == id {
updated.append(id)
result.append(recentId)
break inner
}
}
}
for recentId in recentIds.reversed() {
if !updated.contains(recentId) {
updated.insert(recentId, at: 0)
result.insert(recentId, at: 0)
}
}
return updated
}
return result
}
let recentSearchItems = combineLatest(searchableItems.get(), fixedRecentlySearchedItems)
|> map { searchableItems, recentItems -> [SettingsSearchableItem] in
let searchableItemsMap = searchableItems.reduce([SettingsSearchableItemId : SettingsSearchableItem]()) { (map, item) -> [SettingsSearchableItemId: SettingsSearchableItem] in
var map = map
map[item.id] = item
return map
}
var result: [SettingsSearchableItem] = []
for itemId in recentItems {
if let searchItem = searchableItemsMap[itemId] {
if case let .language(id) = searchItem.id, id > 0 {
} else {
result.append(searchItem)
}
}
}
return result
}
let previousRecentItems = Atomic<[SettingsSearchRecentEntry]?>(value: nil)
self.recentDisposable = (combineLatest(recentSearchItems, faqItems.get(), self.presentationDataPromise.get())
|> deliverOnMainQueue).start(next: { [weak self] recentSearchItems, faqItems, presentationData in
if let strongSelf = self {
let recentHeader = ChatListSearchItemHeader(type: .recentPeers, theme: presentationData.theme, strings: presentationData.strings, actionTitle: presentationData.strings.WebSearch_RecentSectionClear, action: { _ in
clearRecentSettingsSearchItems(engine: context.engine)
})
let faqHeader = ChatListSearchItemHeader(type: .faq, theme: presentationData.theme, strings: presentationData.strings)
var entries: [SettingsSearchRecentEntry] = []
for i in 0 ..< recentSearchItems.count {
entries.append(.recent(i, recentSearchItems[i], recentHeader))
}
for i in 0 ..< faqItems.count {
entries.append(.faq(i, faqItems[i], faqHeader))
}
let previousEntries = previousRecentItems.swap(entries)
let transition = preparedSettingsSearchContainerRecentTransition(from: previousEntries ?? [], to: entries, account: context.account, theme: presentationData.theme, strings: presentationData.strings, interaction: interaction)
strongSelf.enqueueRecentTransition(transition, firstTime: previousEntries == nil)
}
})
let previousEntriesHolder = Atomic<([SettingsSearchEntry], PresentationTheme, PresentationStrings)?>(value: nil)
self.searchDisposable.set(combineLatest(queue: .mainQueue(), queryAndFoundItems, self.presentationDataPromise.get()).start(next: { [weak self] queryAndFoundItems, presentationData in
guard let strongSelf = self else {
return
}
var currentQuery: String?
var entries: [SettingsSearchEntry] = []
if let (query, items) = queryAndFoundItems {
currentQuery = query
var previousIcon: SettingsSearchableItemIcon?
for item in items {
var image: UIImage?
if previousIcon != item.icon {
image = item.icon.image()
}
entries.append(.result(index: entries.count, item: item, icon: image))
previousIcon = item.icon
}
}
if !entries.isEmpty || currentQuery == nil {
let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings))
let transition = preparedSettingsSearchContainerTransition(theme: presentationData.theme, strings: presentationData.strings, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, interaction: interaction, isSearching: queryAndFoundItems != nil, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings)
strongSelf.enqueueTransition(transition)
}
}))
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
strongSelf.presentationDataPromise.set(.single(presentationData))
}
}
})
self.listNode.beganInteractiveDragging = { [weak self] _ in
self?.dismissInput?()
}
self.recentListNode.beganInteractiveDragging = { [weak self] _ in
self?.dismissInput?()
}
}
deinit {
self.searchDisposable.dispose()
self.recentDisposable?.dispose()
self.presentationDataDisposable?.dispose()
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.listNode.backgroundColor = theme.chatList.backgroundColor
self.recentListNode.backgroundColor = theme.chatList.backgroundColor
self.recentListNode.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
}
public override func searchTextUpdated(text: String) {
if text.isEmpty {
self.searchQuery.set(.single(nil))
} else {
self.searchQuery.set(.single(text))
}
}
private func enqueueTransition(_ transition: SettingsSearchContainerTransition) {
self.enqueuedTransitions.append(transition)
if self.hasValidLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
if let transition = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.PreferSynchronousDrawing)
let isSearching = transition.isSearching
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
self?.listNode.isHidden = !isSearching
})
}
}
private func enqueueRecentTransition(_ transition: SettingsSearchContainerRecentTransition, firstTime: Bool) {
self.enqueuedRecentTransitions.append((transition, firstTime))
if self.hasValidLayout {
while !self.enqueuedRecentTransitions.isEmpty {
self.dequeueRecentTransition()
}
}
}
private func dequeueRecentTransition() {
if let (transition, firstTime) = self.enqueuedRecentTransitions.first {
self.enqueuedRecentTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
if firstTime {
options.insert(.PreferSynchronousDrawing)
} else {
options.insert(.AnimateInsertion)
}
self.recentListNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
self?.recentListNode.backgroundColor = transition.isEmpty ? .clear : self?.presentationData.theme.chatList.backgroundColor
})
}
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
insets.left += layout.safeInsets.left
insets.right += layout.safeInsets.right
self.recentListNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if !self.hasValidLayout {
self.hasValidLayout = true
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
public override func scrollToTop() {
let listNodeToScroll: ListView
if !self.listNode.isHidden {
listNodeToScroll = self.listNode
} else {
listNodeToScroll = self.recentListNode
}
listNodeToScroll.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancel?()
}
}
}
private final class SettingsSearchItemNode: ItemListControllerSearchNode {
private let context: AccountContext
private var presentationData: PresentationData
private var containerLayout: (ContainerViewLayout, CGFloat)?
private var searchDisplayController: SearchDisplayController?
let pushController: (ViewController) -> Void
let presentController: (ViewController, Any?) -> Void
let getNavigationController: (() -> NavigationController?)?
let resolvedFaqUrl: Signal<ResolvedUrl?, NoError>
let exceptionsList: Signal<NotificationExceptionsList?, NoError>
let archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>
let privacySettings: Signal<AccountPrivacySettings?, NoError>
let hasTwoStepAuth: Signal<Bool?, NoError>
let twoStepAuthData: Signal<TwoStepVerificationAccessConfiguration?, NoError>
let activeSessionsContext: Signal<ActiveSessionsContext?, NoError>
let webSessionsContext: Signal<WebSessionsContext?, NoError>
var cancel: () -> Void
init(context: AccountContext, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: (() -> NavigationController?)?, resolvedFaqUrl: Signal<ResolvedUrl?, NoError>, exceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, hasTwoStepAuth: Signal<Bool?, NoError>, twoStepAuthData: Signal<TwoStepVerificationAccessConfiguration?, NoError>, activeSessionsContext: Signal<ActiveSessionsContext?, NoError>, webSessionsContext: Signal<WebSessionsContext?, NoError>) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.cancel = cancel
self.pushController = pushController
self.presentController = presentController
self.getNavigationController = getNavigationController
self.resolvedFaqUrl = resolvedFaqUrl
self.exceptionsList = exceptionsList
self.archivedStickerPacks = archivedStickerPacks
self.privacySettings = privacySettings
self.hasTwoStepAuth = hasTwoStepAuth
self.twoStepAuthData = twoStepAuthData
self.activeSessionsContext = activeSessionsContext
self.webSessionsContext = webSessionsContext
super.init()
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.searchDisplayController?.updatePresentationData(presentationData)
}
func activateSearch(placeholderNode: SearchBarPlaceholderNode) {
guard let (containerLayout, navigationBarHeight) = self.containerLayout, self.searchDisplayController == nil else {
return
}
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: SettingsSearchContainerNode(context: self.context, openResult: { [weak self] result in
if let strongSelf = self {
result.present(strongSelf.context, strongSelf.getNavigationController?(), { [weak self] mode, controller in
if let strongSelf = self {
switch mode {
case .push:
if let controller = controller {
strongSelf.pushController(controller)
}
case .modal:
if let controller = controller {
strongSelf.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet, completion: { [weak self] in
self?.cancel()
}))
}
case .immediate:
if let controller = controller {
strongSelf.presentController(controller, nil)
}
case .dismiss:
strongSelf.cancel()
}
}
})
}
}, resolvedFaqUrl: self.resolvedFaqUrl, exceptionsList: self.exceptionsList, archivedStickerPacks: self.archivedStickerPacks, privacySettings: self.privacySettings, hasTwoStepAuth: self.hasTwoStepAuth, twoStepAuthData: self.twoStepAuthData, activeSessionsContext: self.activeSessionsContext, webSessionsContext: self.webSessionsContext), cancel: { [weak self] in
self?.cancel()
})
self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate)
self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in
if let strongSelf = self, let strongPlaceholderNode = placeholderNode {
if isSearchBar {
strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode)
} else {
strongSelf.addSubnode(subnode)
}
}
}, placeholder: placeholderNode)
}
func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) {
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.deactivate(placeholder: placeholderNode)
self.searchDisplayController = nil
}
}
var isSearching: Bool {
return self.searchDisplayController != nil
}
override func scrollToTop() {
self.searchDisplayController?.contentNode.scrollToTop()
}
override func queryUpdated(_ query: String) {
}
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight)
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let searchDisplayController = self.searchDisplayController, let result = searchDisplayController.contentNode.hitTest(self.view.convert(point, to: searchDisplayController.contentNode.view), with: event) {
return result
}
return super.hitTest(point, with: event)
}
}
@@ -0,0 +1,288 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
private enum RevealOptionKey: Int32 {
case delete
}
class SettingsSearchRecentItem: ListViewItem {
let theme: PresentationTheme
let strings: PresentationStrings
let account: Account
let title: String
let breadcrumbs: [String]
let isFaq: Bool
let action: () -> Void
let deleted: () -> Void
let header: ListViewItemHeader?
init(account: Account, theme: PresentationTheme, strings: PresentationStrings, title: String, breadcrumbs: [String], isFaq: Bool, action: @escaping () -> Void, deleted: @escaping () -> Void, header: ListViewItemHeader) {
self.theme = theme
self.strings = strings
self.account = account
self.title = title
self.breadcrumbs = breadcrumbs
self.isFaq = isFaq
self.action = action
self.deleted = deleted
self.header = header
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = SettingsSearchRecentItemNode()
let makeLayout = node.asyncLayout()
var previousHeader: ListViewItemHeader?
if let previousItem = previousItem as? SettingsSearchRecentItem {
previousHeader = previousItem.header
}
var nextHeader: ListViewItemHeader?
if let nextItem = nextItem as? SettingsSearchRecentItem {
nextHeader = nextItem.header
}
let (nodeLayout, nodeApply) = makeLayout(self, params, nextItem == nil || nextHeader?.id != self.header?.id, !(previousItem is SettingsSearchRecentItem) || previousHeader?.id != self.header?.id)
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
completion(node, nodeApply)
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? SettingsSearchRecentItemNode {
let layout = nodeValue.asyncLayout()
async {
var previousHeader: ListViewItemHeader?
if let previousItem = previousItem as? SettingsSearchRecentItem {
previousHeader = previousItem.header
}
var nextHeader: ListViewItemHeader?
if let nextItem = nextItem as? SettingsSearchRecentItem {
nextHeader = nextItem.header
}
let (nodeLayout, apply) = layout(self, params, nextItem == nil || nextHeader?.id != self.header?.id, !(previousItem is SettingsSearchRecentItem) || previousHeader?.id != self.header?.id)
Queue.mainQueue().async {
completion(nodeLayout, { info in
apply().1(info)
})
}
}
}
}
}
var selectable: Bool {
return true
}
func selected(listView: ListView) {
listView.clearHighlightAnimated(true)
self.action()
}
}
private let titleFont = Font.regular(17.0)
private let subtitleFont = Font.regular(13.0)
class SettingsSearchRecentItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let titleNode: TextNode
private let subtitleNode: TextNode
private var item: SettingsSearchRecentItem?
private var layoutParams: ListViewItemLayoutParams?
required init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreenScale
self.subtitleNode = TextNode()
self.subtitleNode.isUserInteractionEnabled = false
self.subtitleNode.contentMode = .left
self.subtitleNode.contentsScale = UIScreenScale
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = self.item {
let makeLayout = self.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(item, params, nextItem == nil, previousItem == nil)
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply()
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
func asyncLayout() -> (_ item: SettingsSearchRecentItem, _ params: ListViewItemLayoutParams, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let currentItem = self.item
return { [weak self] item, params, last, firstWithHeader in
let leftInset: CGFloat = 15.0 + params.leftInset
let rightInset: CGFloat = params.rightInset
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let subtitle = item.breadcrumbs.joined(separator: "")
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: subtitle, font: subtitleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var height = titleLayout.size.height
if subtitle.isEmpty {
height += 22.0
} else {
height += 39.0
}
let contentSize = CGSize(width: params.width, height: height)
let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0))
return (nodeLayout, { [weak self] in
var updatedTheme: PresentationTheme?
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
}
return (nil, { _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
if let _ = updatedTheme {
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
}
let _ = titleApply()
let _ = subtitleApply()
let titleY: CGFloat = subtitle.isEmpty ? 11.0 : 11.0
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: titleY), size: titleLayout.size)
strongSelf.subtitleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: titleY + titleLayout.size.height + 1.0), size: subtitleLayout.size)
let separatorHeight = UIScreenPixel
let topHighlightInset: CGFloat = (firstWithHeader || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset))
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight))
strongSelf.separatorNode.isHidden = last
strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
var revealOptions: [ItemListRevealOption] = []
if item.isFaq {
} else {
revealOptions.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor))
}
strongSelf.setRevealOptions((left: [], right: revealOptions))
}
})
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false)
}
override public func headers() -> [ListViewItemHeader]? {
if let item = self.item {
return item.header.flatMap { [$0] }
} else {
return nil
}
}
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
if let params = self.layoutParams {
let leftInset: CGFloat = 15.0 + params.leftInset
var titleFrame = self.titleNode.frame
titleFrame.origin.x = leftInset + offset
transition.updateFrame(node: self.titleNode, frame: titleFrame)
var subtitleFrame = self.subtitleNode.frame
subtitleFrame.origin.x = leftInset + offset
transition.updateFrame(node: self.subtitleNode, frame: subtitleFrame)
}
}
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
if let item = self.item {
switch option.key {
case RevealOptionKey.delete.rawValue:
item.deleted()
default:
break
}
}
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
}
}
@@ -0,0 +1,67 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramUIPreferences
private struct SettingsSearchRecentQueryItemId {
public let rawValue: MemoryBuffer
var value: Int64 {
return self.rawValue.makeData().withUnsafeBytes { buffer -> Int64 in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: Int64.self) else {
return 0
}
return bytes.pointee
}
}
init(_ rawValue: MemoryBuffer) {
self.rawValue = rawValue
}
init(_ value: Int64) {
var value = value
self.rawValue = MemoryBuffer(data: Data(bytes: &value, count: MemoryLayout.size(ofValue: value)))
}
}
public final class RecentSettingsSearchQueryItem: Codable {
public init() {
}
public init(from decoder: Decoder) throws {
}
public func encode(to encoder: Encoder) throws {
}
}
func addRecentSettingsSearchItem(engine: TelegramEngine, item: SettingsSearchableItemId) {
let itemId = SettingsSearchRecentQueryItemId(item.index)
let _ = engine.orderedLists.addOrMoveToFirstPosition(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems, id: itemId.rawValue, item: RecentSettingsSearchQueryItem(), removeTailIfCountExceeds: 100).start()
}
func removeRecentSettingsSearchItem(engine: TelegramEngine, item: SettingsSearchableItemId) {
let itemId = SettingsSearchRecentQueryItemId(item.index)
let _ = engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems, id: itemId.rawValue).start()
}
func clearRecentSettingsSearchItems(engine: TelegramEngine) {
let _ = engine.orderedLists.clear(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems).start()
}
func settingsSearchRecentItems(engine: TelegramEngine) -> Signal<[SettingsSearchableItemId], NoError> {
return engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems))
|> map { items -> [SettingsSearchableItemId] in
var result: [SettingsSearchableItemId] = []
for item in items {
let index = SettingsSearchRecentQueryItemId(item.id).value
if let itemId = SettingsSearchableItemId(index: index) {
result.append(itemId)
}
}
return result
}
}
@@ -0,0 +1,281 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
class SettingsSearchResultItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let item: SettingsSearchableItem
let icon: UIImage?
let interaction: SettingsSearchInteraction
let sectionId: ItemListSectionId
init(theme: PresentationTheme, strings: PresentationStrings, item: SettingsSearchableItem, icon: UIImage?, interaction: SettingsSearchInteraction, sectionId: ItemListSectionId) {
self.theme = theme
self.strings = strings
self.item = item
self.icon = icon
self.interaction = interaction
self.sectionId = sectionId
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = SettingsSearchResultItemNode()
var neighbors = itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)
if previousItem == nil || self.isAlwaysPlain {
neighbors.top = .sameSection(alwaysPlain: false)
}
let (layout, apply) = node.asyncLayout()(self, params, neighbors)
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(false) })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? SettingsSearchResultItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
var neighbors = itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)
if previousItem == nil || self.isAlwaysPlain {
neighbors.top = .sameSection(alwaysPlain: false)
}
let (layout, apply) = makeLayout(self, params, neighbors)
Queue.mainQueue().async {
completion(layout, { _ in
apply(animation.isAnimated)
})
}
}
}
}
}
var selectable: Bool = true
func selected(listView: ListView){
listView.clearHighlightAnimated(true)
self.interaction.openItem(self.item)
}
}
private let titleFont = Font.regular(17.0)
private let subtitleFont = Font.regular(13.0)
class SettingsSearchResultItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let iconNode: ASImageNode
private let titleNode: TextNode
private let subtitleNode: TextNode
private var item: SettingsSearchResultItem?
private var layoutParams: (ListViewItemLayoutParams, ItemListNeighbors)?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreenScale
self.subtitleNode = TextNode()
self.subtitleNode.isUserInteractionEnabled = false
self.subtitleNode.contentMode = .left
self.subtitleNode.contentsScale = UIScreenScale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.iconNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
}
func asyncLayout() -> (_ item: SettingsSearchResultItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let currentItem = self.item
return { item, params, neighbors in
var leftInset: CGFloat = params.leftInset
let contentInset: CGFloat = 58.0
let insets = itemListNeighborsGroupedInsets(neighbors, params)
leftInset += contentInset
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let subtitle = item.item.breadcrumbs.joined(separator: "")
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: subtitle, font: subtitleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let separatorHeight = UIScreenPixel
var updateIconImage: UIImage?
var updatedTheme: PresentationTheme?
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
}
if currentItem?.icon !== item.icon {
updateIconImage = item.icon
}
var height = titleLayout.size.height
if subtitle.isEmpty {
height += 22.0
} else {
height += 39.0
}
let contentSize = CGSize(width: params.width, height: height)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] animated in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = (params, neighbors)
let transition: ContainedViewLayoutTransition
if animated {
transition = .animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
if let updateIconImage = updateIconImage {
strongSelf.iconNode.image = updateIconImage
} else if item.icon == nil {
strongSelf.iconNode.image = nil
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
}
let _ = titleApply()
let _ = subtitleApply()
if let image = strongSelf.iconNode.image {
transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + floor((contentInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size))
}
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
strongSelf.topStripeNode.isHidden = false
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
default:
bottomStripeInset = 0.0
}
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
let titleY: CGFloat = subtitle.isEmpty ? 11.0 : 11.0
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: titleY), size: titleLayout.size))
transition.updateFrame(node: strongSelf.subtitleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: titleY + titleLayout.size.height + 1.0), size: subtitleLayout.size))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel))
}
})
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,77 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import PasswordSetupUI
public protocol SettingsController: AnyObject {
func updateContext(context: AccountContext)
}
public func makePrivacyAndSecurityController(context: AccountContext) -> ViewController {
return privacyAndSecurityController(context: context, focusOnItemTag: PrivacyAndSecurityEntryTag.autoArchive)
}
public func makeBioPrivacyController(context: AccountContext, settings: Promise<AccountPrivacySettings?>, present: @escaping (ViewController) -> Void) {
let signal = settings.get()
|> take(1)
|> deliverOnMainQueue
let _ = signal.startStandalone(next: { info in
if let info = info {
present(selectivePrivacySettingsController(context: context, kind: .bio, current: info.bio, updated: { updated, _, _, _ in
let applySetting: Signal<Void, NoError> = settings.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue
|> mapToSignal { value -> Signal<Void, NoError> in
if let value = value {
settings.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, bio: updated, birthday: value.birthday, giftsAutoSave: value.giftsAutoSave, noPaidMessages: value.noPaidMessages, savedMusic: value.savedMusic, globalSettings: value.globalSettings, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout)))
}
return .complete()
}
let _ = applySetting.startStandalone()
}))
}
})
}
public func makeBirthdayPrivacyController(context: AccountContext, settings: Promise<AccountPrivacySettings?>, openedFromBirthdayScreen: Bool, present: @escaping (ViewController) -> Void) {
let signal = settings.get()
|> take(1)
|> deliverOnMainQueue
let _ = signal.startStandalone(next: { info in
if let info = info {
present(selectivePrivacySettingsController(context: context, kind: .birthday, current: info.birthday, openedFromBirthdayScreen: openedFromBirthdayScreen, updated: { updated, _, _, _ in
let applySetting: Signal<Void, NoError> = settings.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue
|> mapToSignal { value -> Signal<Void, NoError> in
if let value = value {
settings.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, bio: value.bio, birthday: updated, giftsAutoSave: value.giftsAutoSave, noPaidMessages: value.noPaidMessages, savedMusic: value.savedMusic, globalSettings: value.globalSettings, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout)))
}
return .complete()
}
let _ = applySetting.startStandalone()
}))
}
})
}
public func makeSetupTwoFactorAuthController(context: AccountContext) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = TwoFactorAuthSplashScreen(sharedContext: context.sharedContext, engine: .authorized(context.engine), mode: .intro(.init(
title: presentationData.strings.TwoFactorSetup_Intro_Title,
text: presentationData.strings.TwoFactorSetup_Intro_Text,
actionText: presentationData.strings.TwoFactorSetup_Intro_Action,
doneText: presentationData.strings.TwoFactorSetup_Done_Action,
phoneNumber: nil
)))
return controller
}
@@ -0,0 +1,613 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import AccountContext
import StickerPackPreviewUI
import ItemListStickerPackItem
import UndoUI
import ShareController
public enum ArchivedStickerPacksControllerMode {
case stickers
case masks
case emoji
}
private final class ArchivedStickerPacksControllerArguments {
let context: AccountContext
let openStickerPack: (StickerPackCollectionInfo) -> Void
let setPackIdWithRevealedOptions: (ItemCollectionId?, ItemCollectionId?) -> Void
let addPack: (StickerPackCollectionInfo) -> Void
let removePack: (StickerPackCollectionInfo) -> Void
let togglePackSelected: (ItemCollectionId) -> Void
init(context: AccountContext, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, addPack: @escaping (StickerPackCollectionInfo) -> Void, removePack: @escaping (StickerPackCollectionInfo) -> Void, togglePackSelected: @escaping (ItemCollectionId) -> Void) {
self.context = context
self.openStickerPack = openStickerPack
self.setPackIdWithRevealedOptions = setPackIdWithRevealedOptions
self.addPack = addPack
self.removePack = removePack
self.togglePackSelected = togglePackSelected
}
}
private enum ArchivedStickerPacksSection: Int32 {
case stickers
}
private enum ArchivedStickerPacksEntryId: Hashable {
case index(Int32)
case pack(ItemCollectionId)
}
private enum ArchivedStickerPacksEntry: ItemListNodeEntry {
case info(PresentationTheme, String)
case pack(Int32, PresentationTheme, PresentationStrings, StickerPackCollectionInfo, StickerPackItem?, String, Bool, Bool, ItemListStickerPackItemEditing, Bool?)
var section: ItemListSectionId {
switch self {
case .info, .pack:
return ArchivedStickerPacksSection.stickers.rawValue
}
}
var stableId: ArchivedStickerPacksEntryId {
switch self {
case .info:
return .index(0)
case let .pack(_, _, _, info, _, _, _, _, _, _):
return .pack(info.id)
}
}
static func ==(lhs: ArchivedStickerPacksEntry, rhs: ArchivedStickerPacksEntry) -> Bool {
switch lhs {
case let .info(lhsTheme, lhsText):
if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .pack(lhsIndex, lhsTheme, lhsStrings, lhsInfo, lhsTopItem, lhsCount, lhsPlayAnimatedStickers, lhsEnabled, lhsEditing, lhsSelected):
if case let .pack(rhsIndex, rhsTheme, rhsStrings, rhsInfo, rhsTopItem, rhsCount, rhsPlayAnimatedStickers, rhsEnabled, rhsEditing, rhsSelected) = rhs {
if lhsIndex != rhsIndex {
return false
}
if lhsTheme !== rhsTheme {
return false
}
if lhsStrings !== rhsStrings {
return false
}
if lhsInfo != rhsInfo {
return false
}
if lhsTopItem != rhsTopItem {
return false
}
if lhsCount != rhsCount {
return false
}
if lhsPlayAnimatedStickers != rhsPlayAnimatedStickers {
return false
}
if lhsEnabled != rhsEnabled {
return false
}
if lhsEditing != rhsEditing {
return false
}
if lhsSelected != rhsSelected {
return false
}
return true
} else {
return false
}
}
}
static func <(lhs: ArchivedStickerPacksEntry, rhs: ArchivedStickerPacksEntry) -> Bool {
switch lhs {
case .info:
switch rhs {
case .info:
return false
default:
return true
}
case let .pack(lhsIndex, _, _, _, _, _, _, _, _, _):
switch rhs {
case let .pack(rhsIndex, _, _, _, _, _, _, _, _, _):
return lhsIndex < rhsIndex
default:
return false
}
}
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ArchivedStickerPacksControllerArguments
switch self {
case let .info(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .pack(_, _, _, info, topItem, count, animatedStickers, enabled, editing, selected):
return ItemListStickerPackItem(presentationData: presentationData, context: arguments.context, systemStyle: .glass, packInfo: StickerPackCollectionInfo.Accessor(info), itemCount: count, topItem: topItem, unread: false, control: editing.editing ? .check(checked: selected ?? false) : .installation(installed: false), editing: editing, enabled: enabled, playAnimatedStickers: animatedStickers, sectionId: self.section, action: {
arguments.openStickerPack(info)
}, setPackIdWithRevealedOptions: { current, previous in
arguments.setPackIdWithRevealedOptions(current, previous)
}, addPack: {
arguments.addPack(info)
}, removePack: {
arguments.removePack(info)
}, toggleSelected: {
arguments.togglePackSelected(info.id)
})
}
}
}
private struct ArchivedStickerPacksControllerState: Equatable {
let editing: Bool
let selectedPackIds: Set<ItemCollectionId>?
let packIdWithRevealedOptions: ItemCollectionId?
let removingPackIds: Set<ItemCollectionId>
init() {
self.editing = false
self.selectedPackIds = nil
self.packIdWithRevealedOptions = nil
self.removingPackIds = Set()
}
init(editing: Bool, selectedPackIds: Set<ItemCollectionId>?, packIdWithRevealedOptions: ItemCollectionId?, removingPackIds: Set<ItemCollectionId>) {
self.editing = editing
self.selectedPackIds = selectedPackIds
self.packIdWithRevealedOptions = packIdWithRevealedOptions
self.removingPackIds = removingPackIds
}
static func ==(lhs: ArchivedStickerPacksControllerState, rhs: ArchivedStickerPacksControllerState) -> Bool {
if lhs.editing != rhs.editing {
return false
}
if lhs.selectedPackIds != rhs.selectedPackIds {
return false
}
if lhs.packIdWithRevealedOptions != rhs.packIdWithRevealedOptions {
return false
}
if lhs.removingPackIds != rhs.removingPackIds {
return false
}
return true
}
func withUpdatedEditing(_ editing: Bool) -> ArchivedStickerPacksControllerState {
return ArchivedStickerPacksControllerState(editing: editing, selectedPackIds: self.selectedPackIds, packIdWithRevealedOptions: self.packIdWithRevealedOptions, removingPackIds: self.removingPackIds)
}
func withUpdatedSelectedPackIds(_ selectedPackIds: Set<ItemCollectionId>?) -> ArchivedStickerPacksControllerState {
return ArchivedStickerPacksControllerState(editing: self.editing, selectedPackIds: selectedPackIds, packIdWithRevealedOptions: self.packIdWithRevealedOptions, removingPackIds: self.removingPackIds)
}
func withUpdatedPackIdWithRevealedOptions(_ packIdWithRevealedOptions: ItemCollectionId?) -> ArchivedStickerPacksControllerState {
return ArchivedStickerPacksControllerState(editing: self.editing, selectedPackIds: self.selectedPackIds, packIdWithRevealedOptions: packIdWithRevealedOptions, removingPackIds: self.removingPackIds)
}
func withUpdatedRemovingPackIds(_ removingPackIds: Set<ItemCollectionId>) -> ArchivedStickerPacksControllerState {
return ArchivedStickerPacksControllerState(editing: self.editing, selectedPackIds: self.selectedPackIds, packIdWithRevealedOptions: self.packIdWithRevealedOptions, removingPackIds: removingPackIds)
}
}
private func archivedStickerPacksControllerEntries(context: AccountContext, mode: ArchivedStickerPacksControllerMode, presentationData: PresentationData, state: ArchivedStickerPacksControllerState, packs: [ArchivedStickerPackItem]?, installedView: CombinedView, stickerSettings: StickerSettings) -> [ArchivedStickerPacksEntry] {
var entries: [ArchivedStickerPacksEntry] = []
if let packs = packs {
let info: String
switch mode {
case .emoji:
info = presentationData.strings.EmojiPacksSettings_ArchivedPacks_Info
default:
info = presentationData.strings.StickerPacksSettings_ArchivedPacks_Info
}
entries.append(.info(presentationData.theme, info + "\n\n"))
var installedIds = Set<ItemCollectionId>()
if let view = installedView.views[.itemCollectionIds(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionIdsView, let ids = view.idsByNamespace[Namespaces.ItemCollection.CloudStickerPacks] {
installedIds = ids
}
var index: Int32 = 0
for item in packs {
if !installedIds.contains(item.info.id) {
let countTitle: String
if item.info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks {
countTitle = presentationData.strings.StickerPack_EmojiCount(item.info.count)
} else if item.info.id.namespace == Namespaces.ItemCollection.CloudMaskPacks {
countTitle = presentationData.strings.StickerPack_MaskCount(item.info.count)
} else {
countTitle = presentationData.strings.StickerPack_StickerCount(item.info.count)
}
entries.append(.pack(index, presentationData.theme, presentationData.strings, item.info, item.topItems.first, countTitle, context.sharedContext.energyUsageSettings.loopStickers, !state.removingPackIds.contains(item.info.id), ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == item.info.id, reorderable: false, selectable: true), state.selectedPackIds?.contains(item.info.id)))
index += 1
}
}
}
return entries
}
public func archivedStickerPacksController(context: AccountContext, mode: ArchivedStickerPacksControllerMode, archived: [ArchivedStickerPackItem]?, forceTheme: PresentationTheme? = nil, updatedPacks: @escaping ([ArchivedStickerPackItem]?) -> Void) -> ViewController {
let statePromise = ValuePromise(ArchivedStickerPacksControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: ArchivedStickerPacksControllerState())
let updateState: ((ArchivedStickerPacksControllerState) -> ArchivedStickerPacksControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var navigationControllerImpl: (() -> NavigationController?)?
let actionsDisposable = DisposableSet()
let resolveDisposable = MetaDisposable()
actionsDisposable.add(resolveDisposable)
let removePackDisposables = DisposableDict<ItemCollectionId>()
actionsDisposable.add(removePackDisposables)
let namespace: ArchivedStickerPacksNamespace
switch mode {
case .stickers:
namespace = .stickers
case .emoji:
namespace = .emoji
case .masks:
namespace = .masks
}
let stickerPacks = Promise<[ArchivedStickerPackItem]?>()
stickerPacks.set(.single(archived) |> then(context.engine.stickers.archivedStickerPacks(namespace: namespace) |> map(Optional.init)))
actionsDisposable.add(stickerPacks.get().start(next: { packs in
updatedPacks(packs)
}))
let installedStickerPacks = Promise<CombinedView>()
installedStickerPacks.set(context.account.postbox.combinedView(keys: [.itemCollectionIds(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])]))
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
if let forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme)
}
var presentStickerPackController: ((StickerPackCollectionInfo) -> Void)?
let arguments = ArchivedStickerPacksControllerArguments(context: context, openStickerPack: { info in
presentStickerPackController?(info)
}, setPackIdWithRevealedOptions: { packId, fromPackId in
updateState { state in
if (packId == nil && fromPackId == state.packIdWithRevealedOptions) || (packId != nil && fromPackId == nil) {
return state.withUpdatedPackIdWithRevealedOptions(packId)
} else {
return state
}
}
}, addPack: { info in
var add = false
updateState { state in
var removingPackIds = state.removingPackIds
if !removingPackIds.contains(info.id) {
removingPackIds.insert(info.id)
add = true
}
return state.withUpdatedRemovingPackIds(removingPackIds)
}
if !add {
return
}
let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false)
|> mapToSignal { result -> Signal<(StickerPackCollectionInfo, [StickerPackItem]), NoError> in
switch result {
case let .result(info, items, installed):
if installed {
return .complete()
} else {
let parsedInfo = info._parse()
return context.engine.stickers.addStickerPackInteractively(info: parsedInfo, items: items)
|> ignoreValues
|> mapToSignal { _ -> Signal<(StickerPackCollectionInfo, [StickerPackItem]), NoError> in
}
|> then(.single((parsedInfo, items)))
}
case .fetching:
break
case .none:
break
}
return .complete()
}
|> deliverOnMainQueue).start(next: { info, items in
var animateInAsReplacement = false
if let navigationController = navigationControllerImpl?() {
for controller in navigationController.overlayControllers {
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitActionAndReplacementAnimation()
animateInAsReplacement = true
}
}
}
let title: String
let text: String
switch mode {
case .emoji:
title = presentationData.strings.EmojiPackActionInfo_AddedTitle
text = presentationData.strings.EmojiPackActionInfo_AddedText(info.title).string
default:
title = presentationData.strings.StickerPackActionInfo_AddedTitle
text = presentationData.strings.StickerPackActionInfo_AddedText(info.title).string
}
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: title, text: text, undo: false, info: info, topItem: items.first, context: context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in
return true
}), nil)
let applyPacks: Signal<Void, NoError> = stickerPacks.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue
|> mapToSignal { packs -> Signal<Void, NoError> in
if let packs = packs {
var updatedPacks = packs
for i in 0 ..< updatedPacks.count {
if updatedPacks[i].info.id == info.id {
updatedPacks.remove(at: i)
break
}
}
stickerPacks.set(.single(updatedPacks))
}
return .complete()
}
let _ = applyPacks.start()
})
}, removePack: { info in
var remove = false
updateState { state in
var removingPackIds = state.removingPackIds
if !removingPackIds.contains(info.id) {
removingPackIds.insert(info.id)
remove = true
}
return state.withUpdatedRemovingPackIds(removingPackIds)
}
if remove {
let applyPacks: Signal<Void, NoError> = stickerPacks.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue
|> mapToSignal { packs -> Signal<Void, NoError> in
if let packs = packs {
var updatedPacks = packs
for i in 0 ..< updatedPacks.count {
if updatedPacks[i].info.id == info.id {
updatedPacks.remove(at: i)
break
}
}
stickerPacks.set(.single(updatedPacks))
}
return .complete()
}
removePackDisposables.set((context.engine.stickers.removeArchivedStickerPack(info: info) |> then(applyPacks) |> deliverOnMainQueue).start(completed: {
updateState { state in
var removingPackIds = state.removingPackIds
removingPackIds.remove(info.id)
return state.withUpdatedRemovingPackIds(removingPackIds)
}
}), forKey: info.id)
}
}, togglePackSelected: { packId in
updateState { state in
if var selectedPackIds = state.selectedPackIds {
if selectedPackIds.contains(packId) {
selectedPackIds.remove(packId)
} else {
selectedPackIds.insert(packId)
}
return state.withUpdatedSelectedPackIds(selectedPackIds)
} else {
return state
}
}
})
var previousPackCount: Int?
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get() |> deliverOnMainQueue, stickerPacks.get() |> deliverOnMainQueue, installedStickerPacks.get() |> deliverOnMainQueue, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]) |> deliverOnMainQueue)
|> deliverOnMainQueue
|> map { presentationData, state, packs, installedView, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in
var presentationData = presentationData
if let forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme)
}
var stickerSettings = StickerSettings.defaultSettings
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings]?.get(StickerSettings.self) {
stickerSettings = value
}
var rightNavigationButton: ItemListNavigationButton?
var toolbarItem: ItemListToolbarItem?
if let packs = packs, packs.count != 0 {
if state.editing {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
updateState {
$0.withUpdatedEditing(false)
}
})
let selectedCount = Int32(state.selectedPackIds?.count ?? 0)
toolbarItem = StickersToolbarItem(selectedCount: selectedCount, actions: [.init(title: presentationData.strings.StickerPacks_ActionDelete, isEnabled: selectedCount > 0, action: {
let actionSheet = ActionSheetController(presentationData: presentationData)
var items: [ActionSheetItem] = []
let title: String
switch mode {
case .emoji:
title = presentationData.strings.StickerPacks_DeleteEmojiPacksConfirmation(selectedCount)
default:
title = presentationData.strings.StickerPacks_DeleteStickerPacksConfirmation(selectedCount)
}
items.append(ActionSheetButtonItem(title: title, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
updateState {
$0.withUpdatedEditing(false).withUpdatedSelectedPackIds(nil)
}
for entry in packs {
if let selectedPackIds = state.selectedPackIds, selectedPackIds.contains(entry.info.id) {
let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: entry.info.id.id, accessHash: entry.info.accessHash), forceActualized: false)
|> mapToSignal { result -> Signal<(StickerPackCollectionInfo, [StickerPackItem]), NoError> in
switch result {
case let .result(info, items, installed):
if installed {
return .complete()
} else {
let parsedInfo = info._parse()
return context.engine.stickers.addStickerPackInteractively(info: parsedInfo, items: items)
|> ignoreValues
|> mapToSignal { _ -> Signal<(StickerPackCollectionInfo, [StickerPackItem]), NoError> in
}
|> then(.single((parsedInfo, items)))
}
case .fetching:
break
case .none:
break
}
return .complete()
}).start()
}
}
}))
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, nil)
}), .init(title: presentationData.strings.StickerPacks_ActionUnarchive, isEnabled: selectedCount > 0, action: {
let actionSheet = ActionSheetController(presentationData: presentationData)
let text: String
switch mode {
case .emoji:
text = presentationData.strings.EmojiPacks_UnarchiveEmojiPacksConfirmation(selectedCount)
default:
text = presentationData.strings.StickerPacks_UnarchiveStickerPacksConfirmation(selectedCount)
}
var items: [ActionSheetItem] = []
items.append(ActionSheetButtonItem(title: text, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
updateState {
$0.withUpdatedEditing(false).withUpdatedSelectedPackIds(nil)
}
var packIds: [ItemCollectionId] = []
for entry in packs {
if let selectedPackIds = state.selectedPackIds, selectedPackIds.contains(entry.info.id) {
packIds.append(entry.info.id)
}
}
let _ = context.engine.stickers.removeStickerPacksInteractively(ids: packIds, option: .archive).start()
}))
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, nil)
}), .init(title: presentationData.strings.StickerPacks_ActionShare, isEnabled: selectedCount > 0, action: {
updateState {
$0.withUpdatedEditing(true).withUpdatedSelectedPackIds(nil)
}
var packNames: [String] = []
for entry in packs {
if let selectedPackIds = state.selectedPackIds, selectedPackIds.contains(entry.info.id) {
packNames.append(entry.info.shortName)
}
}
let text = packNames.map { "https://t.me/addstickers/\($0)" }.joined(separator: "\n")
let shareController = ShareController(context: context, subject: .text(text), externalShare: true)
presentControllerImpl?(shareController, nil)
})])
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: {
updateState {
$0.withUpdatedEditing(true).withUpdatedSelectedPackIds(Set())
}
})
}
}
let previous = previousPackCount
previousPackCount = packs?.count
var emptyStateItem: ItemListControllerEmptyStateItem?
if packs == nil {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
let title: String
switch mode {
case .emoji:
title = presentationData.strings.EmojiPacksSettings_ArchivedPacks
default:
title = presentationData.strings.StickerPacksSettings_ArchivedPacks
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: archivedStickerPacksControllerEntries(context: context, mode: mode, presentationData: presentationData, state: state, packs: packs, installedView: installedView, stickerSettings: stickerSettings), style: .blocks, emptyStateItem: emptyStateItem, toolbarItem: toolbarItem, animateChanges: previous != nil && packs != nil && (previous! != 0 && previous! >= packs!.count - 10))
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
navigationControllerImpl = { [weak controller] in
return controller?.navigationController as? NavigationController
}
presentStickerPackController = { [weak controller] info in
let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash)
presentControllerImpl?(StickerPackScreen(context: context, mode: .settings, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controller?.navigationController as? NavigationController), nil)
}
return controller
}
@@ -0,0 +1,259 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import StickerPackPreviewUI
import ItemListStickerPackItem
private final class FeaturedStickerPacksControllerArguments {
let context: AccountContext
let openStickerPack: (StickerPackCollectionInfo) -> Void
let addPack: (StickerPackCollectionInfo) -> Void
init(context: AccountContext, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, addPack: @escaping (StickerPackCollectionInfo) -> Void) {
self.context = context
self.openStickerPack = openStickerPack
self.addPack = addPack
}
}
private enum FeaturedStickerPacksSection: Int32 {
case stickers
}
private enum FeaturedStickerPacksEntryId: Hashable {
case pack(ItemCollectionId)
}
private enum FeaturedStickerPacksEntry: ItemListNodeEntry {
case pack(Int32, PresentationTheme, PresentationStrings, StickerPackCollectionInfo.Accessor, Bool, StickerPackItem?, String, Bool, Bool)
var section: ItemListSectionId {
switch self {
case .pack:
return FeaturedStickerPacksSection.stickers.rawValue
}
}
var stableId: FeaturedStickerPacksEntryId {
switch self {
case let .pack(_, _, _, info, _, _, _, _, _):
return .pack(info.id)
}
}
static func ==(lhs: FeaturedStickerPacksEntry, rhs: FeaturedStickerPacksEntry) -> Bool {
switch lhs {
case let .pack(lhsIndex, lhsTheme, lhsStrings, lhsInfo, lhsUnread, lhsTopItem, lhsCount, lhsPlayAnimatedStickers, lhsInstalled):
if case let .pack(rhsIndex, rhsTheme, rhsStrings, rhsInfo, rhsUnread, rhsTopItem, rhsCount, rhsPlayAnimatedStickers, rhsInstalled) = rhs {
if lhsIndex != rhsIndex {
return false
}
if lhsTheme !== rhsTheme {
return false
}
if lhsStrings !== rhsStrings {
return false
}
if lhsInfo != rhsInfo {
return false
}
if lhsUnread != rhsUnread {
return false
}
if lhsTopItem != rhsTopItem {
return false
}
if lhsCount != rhsCount {
return false
}
if lhsPlayAnimatedStickers != rhsPlayAnimatedStickers {
return false
}
if lhsInstalled != rhsInstalled {
return false
}
return true
} else {
return false
}
}
}
static func <(lhs: FeaturedStickerPacksEntry, rhs: FeaturedStickerPacksEntry) -> Bool {
switch lhs {
case let .pack(lhsIndex, _, _, _, _, _, _, _, _):
switch rhs {
case let .pack(rhsIndex, _, _, _, _, _, _, _, _):
return lhsIndex < rhsIndex
}
}
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! FeaturedStickerPacksControllerArguments
switch self {
case let .pack(_, _, _, info, unread, topItem, count, playAnimatedStickers, installed):
return ItemListStickerPackItem(presentationData: presentationData, context: arguments.context, packInfo: info, itemCount: count, topItem: topItem, unread: unread, control: .installation(installed: installed), editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false, reorderable: false, selectable: false), enabled: true, playAnimatedStickers: playAnimatedStickers, sectionId: self.section, action: {
arguments.openStickerPack(info._parse())
}, setPackIdWithRevealedOptions: { _, _ in
}, addPack: {
arguments.addPack(info._parse())
}, removePack: {
}, toggleSelected: {
})
}
}
}
private struct FeaturedStickerPacksControllerState: Equatable {
init() {
}
static func ==(lhs: FeaturedStickerPacksControllerState, rhs: FeaturedStickerPacksControllerState) -> Bool {
return true
}
}
private func featuredStickerPacksControllerEntries(context: AccountContext, presentationData: PresentationData, state: FeaturedStickerPacksControllerState, view: CombinedView, featured: [FeaturedStickerPackItem], unreadPacks: [ItemCollectionId: Bool], stickerSettings: StickerSettings) -> [FeaturedStickerPacksEntry] {
var entries: [FeaturedStickerPacksEntry] = []
if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionInfosView, !featured.isEmpty {
if let packsEntries = stickerPacksView.entriesByNamespace[Namespaces.ItemCollection.CloudStickerPacks] {
var installedPacks = Set<ItemCollectionId>()
for entry in packsEntries {
installedPacks.insert(entry.id)
}
var index: Int32 = 0
for item in featured {
var unread = false
if let value = unreadPacks[item.info.id] {
unread = value
}
let countTitle: String
if item.info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks {
countTitle = presentationData.strings.StickerPack_EmojiCount(item.info.count)
} else if item.info.id.namespace == Namespaces.ItemCollection.CloudMaskPacks {
countTitle = presentationData.strings.StickerPack_MaskCount(item.info.count)
} else {
countTitle = presentationData.strings.StickerPack_StickerCount(item.info.count)
}
entries.append(.pack(index, presentationData.theme, presentationData.strings, item.info, unread, item.topItems.first, countTitle, context.sharedContext.energyUsageSettings.loopStickers, installedPacks.contains(item.info.id)))
index += 1
}
}
}
return entries
}
public func featuredStickerPacksController(context: AccountContext) -> ViewController {
let statePromise = ValuePromise(FeaturedStickerPacksControllerState(), ignoreRepeated: true)
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
let actionsDisposable = DisposableSet()
let resolveDisposable = MetaDisposable()
actionsDisposable.add(resolveDisposable)
var presentStickerPackController: ((StickerPackCollectionInfo) -> Void)?
let arguments = FeaturedStickerPacksControllerArguments(context: context, openStickerPack: { info in
presentStickerPackController?(info)
}, addPack: { info in
let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false)
|> mapToSignal { result -> Signal<Void, NoError> in
switch result {
case let .result(info, items, installed):
if installed {
return .complete()
} else {
return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items)
}
case .fetching:
break
case .none:
break
}
return .complete()
} |> deliverOnMainQueue).start()
})
let stickerPacks = Promise<CombinedView>()
stickerPacks.set(context.account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])]))
let featured = Promise<[FeaturedStickerPackItem]>()
featured.set(context.account.viewTracker.featuredStickerPacks())
var initialUnreadPacks: [ItemCollectionId: Bool] = [:]
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get() |> deliverOnMainQueue, stickerPacks.get() |> deliverOnMainQueue, featured.get() |> deliverOnMainQueue, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]) |> deliverOnMainQueue)
|> deliverOnMainQueue
|> map { presentationData, state, view, featured, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in
var stickerSettings = StickerSettings.defaultSettings
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings]?.get(StickerSettings.self) {
stickerSettings = value
}
for item in featured {
if initialUnreadPacks[item.info.id] == nil {
initialUnreadPacks[item.info.id] = item.unread
}
}
let rightNavigationButton: ItemListNavigationButton? = nil
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.FeaturedStickerPacks_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: featuredStickerPacksControllerEntries(context: context, presentationData: presentationData, state: state, view: view, featured: featured, unreadPacks: initialUnreadPacks, stickerSettings: stickerSettings), style: .blocks, animateChanges: false)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
var alreadyReadIds = Set<ItemCollectionId>()
controller.visibleEntriesUpdated = { entries in
var unreadIds: [ItemCollectionId] = []
for entry in entries {
if let entry = entry as? FeaturedStickerPacksEntry {
switch entry {
case let .pack(_, _, _, info, unread, _, _, _, _):
if unread && !alreadyReadIds.contains(info.id) {
unreadIds.append(info.id)
}
}
}
}
if !unreadIds.isEmpty {
alreadyReadIds.formUnion(Set(unreadIds))
let _ = context.engine.stickers.markFeaturedStickerPacksAsSeenInteractively(ids: unreadIds).start()
}
}
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
presentStickerPackController = { [weak controller] info in
let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash)
presentControllerImpl?(StickerPackScreen(context: context, mode: .settings, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controller?.navigationController as? NavigationController), nil)
}
return controller
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,162 @@
import Foundation
import UIKit
import TelegramCore
import SwiftSignalKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import TelegramUIPreferences
import ProgressNavigationButtonNode
public class TermsOfServiceControllerTheme {
public let statusBarStyle: StatusBarStyle
public let navigationBackground: UIColor
public let navigationSeparator: UIColor
public let listBackground: UIColor
public let itemBackground: UIColor
public let itemSeparator: UIColor
public let primary: UIColor
public let accent: UIColor
public let disabled: UIColor
public init(statusBarStyle: StatusBarStyle, navigationBackground: UIColor, navigationSeparator: UIColor, listBackground: UIColor, itemBackground: UIColor, itemSeparator: UIColor, primary: UIColor, accent: UIColor, disabled: UIColor) {
self.statusBarStyle = statusBarStyle
self.navigationBackground = navigationBackground
self.navigationSeparator = navigationSeparator
self.listBackground = listBackground
self.itemBackground = itemBackground
self.itemSeparator = itemSeparator
self.primary = primary
self.accent = accent
self.disabled = disabled
}
}
public extension TermsOfServiceControllerTheme {
convenience init(presentationTheme: PresentationTheme) {
self.init(statusBarStyle: presentationTheme.rootController.statusBarStyle.style, navigationBackground: presentationTheme.rootController.navigationBar.opaqueBackgroundColor, navigationSeparator: presentationTheme.rootController.navigationBar.separatorColor, listBackground: presentationTheme.list.blocksBackgroundColor, itemBackground: presentationTheme.list.itemBlocksBackgroundColor, itemSeparator: presentationTheme.list.itemBlocksSeparatorColor, primary: presentationTheme.list.itemPrimaryTextColor, accent: presentationTheme.list.itemAccentColor, disabled: presentationTheme.rootController.navigationBar.disabledButtonColor)
}
}
public class TermsOfServiceController: ViewController, StandalonePresentableController {
private var controllerNode: TermsOfServiceControllerNode {
return self.displayNode as! TermsOfServiceControllerNode
}
private let presentationData: PresentationData
private let text: String
private let entities: [MessageTextEntity]
private let ageConfirmation: Int32?
private let signingUp: Bool
private let accept: (String?) -> Void
private let decline: () -> Void
private let openUrl: (String) -> Void
private var proccessBotNameAfterAccept: String? = nil
private var didPlayPresentationAnimation = false
public var inProgress: Bool = false {
didSet {
if self.inProgress {
let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.list.itemAccentColor))
self.navigationItem.rightBarButtonItem = item
} else {
self.navigationItem.rightBarButtonItem = nil
}
self.controllerNode.inProgress = self.inProgress
}
}
public init(presentationData: PresentationData, text: String, entities: [MessageTextEntity], ageConfirmation: Int32?, signingUp: Bool, accept: @escaping (String?) -> Void, decline: @escaping () -> Void, openUrl: @escaping (String) -> Void) {
self.presentationData = presentationData
self.text = text
self.entities = entities
self.ageConfirmation = ageConfirmation
self.signingUp = signingUp
self.accept = accept
self.decline = decline
self.openUrl = openUrl
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme), strings: NavigationBarStrings(back: presentationData.strings.Common_Back, close: presentationData.strings.Common_Close)))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.title = self.presentationData.strings.Login_TermsOfServiceHeader
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.scrollToTop = { [weak self] in
self?.controllerNode.scrollToTop()
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func loadDisplayNode() {
self.displayNode = TermsOfServiceControllerNode(presentationData: self.presentationData, text: self.text, entities: self.entities, ageConfirmation: self.ageConfirmation, leftAction: { [weak self] in
guard let strongSelf = self else {
return
}
let text: String
let declineTitle: String
if strongSelf.signingUp {
text = strongSelf.presentationData.strings.Login_TermsOfServiceSignupDecline
declineTitle = strongSelf.presentationData.strings.Login_TermsOfServiceDecline
} else {
text = strongSelf.presentationData.strings.PrivacyPolicy_DeclineMessage
declineTitle = strongSelf.presentationData.strings.PrivacyPolicy_DeclineDeclineAndDelete
}
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.PrivacyPolicy_Decline, text: text, actions: [TextAlertAction(type: .destructiveAction, title: declineTitle, action: {
self?.decline()
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
})], actionLayout: .vertical), in: .window(.root))
}, rightAction: { [weak self] in
guard let strongSelf = self else {
return
}
if let ageConfirmation = strongSelf.ageConfirmation {
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationTitle, text: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationMessage("\(ageConfirmation)").string, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationAgree, action: {
self?.accept(self?.proccessBotNameAfterAccept)
})]), in: .window(.root))
} else {
strongSelf.accept(self?.proccessBotNameAfterAccept)
}
}, openUrl: self.openUrl, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}, setToProcceedBot: { [weak self] botName in
self?.proccessBotNameAfterAccept = botName
})
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.didPlayPresentationAnimation {
self.didPlayPresentationAnimation = true
self.controllerNode.animateIn()
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
self.controllerNode.animateOut(completion: { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
completion?()
})
}
}
@@ -0,0 +1,260 @@
import Foundation
import UIKit
import TelegramCore
import SwiftSignalKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import TextFormat
import UndoUI
final class TermsOfServiceControllerNode: ViewControllerTracingNode {
private let presentationData: PresentationData
private let text: String
private let entities: [MessageTextEntity]
private let ageConfirmation: Int32?
private let leftAction: () -> Void
private let rightAction: () -> Void
private let openUrl: (String) -> Void
private let present: (ViewController, Any?) -> Void
private let scrollNode: ASScrollNode
private let contentBackgroundNode: ASDisplayNode
private let contentTextNode: ImmediateTextNode
private let toolbarNode: ASDisplayNode
private let toolbarSeparatorNode: ASDisplayNode
private let leftActionNode: HighlightableButtonNode
private let leftActionTextNode: ImmediateTextNode
private let rightActionNode: HighlightableButtonNode
private let rightActionTextNode: ImmediateTextNode
private var containerLayout: (ContainerViewLayout, CGFloat)?
var inProgress: Bool = false {
didSet {
if self.inProgress != oldValue {
self.leftActionTextNode.alpha = self.inProgress ? 0.5 : 1.0
self.rightActionTextNode.alpha = self.inProgress ? 0.5 : 1.0
self.leftActionNode.isEnabled = !self.inProgress
self.rightActionNode.isEnabled = !self.inProgress
}
}
}
init(presentationData: PresentationData, text: String, entities: [MessageTextEntity], ageConfirmation: Int32?, leftAction: @escaping () -> Void, rightAction: @escaping () -> Void, openUrl: @escaping (String) -> Void, present: @escaping (ViewController, Any?) -> Void, setToProcceedBot:@escaping(String)->Void) {
self.presentationData = presentationData
self.text = text
self.entities = entities
self.ageConfirmation = ageConfirmation
self.leftAction = leftAction
self.rightAction = rightAction
self.openUrl = openUrl
self.present = present
self.scrollNode = ASScrollNode()
self.contentBackgroundNode = ASDisplayNode()
self.contentTextNode = ImmediateTextNode()
self.contentTextNode.displaysAsynchronously = false
self.contentTextNode.maximumNumberOfLines = 0
let fontSize = floor(presentationData.listsFontSize.baseDisplaySize * 15.0 / 17.0)
self.contentTextNode.attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: presentationData.theme.list.itemPrimaryTextColor, linkColor: presentationData.theme.list.itemAccentColor, baseFont: Font.regular(fontSize), linkFont: Font.regular(fontSize), boldFont: Font.semibold(fontSize), italicFont: Font.italic(fontSize), boldItalicFont: Font.semiboldItalic(fontSize), fixedFont: Font.monospace(fontSize), blockQuoteFont: Font.regular(fontSize), message: nil)
self.toolbarNode = ASDisplayNode()
self.toolbarSeparatorNode = ASDisplayNode()
self.leftActionNode = HighlightableButtonNode()
self.leftActionTextNode = ImmediateTextNode()
self.leftActionTextNode.displaysAsynchronously = false
self.leftActionTextNode.isUserInteractionEnabled = false
self.leftActionTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.PrivacyPolicy_Decline, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: self.presentationData.theme.list.itemAccentColor)
self.rightActionNode = HighlightableButtonNode()
self.rightActionTextNode = ImmediateTextNode()
self.rightActionTextNode.displaysAsynchronously = false
self.rightActionTextNode.isUserInteractionEnabled = false
self.rightActionTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.PrivacyPolicy_Accept, font: Font.semibold(presentationData.listsFontSize.baseDisplaySize), textColor: self.presentationData.theme.list.itemAccentColor)
super.init()
self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
self.toolbarNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor
self.toolbarSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.contentBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor
self.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.contentBackgroundNode)
self.scrollNode.addSubnode(self.contentTextNode)
self.addSubnode(self.toolbarNode)
self.addSubnode(self.toolbarSeparatorNode)
self.addSubnode(self.leftActionTextNode)
self.addSubnode(self.leftActionNode)
self.addSubnode(self.rightActionTextNode)
self.addSubnode(self.rightActionNode)
self.leftActionNode.highligthedChanged = { [weak self] highlighted in
guard let strongSelf = self else {
return
}
if highlighted {
strongSelf.leftActionTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.leftActionTextNode.alpha = 0.4
} else {
strongSelf.leftActionTextNode.alpha = 1.0
strongSelf.leftActionTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
self.rightActionNode.highligthedChanged = { [weak self] highlighted in
guard let strongSelf = self else {
return
}
if highlighted {
strongSelf.rightActionTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.rightActionTextNode.alpha = 0.4
} else {
strongSelf.rightActionTextNode.alpha = 1.0
strongSelf.rightActionTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
self.leftActionNode.addTarget(self, action: #selector(self.leftActionPressed), forControlEvents: .touchUpInside)
self.rightActionNode.addTarget(self, action: #selector(self.rightActionPressed), forControlEvents: .touchUpInside)
self.contentTextNode.linkHighlightColor = self.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2)
self.contentTextNode.highlightAttributeAction = { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
}
let showMentionActionSheet:(String) -> Void = { [weak self] mention in
guard let strongSelf = self else {
return
}
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: strongSelf.presentationData.strings.Login_TermsOfService_ProceedBot(mention).string),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.PrivacyPolicy_Accept, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
setToProcceedBot(mention)
rightAction()
})
]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})])])
strongSelf.present(actionSheet, nil)
}
self.contentTextNode.tapAttributeAction = { [weak self] attributes, _ in
guard let strongSelf = self else {
return
}
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
strongSelf.openUrl(url)
} else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
showMentionActionSheet(mention.mention)
} else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
showMentionActionSheet(mention)
}
}
self.contentTextNode.longTapAttributeAction = { [weak self] attributes, _ in
guard let strongSelf = self else {
return
}
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: url),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
self?.openUrl(url)
}),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet, weak self] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = url
if let strongSelf = self {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .linkCopied(title: nil, text: strongSelf.presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.present(actionSheet, nil)
} else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
showMentionActionSheet(mention.mention)
} else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
showMentionActionSheet(mention)
}
}
}
deinit {
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [])
insets.top += navigationBarHeight
let toolbarHeight: CGFloat = 44.0
insets.bottom += layout.safeInsets.bottom
let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - toolbarHeight), size: CGSize(width: layout.size.width, height: insets.bottom + toolbarHeight))
insets.bottom += toolbarHeight
transition.updateFrame(node: self.toolbarNode, frame: toolbarFrame)
transition.updateFrame(node: self.toolbarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: toolbarFrame.minY), size: CGSize(width: toolbarFrame.width, height: UIScreenPixel)))
let leftActionSize = self.leftActionTextNode.updateLayout(CGSize(width: floor(layout.size.width / 2.0), height: CGFloat.greatestFiniteMagnitude))
let rightActionSize = self.rightActionTextNode.updateLayout(CGSize(width: floor(layout.size.width / 2.0), height: CGFloat.greatestFiniteMagnitude))
let leftActionTextFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left + 15.0, y: toolbarFrame.minY + floor((toolbarHeight - leftActionSize.height) / 2.0)), size: leftActionSize)
let rightActionTextFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.left - 15.0 - rightActionSize.width, y: toolbarFrame.minY + floor((toolbarHeight - rightActionSize.height) / 2.0)), size: rightActionSize)
transition.updateFrame(node: self.leftActionTextNode, frame: leftActionTextFrame)
transition.updateFrame(node: self.rightActionTextNode, frame: rightActionTextFrame)
self.leftActionNode.frame = CGRect(origin: CGPoint(x: 0.0, y: toolbarFrame.minY), size: CGSize(width: leftActionTextFrame.maxX + 15.0, height: toolbarHeight))
self.rightActionNode.frame = CGRect(origin: CGPoint(x: rightActionTextFrame.minX - 15.0, y: toolbarFrame.minY), size: CGSize(width: layout.size.width - (rightActionTextFrame.minX - 15.0), height: toolbarHeight))
let scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom))
transition.updateFrame(node: self.scrollNode, frame: scrollFrame)
let containerInset: CGFloat = 32.0
let contentInsets = UIEdgeInsets(top: 15.0, left: 15.0 + layout.safeInsets.left, bottom: 15.0, right: 15.0 + layout.safeInsets.right)
let contentSize = self.contentTextNode.updateLayout(CGSize(width: layout.size.width - contentInsets.left - contentInsets.right, height: CGFloat.greatestFiniteMagnitude))
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: layout.size.width, height: contentSize.height + contentInsets.top + contentInsets.bottom))
self.contentTextNode.frame = CGRect(origin: CGPoint(x: contentFrame.minX + contentInsets.left, y: contentFrame.minY + contentInsets.top), size: contentSize)
self.contentBackgroundNode.frame = contentFrame
self.scrollNode.view.contentSize = CGSize(width: layout.size.width, height: containerInset + contentFrame.height + containerInset)
}
func scrollToTop() {
self.scrollNode.view.scrollRectToVisible(CGRect(origin: CGPoint(), size: CGSize()), animated: true)
}
func animateIn() {
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
func animateOut(completion: (() -> Void)? = nil) {
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
completion?()
})
}
@objc private func leftActionPressed() {
self.leftAction()
}
@objc private func rightActionPressed() {
self.rightAction()
}
}
@@ -0,0 +1,860 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import AsyncDisplayKit
import TelegramCore
import Postbox
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import ChatListUI
import WallpaperResources
import LegacyComponents
import ItemListUI
import WallpaperBackgroundNode
import AnimationCache
import MultiAnimationRenderer
private func generateMaskImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let gradientColors = [color.withAlphaComponent(0.0).cgColor, color.cgColor, color.cgColor] as CFArray
var locations: [CGFloat] = [0.0, 0.75, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 80.0), options: CGGradientDrawingOptions())
})
}
private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollViewDelegate {
private let context: AccountContext
private var presentationThemeSettings: PresentationThemeSettings
private var presentationData: PresentationData
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
private let referenceTimestamp: Int32
private let scrollNode: ASScrollNode
private let pageControlBackgroundNode: ASDisplayNode
private let pageControlNode: PageControlNode
private let chatListBackgroundNode: ASDisplayNode
private var chatNodes: [ListViewItemNode]?
private let maskNode: ASImageNode
private let separatorNode: ASDisplayNode
private let chatBackgroundNode: WallpaperBackgroundNode
private let messagesContainerNode: ASDisplayNode
private var dateHeaderNode: ListViewItemHeaderNode?
private var messageNodes: [ListViewItemNode]?
private let toolbarNode: TextSelectionToolbarNode
private var validLayout: (ContainerViewLayout, CGFloat)?
init(context: AccountContext, presentationThemeSettings: PresentationThemeSettings, dismiss: @escaping () -> Void, apply: @escaping (Bool, PresentationFontSize, PresentationFontSize) -> Void) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationThemeSettings = presentationThemeSettings
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
let calendar = Calendar(identifier: .gregorian)
var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: Date())
components.hour = 13
components.minute = 0
components.second = 0
self.referenceTimestamp = Int32(calendar.date(from: components)?.timeIntervalSince1970 ?? 0.0)
self.scrollNode = ASScrollNode()
self.pageControlBackgroundNode = ASDisplayNode()
self.pageControlBackgroundNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.3)
self.pageControlBackgroundNode.cornerRadius = 10.5
self.pageControlNode = PageControlNode(dotSpacing: 7.0, dotColor: .white, inactiveDotColor: UIColor.white.withAlphaComponent(0.4))
self.chatListBackgroundNode = ASDisplayNode()
self.chatBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: false)
self.chatBackgroundNode.displaysAsynchronously = false
self.messagesContainerNode = ASDisplayNode()
self.messagesContainerNode.clipsToBounds = true
self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
self.chatBackgroundNode.update(wallpaper: self.presentationData.chatWallpaper, animated: false)
self.chatBackgroundNode.updateBubbleTheme(bubbleTheme: self.presentationData.theme, bubbleCorners: self.presentationData.chatBubbleCorners)
self.toolbarNode = TextSelectionToolbarNode(presentationThemeSettings: self.presentationThemeSettings, presentationData: self.presentationData)
self.maskNode = ASImageNode()
self.maskNode.displaysAsynchronously = false
self.maskNode.displayWithoutProcessing = true
self.maskNode.contentMode = .scaleToFill
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = self.presentationData.theme.rootController.tabBar.separatorColor
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.chatListBackgroundNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.maskNode.image = generateMaskImage(color: self.presentationData.theme.chatList.backgroundColor)
self.pageControlNode.isUserInteractionEnabled = false
self.pageControlNode.pagesCount = 2
self.addSubnode(self.scrollNode)
self.chatListBackgroundNode.addSubnode(self.maskNode)
self.addSubnode(self.pageControlBackgroundNode)
self.addSubnode(self.pageControlNode)
self.addSubnode(self.toolbarNode)
self.scrollNode.addSubnode(self.chatListBackgroundNode)
self.scrollNode.addSubnode(self.chatBackgroundNode)
self.scrollNode.addSubnode(self.messagesContainerNode)
self.addSubnode(self.separatorNode)
self.toolbarNode.cancel = {
dismiss()
}
var dismissed = false
self.toolbarNode.done = { [weak self] in
guard let strongSelf = self else {
return
}
if !dismissed {
dismissed = true
apply(strongSelf.presentationThemeSettings.useSystemFont, strongSelf.presentationThemeSettings.fontSize, strongSelf.presentationThemeSettings.listsFontSize)
}
}
self.toolbarNode.updateUseSystemFont = { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.presentationThemeSettings.useSystemFont = value
strongSelf.updatePresentationThemeSettings(strongSelf.presentationThemeSettings)
}
self.toolbarNode.updateCustomFontSize = { [weak self] value in
guard let strongSelf = self else {
return
}
switch strongSelf.toolbarNode.customMode {
case .chat:
strongSelf.presentationThemeSettings.fontSize = value
case .list:
strongSelf.presentationThemeSettings.listsFontSize = value
}
strongSelf.updatePresentationThemeSettings(strongSelf.presentationThemeSettings)
}
let _ = (chatServiceBackgroundColor(wallpaper: self.presentationData.chatWallpaper, mediaBox: context.account.postbox.mediaBox)
|> deliverOnMainQueue).start(next: { [weak self] serviceColor in
self?.pageControlBackgroundNode.backgroundColor = serviceColor
})
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.isPagingEnabled = true
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.pageControlNode.setPage(0.0)
}
func updateFontSize() {
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let bounds = scrollView.bounds
if !bounds.width.isZero {
self.pageControlNode.setPage(scrollView.contentOffset.x / bounds.width)
let pageIndex = Int(round(scrollView.contentOffset.x / bounds.width))
let customMode: TextSelectionCustomMode = pageIndex >= 1 ? .list : .chat
if customMode != self.toolbarNode.customMode {
self.toolbarNode.setCustomMode(customMode)
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
self.recursivelyEnsureDisplaySynchronously(true)
}
}
}
}
func animateIn(completion: (() -> Void)? = nil) {
if let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass {
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
}
func animateOut(completion: (() -> Void)? = nil) {
if let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass {
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
completion?()
})
} else {
completion?()
}
}
private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) {
var items: [ChatListItem] = []
let interaction = ChatListNodeInteraction(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in
}, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {
}, openBirthdaySetup: {
}, performActiveSessionAction: { _, _ in
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
}, openStories: { _, _ in
}, openStarsTopup: { _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
}, openPhotoSetup: {
}, openAdInfo: { _, _ in
}, openAccountFreezeInfo: {
}, openUrl: { _ in
})
let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)
func makeChatListItem(
peer: EnginePeer,
author: EnginePeer,
timestamp: Int32,
text: String,
isPinned: Bool = false,
presenceTimestamp: Int32? = nil,
hasInputActivity: Bool = false,
unreadCount: Int32 = 0
) -> ChatListItem {
return ChatListItem(
presentationData: chatListPresentationData,
context: self.context,
chatListLocation: .chatList(groupId: .root),
filterData: nil,
index: .chatList(ChatListIndex(pinningIndex: isPinned ? 0 : nil, messageIndex: MessageIndex(id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 0), timestamp: timestamp))),
content: .peer(ChatListItemContent.PeerData(
messages: [
EngineMessage(
stableId: 0,
stableVersion: 0,
id: EngineMessage.Id(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 0),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: timestamp,
flags: author.id == peer.id ? [.Incoming] : [],
tags: [],
globalTags: [],
localTags: [],
customTags: [],
forwardInfo: nil,
author: author,
text: text,
attributes: [],
media: [],
peers: [:],
associatedMessages: [:],
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
],
peer: EngineRenderedPeer(peer: peer),
threadInfo: nil,
combinedReadState: EnginePeerReadCounters(incomingReadId: 1000, outgoingReadId: 1000, count: unreadCount, markedUnread: false),
isRemovedFromTotalUnreadCount: false,
presence: presenceTimestamp.flatMap { presenceTimestamp in
EnginePeer.Presence(status: .present(until: presenceTimestamp + 1000), lastActivity: presenceTimestamp)
},
hasUnseenMentions: false,
hasUnseenReactions: false,
draftState: nil,
mediaDraftContentType: nil,
inputActivities: hasInputActivity ? [(author, .typingText)] : [],
promoInfo: nil,
ignoreUnreadBadge: false,
displayAsMessage: false,
hasFailedMessages: false,
forumTopicData: nil,
topForumTopicItems: [],
autoremoveTimeout: nil,
storyState: nil,
requiresPremiumForMessaging: false,
displayAsTopicList: false,
tags: []
)),
editing: false,
hasActiveRevealControls: false,
selected: false,
header: nil,
enabledContextActions: nil,
hiddenOffset: false,
interaction: interaction
)
}
let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil, verificationIconFileId: nil, sendPaidMessageStars: nil, linkedMonoforumId: nil))
let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil, verificationIconFileId: nil, sendPaidMessageStars: nil, linkedMonoforumId: nil))
let peer6: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let timestamp = self.referenceTimestamp
let timestamp1 = timestamp + 120
items.append(makeChatListItem(
peer: peer1,
author: selfPeer,
timestamp: timestamp1,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Text,
isPinned: true
))
let presenceTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + 60 * 60)
let timestamp2 = timestamp + 3660
items.append(makeChatListItem(
peer: peer2,
author: peer2,
timestamp: timestamp2,
text: "",
presenceTimestamp: presenceTimestamp,
hasInputActivity: true
))
let timestamp3 = timestamp + 3200
items.append(makeChatListItem(
peer: peer3,
author: peer3Author,
timestamp: timestamp3,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Text
))
let timestamp4 = timestamp + 3000
items.append(makeChatListItem(
peer: peer4,
author: peer4,
timestamp: timestamp4,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Text
))
let timestamp5 = timestamp + 1000
items.append(makeChatListItem(
peer: peer5,
author: peer5,
timestamp: timestamp5,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Text
))
items.append(makeChatListItem(
peer: peer6,
author: peer6,
timestamp: timestamp - 360,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Text,
unreadCount: 1
))
let width: CGFloat
if case .regular = layout.metrics.widthClass {
width = layout.size.width / 2.0
} else {
width = layout.size.width
}
let params = ListViewItemLayoutParams(width: width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height)
if let chatNodes = self.chatNodes {
for i in 0 ..< items.count {
let itemNode = chatNodes[i]
items[i].updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
apply(ListViewItemApply(isOnScreen: true))
})
}
} else {
var chatNodes: [ListViewItemNode] = []
for i in 0 ..< items.count {
var itemNode: ListViewItemNode?
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
itemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode!.isUserInteractionEnabled = false
chatNodes.append(itemNode!)
if self.maskNode.supernode != nil {
self.chatListBackgroundNode.insertSubnode(itemNode!, belowSubnode: self.maskNode)
} else {
self.chatListBackgroundNode.addSubnode(itemNode!)
}
}
self.chatNodes = chatNodes
}
if let chatNodes = self.chatNodes {
var topOffset: CGFloat = topInset
for itemNode in chatNodes {
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: itemNode.frame.size))
topOffset += itemNode.frame.height
}
}
}
private func updateMessagesLayout(layout: ContainerViewLayout, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
let headerItem = self.context.sharedContext.makeChatMessageDateHeaderItem(context: self.context, timestamp: self.referenceTimestamp, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder)
var items: [ListViewItem] = []
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1))
let otherPeerId = self.context.account.peerId
var peers = SimpleDictionary<PeerId, Peer>()
var messages = SimpleDictionary<MessageId, Message>()
peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .preset(.blue), backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .preset(.blue), backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3)
messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA="
let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)]
let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes, alternativeRepresentations: [])
let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
let message4 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
let width: CGFloat
if case .regular = layout.metrics.widthClass {
width = layout.size.width / 2.0
} else {
width = layout.size.width
}
let params = ListViewItemLayoutParams(width: width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height)
if let messageNodes = self.messageNodes {
for i in 0 ..< items.count {
let itemNode = messageNodes[i]
items[i].updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
apply(ListViewItemApply(isOnScreen: true))
})
}
} else {
var messageNodes: [ListViewItemNode] = []
for i in 0 ..< items.count {
var itemNode: ListViewItemNode?
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
itemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
itemNode!.isUserInteractionEnabled = false
messageNodes.append(itemNode!)
self.messagesContainerNode.addSubnode(itemNode!)
}
self.messageNodes = messageNodes
}
var bottomOffset: CGFloat = 9.0 + bottomInset
if let messageNodes = self.messageNodes {
for itemNode in messageNodes {
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: itemNode.frame.size))
bottomOffset += itemNode.frame.height
itemNode.updateFrame(itemNode.frame, within: layout.size)
}
}
let dateHeaderNode: ListViewItemHeaderNode
if let currentDateHeaderNode = self.dateHeaderNode {
dateHeaderNode = currentDateHeaderNode
headerItem.updateNode(dateHeaderNode, previous: nil, next: headerItem)
} else {
dateHeaderNode = headerItem.node(synchronousLoad: true)
dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
self.messagesContainerNode.addSubnode(dateHeaderNode)
self.dateHeaderNode = dateHeaderNode
}
transition.updateFrame(node: dateHeaderNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: CGSize(width: layout.size.width, height: headerItem.height)))
dateHeaderNode.updateLayout(size: self.messagesContainerNode.frame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: .immediate)
}
func updatePresentationThemeSettings(_ presentationThemeSettings: PresentationThemeSettings) {
let fontSize: PresentationFontSize
let listsFontSize: PresentationFontSize
if presentationThemeSettings.useSystemFont {
let pointSize = UIFont.preferredFont(forTextStyle: .body).pointSize
fontSize = PresentationFontSize(systemFontSize: pointSize)
listsFontSize = fontSize
} else {
fontSize = presentationThemeSettings.fontSize
listsFontSize = presentationThemeSettings.listsFontSize
}
self.presentationData = self.presentationData.withFontSizes(chatFontSize: fontSize, listsFontSize: listsFontSize)
self.toolbarNode.updatePresentationData(presentationData: self.presentationData)
self.toolbarNode.updatePresentationThemeSettings(presentationThemeSettings: self.presentationThemeSettings)
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
self.recursivelyEnsureDisplaySynchronously(true)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
let bounds = CGRect(origin: CGPoint(), size: layout.size)
self.scrollNode.frame = bounds
let toolbarHeight = self.toolbarNode.updateLayout(width: layout.size.width, bottomInset: layout.intrinsicInsets.bottom, layout: layout, transition: transition)
self.chatListBackgroundNode.frame = CGRect(x: bounds.width, y: 0.0, width: bounds.width, height: bounds.height)
var chatFrame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)
let bottomInset: CGFloat
if case .regular = layout.metrics.widthClass {
self.chatListBackgroundNode.frame = CGRect(x: 0.0, y: 0.0, width: bounds.width / 2.0, height: bounds.height)
chatFrame = CGRect(x: bounds.width / 2.0, y: 0.0, width: bounds.width / 2.0, height: bounds.height)
self.scrollNode.view.contentSize = CGSize(width: bounds.width, height: bounds.height)
self.pageControlNode.isHidden = true
self.pageControlBackgroundNode.isHidden = true
self.separatorNode.isHidden = false
self.separatorNode.frame = CGRect(x: bounds.width / 2.0, y: 0.0, width: UIScreenPixel, height: bounds.height - toolbarHeight)
bottomInset = 0.0
} else {
self.chatListBackgroundNode.frame = CGRect(x: bounds.width, y: 0.0, width: bounds.width, height: bounds.height)
chatFrame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)
self.scrollNode.view.contentSize = CGSize(width: bounds.width * 2.0, height: bounds.height)
self.pageControlNode.isHidden = false
self.pageControlBackgroundNode.isHidden = false
self.separatorNode.isHidden = true
bottomInset = 37.0
}
self.chatBackgroundNode.frame = chatFrame
self.chatBackgroundNode.updateLayout(size: chatFrame.size, displayMode: .aspectFill, transition: transition)
self.messagesContainerNode.frame = chatFrame
transition.updateFrame(node: self.toolbarNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: toolbarHeight + layout.intrinsicInsets.bottom)))
self.updateChatsLayout(layout: layout, topInset: navigationBarHeight, transition: transition)
self.updateMessagesLayout(layout: layout, bottomInset: toolbarHeight + bottomInset, transition: transition)
let pageControlSize = self.pageControlNode.measure(CGSize(width: bounds.width, height: 100.0))
let pageControlFrame = CGRect(origin: CGPoint(x: floor((bounds.width - pageControlSize.width) / 2.0), y: layout.size.height - toolbarHeight - 28.0), size: pageControlSize)
self.pageControlNode.frame = pageControlFrame
self.pageControlBackgroundNode.frame = CGRect(x: pageControlFrame.minX - 7.0, y: pageControlFrame.minY - 7.0, width: pageControlFrame.width + 14.0, height: 21.0)
transition.updateFrame(node: self.maskNode, frame: CGRect(x: 0.0, y: layout.size.height - toolbarHeight - 80.0, width: bounds.width, height: 80.0))
}
}
final class TextSizeSelectionController: ViewController {
private let context: AccountContext
private var controllerNode: TextSizeSelectionControllerNode {
return self.displayNode as! TextSizeSelectionControllerNode
}
private var didPlayPresentationAnimation = false
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var presentationThemeSettings: PresentationThemeSettings
private var presentationThemeSettingsDisposable: Disposable?
private var disposable: Disposable?
private var applyDisposable = MetaDisposable()
public init(context: AccountContext, presentationThemeSettings: PresentationThemeSettings) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationThemeSettings = presentationThemeSettings
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationTheme: self.presentationData.theme, presentationStrings: self.presentationData.strings))
self.blocksBackgroundWhenInOverlay = true
self.acceptsFocusWhenInOverlay = true
self.navigationPresentation = .modal
self.navigationItem.title = self.presentationData.strings.Appearance_TextSize_Title
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView())
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.presentationData = presentationData
}
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
self.presentationThemeSettingsDisposable?.dispose()
self.disposable?.dispose()
self.applyDisposable.dispose()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation {
self.didPlayPresentationAnimation = true
if case .modalSheet = presentationArguments.presentationAnimation {
self.controllerNode.animateIn()
}
}
}
override public func loadDisplayNode() {
super.loadDisplayNode()
self.displayNode = TextSizeSelectionControllerNode(context: self.context, presentationThemeSettings: self.presentationThemeSettings, dismiss: { [weak self] in
if let strongSelf = self {
strongSelf.dismiss()
}
}, apply: { [weak self] useSystemFont, fontSize, listsFontSize in
if let strongSelf = self {
strongSelf.apply(useSystemFont: useSystemFont, fontSize: fontSize, listsFontSize: listsFontSize)
}
})
self.displayNodeDidLoad()
}
private func apply(useSystemFont: Bool, fontSize: PresentationFontSize, listsFontSize: PresentationFontSize) {
let _ = (updatePresentationThemeSettingsInteractively(accountManager: self.context.sharedContext.accountManager, { current in
var current = current
current.useSystemFont = useSystemFont
current.fontSize = fontSize
current.listsFontSize = listsFontSize
return current
})
|> deliverOnMainQueue).start(completed: { [weak self] in
self?.dismiss()
})
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}
private enum TextSelectionCustomMode {
case list
case chat
}
private final class TextSelectionToolbarNode: ASDisplayNode {
private var presentationThemeSettings: PresentationThemeSettings
private var presentationData: PresentationData
private let cancelButton = HighlightableButtonNode()
private let doneButton = HighlightableButtonNode()
private let separatorNode = ASDisplayNode()
private let topSeparatorNode = ASDisplayNode()
private var switchItemNode: ItemListSwitchItemNode
private var fontSizeItemNode: ThemeSettingsFontSizeItemNode
private(set) var customMode: TextSelectionCustomMode = .chat
var cancel: (() -> Void)?
var done: (() -> Void)?
var updateUseSystemFont: ((Bool) -> Void)?
var updateCustomFontSize: ((PresentationFontSize) -> Void)?
init(presentationThemeSettings: PresentationThemeSettings, presentationData: PresentationData) {
self.presentationThemeSettings = presentationThemeSettings
self.presentationData = presentationData
self.switchItemNode = ItemListSwitchItemNode(type: .regular)
self.fontSizeItemNode = ThemeSettingsFontSizeItemNode()
super.init()
self.cancelButton.accessibilityTraits = [.button]
self.doneButton.accessibilityTraits = [.button]
self.addSubnode(self.switchItemNode)
self.addSubnode(self.fontSizeItemNode)
self.addSubnode(self.cancelButton)
self.addSubnode(self.doneButton)
self.addSubnode(self.separatorNode)
self.addSubnode(self.topSeparatorNode)
self.updatePresentationData(presentationData: self.presentationData)
self.cancelButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.cancelButton.backgroundColor = strongSelf.presentationData.theme.list.itemHighlightedBackgroundColor
} else {
UIView.animate(withDuration: 0.3, animations: {
strongSelf.cancelButton.backgroundColor = .clear
})
}
}
}
self.doneButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.doneButton.backgroundColor = strongSelf.presentationData.theme.list.itemHighlightedBackgroundColor
} else {
UIView.animate(withDuration: 0.3, animations: {
strongSelf.doneButton.backgroundColor = .clear
})
}
}
}
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
self.doneButton.addTarget(self, action: #selector(self.donePressed), forControlEvents: .touchUpInside)
}
func setDoneEnabled(_ enabled: Bool) {
self.doneButton.alpha = enabled ? 1.0 : 0.4
self.doneButton.isUserInteractionEnabled = enabled
}
func setCustomMode(_ customMode: TextSelectionCustomMode) {
self.customMode = customMode
}
func updatePresentationData(presentationData: PresentationData) {
self.backgroundColor = presentationData.theme.rootController.tabBar.backgroundColor
self.separatorNode.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor
self.topSeparatorNode.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor
self.cancelButton.setTitle(presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: presentationData.theme.list.itemPrimaryTextColor, for: [])
self.doneButton.setTitle(presentationData.strings.Wallpaper_Set, with: Font.regular(17.0), with: presentationData.theme.list.itemPrimaryTextColor, for: [])
self.cancelButton.accessibilityLabel = presentationData.strings.Common_Cancel
self.doneButton.accessibilityLabel = presentationData.strings.Wallpaper_Set
}
func updatePresentationThemeSettings(presentationThemeSettings: PresentationThemeSettings) {
self.presentationThemeSettings = presentationThemeSettings
}
func updateLayout(width: CGFloat, bottomInset: CGFloat, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat {
var contentHeight: CGFloat = 0.0
let switchItem = ItemListSwitchItem(presentationData: ItemListPresentationData(self.presentationData), title: self.presentationData.strings.Appearance_TextSize_UseSystem, value: self.presentationThemeSettings.useSystemFont, disableLeadingInset: true, sectionId: 0, style: .blocks, updated: { [weak self] value in
self?.updateUseSystemFont?(value)
})
let fontSizeItem = ThemeSettingsFontSizeItem(theme: self.presentationData.theme, fontSize: self.customMode == .chat ? self.presentationThemeSettings.fontSize : self.presentationThemeSettings.listsFontSize, enabled: !self.presentationThemeSettings.useSystemFont, disableLeadingInset: true, disableDecorations: true, force: true, sectionId: 0, updated: { [weak self] value in
self?.updateCustomFontSize?(value)
})
switchItem.updateNode(async: { f in
f()
}, node: {
return self.switchItemNode
}, params: ListViewItemLayoutParams(width: width, leftInset: layout.intrinsicInsets.left, rightInset: layout.intrinsicInsets.right, availableHeight: 1000.0), previousItem: nil, nextItem: fontSizeItem, animation: .None, completion: { layout, apply in
self.switchItemNode.contentSize = layout.contentSize
self.switchItemNode.insets = layout.insets
transition.updateFrame(node: self.switchItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: layout.contentSize))
contentHeight += layout.contentSize.height
apply(ListViewItemApply(isOnScreen: true))
})
fontSizeItem.updateNode(async: { f in
f()
}, node: {
return self.fontSizeItemNode
}, params: ListViewItemLayoutParams(width: width, leftInset: layout.intrinsicInsets.left, rightInset: layout.intrinsicInsets.right, availableHeight: 1000.0), previousItem: switchItem, nextItem: nil, animation: .None, completion: { layout, apply in
self.fontSizeItemNode.contentSize = layout.contentSize
self.fontSizeItemNode.insets = layout.insets
transition.updateFrame(node: self.fontSizeItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: layout.contentSize))
contentHeight += layout.contentSize.height
apply(ListViewItemApply(isOnScreen: true))
})
self.cancelButton.frame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: floor(width / 2.0), height: 49.0))
self.doneButton.frame = CGRect(origin: CGPoint(x: floor(width / 2.0), y: contentHeight), size: CGSize(width: width - floor(width / 2.0), height: 49.0))
contentHeight += 49.0
self.topSeparatorNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel))
let resultHeight = contentHeight + bottomInset
self.separatorNode.frame = CGRect(origin: CGPoint(x: floor(width / 2.0), y: self.cancelButton.frame.minY), size: CGSize(width: UIScreenPixel, height: resultHeight - self.cancelButton.frame.minY))
return resultHeight
}
@objc func cancelPressed() {
self.cancel?()
}
@objc func donePressed() {
self.doneButton.isUserInteractionEnabled = false
self.done?()
}
}
@@ -0,0 +1,304 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import LegacyComponents
import ItemListUI
import PresentationDataUtils
import AppBundle
class BubbleSettingsRadiusItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let value: Int
let disableLeadingInset: Bool
let displayIcons: Bool
let disableDecorations: Bool
let force: Bool
let enabled: Bool
let sectionId: ItemListSectionId
let updated: (Int) -> Void
let tag: ItemListItemTag?
init(theme: PresentationTheme, value: Int, enabled: Bool = true, disableLeadingInset: Bool = false, displayIcons: Bool = true, disableDecorations: Bool = false, force: Bool = false, sectionId: ItemListSectionId, updated: @escaping (Int) -> Void, tag: ItemListItemTag? = nil) {
self.theme = theme
self.value = value
self.enabled = enabled
self.disableLeadingInset = disableLeadingInset
self.displayIcons = displayIcons
self.disableDecorations = disableDecorations
self.force = force
self.sectionId = sectionId
self.updated = updated
self.tag = tag
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = BubbleSettingsRadiusItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? BubbleSettingsRadiusItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
class BubbleSettingsRadiusItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private var sliderView: TGPhotoEditorSliderView?
private let leftIconNode: ASImageNode
private let rightIconNode: ASImageNode
private let disabledOverlayNode: ASDisplayNode
private var item: BubbleSettingsRadiusItem?
private var layoutParams: ListViewItemLayoutParams?
var tag: ItemListItemTag? {
return self.item?.tag
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.leftIconNode = ASImageNode()
self.leftIconNode.displaysAsynchronously = false
self.leftIconNode.displayWithoutProcessing = true
self.rightIconNode = ASImageNode()
self.rightIconNode.displaysAsynchronously = false
self.rightIconNode.displayWithoutProcessing = true
self.disabledOverlayNode = ASDisplayNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.leftIconNode)
self.addSubnode(self.rightIconNode)
self.addSubnode(self.disabledOverlayNode)
}
override func didLoad() {
super.didLoad()
self.accessibilityTraits = [.adjustable]
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 2.0
sliderView.lineSize = 4.0
sliderView.dotSize = 8.0
sliderView.minimumValue = 0.0
sliderView.maximumValue = 4.0
sliderView.startValue = 0.0
sliderView.positionsCount = 5
sliderView.useLinesForPositions = true
sliderView.disablesInteractiveTransitionGestureRecognizer = true
if let item = self.item, let params = self.layoutParams {
sliderView.isUserInteractionEnabled = item.enabled
sliderView.value = CGFloat((item.value - 8) / 2)
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.enabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
let sliderInset: CGFloat = item.displayIcons ? 38.0 : 16.0
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + sliderInset, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - sliderInset * 2.0, height: 44.0))
}
self.view.insertSubview(sliderView, belowSubview: self.disabledOverlayNode.view)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
}
func asyncLayout() -> (_ item: BubbleSettingsRadiusItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { item, params, neighbors in
var updatedLeftIcon: UIImage?
var updatedRightIcon: UIImage?
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
updatedLeftIcon = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsFontMinIcon"), color: item.theme.list.itemPrimaryTextColor)
updatedRightIcon = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsFontMaxIcon"), color: item.theme.list.itemPrimaryTextColor)
}
let contentSize: CGSize
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
contentSize = CGSize(width: params.width, height: 60.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
if item.disableLeadingInset {
insets.top = 0.0
insets.bottom = 0.0
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
let firstTime = strongSelf.item == nil || item.force
strongSelf.item = item
strongSelf.layoutParams = params
if item.enabled {
strongSelf.accessibilityTraits.remove(.notEnabled)
} else {
strongSelf.accessibilityTraits.insert(.notEnabled)
}
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.disabledOverlayNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4)
strongSelf.disabledOverlayNode.isHidden = item.enabled
strongSelf.disabledOverlayNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: 44.0))
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params) && !item.disableDecorations
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
if let updatedLeftIcon = updatedLeftIcon {
strongSelf.leftIconNode.image = updatedLeftIcon
}
if let image = strongSelf.leftIconNode.image {
strongSelf.leftIconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 25.0), size: CGSize(width: image.size.width, height: image.size.height))
}
if let updatedRightIcon = updatedRightIcon {
strongSelf.rightIconNode.image = updatedRightIcon
}
if let image = strongSelf.rightIconNode.image {
strongSelf.rightIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 14.0 - image.size.width, y: 21.0), size: CGSize(width: image.size.width, height: image.size.height))
}
strongSelf.leftIconNode.isHidden = !item.displayIcons
strongSelf.rightIconNode.isHidden = !item.displayIcons
if let sliderView = strongSelf.sliderView {
sliderView.isUserInteractionEnabled = item.enabled
sliderView.trackColor = item.enabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
}
let value: CGFloat = CGFloat((item.value - 8) / 2)
if firstTime {
sliderView.value = value
}
let sliderInset: CGFloat = item.displayIcons ? 38.0 : 16.0
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + sliderInset, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - sliderInset * 2.0, height: 44.0))
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func sliderValueChanged() {
guard let sliderView = self.sliderView else {
return
}
let value = Int(sliderView.value) * 2 + 8
self.item?.updated(value)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,594 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import MergeLists
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import WallpaperResources
import AccountContext
import AppBundle
import ContextUI
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import ShimmerEffect
import StickerResources
import ThemeCarouselItem
private var cachedBorderImages: [String: UIImage] = [:]
private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selected: Bool) -> UIImage? {
let key = "\(theme.list.itemBlocksBackgroundColor.hexString)_\(selected ? "s" + theme.list.itemAccentColor.hexString : theme.list.disclosureArrowColor.hexString)"
if let image = cachedBorderImages[key] {
return image
} else {
let image = generateImage(CGSize(width: 18.0, height: 18.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let lineWidth: CGFloat
if selected {
lineWidth = 2.0
context.setLineWidth(lineWidth)
context.setStrokeColor(theme.list.itemBlocksBackgroundColor.cgColor)
context.strokeEllipse(in: bounds.insetBy(dx: 3.0 + lineWidth / 2.0, dy: 3.0 + lineWidth / 2.0))
var accentColor = theme.list.itemAccentColor
if accentColor.rgb == 0xffffff {
accentColor = UIColor(rgb: 0x999999)
}
context.setStrokeColor(accentColor.cgColor)
} else {
context.setStrokeColor(theme.list.disclosureArrowColor.withAlphaComponent(0.4).cgColor)
lineWidth = 1.0
}
if bordered || selected {
context.setLineWidth(lineWidth)
context.strokeEllipse(in: bounds.insetBy(dx: 1.0 + lineWidth / 2.0, dy: 1.0 + lineWidth / 2.0))
}
})?.stretchableImage(withLeftCapWidth: 9, topCapHeight: 9)
cachedBorderImages[key] = image
return image
}
}
private final class ThemeGridThemeItemIconNode : ASDisplayNode {
private let containerNode: ASDisplayNode
private let emojiContainerNode: ASDisplayNode
private let imageNode: TransformImageNode
private let overlayNode: ASImageNode
private let textNode: TextNode
private let emojiNode: TextNode
private let emojiImageNode: TransformImageNode
private var animatedStickerNode: AnimatedStickerNode?
private var placeholderNode: StickerShimmerEffectNode
var snapshotView: UIView?
private let stickerFetchedDisposable = MetaDisposable()
private var item: ThemeCarouselThemeIconItem?
private var size: CGSize?
override init() {
self.containerNode = ASDisplayNode()
self.emojiContainerNode = ASDisplayNode()
self.imageNode = TransformImageNode()
self.imageNode.isLayerBacked = true
self.imageNode.cornerRadius = 8.0
self.imageNode.clipsToBounds = true
self.overlayNode = ASImageNode()
self.overlayNode.isLayerBacked = true
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.emojiNode = TextNode()
self.emojiNode.isUserInteractionEnabled = false
self.emojiNode.displaysAsynchronously = false
self.emojiImageNode = TransformImageNode()
self.placeholderNode = StickerShimmerEffectNode()
super.init()
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.imageNode)
self.containerNode.addSubnode(self.overlayNode)
self.containerNode.addSubnode(self.textNode)
self.addSubnode(self.emojiContainerNode)
self.emojiContainerNode.addSubnode(self.emojiNode)
self.emojiContainerNode.addSubnode(self.emojiImageNode)
self.emojiContainerNode.addSubnode(self.placeholderNode)
var firstTime = true
self.emojiImageNode.imageUpdated = { [weak self] image in
guard let strongSelf = self else {
return
}
if image != nil {
strongSelf.removePlaceholder(animated: !firstTime)
if firstTime {
strongSelf.emojiImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
firstTime = false
}
}
deinit {
self.stickerFetchedDisposable.dispose()
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap)))
}
@objc private func tap() {
guard let item = self.item else {
return
}
item.action(item.themeReference)
}
private func removePlaceholder(animated: Bool) {
if !animated {
self.placeholderNode.removeFromSupernode()
} else {
self.placeholderNode.alpha = 0.0
self.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
self?.placeholderNode.removeFromSupernode()
})
}
}
// override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
// let emojiFrame = CGRect(origin: CGPoint(x: 33.0, y: 79.0), size: CGSize(width: 24.0, height: 24.0))
// self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + emojiFrame.minX, y: rect.minY + emojiFrame.minY), size: emojiFrame.size), within: containerSize)
// }
func setup(item: ThemeCarouselThemeIconItem, size: CGSize) {
let currentItem = self.item
let currentSize = self.size
self.item = item
self.size = size
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let makeEmojiLayout = TextNode.asyncLayout(self.emojiNode)
let makeImageLayout = self.imageNode.asyncLayout()
var updatedThemeReference = false
var updatedTheme = false
var updatedNightMode = false
var updatedWallpaper = false
var updatedSelected = false
let updatedSize = currentSize != size
if currentItem?.themeReference != item.themeReference {
updatedThemeReference = true
}
if currentItem?.nightMode != item.nightMode {
updatedNightMode = true
}
if currentItem?.wallpaper != item.wallpaper {
updatedWallpaper = true
}
if currentItem?.theme !== item.theme {
updatedTheme = true
}
if currentItem?.selected != item.selected {
updatedSelected = true
}
let string: String?
if let themeReference = item.themeReference {
if let _ = themeReference.emoticon {
string = nil
} else {
string = themeDisplayName(strings: item.strings, reference: themeReference)
}
} else {
string = nil
}
let text = NSAttributedString(string: string ?? item.strings.Conversation_Theme_NoTheme, font: Font.bold(14.0), textColor: .white)
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: 70.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let title = NSAttributedString(string: "", font: Font.regular(22.0), textColor: .black)
let (_, emojiApply) = makeEmojiLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: size.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
if updatedThemeReference || updatedWallpaper || updatedNightMode || updatedSize {
if var themeReference = item.themeReference {
if case .builtin = themeReference, item.nightMode {
themeReference = .builtin(.night)
}
let color = item.themeSpecificAccentColors[themeReference.index]
let wallpaper = item.themeSpecificChatWallpapers[themeReference.index]
self.imageNode.setSignal(themeIconImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, theme: themeReference, color: color, wallpaper: wallpaper ?? item.wallpaper, nightMode: item.nightMode, emoticon: true, large: true))
self.imageNode.backgroundColor = nil
}
}
if updatedTheme || updatedSelected {
self.overlayNode.image = generateBorderImage(theme: item.theme, bordered: false, selected: item.selected)
}
if !item.selected && currentItem?.selected == true, let animatedStickerNode = self.animatedStickerNode {
animatedStickerNode.transform = CATransform3DIdentity
let initialScale: CGFloat = CGFloat((animatedStickerNode.value(forKeyPath: "layer.presentationLayer.transform.scale.x") as? NSNumber)?.floatValue ?? 1.0)
animatedStickerNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45)
}
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((90.0 - textLayout.size.width) / 2.0), y: 83.0), size: textLayout.size)
self.textNode.isHidden = string == nil
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
self.emojiContainerNode.frame = CGRect(origin: CGPoint(), size: size)
let _ = textApply()
let _ = emojiApply()
let imageSize = size
self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: imageSize)
let applyLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear))
applyLayout()
self.overlayNode.frame = self.imageNode.frame.insetBy(dx: -1.0, dy: -1.0)
self.emojiNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 79.0), size: CGSize(width: 90.0, height: 30.0))
let emojiFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - 42.0) / 2.0), y: 98.0), size: CGSize(width: 42.0, height: 42.0))
if let file = item.emojiFile, currentItem?.emojiFile == nil {
let imageApply = self.emojiImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: emojiFrame.size, boundingSize: emojiFrame.size, intrinsicInsets: UIEdgeInsets()))
imageApply()
self.emojiImageNode.setSignal(chatMessageStickerPackThumbnail(postbox: item.context.account.postbox, resource: file.resource, animated: true, nilIfEmpty: true))
self.emojiImageNode.frame = emojiFrame
let animatedStickerNode: AnimatedStickerNode
if let current = self.animatedStickerNode {
animatedStickerNode = current
} else {
animatedStickerNode = DefaultAnimatedStickerNodeImpl()
animatedStickerNode.started = { [weak self] in
self?.emojiImageNode.isHidden = true
}
self.animatedStickerNode = animatedStickerNode
self.emojiContainerNode.insertSubnode(animatedStickerNode, belowSubnode: self.placeholderNode)
let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 128, height: 128, playbackMode: .still(.start), mode: .direct(cachePathPrefix: pathPrefix))
animatedStickerNode.anchorPoint = CGPoint(x: 0.5, y: 1.0)
}
animatedStickerNode.autoplay = true
animatedStickerNode.visibility = true
self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start())
let thumbnailDimensions = PixelDimensions(width: 512, height: 512)
self.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency, imageSize: thumbnailDimensions.cgSize)
self.placeholderNode.frame = emojiFrame
}
if let animatedStickerNode = self.animatedStickerNode {
animatedStickerNode.frame = emojiFrame
animatedStickerNode.updateLayout(size: emojiFrame.size)
}
}
func crossfade() {
// if let snapshotView = self.containerNode.view.snapshotView(afterScreenUpdates: false) {
// snapshotView.transform = self.containerNode.view.transform
// snapshotView.frame = self.containerNode.view.frame
// self.view.insertSubview(snapshotView, aboveSubview: self.containerNode.view)
//
// snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, delay: ChatThemeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
// snapshotView?.removeFromSuperview()
// })
// }
}
}
class ThemeGridThemeItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let themes: [PresentationThemeReference]
let animatedEmojiStickers: [String: [StickerPackItem]]
let themeSpecificAccentColors: [Int64: PresentationThemeAccentColor]
let themeSpecificChatWallpapers: [Int64: TelegramWallpaper]
let nightMode: Bool
let currentTheme: PresentationThemeReference
let updatedTheme: (PresentationThemeReference) -> Void
let contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?
let tag: ItemListItemTag?
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, themes: [PresentationThemeReference], animatedEmojiStickers: [String: [StickerPackItem]], themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], nightMode: Bool, currentTheme: PresentationThemeReference, updatedTheme: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil) {
self.context = context
self.theme = theme
self.strings = strings
self.themes = themes
self.animatedEmojiStickers = animatedEmojiStickers
self.themeSpecificAccentColors = themeSpecificAccentColors
self.themeSpecificChatWallpapers = themeSpecificChatWallpapers
self.nightMode = nightMode
self.currentTheme = currentTheme
self.updatedTheme = updatedTheme
self.contextAction = contextAction
self.tag = tag
self.sectionId = sectionId
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ThemeGridThemeItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ThemeGridThemeItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
class ThemeGridThemeItemNode: ListViewItemNode, ItemListItemNode {
private let containerNode: ASDisplayNode
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private var snapshotView: UIView?
private let scrollNode: ASScrollNode
private var items: [ThemeCarouselThemeIconItem]?
private var itemNodes: [Int64: ThemeGridThemeItemIconNode] = [:]
private var initialized = false
private var item: ThemeGridThemeItem?
private var layoutParams: ListViewItemLayoutParams?
var tag: ItemListItemTag? {
return self.item?.tag
}
init() {
self.containerNode = ASDisplayNode()
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.scrollNode = ASScrollNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.containerNode)
self.addSubnode(self.scrollNode)
}
func asyncLayout() -> (_ item: ThemeGridThemeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, neighbors in
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let minSpacing: CGFloat = 6.0
let referenceImageSize: CGSize
let screenWidth = min(params.width, params.availableHeight)
if screenWidth >= 390.0 {
referenceImageSize = CGSize(width: 110.0, height: 150.0)
} else {
referenceImageSize = CGSize(width: 90.0, height: 150.0)
}
let totalWidth = params.width - params.leftInset - params.rightInset
let imageCount = Int((totalWidth - minSpacing) / (referenceImageSize.width + minSpacing))
var itemSize = referenceImageSize.aspectFilled(CGSize(width: floorToScreenPixels((totalWidth - CGFloat(imageCount + 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height))
itemSize.height = referenceImageSize.height
let itemSpacing = floorToScreenPixels((totalWidth - CGFloat(imageCount) * itemSize.width) / CGFloat(imageCount + 1))
var spacingOffset: CGFloat = 0.0
if totalWidth - CGFloat(imageCount) * itemSize.width - CGFloat(imageCount + 1) * itemSpacing == 1.0 {
spacingOffset = UIScreenPixel
}
let rows = ceil(CGFloat(item.themes.count) / CGFloat(imageCount))
contentSize = CGSize(width: params.width, height: minSpacing + rows * (itemSize.height + itemSpacing))
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.containerNode.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.containerNode.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.containerNode.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.containerNode.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.containerNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: contentSize.height)
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
var validIds: [Int64] = []
var index = 0
for theme in item.themes {
let selected = item.currentTheme.index == theme.index
let iconItem = ThemeCarouselThemeIconItem(context: item.context, emojiFile: theme.emoticon.flatMap { item.animatedEmojiStickers[$0]?.first?.file._parse() }, themeReference: theme, nightMode: item.nightMode, channelMode: false, themeSpecificAccentColors: item.themeSpecificAccentColors, themeSpecificChatWallpapers: item.themeSpecificChatWallpapers, selected: selected, theme: item.theme, strings: item.strings, wallpaper: nil, action: { theme in
if let theme {
item.updatedTheme(theme)
}
}, contextAction: nil)
validIds.append(theme.index)
var itemNode: ThemeGridThemeItemIconNode
if let current = strongSelf.itemNodes[theme.index] {
itemNode = current
itemNode.setup(item: iconItem, size: itemSize)
} else {
let addedItemNode = ThemeGridThemeItemIconNode()
itemNode = addedItemNode
addedItemNode.setup(item: iconItem, size: itemSize)
strongSelf.itemNodes[theme.index] = addedItemNode
strongSelf.addSubnode(addedItemNode)
}
let col = CGFloat(index % imageCount)
let row = floor(CGFloat(index) / CGFloat(imageCount))
let itemFrame = CGRect(origin: CGPoint(x: params.leftInset + spacingOffset + itemSpacing + (itemSize.width + itemSpacing) * col, y: minSpacing + (itemSize.height + itemSpacing) * row), size: itemSize)
itemNode.frame = itemFrame
index += 1
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
func prepareCrossfadeTransition() {
guard self.snapshotView == nil else {
return
}
if let snapshotView = self.containerNode.view.snapshotView(afterScreenUpdates: false) {
self.view.insertSubview(snapshotView, aboveSubview: self.containerNode.view)
self.snapshotView = snapshotView
}
// self.listNode.forEachVisibleItemNode { node in
// if let node = node as? ThemeCarouselThemeItemIconNode {
// node.prepareCrossfadeTransition()
// }
// }
}
func animateCrossfadeTransition() {
// guard self.snapshotView?.layer.animationKeys()?.isEmpty ?? true else {
// return
// }
//
// var views: [UIView] = []
// if let snapshotView = self.snapshotView {
// views.append(snapshotView)
// self.snapshotView = nil
// }
//
// self.listNode.forEachVisibleItemNode { node in
// if let node = node as? ThemeCarouselThemeItemIconNode {
// if let snapshotView = node.snapshotView {
// views.append(snapshotView)
// node.snapshotView = nil
// }
// }
// }
//
// UIView.animate(withDuration: 0.3, animations: {
// for view in views {
// view.alpha = 0.0
// }
// }, completion: { _ in
// for view in views {
// view.removeFromSuperview()
// }
// })
}
}
@@ -0,0 +1,51 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import LegacyComponents
import TelegramUIPreferences
import MediaResources
import AccountContext
import LegacyUI
import LegacyMediaPickerUI
import LegacyComponents
import LocalMediaResources
import ImageBlur
import WallpaperGridScreen
import WallpaperGalleryScreen
func presentCustomWallpaperPicker(context: AccountContext, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = legacyWallpaperPicker(context: context, presentationData: presentationData).start(next: { generator in
let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: presentationData.theme)
legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style
let controller = generator(legacyController.context)
legacyController.bind(controller: controller)
legacyController.deferScreenEdgeGestures = [.top]
controller.selectionBlock = { [weak legacyController] asset, _ in
if let asset = asset {
let controller = WallpaperGalleryController(context: context, source: .asset(asset.backingAsset))
controller.apply = { [weak legacyController, weak controller] wallpaper, mode, editedImage, cropRect, brightness, _ in
if let legacyController = legacyController, let controller = controller {
uploadCustomWallpaper(context: context, wallpaper: wallpaper, mode: mode, editedImage: nil, cropRect: cropRect, brightness: brightness, completion: { [weak legacyController, weak controller] in
if let legacyController = legacyController, let controller = controller {
legacyController.dismiss()
controller.dismiss(forceAway: true)
}
})
}
}
push(controller)
}
}
controller.dismissalBlock = { [weak legacyController] in
if let legacyController = legacyController {
legacyController.dismiss()
}
}
present(legacyController)
})
}
@@ -0,0 +1,762 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AlertUI
import PresentationDataUtils
import LegacyMediaPickerUI
import WallpaperResources
import AccountContext
import MediaResources
import ThemeAccentColorScreen
import GenerateThemeName
private final class EditThemeControllerArguments {
let context: AccountContext
let updateState: ((EditThemeControllerState) -> EditThemeControllerState) -> Void
let openColors: () -> Void
let openFile: () -> Void
let toggleDark: () -> Void
let convertToPresetTheme: () -> Void
init(context: AccountContext, updateState: @escaping ((EditThemeControllerState) -> EditThemeControllerState) -> Void, openColors: @escaping () -> Void, openFile: @escaping () -> Void, toggleDark: @escaping () -> Void, convertToPresetTheme: @escaping () -> Void) {
self.context = context
self.updateState = updateState
self.openColors = openColors
self.openFile = openFile
self.toggleDark = toggleDark
self.convertToPresetTheme = convertToPresetTheme
}
}
private enum EditThemeEntryTag: ItemListItemTag {
case title
case slug
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? EditThemeEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum EditThemeControllerSection: Int32 {
case info
case chatPreview
}
private enum EditThemeControllerEntry: ItemListNodeEntry {
case title(PresentationTheme, PresentationStrings, String, String, Bool)
case slug(PresentationTheme, PresentationStrings, String, String, Bool)
case slugInfo(PresentationTheme, String)
case chatPreviewHeader(PresentationTheme, String)
case chatPreview(PresentationTheme, PresentationTheme, TelegramWallpaper, PresentationFontSize, PresentationChatBubbleCorners, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, [ChatPreviewMessageItem])
case changeColors(PresentationTheme, String)
case toggleDark(PresentationTheme, String)
case convertToPresetTheme(PresentationTheme, String)
case uploadTheme(PresentationTheme, String)
case uploadInfo(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .title, .slug, .slugInfo:
return EditThemeControllerSection.info.rawValue
case .chatPreviewHeader, .chatPreview, .changeColors, .toggleDark, .convertToPresetTheme, .uploadTheme, .uploadInfo:
return EditThemeControllerSection.chatPreview.rawValue
}
}
var stableId: Int32 {
switch self {
case .title:
return 0
case .slug:
return 1
case .slugInfo:
return 2
case .chatPreviewHeader:
return 3
case .chatPreview:
return 4
case .changeColors:
return 5
case .toggleDark:
return 6
case .convertToPresetTheme:
return 7
case .uploadTheme:
return 8
case .uploadInfo:
return 9
}
}
static func ==(lhs: EditThemeControllerEntry, rhs: EditThemeControllerEntry) -> Bool {
switch lhs {
case let .title(lhsTheme, lhsStrings, lhsTitle, lhsValue, lhsDone):
if case let .title(rhsTheme, rhsStrings, rhsTitle, rhsValue, rhsDone) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsTitle == rhsTitle, lhsValue == rhsValue, lhsDone == rhsDone {
return true
} else {
return false
}
case let .slug(lhsTheme, lhsStrings, lhsTitle, lhsValue, lhsEnabled):
if case let .slug(rhsTheme, rhsStrings, rhsTitle, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsTitle == rhsTitle, lhsValue == rhsValue, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .slugInfo(lhsTheme, lhsText):
if case let .slugInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .chatPreviewHeader(lhsTheme, lhsText):
if case let .chatPreviewHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .chatPreview(lhsTheme, lhsComponentTheme, lhsWallpaper, lhsFontSize, lhsChatBubbleCorners, lhsStrings, lhsTimeFormat, lhsNameOrder, lhsItems):
if case let .chatPreview(rhsTheme, rhsComponentTheme, rhsWallpaper, rhsFontSize, rhsChatBubbleCorners, rhsStrings, rhsTimeFormat, rhsNameOrder, rhsItems) = rhs, lhsComponentTheme === rhsComponentTheme, lhsTheme === rhsTheme, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsChatBubbleCorners == rhsChatBubbleCorners, lhsStrings === rhsStrings, lhsTimeFormat == rhsTimeFormat, lhsNameOrder == rhsNameOrder, lhsItems == rhsItems {
return true
} else {
return false
}
case let .changeColors(lhsTheme, lhsText):
if case let .changeColors(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .toggleDark(lhsTheme, lhsText):
if case let .toggleDark(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .convertToPresetTheme(lhsTheme, lhsText):
if case let .convertToPresetTheme(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .uploadTheme(lhsTheme, lhsText):
if case let .uploadTheme(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .uploadInfo(lhsTheme, lhsText):
if case let .uploadInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: EditThemeControllerEntry, rhs: EditThemeControllerEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! EditThemeControllerArguments
switch self {
case let .title(_, _, title, text, _):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(), text: text, placeholder: title, type: .regular(capitalization: true, autocorrection: false), returnKeyType: .default, clearType: .onFocus, tag: EditThemeEntryTag.title, sectionId: self.section, textUpdated: { value in
arguments.updateState { current in
var state = current
state.title = value
return state
}
}, action: {
})
case let .slug(_, _, title, text, enabled):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: "t.me/addtheme/", textColor: presentationData.theme.list.itemPrimaryTextColor), text: text, placeholder: title, type: .username, clearType: .onFocus, enabled: enabled, tag: EditThemeEntryTag.slug, sectionId: self.section, textUpdated: { value in
arguments.updateState { current in
var state = current
state.slug = value
return state
}
}, action: {
})
case let .slugInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .chatPreviewHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .chatPreview(theme, componentTheme, wallpaper, fontSize, chatBubbleCorners, strings, dateTimeFormat, nameDisplayOrder, items):
return ThemeSettingsChatPreviewItem(context: arguments.context, systemStyle: .glass, theme: theme, componentTheme: componentTheme, strings: strings, sectionId: self.section, fontSize: fontSize, chatBubbleCorners: chatBubbleCorners, wallpaper: wallpaper, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, messageItems: items)
case let .changeColors(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openColors()
})
case let .toggleDark(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.toggleDark()
})
case let .convertToPresetTheme(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.convertToPresetTheme()
})
case let .uploadTheme(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openFile()
})
case let .uploadInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
}
}
}
public enum EditThemeControllerMode: Equatable {
case create(PresentationTheme?, TelegramThemeSettings?)
case edit(PresentationCloudTheme)
}
private struct EditThemeControllerState: Equatable {
var mode: EditThemeControllerMode
var title: String
var slug: String
var updatedTheme: PresentationTheme?
var updating: Bool
var converting: Bool
}
private func editThemeControllerEntries(presentationData: PresentationData, state: EditThemeControllerState, previewTheme: PresentationTheme, hasSettings: Bool) -> [EditThemeControllerEntry] {
var entries: [EditThemeControllerEntry] = []
var isCreate = false
if case .create = state.mode {
isCreate = true
}
entries.append(.title(presentationData.theme, presentationData.strings, presentationData.strings.EditTheme_Title, state.title, isCreate))
let infoText: String
let uploadText: String
let uploadInfo: String
let previewIncomingReplyName: String
let previewIncomingReplyText: String
let previewIncomingText: String
let previewOutgoingText: String
switch state.mode {
case .create:
infoText = presentationData.strings.EditTheme_Create_TopInfo
uploadText = presentationData.strings.EditTheme_UploadNewTheme
uploadInfo = presentationData.strings.EditTheme_Create_BottomInfo
previewIncomingReplyName = presentationData.strings.EditTheme_Create_Preview_IncomingReplyName
previewIncomingReplyText = presentationData.strings.EditTheme_Create_Preview_IncomingReplyText
previewIncomingText = presentationData.strings.EditTheme_Create_Preview_IncomingText
previewOutgoingText = presentationData.strings.EditTheme_Create_Preview_OutgoingText
case let .edit(theme):
if let _ = theme.theme.file {
infoText = presentationData.strings.EditTheme_Edit_TopInfo
uploadText = presentationData.strings.EditTheme_UploadEditedTheme
uploadInfo = presentationData.strings.EditTheme_Edit_BottomInfo
previewIncomingReplyName = presentationData.strings.EditTheme_Expand_Preview_IncomingReplyName
previewIncomingReplyText = presentationData.strings.EditTheme_Expand_Preview_IncomingReplyText
previewIncomingText = presentationData.strings.EditTheme_Expand_Preview_IncomingText
previewOutgoingText = presentationData.strings.EditTheme_Expand_Preview_OutgoingText
} else {
infoText = presentationData.strings.EditTheme_Expand_TopInfo
uploadText = presentationData.strings.EditTheme_UploadNewTheme
uploadInfo = presentationData.strings.EditTheme_Expand_BottomInfo
previewIncomingReplyName = presentationData.strings.EditTheme_Edit_Preview_IncomingReplyName
previewIncomingReplyText = presentationData.strings.EditTheme_Edit_Preview_IncomingReplyText
previewIncomingText = presentationData.strings.EditTheme_Edit_Preview_IncomingText
previewOutgoingText = presentationData.strings.EditTheme_Edit_Preview_OutgoingText
}
}
if case .edit = state.mode {
entries.append(.slug(presentationData.theme, presentationData.strings, presentationData.strings.EditTheme_ShortLink, state.slug, true))
}
entries.append(.slugInfo(presentationData.theme, infoText))
entries.append(.chatPreviewHeader(presentationData.theme, presentationData.strings.EditTheme_Preview.uppercased()))
entries.append(.chatPreview(presentationData.theme, previewTheme, previewTheme.chat.defaultWallpaper, presentationData.chatFontSize, presentationData.chatBubbleCorners, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, [ChatPreviewMessageItem(outgoing: false, reply: (previewIncomingReplyName, previewIncomingReplyText), text: previewIncomingText, nameColor: .preset(.blue), backgroundEmojiId: nil), ChatPreviewMessageItem(outgoing: true, reply: nil, text: previewOutgoingText, nameColor: .preset(.blue), backgroundEmojiId: nil)]))
entries.append(.changeColors(presentationData.theme, presentationData.strings.EditTheme_ChangeColors))
if hasSettings {
if previewTheme.overallDarkAppearance {
// entries.append(.toggleDark(presentationData.theme, "Toggle Base Theme"))
}
} else {
if !isCreate {
// entries.append(.convertToPresetTheme(presentationData.theme, "Convert to Preset Theme"))
}
entries.append(.uploadTheme(presentationData.theme, uploadText))
entries.append(.uploadInfo(presentationData.theme, uploadInfo))
}
return entries
}
public func editThemeController(context: AccountContext, mode: EditThemeControllerMode, navigateToChat: ((PeerId) -> Void)? = nil, completion: ((PresentationThemeReference) -> Void)? = nil) -> ViewController {
let initialState: EditThemeControllerState
let previewThemePromise = Promise<PresentationTheme>()
let settingsPromise = Promise<TelegramThemeSettings?>(nil)
let hasSettings: Bool
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
switch mode {
case let .create(existingTheme, settings):
let theme: PresentationTheme
let wallpaper: TelegramWallpaper
if let existingTheme = existingTheme {
theme = existingTheme
wallpaper = theme.chat.defaultWallpaper
settingsPromise.set(.single(settings))
hasSettings = settings != nil
} else if let settings = settings {
theme = makePresentationTheme(settings: settings) ?? presentationData.theme
wallpaper = theme.chat.defaultWallpaper
settingsPromise.set(.single(settings))
hasSettings = true
} else {
theme = presentationData.theme
wallpaper = presentationData.chatWallpaper
settingsPromise.set(.single(nil))
hasSettings = false
}
initialState = EditThemeControllerState(mode: mode, title: generateThemeName(accentColor: theme.rootController.navigationBar.buttonColor), slug: "", updatedTheme: nil, updating: false, converting: false)
previewThemePromise.set(.single(theme.withUpdated(name: "", defaultWallpaper: wallpaper)))
case let .edit(info):
hasSettings = info.theme.settings != nil
settingsPromise.set(.single(info.theme.settings?.first))
if let file = info.theme.file, let path = context.sharedContext.accountManager.mediaBox.completedResourcePath(file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path)), let theme = makePresentationTheme(data: data, resolvedWallpaper: info.resolvedWallpaper) {
if case let .file(file) = theme.chat.defaultWallpaper, file.id == 0 {
previewThemePromise.set(cachedWallpaper(account: context.account, slug: file.slug, settings: file.settings)
|> map ({ wallpaper -> PresentationTheme in
if let wallpaper = wallpaper {
return theme.withUpdated(name: nil, defaultWallpaper: wallpaper.wallpaper)
} else {
return theme.withUpdated(name: nil, defaultWallpaper: .color(theme.chatList.backgroundColor.argb))
}
}))
} else {
previewThemePromise.set(.single(theme.withUpdated(name: nil, defaultWallpaper: info.resolvedWallpaper)))
}
} else {
previewThemePromise.set(.single(presentationData.theme.withUpdated(name: "", defaultWallpaper: presentationData.chatWallpaper)))
}
initialState = EditThemeControllerState(mode: mode, title: info.theme.title, slug: info.theme.slug, updatedTheme: nil, updating: false, converting: false)
}
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((EditThemeControllerState) -> EditThemeControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)?
var errorImpl: ((EditThemeEntryTag) -> Void)?
var generalThemeReference: PresentationThemeReference?
if case let .edit(cloudTheme) = mode {
generalThemeReference = PresentationThemeReference.cloud(cloudTheme).generalThemeReference
} else if case let .create(existingTheme, _) = mode, existingTheme == nil {
generalThemeReference = PresentationThemeReference.builtin(presentationData.theme.referenceTheme)
}
let arguments = EditThemeControllerArguments(context: context, updateState: { f in
updateState(f)
}, openColors: {
let _ = (combineLatest(queue: Queue.mainQueue(), previewThemePromise.get(), settingsPromise.get())
|> take(1)).start(next: { theme, previousSettings in
var controllerDismissImpl: (() -> Void)?
var generalThemeReference = generalThemeReference
if let settings = previousSettings {
generalThemeReference = .builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme))
}
let controller = ThemeAccentColorController(context: context, mode: .edit(settings: previousSettings, theme: theme, wallpaper: nil, generalThemeReference: generalThemeReference, defaultThemeReference: nil, create: false, completion: { updatedTheme, settings in
updateState { current in
var state = current
previewThemePromise.set(.single(updatedTheme))
state.updatedTheme = updatedTheme
return state
}
if previousSettings != nil {
settingsPromise.set(.single(settings))
}
controllerDismissImpl?()
}))
controllerDismissImpl = { [weak controller] in
controller?.dismiss()
}
pushControllerImpl?(controller)
})
}, openFile: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = legacyICloudFilePicker(theme: presentationData.theme, mode: .import, documentTypes: ["org.telegram.Telegram-iOS.theme"], completion: { urls in
if let url = urls.first{
if let data = try? Data(contentsOf: url), let theme = makePresentationTheme(data: data) {
if case let .file(file) = theme.chat.defaultWallpaper, file.id == 0 {
let _ = (cachedWallpaper(account: context.account, slug: file.slug, settings: file.settings)
|> mapToSignal { wallpaper -> Signal<TelegramWallpaper?, NoError> in
if let wallpaper = wallpaper, case let .file(file) = wallpaper.wallpaper {
var convertedRepresentations: [ImageRepresentationWithReference] = []
convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)))
return wallpaperDatas(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, alwaysShowThumbnailFirst: false, thumbnail: false, onlyFullSize: true, autoFetchFullSize: true, synchronousLoad: false)
|> mapToSignal { _, fullSizeData, complete -> Signal<TelegramWallpaper?, NoError> in
guard complete, let fullSizeData = fullSizeData else {
return .complete()
}
context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData, synchronous: true)
return .single(wallpaper.wallpaper)
}
} else {
return .single(nil)
}
}
|> deliverOnMainQueue).start(next: { wallpaper in
let updatedTheme = theme.withUpdated(name: nil, defaultWallpaper: wallpaper)
updateState { current in
var state = current
previewThemePromise.set(.single(updatedTheme))
state.updatedTheme = updatedTheme
return state
}
})
} else {
updateState { current in
var state = current
previewThemePromise.set(.single(theme))
state.updatedTheme = theme
return state
}
}
settingsPromise.set(.single(nil))
}
else {
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.EditTheme_FileReadError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
}
}
})
presentControllerImpl?(controller, nil)
}, toggleDark: {
let _ = (combineLatest(queue: Queue.mainQueue(), previewThemePromise.get(), settingsPromise.get())
|> take(1)).start(next: { theme, previousSettings in
var updatedTheme = theme
var updatedSettings: TelegramThemeSettings?
if let themeSettings = previousSettings {
if updatedTheme.referenceTheme == .night {
updatedTheme = updatedTheme.withUpdated(referenceTheme: .nightAccent)
updatedSettings = TelegramThemeSettings(baseTheme: .tinted, accentColor: themeSettings.accentColor, outgoingAccentColor: themeSettings.outgoingAccentColor, messageColors: themeSettings.messageColors, animateMessageColors: themeSettings.animateMessageColors, wallpaper: themeSettings.wallpaper)
if let settings = updatedSettings, let theme = makePresentationTheme(settings: settings) {
updatedTheme = theme
}
} else if updatedTheme.referenceTheme == .nightAccent {
updatedSettings = TelegramThemeSettings(baseTheme: .night, accentColor: themeSettings.accentColor, outgoingAccentColor: themeSettings.outgoingAccentColor, messageColors: themeSettings.messageColors, animateMessageColors: themeSettings.animateMessageColors, wallpaper: themeSettings.wallpaper)
if let settings = updatedSettings, let theme = makePresentationTheme(settings: settings) {
updatedTheme = theme
}
}
}
updateState { current in
var state = current
previewThemePromise.set(.single(updatedTheme))
state.updatedTheme = updatedTheme
return state
}
if previousSettings != nil {
settingsPromise.set(.single(updatedSettings))
}
})
}, convertToPresetTheme: {
let _ = (previewThemePromise.get()
|> take(1)).start(next: { theme in
var outgoingAccentColor: UIColor?
if case .classic = theme.referenceTheme.baseTheme {
outgoingAccentColor = theme.chat.message.outgoing.accentTextColor
}
let settings = TelegramThemeSettings(baseTheme: theme.referenceTheme.baseTheme, accentColor: theme.rootController.navigationBar.accentTextColor, outgoingAccentColor: outgoingAccentColor, messageColors: theme.chat.message.outgoing.bubble.withWallpaper.fill.map { $0.argb }, animateMessageColors: theme.chat.animateMessageColors, wallpaper: theme.chat.defaultWallpaper)
settingsPromise.set(.single(settings))
updateState { current in
var state = current
state.converting = true
return state
}
})
})
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), previewThemePromise.get(), settingsPromise.get())
|> map { presentationData, state, previewTheme, currentSettings -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
var focusItemTag: ItemListItemTag?
if case .create = state.mode {
focusItemTag = EditThemeEntryTag.title
}
let rightNavigationButton: ItemListNavigationButton
if state.updating {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
} else {
let isComplete: Bool
if case .create = mode {
isComplete = !state.title.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty
} else {
isComplete = !state.title.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty && !state.slug.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty
}
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: isComplete, action: {
if state.title.count > 128 {
errorImpl?(.title)
return
}
if case .create = mode {
} else if state.slug.count < 5 || state.slug.count > 64 {
errorImpl?(.slug)
return
}
dismissInputImpl?()
arguments.updateState { current in
var state = current
state.updating = true
return state
}
let _ = (combineLatest(queue: Queue.mainQueue(), previewThemePromise.get(), settingsPromise.get())
|> take(1)).start(next: { previewTheme, settings in
let saveThemeTemplateFile: (String, LocalFileMediaResource, @escaping () -> Void) -> Void = { title, resource, completion in
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: resource.fileId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/x-tgtheme-ios", size: nil, attributes: [.FileName(fileName: "\(title).tgios-theme")], alternativeRepresentations: [])
let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
let _ = enqueueMessages(account: context.account, peerId: context.account.peerId, messages: [message]).start()
if let navigateToChat = navigateToChat {
presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.EditTheme_ThemeTemplateAlertTitle, text: presentationData.strings.EditTheme_ThemeTemplateAlertText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_SavedMessages, action: {
completion()
navigateToChat(context.account.peerId)
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
completion()
})], actionLayout: .vertical), nil)
} else {
completion()
}
}
let theme: PresentationTheme?
let hasCustomFile: Bool
if let updatedTheme = state.updatedTheme {
theme = updatedTheme.withUpdated(name: state.title, defaultWallpaper: nil)
hasCustomFile = true
} else {
if case let .edit(info) = mode, let _ = info.theme.file {
theme = nil
hasCustomFile = true
} else {
theme = previewTheme.withUpdated(name: state.title, defaultWallpaper: nil)
hasCustomFile = false
}
}
let themeResource: LocalFileMediaResource?
let themeData: Data?
let themeThumbnailData: Data?
if let theme = theme, let themeString = encodePresentationTheme(theme), let data = themeString.data(using: .utf8) {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
themeResource = resource
themeData = data
var wallpaperImage: UIImage?
if case .file = theme.chat.defaultWallpaper {
wallpaperImage = chatControllerBackgroundImage(theme: theme, wallpaper: theme.chat.defaultWallpaper, mediaBox: context.sharedContext.accountManager.mediaBox, knockoutMode: false)
}
let themeThumbnail = generateImage(CGSize(width: 213, height: 320.0), contextGenerator: { size, context in
if let image = generateImage(CGSize(width: 194.0, height: 291.0), contextGenerator: { size, c in
drawThemeImage(context: c, theme: theme, wallpaperImage: wallpaperImage, size: size)
})?.cgImage {
context.draw(image, in: CGRect(origin: CGPoint(), size: size))
}
}, scale: 1.0)
themeThumbnailData = themeThumbnail?.jpegData(compressionQuality: 0.6)
} else {
themeResource = nil
themeData = nil
themeThumbnailData = nil
}
let resolvedWallpaper: TelegramWallpaper?
if let theme = theme, case let .file(file) = theme.chat.defaultWallpaper, file.id != 0 {
resolvedWallpaper = theme.chat.defaultWallpaper
updateCachedWallpaper(engine: context.engine, wallpaper: theme.chat.defaultWallpaper)
} else {
resolvedWallpaper = nil
}
let prepare: Signal<CreateThemeResult, CreateThemeError>
if let resolvedWallpaper = resolvedWallpaper, case let .file(file) = resolvedWallpaper, resolvedWallpaper.isPattern {
let resource = file.file.resource
var data: Data?
if let path = context.account.postbox.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) {
data = maybeData
} else if let path = context.sharedContext.accountManager.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) {
data = maybeData
}
if let data = data {
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
prepare = .complete()
} else {
prepare = .complete()
}
} else {
prepare = .complete()
}
switch mode {
case .create:
if let themeResource = themeResource {
let _ = (prepare |> then(createTheme(account: context.account, title: state.title, resource: themeResource, thumbnailData: themeThumbnailData, settings: settings.flatMap { [$0] })
|> deliverOnMainQueue)).start(next: { next in
if case let .result(resultTheme) = next {
let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: resultTheme).start()
let _ = (updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
if let resource = resultTheme.file?.resource, let data = themeData {
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
}
let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: context.account.id))
var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers
themeSpecificChatWallpapers[themeReference.index] = nil
return PresentationThemeSettings(theme: themeReference, themePreferredBaseTheme: current.themePreferredBaseTheme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, useSystemFont: current.useSystemFont, fontSize: current.fontSize, listsFontSize: current.listsFontSize, chatBubbleSettings: current.chatBubbleSettings, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, reduceMotion: current.reduceMotion)
}) |> deliverOnMainQueue).start(completed: {
if !hasCustomFile {
saveThemeTemplateFile(state.title, themeResource, {
dismissImpl?()
})
} else {
dismissImpl?()
}
})
}
}, error: { error in
arguments.updateState { current in
var state = current
state.updating = false
return state
}
})
}
case let .edit(info):
let _ = (prepare |> then(updateTheme(account: context.account, accountManager: context.sharedContext.accountManager, theme: info.theme, title: state.title, slug: state.slug, resource: state.converting ? nil : themeResource, settings: settings.flatMap { [$0] })
|> deliverOnMainQueue)).start(next: { next in
if case let .result(resultTheme) = next {
let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: resultTheme).start()
let _ = (updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
if let resource = resultTheme.file?.resource, let data = themeData {
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
}
let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: context.account.id))
var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers
themeSpecificChatWallpapers[themeReference.index] = nil
return PresentationThemeSettings(theme: themeReference, themePreferredBaseTheme: current.themePreferredBaseTheme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, useSystemFont: current.useSystemFont, fontSize: current.fontSize, listsFontSize: current.listsFontSize, chatBubbleSettings: current.chatBubbleSettings, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, reduceMotion: current.reduceMotion)
}) |> deliverOnMainQueue).start(completed: {
if let themeResource = themeResource, !hasCustomFile {
saveThemeTemplateFile(state.title, themeResource, {
dismissImpl?()
})
} else {
dismissImpl?()
}
})
}
}, error: { error in
arguments.updateState { current in
var state = current
state.updating = false
return state
}
var errorText: String?
switch error {
case .slugOccupied:
errorText = presentationData.strings.EditTheme_ErrorLinkTaken
case .slugInvalid:
errorText = presentationData.strings.EditTheme_ErrorInvalidCharacters
default:
break
}
if let errorText = errorText {
presentControllerImpl?(textAlertController(context: context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
}
})
}
})
})
}
let title: String
switch mode {
case .create:
title = presentationData.strings.EditTheme_CreateTitle
case let .edit(theme):
if theme.theme.file == nil {
title = presentationData.strings.EditTheme_CreateTitle
} else {
title = presentationData.strings.EditTheme_EditTitle
}
}
let hasSettings = hasSettings || currentSettings != nil
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: editThemeControllerEntries(presentationData: presentationData, state: state, previewTheme: previewTheme, hasSettings: hasSettings), style: .blocks, focusItemTag: focusItemTag, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
controller.navigationPresentation = .modal
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
let _ = controller?.dismiss()
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
let hapticFeedback = HapticFeedback()
errorImpl = { [weak controller] targetTag in
hapticFeedback.error()
controller?.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListSingleLineInputItemNode, let tag = itemNode.tag, tag.isEqual(to: targetTag) {
itemNode.animateError()
}
}
}
return controller
}
@@ -0,0 +1,596 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import TelegramStringFormatting
import AccountContext
import DeviceLocationManager
import Geocoding
import WallpaperResources
import Sunrise
import ThemeSettingsThemeItem
private enum TriggerMode {
case system
case none
case timeBased
case brightness
}
private enum TimeBasedManualField {
case from
case to
}
private final class ThemeAutoNightSettingsControllerArguments {
let context: AccountContext
let updateMode: (TriggerMode) -> Void
let updateTimeBasedAutomatic: (Bool) -> Void
let openTimeBasedManual: (TimeBasedManualField) -> Void
let updateTimeBasedAutomaticLocation: () -> Void
let updateAutomaticBrightness: (Double) -> Void
let updateTheme: (PresentationThemeReference) -> Void
init(context: AccountContext, updateMode: @escaping (TriggerMode) -> Void, updateTimeBasedAutomatic: @escaping (Bool) -> Void, openTimeBasedManual: @escaping (TimeBasedManualField) -> Void, updateTimeBasedAutomaticLocation: @escaping () -> Void, updateAutomaticBrightness: @escaping (Double) -> Void, updateTheme: @escaping (PresentationThemeReference) -> Void) {
self.context = context
self.updateMode = updateMode
self.updateTimeBasedAutomatic = updateTimeBasedAutomatic
self.openTimeBasedManual = openTimeBasedManual
self.updateTimeBasedAutomaticLocation = updateTimeBasedAutomaticLocation
self.updateAutomaticBrightness = updateAutomaticBrightness
self.updateTheme = updateTheme
}
}
private enum ThemeAutoNightSettingsControllerSection: Int32 {
case mode
case settings
case theme
}
private enum ThemeAutoNightSettingsControllerEntry: ItemListNodeEntry {
case modeSystem(PresentationTheme, String, Bool)
case modeDisabled(PresentationTheme, String, Bool)
case modeTimeBased(PresentationTheme, String, Bool)
case modeBrightness(PresentationTheme, String, Bool)
case settingsHeader(PresentationTheme, String)
case timeBasedAutomaticLocation(PresentationTheme, String, Bool)
case timeBasedAutomaticLocationValue(PresentationTheme, String, String)
case timeBasedManualFrom(PresentationTheme, String, String)
case timeBasedManualTo(PresentationTheme, String, String)
case brightnessValue(PresentationTheme, Double)
case settingInfo(PresentationTheme, String)
case themeHeader(PresentationTheme, String)
case themeItem(PresentationTheme, PresentationStrings, [PresentationThemeReference], [PresentationThemeReference], PresentationThemeReference, [Int64: PresentationThemeAccentColor], [Int64: TelegramWallpaper])
var section: ItemListSectionId {
switch self {
case .modeSystem, .modeDisabled, .modeTimeBased, .modeBrightness:
return ThemeAutoNightSettingsControllerSection.mode.rawValue
case .settingsHeader, .timeBasedAutomaticLocation, .timeBasedAutomaticLocationValue, .timeBasedManualFrom, .timeBasedManualTo, .brightnessValue, .settingInfo:
return ThemeAutoNightSettingsControllerSection.settings.rawValue
case .themeHeader, .themeItem:
return ThemeAutoNightSettingsControllerSection.theme.rawValue
}
}
var stableId: Int32 {
switch self {
case .modeSystem:
return 0
case .modeDisabled:
return 1
case .modeTimeBased:
return 2
case .modeBrightness:
return 3
case .settingsHeader:
return 4
case .timeBasedAutomaticLocation:
return 5
case .timeBasedAutomaticLocationValue:
return 6
case .timeBasedManualFrom:
return 7
case .timeBasedManualTo:
return 8
case .brightnessValue:
return 9
case .settingInfo:
return 10
case .themeHeader:
return 11
case .themeItem:
return 12
}
}
static func ==(lhs: ThemeAutoNightSettingsControllerEntry, rhs: ThemeAutoNightSettingsControllerEntry) -> Bool {
switch lhs {
case let .modeSystem(lhsTheme, lhsTitle, lhsValue):
if case let .modeSystem(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .modeDisabled(lhsTheme, lhsTitle, lhsValue):
if case let .modeDisabled(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .modeTimeBased(lhsTheme, lhsTitle, lhsValue):
if case let .modeTimeBased(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .modeBrightness(lhsTheme, lhsTitle, lhsValue):
if case let .modeBrightness(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .settingsHeader(lhsTheme, lhsTitle):
if case let .settingsHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
case let .timeBasedAutomaticLocation(lhsTheme, lhsTitle, lhsValue):
if case let .timeBasedAutomaticLocation(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .timeBasedAutomaticLocationValue(lhsTheme, lhsTitle, lhsValue):
if case let .timeBasedAutomaticLocationValue(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .timeBasedManualFrom(lhsTheme, lhsTitle, lhsValue):
if case let .timeBasedManualFrom(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .timeBasedManualTo(lhsTheme, lhsTitle, lhsValue):
if case let .timeBasedManualTo(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .brightnessValue(lhsTheme, lhsValue):
if case let .brightnessValue(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue {
return true
} else {
return false
}
case let .settingInfo(lhsTheme, lhsValue):
if case let .settingInfo(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue {
return true
} else {
return false
}
case let .themeHeader(lhsTheme, lhsValue):
if case let .themeHeader(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue {
return true
} else {
return false
}
case let .themeItem(lhsTheme, lhsStrings, lhsThemes, lhsAllThemes, lhsCurrentTheme, lhsThemeAccentColors, lhsThemeChatWallpapers):
if case let .themeItem(rhsTheme, rhsStrings, rhsThemes, rhsAllThemes, rhsCurrentTheme, rhsThemeAccentColors, rhsThemeChatWallpapers) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsThemes == rhsThemes, lhsAllThemes == rhsAllThemes, lhsCurrentTheme == rhsCurrentTheme, lhsThemeAccentColors == rhsThemeAccentColors, lhsThemeChatWallpapers == rhsThemeChatWallpapers {
return true
} else {
return false
}
}
}
static func <(lhs: ThemeAutoNightSettingsControllerEntry, rhs: ThemeAutoNightSettingsControllerEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ThemeAutoNightSettingsControllerArguments
switch self {
case let .modeSystem(_, title, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateMode(.system)
})
case let .modeDisabled(_, title, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateMode(.none)
})
case let .modeTimeBased(_, title, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateMode(.timeBased)
})
case let .modeBrightness(_, title, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateMode(.brightness)
})
case let .settingsHeader(_, title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .timeBasedAutomaticLocation(_, title, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateTimeBasedAutomatic(value)
})
case let .timeBasedAutomaticLocationValue(_, title, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: nil, title: title, titleColor: .accent, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
arguments.updateTimeBasedAutomaticLocation()
})
case let .timeBasedManualFrom(_, title, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: nil, title: title, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openTimeBasedManual(.from)
})
case let .timeBasedManualTo(_, title, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: nil, title: title, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openTimeBasedManual(.to)
})
case let .brightnessValue(theme, value):
return ThemeSettingsBrightnessItem(theme: theme, value: Int32(value * 100.0), sectionId: self.section, updated: { value in
arguments.updateAutomaticBrightness(Double(value) / 100.0)
})
case let .settingInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .themeHeader(_, title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .themeItem(theme, strings, themes, allThemes, currentTheme, themeSpecificAccentColors, themeSpecificChatWallpapers):
return ThemeSettingsThemeItem(context: arguments.context, theme: theme, strings: strings, sectionId: self.section, themes: themes, allThemes: allThemes, displayUnsupported: false, themeSpecificAccentColors: themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, themePreferredBaseTheme: [:], currentTheme: currentTheme, updatedTheme: { theme in
arguments.updateTheme(theme)
}, contextAction: nil)
}
}
}
private func themeAutoNightSettingsControllerEntries(theme: PresentationTheme, strings: PresentationStrings, settings: PresentationThemeSettings, switchSetting: AutomaticThemeSwitchSetting, availableThemes: [PresentationThemeReference], dateTimeFormat: PresentationDateTimeFormat) -> [ThemeAutoNightSettingsControllerEntry] {
var entries: [ThemeAutoNightSettingsControllerEntry] = []
let activeTriggerMode: TriggerMode
switch switchSetting.trigger {
case .system:
if #available(iOSApplicationExtension 13.0, iOS 13.0, *) {
activeTriggerMode = .system
} else {
activeTriggerMode = .none
}
case .explicitNone:
activeTriggerMode = .none
case .timeBased:
activeTriggerMode = .timeBased
case .brightness:
activeTriggerMode = .brightness
}
if #available(iOSApplicationExtension 13.0, iOS 13.0, *) {
entries.append(.modeSystem(theme, strings.AutoNightTheme_System, activeTriggerMode == .system))
}
entries.append(.modeDisabled(theme, strings.AutoNightTheme_Disabled, activeTriggerMode == .none))
entries.append(.modeTimeBased(theme, strings.AutoNightTheme_Scheduled, activeTriggerMode == .timeBased))
entries.append(.modeBrightness(theme, strings.AutoNightTheme_Automatic, activeTriggerMode == .brightness))
switch switchSetting.trigger {
case .system, .explicitNone:
break
case let .timeBased(setting):
entries.append(.settingsHeader(theme, strings.AutoNightTheme_ScheduleSection))
var automaticLocation = false
if case .automatic = setting {
automaticLocation = true
}
entries.append(.timeBasedAutomaticLocation(theme, strings.AutoNightTheme_UseSunsetSunrise, automaticLocation))
switch setting {
case let .automatic(latitude, longitude, localizedName):
let calculator = EDSunriseSet(date: Date(), timezone: TimeZone.current, latitude: latitude, longitude: longitude)!
let sunset = roundTimeToDay(Int32(calculator.sunset.timeIntervalSince1970))
let sunrise = roundTimeToDay(Int32(calculator.sunrise.timeIntervalSince1970))
entries.append(.timeBasedAutomaticLocationValue(theme, strings.AutoNightTheme_UpdateLocation, localizedName))
if sunset != 0 || sunrise != 0 {
entries.append(.settingInfo(theme, strings.AutoNightTheme_LocationHelp(stringForMessageTimestamp(timestamp: sunset, dateTimeFormat: dateTimeFormat, local: false), stringForMessageTimestamp(timestamp: sunrise, dateTimeFormat: dateTimeFormat, local: false)).string))
}
case let .manual(fromSeconds, toSeconds):
entries.append(.timeBasedManualFrom(theme, strings.AutoNightTheme_ScheduledFrom, stringForMessageTimestamp(timestamp: fromSeconds, dateTimeFormat: dateTimeFormat, local: false)))
entries.append(.timeBasedManualTo(theme, strings.AutoNightTheme_ScheduledTo, stringForMessageTimestamp(timestamp: toSeconds, dateTimeFormat: dateTimeFormat, local: false)))
}
case let .brightness(threshold):
entries.append(.settingsHeader(theme, strings.AutoNightTheme_AutomaticSection))
entries.append(.brightnessValue(theme, threshold))
entries.append(.settingInfo(theme, strings.AutoNightTheme_AutomaticHelp("\(Int(threshold * 100.0))").string.replacingOccurrences(of: "%%", with: "%")))
}
switch switchSetting.trigger {
case .explicitNone:
break
case .system, .timeBased, .brightness:
entries.append(.themeHeader(theme, strings.AutoNightTheme_PreferredTheme))
let generalThemes: [PresentationThemeReference] = availableThemes.filter { reference in
if case let .cloud(theme) = reference {
return theme.theme.settings == nil
} else {
return true
}
}
entries.append(.themeItem(theme, strings, generalThemes, availableThemes, switchSetting.theme, settings.themeSpecificAccentColors, settings.themeSpecificChatWallpapers))
}
return entries
}
private func roundTimeToDay(_ timestamp: Int32) -> Int32 {
let calendar = Calendar.current
let offset = 0
let components = calendar.dateComponents([.hour, .minute, .second], from: Date(timeIntervalSince1970: Double(timestamp + Int32(offset))))
return Int32(components.hour! * 60 * 60 + components.minute! * 60 + components.second!)
}
private func areSettingsValid(_ settings: AutomaticThemeSwitchSetting) -> Bool {
switch settings.trigger {
case .system, .explicitNone, .brightness:
return true
case let .timeBased(setting):
switch setting {
case let .automatic(latitude, longitude, _):
if !latitude.isZero || !longitude.isZero {
return true
} else {
return false
}
case .manual:
return true
}
}
}
public func themeAutoNightSettingsController(context: AccountContext) -> ViewController {
var presentControllerImpl: ((ViewController) -> Void)?
let actionsDisposable = DisposableSet()
let updateAutomaticBrightnessDisposable = MetaDisposable()
let stagingSettingsPromise = ValuePromise<AutomaticThemeSwitchSetting?>(nil)
let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings])
let updateLocationDisposable = MetaDisposable()
actionsDisposable.add(updateLocationDisposable)
let updateSettings: (@escaping (AutomaticThemeSwitchSetting) -> AutomaticThemeSwitchSetting) -> Void = { f in
let _ = (combineLatest(stagingSettingsPromise.get(), sharedData)
|> take(1)
|> deliverOnMainQueue).start(next: { stagingSettings, sharedData in
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings
let updated = f(stagingSettings ?? settings.automaticThemeSwitchSetting)
stagingSettingsPromise.set(updated)
if areSettingsValid(updated) {
let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
var current = current
current.automaticThemeSwitchSetting = updated
return current
}).start()
}
})
}
let forceUpdateLocation: () -> Void = {
let locationCoordinates = Signal<(Double, Double), NoError> { subscriber in
return context.sharedContext.locationManager!.push(mode: DeviceLocationMode.preciseForeground, updated: { location, _ in
subscriber.putNext((location.coordinate.latitude, location.coordinate.longitude))
subscriber.putCompletion()
})
}
let geocodedLocation = locationCoordinates
|> mapToSignal { coordinates -> Signal<(Double, Double, String), NoError> in
return reverseGeocodeLocation(latitude: coordinates.0, longitude: coordinates.1)
|> map { locality in
return (coordinates.0, coordinates.1, locality?.city ?? "")
}
}
let disposable = (geocodedLocation
|> take(1)
|> deliverOnMainQueue).start(next: { location in
updateSettings { settings in
var settings = settings
if case let .timeBased(setting) = settings.trigger, case .automatic = setting {
settings.trigger = .timeBased(setting: .automatic(latitude: location.0, longitude: location.1, localizedName: location.2))
}
return settings
}
})
updateLocationDisposable.set(disposable)
}
let arguments = ThemeAutoNightSettingsControllerArguments(context: context, updateMode: { mode in
var updateLocation = false
updateSettings { settings in
var settings = settings
switch mode {
case .system:
settings.trigger = .system
case .none:
settings.trigger = .explicitNone
case .timeBased:
if case .timeBased = settings.trigger {
} else {
settings.trigger = .timeBased(setting: .automatic(latitude: 0.0, longitude: 0.0, localizedName: ""))
updateLocation = true
}
case .brightness:
if case .brightness = settings.trigger {
} else {
settings.trigger = .brightness(threshold: 0.2)
}
}
if updateLocation {
forceUpdateLocation()
}
return settings
}
}, updateTimeBasedAutomatic: { value in
var updateLocation = false
updateSettings { settings in
var settings = settings
if case let .timeBased(setting) = settings.trigger {
switch setting {
case .automatic:
if !value {
settings.trigger = .timeBased(setting: .manual(fromSeconds: 22 * 60 * 60, toSeconds: 9 * 60 * 60))
}
case .manual:
if value {
settings.trigger = .timeBased(setting: .automatic(latitude: 0.0, longitude: 0.0, localizedName: ""))
updateLocation = true
}
}
}
if updateLocation {
forceUpdateLocation()
}
return settings
}
}, openTimeBasedManual: { field in
var currentValue: Int32
switch field {
case .from:
currentValue = 22 * 60 * 60
case .to:
currentValue = 9 * 60 * 60
}
updateSettings { settings in
let settings = settings
switch settings.trigger {
case let .timeBased(setting):
switch setting {
case let .manual(fromSeconds, toSeconds):
switch field {
case .from:
currentValue = fromSeconds
case .to:
currentValue = toSeconds
}
default:
break
}
default:
break
}
presentControllerImpl?(ThemeAutoNightTimeSelectionActionSheet(context: context, currentValue: currentValue, applyValue: { value in
guard let value = value else {
return
}
updateSettings { settings in
var settings = settings
switch settings.trigger {
case let .timeBased(setting):
switch setting {
case var .manual(fromSeconds, toSeconds):
switch field {
case .from:
fromSeconds = value
case .to:
toSeconds = value
}
settings.trigger = .timeBased(setting: .manual(fromSeconds: fromSeconds, toSeconds: toSeconds))
default:
break
}
default:
break
}
return settings
}
}))
return settings
}
}, updateTimeBasedAutomaticLocation: {
forceUpdateLocation()
}, updateAutomaticBrightness: { value in
updateAutomaticBrightnessDisposable.set((Signal<Never, NoError>.complete()
|> delay(0.1, queue: Queue.mainQueue())).start(completed: {
updateSettings { settings in
var settings = settings
switch settings.trigger {
case .brightness:
settings.trigger = .brightness(threshold: max(0.0, min(1.0, value)))
default:
break
}
return settings
}
}))
}, updateTheme: { theme in
guard let presentationTheme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: theme) else {
return
}
let resolvedWallpaper: Signal<TelegramWallpaper?, NoError>
if case let .file(file) = presentationTheme.chat.defaultWallpaper, file.id == 0 {
resolvedWallpaper = cachedWallpaper(account: context.account, slug: file.slug, settings: file.settings)
|> map { wallpaper -> TelegramWallpaper? in
return wallpaper?.wallpaper
}
} else {
resolvedWallpaper = .single(nil)
}
let _ = (resolvedWallpaper
|> mapToSignal { resolvedWallpaper -> Signal<Void, NoError> in
var updatedTheme = theme
if case let .cloud(info) = theme {
updatedTheme = .cloud(PresentationCloudTheme(theme: info.theme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: info.theme.isCreator ? context.account.id : nil))
}
updateSettings { settings in
var settings = settings
settings.theme = updatedTheme
return settings
}
return .complete()
}).start()
})
let cloudThemes = Promise<[TelegramTheme]>()
let updatedCloudThemes = telegramThemes(postbox: context.account.postbox, network: context.account.network, accountManager: context.sharedContext.accountManager)
cloudThemes.set(updatedCloudThemes)
let signal = combineLatest(context.sharedContext.presentationData |> deliverOnMainQueue, sharedData |> deliverOnMainQueue, cloudThemes.get() |> deliverOnMainQueue, stagingSettingsPromise.get() |> deliverOnMainQueue)
|> map { presentationData, sharedData, cloudThemes, stagingSettings -> (ItemListControllerState, (ItemListNodeState, Any)) in
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings
let defaultThemes: [PresentationThemeReference] = [.builtin(.night), .builtin(.nightAccent)]
let cloudThemes: [PresentationThemeReference] = cloudThemes.map { .cloud(PresentationCloudTheme(theme: $0, resolvedWallpaper: nil, creatorAccountId: $0.isCreator ? context.account.id : nil)) }
var availableThemes = defaultThemes
availableThemes.append(contentsOf: cloudThemes)
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.AutoNightTheme_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeAutoNightSettingsControllerEntries(theme: presentationData.theme, strings: presentationData.strings, settings: settings, switchSetting: stagingSettings ?? settings.automaticThemeSwitchSetting, availableThemes: availableThemes, dateTimeFormat: presentationData.dateTimeFormat), style: .blocks, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
controller.alwaysSynchronous = true
presentControllerImpl = { [weak controller] c in
controller?.present(c, in: .window(.root))
}
return controller
}
@@ -0,0 +1,132 @@
import Foundation
import Display
import AsyncDisplayKit
import UIKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramStringFormatting
import AccountContext
import UIKitRuntimeUtils
final class ThemeAutoNightTimeSelectionActionSheet: ActionSheetController {
private var presentationDisposable: Disposable?
private let _ready = Promise<Bool>()
override var ready: Promise<Bool> {
return self._ready
}
init(context: AccountContext, currentValue: Int32, emptyTitle: String? = nil, applyValue: @escaping (Int32?) -> Void) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
super.init(theme: ActionSheetControllerTheme(presentationData: presentationData))
self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData)
}
})
self._ready.set(.single(true))
var updatedValue = currentValue
var items: [ActionSheetItem] = []
items.append(ThemeAutoNightTimeSelectionActionSheetItem(strings: strings, currentValue: currentValue, valueChanged: { value in
updatedValue = value
}))
if let emptyTitle = emptyTitle {
items.append(ActionSheetButtonItem(title: emptyTitle, action: { [weak self] in
self?.dismissAnimated()
applyValue(nil)
}))
}
items.append(ActionSheetButtonItem(title: strings.Wallpaper_Set, action: { [weak self] in
self?.dismissAnimated()
applyValue(updatedValue)
}))
self.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in
self?.dismissAnimated()
}),
])
])
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDisposable?.dispose()
}
}
private final class ThemeAutoNightTimeSelectionActionSheetItem: ActionSheetItem {
let strings: PresentationStrings
let currentValue: Int32
let valueChanged: (Int32) -> Void
init(strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) {
self.strings = strings
self.currentValue = currentValue
self.valueChanged = valueChanged
}
func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
return ThemeAutoNightTimeSelectionActionSheetItemNode(theme: theme, strings: self.strings, currentValue: self.currentValue, valueChanged: self.valueChanged)
}
func updateNode(_ node: ActionSheetItemNode) {
}
}
private final class ThemeAutoNightTimeSelectionActionSheetItemNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
private let strings: PresentationStrings
private let valueChanged: (Int32) -> Void
private let pickerView: UIDatePicker
init(theme: ActionSheetControllerTheme, strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.valueChanged = valueChanged
UILabel.setDateLabel(theme.primaryTextColor)
self.pickerView = UIDatePicker()
self.pickerView.datePickerMode = .countDownTimer
self.pickerView.datePickerMode = .time
self.pickerView.timeZone = TimeZone(secondsFromGMT: 0)
self.pickerView.date = Date(timeIntervalSince1970: Double(currentValue))
self.pickerView.locale = Locale.current
if #available(iOS 13.4, *) {
self.pickerView.preferredDatePickerStyle = .wheels
}
self.pickerView.setValue(theme.primaryTextColor, forKey: "textColor")
super.init(theme: theme)
self.view.addSubview(self.pickerView)
self.pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged)
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 216.0)
self.pickerView.frame = CGRect(origin: CGPoint(), size: size)
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
@objc private func datePickerUpdated() {
self.valueChanged(Int32(self.pickerView.date.timeIntervalSince1970))
}
}
@@ -0,0 +1,45 @@
import Foundation
import TelegramUIPreferences
import TelegramPresentationData
import TelegramCore
private func patternWallpaper(data: BuiltinWallpaperData, colors: [UInt32], intensity: Int32?, rotation: Int32?) -> TelegramWallpaper {
return defaultBuiltinWallpaper(data: data, colors: colors, intensity: intensity ?? 50, rotation: rotation)
}
var dayClassicColorPresets: [PresentationThemeAccentColor] = [
// Pink with Blue
PresentationThemeAccentColor(index: 106, baseColor: .preset, accentColor: 0xfff55783, bubbleColors: [0xffd6f5ff, 0xffc9fdfe], wallpaper: patternWallpaper(data: .default, colors: [0x8dc0eb, 0xb9d1ea, 0xc6b1ef, 0xebd7ef], intensity: 50, rotation: nil)),
// Pink with Gold
PresentationThemeAccentColor(index: 102, baseColor: .preset, accentColor: 0xFFFF5FA9, bubbleColors: [0xFFFFF4D7], wallpaper: patternWallpaper(data: .variant12, colors: [0xeaa36e, 0xf0e486, 0xf29ebf, 0xe8c06e], intensity: 50, rotation: nil)),
// Green
PresentationThemeAccentColor(index: 104, baseColor: .preset, accentColor: 0xFF5A9E29, bubbleColors: [0xffFFF8DF], wallpaper: patternWallpaper(data: .variant13, colors: [0x7fc289, 0xe4d573, 0xafd677, 0xf0c07a], intensity: 50, rotation: nil)),
// Purple
PresentationThemeAccentColor(index: 101, baseColor: .preset, accentColor: 0xFF7E5FE5, bubbleColors: [0xFFF5e2FF], wallpaper: patternWallpaper(data: .variant14, colors: [0xe4b2ea, 0x8376c2, 0xeab9d9, 0xb493e6], intensity: 50, rotation: nil)),
// Light Blue
PresentationThemeAccentColor(index: 107, baseColor: .preset, accentColor: 0xFF2CB9ED, bubbleColors: [0xFFADF7B5, 0xFFFCFF8B], wallpaper: patternWallpaper(data: .variant3, colors: [0x1a2e1a, 0x47623c, 0x222e24, 0x314429], intensity: 50, rotation: nil)),
// Mint
PresentationThemeAccentColor(index: 103, baseColor: .preset, accentColor: 0xFF199972, bubbleColors: [0xFFFFFEC7], wallpaper: patternWallpaper(data: .variant3, colors: [0xdceb92, 0x8fe1d6, 0x67a3f2, 0x85d685], intensity: 50, rotation: nil)),
// Pink with Green
PresentationThemeAccentColor(index: 105, baseColor: .preset, accentColor: 0xFFDA90D9, bubbleColors: [0xFF94FFF9, 0xFFCCFFC7], wallpaper: patternWallpaper(data: .variant9, colors: [0xffc3b2, 0xe2c0ff, 0xffe7b2], intensity: 50, rotation: nil))
]
var dayColorPresets: [PresentationThemeAccentColor] = [
PresentationThemeAccentColor(index: 101, baseColor: .preset, accentColor: 0x0088ff, bubbleColors: [0x0088ff, 0xff53f4], wallpaper: nil),
PresentationThemeAccentColor(index: 102, baseColor: .preset, accentColor: 0x00b09b, bubbleColors: [0xaee946, 0x00b09b], wallpaper: nil),
PresentationThemeAccentColor(index: 103, baseColor: .preset, accentColor: 0xd33213, bubbleColors: [0xf9db00, 0xd33213], wallpaper: nil),
PresentationThemeAccentColor(index: 104, baseColor: .preset, accentColor: 0xea8ced, bubbleColors: [0xea8ced, 0x00c2ed], wallpaper: nil)
]
var nightColorPresets: [PresentationThemeAccentColor] = [
// PresentationThemeAccentColor(index: 101, baseColor: .preset, accentColor: 0x0088ff, bubbleColors: [0x0088ff, 0xff53f4], wallpaper: patternWallpaper(data: .variant4, colors: [0xe4b2ea, 0x8376c2, 0xeab9d9, 0xb493e6], intensity: -35, rotation: nil)),
PresentationThemeAccentColor(index: 102, baseColor: .preset, accentColor: 0x00b09b, bubbleColors: [0xaee946, 0x00b09b], wallpaper: patternWallpaper(data: .variant9, colors: [0xe4b2ea, 0x8376c2, 0xeab9d9, 0xb493e6], intensity: -35, rotation: nil)),
PresentationThemeAccentColor(index: 103, baseColor: .preset, accentColor: 0xd33213, bubbleColors: [0xf9db00, 0xd33213], wallpaper: patternWallpaper(data: .variant2, colors: [0xfec496, 0xdd6cb9, 0x962fbf, 0x4f5bd5], intensity: -40, rotation: nil)),
PresentationThemeAccentColor(index: 104, baseColor: .preset, accentColor: 0xea8ced, bubbleColors: [0xea8ced, 0x00c2ed], wallpaper: patternWallpaper(data: .variant6, colors: [0x8adbf2, 0x888dec, 0xe39fea, 0x679ced], intensity: -30, rotation: nil))
]
@@ -0,0 +1,530 @@
import Foundation
import UIKit
import Display
import Postbox
import SwiftSignalKit
import AsyncDisplayKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import ShareController
import CounterControllerTitleView
import WallpaperResources
import OverlayStatusController
import AppBundle
import PresentationDataUtils
import UndoUI
import TelegramNotices
public enum ThemePreviewSource {
case settings(PresentationThemeReference, TelegramWallpaper?, Bool)
case theme(TelegramTheme)
case slug(String, TelegramMediaFile)
case themeSettings(String, TelegramThemeSettings)
case media(AnyMediaReference)
}
public final class ThemePreviewController: ViewController {
private let context: AccountContext
private let previewTheme: PresentationTheme
private let source: ThemePreviewSource
private let theme = Promise<TelegramTheme?>()
private let presentationTheme = Promise<PresentationTheme>()
private var controllerNode: ThemePreviewControllerNode {
return self.displayNode as! ThemePreviewControllerNode
}
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private var validLayout: ContainerViewLayout?
private var didPlayPresentationAnimation = false
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var disposable: Disposable?
private var applyDisposable = MetaDisposable()
var customApply: (() -> Void)?
public init(context: AccountContext, previewTheme: PresentationTheme, source: ThemePreviewSource) {
self.context = context
self.previewTheme = previewTheme
self.source = source
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationTheme.set(.single(previewTheme))
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationTheme: self.previewTheme, presentationStrings: self.presentationData.strings))
self.blocksBackgroundWhenInOverlay = true
self.acceptsFocusWhenInOverlay = true
self.navigationPresentation = .modal
var hasInstallsCount = false
let themeName: String
switch source {
case let .theme(theme):
themeName = theme.title
self.theme.set(.single(theme)
|> then(
getTheme(account: context.account, slug: theme.slug)
|> map(Optional.init)
|> `catch` { _ -> Signal<TelegramTheme?, NoError> in
return .single(nil)
}
|> filter { $0 != nil }
))
hasInstallsCount = true
case let .slug(slug, _), let .themeSettings(slug, _):
self.theme.set(getTheme(account: context.account, slug: slug)
|> map(Optional.init)
|> `catch` { _ -> Signal<TelegramTheme?, NoError> in
return .single(nil)
})
themeName = previewTheme.name.string
self.presentationTheme.set(.single(self.previewTheme)
|> then(
self.theme.get()
|> mapToSignal { theme in
if let file = theme?.file {
return telegramThemeData(account: context.account, accountManager: context.sharedContext.accountManager, reference: .standalone(resource: file.resource))
|> mapToSignal { data -> Signal<PresentationTheme, NoError> in
guard let data = data, let presentationTheme = makePresentationTheme(data: data) else {
return .complete()
}
return .single(presentationTheme)
}
} else {
return .complete()
}
}
))
hasInstallsCount = true
case let .settings(themeReference, _, _):
if case let .cloud(theme) = themeReference {
self.theme.set(getTheme(account: context.account, slug: theme.theme.slug)
|> map(Optional.init)
|> `catch` { _ -> Signal<TelegramTheme?, NoError> in
return .single(nil)
})
if let emoticon = theme.theme.emoticon{
themeName = emoticon
} else {
themeName = theme.theme.title
hasInstallsCount = true
}
} else {
self.theme.set(.single(nil))
if [.builtin(.dayClassic), .builtin(.night)].contains(themeReference) {
themeName = "🏠"
} else {
themeName = previewTheme.name.string
}
}
default:
self.theme.set(.single(nil))
themeName = previewTheme.name.string
}
var isPreview = false
if case .settings = source {
isPreview = true
}
let titleView = CounterControllerTitleView(theme: self.previewTheme)
titleView.title = CounterControllerTitle(title: themeName, counter: hasInstallsCount ? " " : "")
self.navigationItem.titleView = titleView
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.statusBar.statusBarStyle = self.previewTheme.rootController.statusBarStyle.style
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
if !isPreview {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: self.previewTheme.rootController.navigationBar.accentTextColor), style: .plain, target: self, action: #selector(self.actionPressed))
}
self.disposable = (combineLatest(self.theme.get(), self.presentationTheme.get())
|> deliverOnMainQueue).start(next: { [weak self] theme, presentationTheme in
if let strongSelf = self, let theme = theme {
let titleView = CounterControllerTitleView(theme: strongSelf.previewTheme)
titleView.title = CounterControllerTitle(title: themeName, counter: hasInstallsCount ? strongSelf.presentationData.strings.Theme_UsersCount(max(1, theme.installCount ?? 0)) : "")
strongSelf.navigationItem.titleView = titleView
strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationTheme: presentationTheme, presentationStrings: strongSelf.presentationData.strings))
}
})
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.presentationData = presentationData
}
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
self.disposable?.dispose()
self.applyDisposable.dispose()
}
@objc private func cancelPressed() {
self.dismiss(animated: true)
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation {
self.didPlayPresentationAnimation = true
if case .modalSheet = presentationArguments.presentationAnimation {
self.controllerNode.animateIn()
}
}
}
override public func loadDisplayNode() {
super.loadDisplayNode()
var isPreview = false
var forceReady = false
var initialWallpaper: TelegramWallpaper?
if case let .settings(_, currentWallpaper, preview) = self.source {
isPreview = preview
forceReady = true
if let wallpaper = currentWallpaper {
initialWallpaper = wallpaper
}
}
self.displayNode = ThemePreviewControllerNode(context: self.context, previewTheme: self.previewTheme, initialWallpaper: initialWallpaper, dismiss: { [weak self] in
if let strongSelf = self {
strongSelf.dismiss()
}
}, apply: { [weak self] in
if let strongSelf = self {
strongSelf.apply()
}
}, isPreview: isPreview, forceReady: forceReady, ready: self._ready)
self.displayNodeDidLoad()
let previewTheme = self.previewTheme
if let initialWallpaper = initialWallpaper {
self.controllerNode.wallpaperPromise.set(.single(initialWallpaper))
} else if case let .file(file) = previewTheme.chat.defaultWallpaper, file.id == 0 {
self.controllerNode.wallpaperPromise.set(cachedWallpaper(account: self.context.account, slug: file.slug, settings: file.settings)
|> mapToSignal { wallpaper in
return .single(wallpaper?.wallpaper ?? .color(previewTheme.chatList.backgroundColor.argb))
})
} else {
self.controllerNode.wallpaperPromise.set(.single(previewTheme.chat.defaultWallpaper))
}
}
private func apply() {
if let customApply = self.customApply {
customApply()
Queue.mainQueue().after(0.2) {
self.dismiss()
}
return
}
let previewTheme = self.previewTheme
let theme: Signal<PresentationThemeReference?, NoError>
let context = self.context
let wallpaperPromise = self.controllerNode.wallpaperPromise
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let disposable = self.applyDisposable
switch self.source {
case let .settings(reference, _, _):
theme = .single(reference)
case .theme, .slug, .themeSettings:
theme = combineLatest(self.theme.get() |> take(1), wallpaperPromise.get() |> take(1))
|> mapToSignal { theme, wallpaper -> Signal<PresentationThemeReference?, NoError> in
if let theme = theme {
if case let .file(file) = wallpaper, file.id != 0 {
return .single(.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: wallpaper, creatorAccountId: theme.isCreator ? context.account.id : nil)))
} else {
return .single(.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: theme.isCreator ? context.account.id : nil)))
}
} else {
return .complete()
}
}
case .media:
if let strings = encodePresentationTheme(previewTheme), let data = strings.data(using: .utf8) {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data)
theme = .single(.local(PresentationLocalTheme(title: previewTheme.name.string, resource: resource, resolvedWallpaper: nil)))
} else {
theme = .single(.builtin(.dayClassic))
}
}
var resolvedWallpaper: TelegramWallpaper?
let setup = theme
|> mapToSignal { theme -> Signal<(PresentationThemeReference, Bool), NoError> in
guard let theme = theme else {
return .complete()
}
switch theme {
case let .cloud(info):
resolvedWallpaper = info.resolvedWallpaper
return telegramThemes(postbox: context.account.postbox, network: context.account.network, accountManager: context.sharedContext.accountManager)
|> take(1)
|> map { themes -> Bool in
if let _ = themes.first(where: { $0.id == info.theme.id }) {
return true
} else {
return false
}
}
|> map { exists in
return (theme, exists)
}
case let .local(info):
return wallpaperPromise.get()
|> take(1)
|> mapToSignal { currentWallpaper -> Signal<(PresentationThemeReference, Bool), NoError> in
if case let .file(file) = currentWallpaper, file.id != 0 {
resolvedWallpaper = currentWallpaper
}
var wallpaperImage: UIImage?
if case .file = currentWallpaper {
wallpaperImage = chatControllerBackgroundImage(theme: previewTheme, wallpaper: currentWallpaper, mediaBox: context.sharedContext.accountManager.mediaBox, knockoutMode: false)
}
let themeThumbnail = generateImage(CGSize(width: 213, height: 320.0), contextGenerator: { size, context in
if let image = generateImage(CGSize(width: 194.0, height: 291.0), contextGenerator: { size, c in
drawThemeImage(context: c, theme: previewTheme, wallpaperImage: wallpaperImage, size: size)
})?.cgImage {
context.draw(image, in: CGRect(origin: CGPoint(), size: size))
}
}, scale: 1.0)
let themeThumbnailData = themeThumbnail?.jpegData(compressionQuality: 0.6)
return telegramThemes(postbox: context.account.postbox, network: context.account.network, accountManager: context.sharedContext.accountManager)
|> take(1)
|> mapToSignal { themes -> Signal<(PresentationThemeReference, Bool), NoError> in
let similarTheme = themes.first(where: { $0.isCreator && $0.title == info.title })
if let similarTheme = similarTheme {
return updateTheme(account: context.account, accountManager: context.sharedContext.accountManager, theme: similarTheme, title: nil, slug: nil, resource: info.resource, thumbnailData: themeThumbnailData, settings: nil)
|> map(Optional.init)
|> `catch` { _ -> Signal<CreateThemeResult?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<(PresentationThemeReference, Bool), NoError> in
guard let result = result else {
let updatedTheme = PresentationLocalTheme(title: info.title, resource: info.resource, resolvedWallpaper: resolvedWallpaper)
return .single((.local(updatedTheme), true))
}
if case let .result(theme) = result, let file = theme.file {
context.sharedContext.accountManager.mediaBox.moveResourceData(from: info.resource.id, to: file.resource.id)
return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: theme.isCreator ? context.account.id : nil)), true))
} else {
return .complete()
}
}
} else {
return createTheme(account: context.account, title: info.title, resource: info.resource, thumbnailData: themeThumbnailData, settings: nil)
|> map(Optional.init)
|> `catch` { _ -> Signal<CreateThemeResult?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<(PresentationThemeReference, Bool), NoError> in
guard let result = result else {
let updatedTheme = PresentationLocalTheme(title: info.title, resource: info.resource, resolvedWallpaper: resolvedWallpaper)
return .single((.local(updatedTheme), true))
}
if case let .result(updatedTheme) = result, let file = updatedTheme.file {
context.sharedContext.accountManager.mediaBox.moveResourceData(from: info.resource.id, to: file.resource.id)
return .single((.cloud(PresentationCloudTheme(theme: updatedTheme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: updatedTheme.isCreator ? context.account.id : nil)), true))
} else {
return .complete()
}
}
}
}
}
case .builtin:
return .single((theme, true))
}
}
|> mapToSignal { updatedTheme, existing -> Signal<(PresentationThemeReference, PresentationThemeAccentColor?, Bool, PresentationThemeReference, Bool)?, NoError> in
if case let .cloud(info) = updatedTheme {
let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: info.theme).start()
if info.theme.emoticon == nil {
let _ = saveThemeInteractively(account: context.account, accountManager: context.sharedContext.accountManager, theme: info.theme).start()
}
}
let autoNightModeTriggered = context.sharedContext.currentPresentationData.with { $0 }.autoNightModeTriggered
return context.sharedContext.accountManager.transaction { transaction -> (PresentationThemeReference, PresentationThemeAccentColor?, Bool, PresentationThemeReference, Bool)? in
var previousDefaultTheme: (PresentationThemeReference, PresentationThemeAccentColor?, Bool, PresentationThemeReference, Bool)?
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.presentationThemeSettings, { entry in
let currentSettings: PresentationThemeSettings
if let entry = entry?.get(PresentationThemeSettings.self) {
currentSettings = entry
} else {
currentSettings = PresentationThemeSettings.defaultSettings
}
var updatedSettings: PresentationThemeSettings
if autoNightModeTriggered {
if case .builtin = currentSettings.automaticThemeSwitchSetting.theme {
previousDefaultTheme = (currentSettings.automaticThemeSwitchSetting.theme, currentSettings.themeSpecificAccentColors[currentSettings.automaticThemeSwitchSetting.theme.index], true, updatedTheme, existing)
}
var automaticThemeSwitchSetting = currentSettings.automaticThemeSwitchSetting
automaticThemeSwitchSetting.theme = updatedTheme
updatedSettings = currentSettings.withUpdatedAutomaticThemeSwitchSetting(automaticThemeSwitchSetting)
} else {
if case .builtin = currentSettings.theme {
previousDefaultTheme = (currentSettings.theme, currentSettings.themeSpecificAccentColors[currentSettings.theme.index], false, updatedTheme, existing)
}
updatedSettings = currentSettings.withUpdatedTheme(updatedTheme)
}
var themeSpecificAccentColors = updatedSettings.themeSpecificAccentColors
if case let .cloud(info) = updatedTheme, let settings = info.theme.settings?.first {
let baseThemeReference = PresentationThemeReference.builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme))
themeSpecificAccentColors[baseThemeReference.index] = PresentationThemeAccentColor(themeIndex: updatedTheme.index)
}
var themeSpecificChatWallpapers = updatedSettings.themeSpecificChatWallpapers
themeSpecificChatWallpapers[updatedTheme.index] = nil
return PreferencesEntry(updatedSettings.withUpdatedThemeSpecificChatWallpapers(themeSpecificChatWallpapers).withUpdatedThemeSpecificAccentColors(themeSpecificAccentColors))
})
return previousDefaultTheme
}
}
var cancelImpl: (() -> Void)?
let progress = Signal<Never, NoError> { [weak self] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
self?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.35, queue: Queue.mainQueue())
let progressDisposable = progress.start()
cancelImpl = {
disposable.set(nil)
}
disposable.set((setup
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
|> deliverOnMainQueue).start(next: { [weak self] previousDefaultTheme in
if let strongSelf = self, let layout = strongSelf.validLayout {
Queue.mainQueue().after(0.3) {
if case .settings = strongSelf.source {
} else if layout.size.width >= 375.0 {
let navigationController = strongSelf.navigationController as? NavigationController
if let (previousDefaultTheme, previousAccentColor, autoNightMode, theme, _) = previousDefaultTheme {
let _ = (ApplicationSpecificNotice.getThemeChangeTip(accountManager: strongSelf.context.sharedContext.accountManager)
|> deliverOnMainQueue).start(next: { [weak self] displayed in
guard let strongSelf = self, !displayed else {
return
}
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .actionSucceeded(title: strongSelf.presentationData.strings.Theme_ThemeChanged, text: strongSelf.presentationData.strings.Theme_ThemeChangedText, cancel: strongSelf.presentationData.strings.Undo_Undo, destructive: false), elevatedLayout: true, animateInAsReplacement: false, action: { value in
if value == .undo {
Queue.mainQueue().after(0.2) {
let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current -> PresentationThemeSettings in
var updated: PresentationThemeSettings
if autoNightMode {
var automaticThemeSwitchSetting = current.automaticThemeSwitchSetting
automaticThemeSwitchSetting.theme = previousDefaultTheme
updated = current.withUpdatedAutomaticThemeSwitchSetting(automaticThemeSwitchSetting)
} else {
updated = current.withUpdatedTheme(previousDefaultTheme)
}
var themeSpecificAccentColors = current.themeSpecificAccentColors
themeSpecificAccentColors[previousDefaultTheme.index] = previousAccentColor
updated = updated.withUpdatedThemeSpecificAccentColors(themeSpecificAccentColors)
return updated
}).start()
}
if case let .cloud(info) = theme {
let _ = deleteThemeInteractively(account: context.account, accountManager: context.sharedContext.accountManager, theme: info.theme).start()
}
return true
} else if value == .info {
let controller = themeSettingsController(context: context)
controller.navigationPresentation = .modal
navigationController?.pushViewController(controller, animated: true)
return true
}
return false
}), in: .window(.root))
ApplicationSpecificNotice.markThemeChangeTipAsSeen(accountManager: strongSelf.context.sharedContext.accountManager)
})
}
}
strongSelf.dismiss()
}
}
}))
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.validLayout = layout
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@objc private func actionPressed() {
let subject: ShareControllerSubject
let preferredAction: ShareControllerPreferredAction
switch self.source {
case .settings:
return
case let .theme(theme):
subject = .url("https://t.me/addtheme/\(theme.slug)")
preferredAction = .default
case let .slug(slug, _), let .themeSettings(slug, _):
subject = .url("https://t.me/addtheme/\(slug)")
preferredAction = .default
case let .media(media):
subject = .media(media, nil)
preferredAction = .default
}
let controller = ShareController(context: self.context, subject: subject, preferredAction: preferredAction)
self.present(controller, in: .window(.root), blockInteraction: true)
}
}
@@ -0,0 +1,757 @@
import Foundation
import UIKit
import Display
import Postbox
import SwiftSignalKit
import AsyncDisplayKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import ChatListUI
import WallpaperResources
import LegacyComponents
import WallpaperBackgroundNode
import AnimationCache
import MultiAnimationRenderer
import WallpaperGalleryScreen
private func generateMaskImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let gradientColors = [color.withAlphaComponent(0.0).cgColor, color.cgColor, color.cgColor] as CFArray
var locations: [CGFloat] = [0.0, 0.75, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 80.0), options: CGGradientDrawingOptions())
})
}
final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate {
private let context: AccountContext
private var previewTheme: PresentationTheme
private var presentationData: PresentationData
private let isPreview: Bool
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
private let ready: Promise<Bool>
public let wallpaperPromise = Promise<TelegramWallpaper>()
private let referenceTimestamp: Int32
private let scrollNode: ASScrollNode
private let pageControlBackgroundNode: ASDisplayNode
private let pageControlNode: PageControlNode
private let chatListBackgroundNode: ASDisplayNode
private var chatNodes: [ListViewItemNode]?
private let maskNode: ASImageNode
private let separatorNode: ASDisplayNode
private let chatContainerNode: ASDisplayNode
private let messagesContainerNode: ASDisplayNode
private let instantChatBackgroundNode: WallpaperBackgroundNode
private let remoteChatBackgroundNode: TransformImageNode
private let blurredNode: BlurredImageNode
private let wallpaperNode: WallpaperBackgroundNode
private var dateHeaderNode: ListViewItemHeaderNode?
private var messageNodes: [ListViewItemNode]?
private let toolbarNode: WallpaperGalleryToolbarNode
private var validLayout: (ContainerViewLayout, CGFloat)?
private var wallpaperDisposable: Disposable?
private var colorDisposable: Disposable?
private var statusDisposable: Disposable?
private var fetchDisposable = MetaDisposable()
private var dismissed = false
private var wallpaper: TelegramWallpaper
init(context: AccountContext, previewTheme: PresentationTheme, initialWallpaper: TelegramWallpaper?, dismiss: @escaping () -> Void, apply: @escaping () -> Void, isPreview: Bool, forceReady: Bool, ready: Promise<Bool>) {
self.context = context
self.previewTheme = previewTheme
self.isPreview = isPreview
self.wallpaper = initialWallpaper ?? previewTheme.chat.defaultWallpaper
self.ready = ready
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
let calendar = Calendar(identifier: .gregorian)
var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: Date())
components.hour = 13
components.minute = 0
components.second = 0
self.referenceTimestamp = Int32(calendar.date(from: components)?.timeIntervalSince1970 ?? 0.0)
self.scrollNode = ASScrollNode()
self.pageControlBackgroundNode = ASDisplayNode()
self.pageControlBackgroundNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.3)
self.pageControlBackgroundNode.cornerRadius = 10.5
self.pageControlNode = PageControlNode(dotSpacing: 7.0, dotColor: .white, inactiveDotColor: UIColor.white.withAlphaComponent(0.4))
self.chatListBackgroundNode = ASDisplayNode()
self.chatContainerNode = ASDisplayNode()
self.chatContainerNode.clipsToBounds = true
self.messagesContainerNode = ASDisplayNode()
self.messagesContainerNode.clipsToBounds = true
self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
self.instantChatBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: false)
self.instantChatBackgroundNode.displaysAsynchronously = false
self.ready.set(.single(true))
self.instantChatBackgroundNode.update(wallpaper: wallpaper, animated: false)
self.instantChatBackgroundNode.view.contentMode = .scaleAspectFill
self.remoteChatBackgroundNode = TransformImageNode()
self.remoteChatBackgroundNode.view.contentMode = .scaleAspectFill
self.blurredNode = BlurredImageNode()
self.blurredNode.blurView.contentMode = .scaleAspectFill
self.wallpaperNode = createWallpaperBackgroundNode(context: context, forChatDisplay: false)
self.toolbarNode = WallpaperGalleryToolbarNode(theme: self.previewTheme, strings: self.presentationData.strings, doneButtonType: .set)
if case .file = previewTheme.chat.defaultWallpaper, !forceReady {
self.toolbarNode.setDoneEnabled(false)
}
self.maskNode = ASImageNode()
self.maskNode.displaysAsynchronously = false
self.maskNode.displayWithoutProcessing = true
self.maskNode.contentMode = .scaleToFill
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = previewTheme.rootController.tabBar.separatorColor
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = self.previewTheme.list.plainBackgroundColor
self.chatListBackgroundNode.backgroundColor = self.previewTheme.chatList.backgroundColor
self.maskNode.image = generateMaskImage(color: self.previewTheme.chatList.backgroundColor)
if case let .color(value) = self.wallpaper {
self.instantChatBackgroundNode.backgroundColor = UIColor(rgb: value)
}
self.pageControlNode.isUserInteractionEnabled = false
self.pageControlNode.pagesCount = 2
self.addSubnode(self.scrollNode)
if !isPreview {
self.chatListBackgroundNode.addSubnode(self.maskNode)
self.addSubnode(self.pageControlBackgroundNode)
self.addSubnode(self.pageControlNode)
self.addSubnode(self.toolbarNode)
}
self.scrollNode.addSubnode(self.chatListBackgroundNode)
self.scrollNode.addSubnode(self.chatContainerNode)
self.chatContainerNode.addSubnode(self.instantChatBackgroundNode)
self.chatContainerNode.addSubnode(self.remoteChatBackgroundNode)
self.chatContainerNode.addSubnode(self.messagesContainerNode)
self.addSubnode(self.separatorNode)
self.toolbarNode.cancel = {
dismiss()
}
self.toolbarNode.done = { [weak self] _ in
if let strongSelf = self {
if !strongSelf.dismissed {
strongSelf.dismissed = true
apply()
}
}
}
var gradientColors: [UInt32] = []
if case let .file(file) = self.wallpaper {
gradientColors = file.settings.colors
if file.settings.blur {
self.chatContainerNode.insertSubnode(self.blurredNode, belowSubnode: self.messagesContainerNode)
}
} else if case let .gradient(gradient) = self.wallpaper {
gradientColors = gradient.colors
}
if gradientColors.count >= 3 {
self.chatContainerNode.insertSubnode(self.wallpaperNode, belowSubnode: self.messagesContainerNode)
}
self.wallpaperNode.update(wallpaper: self.wallpaper, animated: false)
self.wallpaperNode.updateBubbleTheme(bubbleTheme: self.previewTheme, bubbleCorners: self.presentationData.chatBubbleCorners)
self.remoteChatBackgroundNode.imageUpdated = { [weak self] image in
if let strongSelf = self, strongSelf.blurredNode.supernode != nil {
var image = image
if let imageToScale = image {
let actualSize = CGSize(width: imageToScale.size.width * imageToScale.scale, height: imageToScale.size.height * imageToScale.scale)
if actualSize.width > 1280.0 || actualSize.height > 1280.0 {
image = TGScaleImageToPixelSize(image, actualSize.fitted(CGSize(width: 1280.0, height: 1280.0)))
}
}
strongSelf.blurredNode.image = image
strongSelf.blurredNode.blurView.blurRadius = 45.0
}
self?.ready.set(.single(true))
}
self.colorDisposable = (self.wallpaperPromise.get()
|> mapToSignal { wallpaper -> Signal<UIColor, NoError> in
if case let .file(file) = wallpaper, file.id == 0 {
return .complete()
} else {
return chatServiceBackgroundColor(wallpaper: wallpaper, mediaBox: context.account.postbox.mediaBox)
}
}
|> deliverOnMainQueue).start(next: { [weak self] color in
if let strongSelf = self {
strongSelf.pageControlBackgroundNode.backgroundColor = color
}
})
self.wallpaperDisposable = (self.wallpaperPromise.get()
|> deliverOnMainQueue).start(next: { [weak self] wallpaper in
guard let strongSelf = self else {
return
}
var useDarkButton = true
if case let .file(file) = wallpaper {
let dimensions = file.file.dimensions ?? PixelDimensions(width: 100, height: 100)
let displaySize = dimensions.cgSize.dividedByScreenScale().integralFloor
var convertedRepresentations: [ImageRepresentationWithReference] = []
for representation in file.file.previewRepresentations {
convertedRepresentations.append(ImageRepresentationWithReference(representation: representation, reference: .wallpaper(wallpaper: .slug(file.slug), resource: representation.resource)))
}
convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)))
let signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>
if wallpaper.isPattern {
signal = .complete()
} else {
useDarkButton = false
signal = .complete()
}
strongSelf.remoteChatBackgroundNode.setSignal(signal)
strongSelf.fetchDisposable.set(fetchedMediaResource(mediaBox: context.sharedContext.accountManager.mediaBox, userLocation: .other, userContentType: .other, reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)).start())
let account = strongSelf.context.account
let statusSignal = strongSelf.context.sharedContext.accountManager.mediaBox.resourceStatus(file.file.resource)
|> take(1)
|> mapToSignal { status -> Signal<MediaResourceStatus, NoError> in
if case .Local = status {
return .single(status)
} else {
return account.postbox.mediaBox.resourceStatus(file.file.resource)
}
}
strongSelf.statusDisposable = (statusSignal
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self, case .Local = status {
strongSelf.toolbarNode.setDoneEnabled(true)
}
})
var patternArguments: PatternWallpaperArguments?
if !file.settings.colors.isEmpty {
var patternIntensity: CGFloat = 0.5
if let intensity = file.settings.intensity {
patternIntensity = CGFloat(intensity) / 100.0
}
var patternColors = [UIColor(rgb: file.settings.colors[0], alpha: patternIntensity)]
if file.settings.colors.count >= 2 {
patternColors.append(UIColor(rgb: file.settings.colors[1], alpha: patternIntensity))
}
patternArguments = PatternWallpaperArguments(colors: patternColors, rotation: file.settings.rotation)
}
strongSelf.remoteChatBackgroundNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets(), custom: patternArguments))()
strongSelf.toolbarNode.dark = useDarkButton
}
})
}
deinit {
self.colorDisposable?.dispose()
self.wallpaperDisposable?.dispose()
self.statusDisposable?.dispose()
self.fetchDisposable.dispose()
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.bounces = false
self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.isPagingEnabled = true
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.pageControlNode.setPage(0.0)
}
func updateTheme(_ theme: PresentationTheme) {
self.previewTheme = theme
self.backgroundColor = self.previewTheme.list.plainBackgroundColor
self.chatListBackgroundNode.backgroundColor = self.previewTheme.chatList.backgroundColor
self.maskNode.image = generateMaskImage(color: self.previewTheme.chatList.backgroundColor)
if case let .color(value) = self.wallpaper {
self.instantChatBackgroundNode.backgroundColor = UIColor(rgb: value)
}
self.toolbarNode.updateThemeAndStrings(theme: self.previewTheme, strings: self.presentationData.strings)
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let bounds = scrollView.bounds
if !bounds.width.isZero {
self.pageControlNode.setPage(scrollView.contentOffset.x / bounds.width)
}
}
func animateIn(completion: (() -> Void)? = nil) {
if let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass {
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
}
func animateOut(completion: (() -> Void)? = nil) {
if let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass {
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
completion?()
})
} else {
completion?()
}
}
private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) {
var items: [ChatListItem] = []
let interaction = ChatListNodeInteraction(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in
}, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {
}, openBirthdaySetup: {
}, performActiveSessionAction: { _, _ in
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
}, openStories: { _, _ in
}, openStarsTopup: { _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
}, openPhotoSetup: {
}, openAdInfo: { _, _ in
}, openAccountFreezeInfo: {
}, openUrl: { _ in
})
func makeChatListItem(
peer: EnginePeer,
author: EnginePeer,
timestamp: Int32,
text: String,
isPinned: Bool = false,
presenceTimestamp: Int32? = nil,
hasInputActivity: Bool = false,
unreadCount: Int32 = 0
) -> ChatListItem {
return ChatListItem(
presentationData: chatListPresentationData,
context: self.context,
chatListLocation: .chatList(groupId: .root),
filterData: nil,
index: .chatList(ChatListIndex(pinningIndex: isPinned ? 0 : nil, messageIndex: MessageIndex(id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 0), timestamp: timestamp))),
content: .peer(ChatListItemContent.PeerData(
messages: [
EngineMessage(
stableId: 0,
stableVersion: 0,
id: EngineMessage.Id(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 0),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: timestamp,
flags: author.id == peer.id ? [.Incoming] : [],
tags: [],
globalTags: [],
localTags: [],
customTags: [],
forwardInfo: nil,
author: author,
text: text,
attributes: [],
media: [],
peers: [:],
associatedMessages: [:],
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
],
peer: EngineRenderedPeer(peer: peer),
threadInfo: nil,
combinedReadState: EnginePeerReadCounters(incomingReadId: 1000, outgoingReadId: 1000, count: unreadCount, markedUnread: false),
isRemovedFromTotalUnreadCount: false,
presence: presenceTimestamp.flatMap { presenceTimestamp in
EnginePeer.Presence(status: .present(until: presenceTimestamp + 1000), lastActivity: presenceTimestamp)
},
hasUnseenMentions: false,
hasUnseenReactions: false,
draftState: nil,
mediaDraftContentType: nil,
inputActivities: hasInputActivity ? [(author, .typingText)] : [],
promoInfo: nil,
ignoreUnreadBadge: false,
displayAsMessage: false,
hasFailedMessages: false,
forumTopicData: nil,
topForumTopicItems: [],
autoremoveTimeout: nil,
storyState: nil,
requiresPremiumForMessaging: false,
displayAsTopicList: false,
tags: []
)),
editing: false,
hasActiveRevealControls: false,
selected: false,
header: nil,
enabledContextActions: nil,
hiddenOffset: false,
interaction: interaction
)
}
let chatListPresentationData = ChatListPresentationData(theme: self.previewTheme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)
let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil, verificationIconFileId: nil, sendPaidMessageStars: nil, linkedMonoforumId: nil))
let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil, verificationIconFileId: nil, sendPaidMessageStars: nil, linkedMonoforumId: nil))
let peer6: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer7: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(6)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_7_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let timestamp = self.referenceTimestamp
let timestamp1 = timestamp + 120
items.append(makeChatListItem(
peer: peer1,
author: selfPeer,
timestamp: timestamp1,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Text
))
let presenceTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + 60 * 60)
let timestamp2 = timestamp + 3660
items.append(makeChatListItem(
peer: peer2,
author: peer2,
timestamp: timestamp2,
text: "",
presenceTimestamp: presenceTimestamp,
hasInputActivity: true
))
let timestamp3 = timestamp + 3200
items.append(makeChatListItem(
peer: peer3,
author: peer3Author,
timestamp: timestamp3,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Text
))
let timestamp4 = timestamp + 3000
items.append(makeChatListItem(
peer: peer4,
author: peer4,
timestamp: timestamp4,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Text
))
let timestamp5 = timestamp + 1000
items.append(makeChatListItem(
peer: peer5,
author: peer5,
timestamp: timestamp5,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Text
))
items.append(makeChatListItem(
peer: peer6,
author: peer6,
timestamp: timestamp - 360,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Text
))
items.append(makeChatListItem(
peer: peer7,
author: peer6,
timestamp: timestamp - 420,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_7_Text
))
let width: CGFloat
if case .regular = layout.metrics.widthClass {
width = layout.size.width / 2.0
} else {
width = layout.size.width
}
let params = ListViewItemLayoutParams(width: width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height)
if let chatNodes = self.chatNodes {
for i in 0 ..< items.count {
let itemNode = chatNodes[i]
items[i].updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
apply(ListViewItemApply(isOnScreen: true))
})
}
} else {
var chatNodes: [ListViewItemNode] = []
for i in 0 ..< items.count {
var itemNode: ListViewItemNode?
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
itemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode!.isUserInteractionEnabled = false
chatNodes.append(itemNode!)
if self.maskNode.supernode != nil {
self.chatListBackgroundNode.insertSubnode(itemNode!, belowSubnode: self.maskNode)
} else {
self.chatListBackgroundNode.addSubnode(itemNode!)
}
}
self.chatNodes = chatNodes
}
if let chatNodes = self.chatNodes {
var topOffset: CGFloat = topInset
for itemNode in chatNodes {
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: itemNode.frame.size))
topOffset += itemNode.frame.height
}
}
}
private func updateMessagesLayout(layout: ContainerViewLayout, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
let headerItem = self.context.sharedContext.makeChatMessageDateHeaderItem(context: self.context, timestamp: self.referenceTimestamp, theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder)
var items: [ListViewItem] = []
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1))
let otherPeerId = self.context.account.peerId
var peers = SimpleDictionary<PeerId, Peer>()
var messages = SimpleDictionary<MessageId, Message>()
peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .preset(.blue), backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .preset(.blue), backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
var sampleMessages: [Message] = []
let message1 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_4_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
sampleMessages.append(message1)
let message2 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_5_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
sampleMessages.append(message2)
let message3 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_6_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
sampleMessages.append(message3)
let message4 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_7_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
messages[message4.id] = message4
sampleMessages.append(message4)
let message5 = Message(stableId: 5, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 5), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66004, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [ReplyMessageAttribute(messageId: message4.id, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
messages[message5.id] = message5
sampleMessages.append(message5)
let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA="
let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)]
let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes, alternativeRepresentations: [])
let message6 = Message(stableId: 6, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 6), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66005, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
sampleMessages.append(message6)
let message7 = Message(stableId: 7, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 7), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66006, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: message5.id, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
sampleMessages.append(message7)
let message8 = Message(stableId: 8, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 8), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66007, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
sampleMessages.append(message8)
items = sampleMessages.reversed().map { message in
self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message], theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.wallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: !message.media.isEmpty ? FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local) : nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)
}
let width: CGFloat
if case .regular = layout.metrics.widthClass {
width = layout.size.width / 2.0
} else {
width = layout.size.width
}
let params = ListViewItemLayoutParams(width: width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height)
if let messageNodes = self.messageNodes {
for i in 0 ..< items.count {
let itemNode = messageNodes[i]
items[i].updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
apply(ListViewItemApply(isOnScreen: true))
})
}
} else {
var messageNodes: [ListViewItemNode] = []
for i in 0 ..< items.count {
var itemNode: ListViewItemNode?
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
itemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
itemNode!.isUserInteractionEnabled = false
messageNodes.append(itemNode!)
self.messagesContainerNode.addSubnode(itemNode!)
}
self.messageNodes = messageNodes
}
var bottomOffset: CGFloat = 9.0 + bottomInset
if let messageNodes = self.messageNodes {
for itemNode in messageNodes {
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: itemNode.frame.size))
bottomOffset += itemNode.frame.height
itemNode.updateFrame(itemNode.frame, within: layout.size)
}
}
let dateHeaderNode: ListViewItemHeaderNode
if let currentDateHeaderNode = self.dateHeaderNode {
dateHeaderNode = currentDateHeaderNode
headerItem.updateNode(dateHeaderNode, previous: nil, next: headerItem)
} else {
dateHeaderNode = headerItem.node(synchronousLoad: true)
dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
self.messagesContainerNode.addSubnode(dateHeaderNode)
self.dateHeaderNode = dateHeaderNode
}
transition.updateFrame(node: dateHeaderNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: CGSize(width: layout.size.width, height: headerItem.height)))
dateHeaderNode.updateLayout(size: self.messagesContainerNode.frame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: .immediate)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
let bounds = CGRect(origin: CGPoint(), size: layout.size)
self.scrollNode.frame = bounds
let toolbarHeight = 49.0 + layout.intrinsicInsets.bottom
self.chatListBackgroundNode.frame = CGRect(x: bounds.width, y: 0.0, width: bounds.width, height: bounds.height)
self.chatContainerNode.frame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)
let bottomInset: CGFloat
if case .regular = layout.metrics.widthClass {
self.chatListBackgroundNode.frame = CGRect(x: 0.0, y: 0.0, width: bounds.width / 2.0, height: bounds.height)
self.chatContainerNode.frame = CGRect(x: bounds.width / 2.0, y: 0.0, width: bounds.width / 2.0, height: bounds.height)
self.scrollNode.view.contentSize = CGSize(width: bounds.width, height: bounds.height)
self.pageControlNode.isHidden = true
self.pageControlBackgroundNode.isHidden = true
self.separatorNode.isHidden = false
self.separatorNode.frame = CGRect(x: bounds.width / 2.0, y: 0.0, width: UIScreenPixel, height: bounds.height - toolbarHeight)
bottomInset = 0.0
} else {
self.chatListBackgroundNode.frame = CGRect(x: bounds.width, y: 0.0, width: bounds.width, height: bounds.height)
self.chatContainerNode.frame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)
self.scrollNode.view.contentSize = CGSize(width: bounds.width * 2.0, height: bounds.height)
self.pageControlNode.isHidden = false
self.pageControlBackgroundNode.isHidden = false
self.separatorNode.isHidden = true
bottomInset = 38.0
}
self.messagesContainerNode.frame = self.chatContainerNode.bounds
self.instantChatBackgroundNode.frame = self.chatContainerNode.bounds
self.instantChatBackgroundNode.updateLayout(size: self.instantChatBackgroundNode.bounds.size, displayMode: .aspectFill, transition: .immediate)
self.remoteChatBackgroundNode.frame = self.chatContainerNode.bounds
self.blurredNode.frame = self.chatContainerNode.bounds
self.wallpaperNode.frame = self.chatContainerNode.bounds
self.wallpaperNode.updateLayout(size: self.wallpaperNode.bounds.size, displayMode: .aspectFill, transition: .immediate)
transition.updateFrame(node: self.toolbarNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: toolbarHeight)))
self.toolbarNode.updateLayout(size: CGSize(width: layout.size.width, height: 49.0), layout: layout, transition: transition)
self.updateChatsLayout(layout: layout, topInset: navigationBarHeight, transition: transition)
self.updateMessagesLayout(layout: layout, bottomInset: self.isPreview ? 0.0 : (toolbarHeight + bottomInset), transition: transition)
let pageControlSize = self.pageControlNode.measure(CGSize(width: bounds.width, height: 100.0))
let pageControlFrame = CGRect(origin: CGPoint(x: floor((bounds.width - pageControlSize.width) / 2.0), y: layout.size.height - toolbarHeight - 28.0), size: pageControlSize)
self.pageControlNode.frame = pageControlFrame
self.pageControlBackgroundNode.frame = CGRect(x: pageControlFrame.minX - 7.0, y: pageControlFrame.minY - 7.0, width: pageControlFrame.width + 14.0, height: 21.0)
transition.updateFrame(node: self.maskNode, frame: CGRect(x: 0.0, y: layout.size.height - toolbarHeight - 80.0, width: bounds.width, height: 80.0))
}
}
@@ -0,0 +1,971 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import MergeLists
import ItemListUI
import ContextUI
import PresentationDataUtils
private enum ThemeSettingsColorEntryId: Hashable {
case color(Int64)
case theme(Int64)
case picker
}
private enum ThemeSettingsColorEntry: Comparable, Identifiable {
case color(Int, PresentationTheme, PresentationThemeReference, PresentationThemeAccentColor?, Bool)
case theme(Int, PresentationTheme, PresentationThemeReference, PresentationThemeReference, Bool)
case picker
var stableId: ThemeSettingsColorEntryId {
switch self {
case let .color(_, _, themeReference, accentColor, _):
return .color(themeReference.index &+ Int64(accentColor?.index ?? 0))
case let .theme(_, _, _, theme, _):
return .theme(theme.index)
case .picker:
return .picker
}
}
static func ==(lhs: ThemeSettingsColorEntry, rhs: ThemeSettingsColorEntry) -> Bool {
switch lhs {
case let .color(lhsIndex, lhsCurrentTheme, lhsThemeReference, lhsAccentColor, lhsSelected):
if case let .color(rhsIndex, rhsCurrentTheme, rhsThemeReference, rhsAccentColor, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsCurrentTheme === rhsCurrentTheme, lhsThemeReference.index == rhsThemeReference.index, lhsAccentColor == rhsAccentColor, lhsSelected == rhsSelected {
return true
} else {
return false
}
case let .theme(lhsIndex, lhsCurrentTheme, lhsBaseThemeReference, lhsTheme, lhsSelected):
if case let .theme(rhsIndex, rhsCurrentTheme, rhsBaseThemeReference, rhsTheme, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsCurrentTheme === rhsCurrentTheme, lhsBaseThemeReference.index == rhsBaseThemeReference.index, lhsTheme == rhsTheme, lhsSelected == rhsSelected {
return true
} else {
return false
}
case .picker:
if case .picker = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: ThemeSettingsColorEntry, rhs: ThemeSettingsColorEntry) -> Bool {
switch lhs {
case .picker:
return true
case let .color(lhsIndex, _, _, _, _), let .theme(lhsIndex, _, _, _, _):
switch rhs {
case let .color(rhsIndex, _, _, _, _):
return lhsIndex < rhsIndex
case let .theme(rhsIndex, _, _, _, _):
return lhsIndex < rhsIndex
case .picker:
return false
}
}
}
func item(action: @escaping (ThemeSettingsColorOption?, Bool) -> Void, contextAction: ((ThemeSettingsColorOption?, Bool, ASDisplayNode, ContextGesture?) -> Void)?, openColorPicker: @escaping (Bool) -> Void) -> ListViewItem {
switch self {
case let .color(_, currentTheme, themeReference, accentColor, selected):
return ThemeSettingsAccentColorIconItem(themeReference: themeReference, theme: currentTheme, color: accentColor.flatMap { .accentColor($0) }, selected: selected, action: action, contextAction: contextAction)
case let .theme(_, currentTheme, baseThemeReference, theme, selected):
return ThemeSettingsAccentColorIconItem(themeReference: baseThemeReference, theme: currentTheme, color: .theme(theme), selected: selected, action: action, contextAction: contextAction)
case .picker:
return ThemeSettingsAccentColorPickerItem(action: openColorPicker)
}
}
}
enum ThemeSettingsColorOption: Equatable {
case accentColor(PresentationThemeAccentColor)
case theme(PresentationThemeReference)
var accentColor: UIColor? {
switch self {
case let .accentColor(color):
return color.color
case let .theme(reference):
if case let .cloud(theme) = reference, let settings = theme.theme.settings?.first {
return UIColor(argb: settings.accentColor)
} else {
return nil
}
}
}
var baseColor: UIColor? {
switch self {
case let .accentColor(color):
return color.baseColor.color
case .theme:
return .clear
}
}
var plainBubbleColors: [UInt32] {
switch self {
case let .accentColor(color):
return color.plainBubbleColors
case let .theme(reference):
if case let .cloud(theme) = reference, let settings = theme.theme.settings?.first, !settings.messageColors.isEmpty {
return settings.messageColors
} else {
return []
}
}
}
var customBubbleColors: [UInt32] {
switch self {
case let .accentColor(color):
return color.customBubbleColors
case let .theme(reference):
if case let .cloud(theme) = reference, let settings = theme.theme.settings?.first, !settings.messageColors.isEmpty {
return settings.messageColors
} else {
return []
}
}
}
var wallpaper: TelegramWallpaper? {
switch self {
case let .accentColor(color):
return color.wallpaper
case .theme:
return nil
}
}
var index: Int64 {
switch self {
case let .accentColor(color):
return Int64(color.index)
case let .theme(reference):
return reference.index
}
}
}
private class ThemeSettingsAccentColorIconItem: ListViewItem {
let themeReference: PresentationThemeReference
let theme: PresentationTheme
let color: ThemeSettingsColorOption?
let selected: Bool
let action: (ThemeSettingsColorOption?, Bool) -> Void
let contextAction: ((ThemeSettingsColorOption?, Bool, ASDisplayNode, ContextGesture?) -> Void)?
public init(themeReference: PresentationThemeReference, theme: PresentationTheme, color: ThemeSettingsColorOption?, selected: Bool, action: @escaping (ThemeSettingsColorOption?, Bool) -> Void, contextAction: ((ThemeSettingsColorOption?, Bool, ASDisplayNode, ContextGesture?) -> Void)?) {
self.themeReference = themeReference
self.theme = theme
self.color = color
self.selected = selected
self.action = action
self.contextAction = contextAction
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ThemeSettingsAccentColorIconItemNode()
let (nodeLayout, apply) = node.asyncLayout()(self, params)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
apply(false)
})
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
assert(node() is ThemeSettingsAccentColorIconItemNode)
if let nodeValue = node() as? ThemeSettingsAccentColorIconItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
let animated: Bool
if case .Crossfade = animation {
animated = true
} else {
animated = false
}
apply(animated)
})
}
}
}
}
}
public var selectable = true
public func selected(listView: ListView) {
self.action(self.color, self.selected)
}
}
private func generateRingImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setStrokeColor(color.cgColor)
context.setLineWidth(2.0)
context.strokeEllipse(in: bounds.insetBy(dx: 1.0, dy: 1.0))
})
}
private func generateFillImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(color.cgColor)
context.fillEllipse(in: bounds.insetBy(dx: 4.0, dy: 4.0))
})
}
private func generateCenterImage(topColor: UIColor, bottomColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.addEllipse(in: bounds.insetBy(dx: 10.0, dy: 10.0))
context.clip()
let gradientColors = [topColor.cgColor, bottomColor.cgColor] as CFArray
var locations: [CGFloat] = [0.0, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 10.0), end: CGPoint(x: 0.0, y: size.height - 10.0), options: CGGradientDrawingOptions())
})
}
private func generateDotsImage() -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(UIColor.white.cgColor)
let dotSize = CGSize(width: 4.0, height: 4.0)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 11.0, y: 18.0), size: dotSize))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 18.0, y: 18.0), size: dotSize))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 25.0, y: 18.0), size: dotSize))
})
}
private final class ThemeSettingsAccentColorIconItemNode : ListViewItemNode {
private let containerNode: ContextControllerSourceNode
private let fillNode: ASImageNode
private let ringNode: ASImageNode
private let centerNode: ASImageNode
private let dotsNode: ASImageNode
var item: ThemeSettingsAccentColorIconItem?
init() {
self.containerNode = ContextControllerSourceNode()
self.fillNode = ASImageNode()
self.fillNode.displaysAsynchronously = false
self.fillNode.displayWithoutProcessing = true
self.ringNode = ASImageNode()
self.ringNode.displaysAsynchronously = false
self.ringNode.displayWithoutProcessing = true
self.centerNode = ASImageNode()
self.centerNode.displaysAsynchronously = false
self.centerNode.displayWithoutProcessing = true
self.dotsNode = ASImageNode()
self.dotsNode.displaysAsynchronously = false
self.dotsNode.displayWithoutProcessing = true
self.dotsNode.image = generateDotsImage()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.fillNode)
self.containerNode.addSubnode(self.ringNode)
self.containerNode.addSubnode(self.dotsNode)
self.containerNode.addSubnode(self.centerNode)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.item else {
gesture.cancel()
return
}
item.contextAction?(item.color, item.selected, strongSelf.containerNode, gesture)
}
}
override func didLoad() {
super.didLoad()
self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
}
func setSelected(_ selected: Bool, animated: Bool = false) {
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate
if selected {
transition.updateTransformScale(node: self.fillNode, scale: 1.0)
transition.updateTransformScale(node: self.centerNode, scale: 0.16)
transition.updateAlpha(node: self.centerNode, alpha: 0.0)
transition.updateTransformScale(node: self.dotsNode, scale: 1.0)
transition.updateAlpha(node: self.dotsNode, alpha: 1.0)
} else {
transition.updateTransformScale(node: self.fillNode, scale: 1.2)
transition.updateTransformScale(node: self.centerNode, scale: 1.0)
transition.updateAlpha(node: self.centerNode, alpha: 1.0)
transition.updateTransformScale(node: self.dotsNode, scale: 0.85)
transition.updateAlpha(node: self.dotsNode, alpha: 0.0)
}
}
func asyncLayout() -> (ThemeSettingsAccentColorIconItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let currentItem = self.item
return { [weak self] item, params in
var updatedAccentColor = false
var updatedSelected = false
if currentItem == nil || currentItem?.color != item.color || currentItem?.themeReference != item.themeReference {
updatedAccentColor = true
}
if currentItem?.selected != item.selected {
updatedSelected = true
}
let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 60.0, height: 58.0), insets: UIEdgeInsets())
return (itemLayout, { animated in
if let strongSelf = self {
strongSelf.item = item
if updatedAccentColor {
var fillColor = item.color?.accentColor
var strokeColor = item.color?.baseColor
if strokeColor == .clear {
strokeColor = fillColor
}
if let color = strokeColor, color.distance(to: item.theme.list.itemBlocksBackgroundColor) < 200 {
if color.distance(to: UIColor.white) < 200 {
strokeColor = UIColor(rgb: 0x999999)
} else {
strokeColor = item.theme.list.controlSecondaryColor
}
}
var topColor: UIColor?
var bottomColor: UIColor?
if let colors = item.color?.plainBubbleColors, !colors.isEmpty {
topColor = UIColor(rgb: colors[0])
bottomColor = UIColor(rgb: colors.last ?? colors[0])
} else if case .builtin(.dayClassic) = item.themeReference {
if let accentColor = item.color?.accentColor {
let hsb = accentColor.hsb
let bubbleColor = UIColor(hue: hsb.0, saturation: (hsb.1 > 0.0 && hsb.2 > 0.0) ? 0.14 : 0.0, brightness: 0.79 + hsb.2 * 0.21, alpha: 1.0)
topColor = bubbleColor
bottomColor = bubbleColor
} else {
fillColor = UIColor(rgb: 0x0088ff)
strokeColor = fillColor
topColor = UIColor(rgb: 0xe1ffc7)
bottomColor = topColor
}
} else if case .builtin(.nightAccent) = item.themeReference {
if let accentColor = item.color?.accentColor {
bottomColor = accentColor.withMultiplied(hue: 1.019, saturation: 0.731, brightness: 0.59)
topColor = bottomColor!.withMultiplied(hue: 0.966, saturation: 0.61, brightness: 0.98)
} else {
fillColor = UIColor(rgb: 0x2ea6ff)
strokeColor = fillColor
topColor = UIColor(rgb: 0x466f95)
bottomColor = topColor
}
}
strongSelf.fillNode.image = generateFillImage(color: fillColor ?? .clear)
strongSelf.ringNode.image = generateRingImage(color: strokeColor ?? .clear)
strongSelf.centerNode.image = generateCenterImage(topColor: topColor ?? .clear, bottomColor: bottomColor ?? .clear)
}
let center = CGPoint(x: 30.0, y: 29.0)
let bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 40.0, height: 40.0))
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: itemLayout.contentSize)
strongSelf.fillNode.position = center
strongSelf.ringNode.position = center
strongSelf.centerNode.position = center
strongSelf.dotsNode.position = center
strongSelf.fillNode.bounds = bounds
strongSelf.ringNode.bounds = bounds
strongSelf.centerNode.bounds = bounds
strongSelf.dotsNode.bounds = bounds
if updatedSelected {
strongSelf.setSelected(item.selected, animated: !updatedAccentColor && currentItem != nil)
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
super.animateInsertion(currentTimestamp, duration: duration, options: options)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
super.animateRemoved(currentTimestamp, duration: duration)
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
super.animateAdded(currentTimestamp, duration: duration)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
private class ThemeSettingsAccentColorPickerItem: ListViewItem {
let action: (Bool) -> Void
public init(action: @escaping (Bool) -> Void) {
self.action = action
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ThemeSettingsAccentColorPickerItemNode()
let (nodeLayout, apply) = node.asyncLayout()(self, params)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
apply(false)
})
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
assert(node() is ThemeSettingsAccentColorPickerItemNode)
if let nodeValue = node() as? ThemeSettingsAccentColorPickerItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply(animation.isAnimated)
})
}
}
}
}
}
public var selectable = true
public func selected(listView: ListView) {
self.action(true)
}
}
private func generateCustomSwatchImage() -> UIImage? {
return generateImage(CGSize(width: 42.0, height: 42.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let dotSize = CGSize(width: 10.0, height: 10.0)
context.setFillColor(UIColor(rgb: 0xd33213).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 14.0, y: 16.0), size: dotSize))
context.setFillColor(UIColor(rgb: 0xf08200).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 14.0, y: 0.0), size: dotSize))
context.setFillColor(UIColor(rgb: 0xedb400).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 28.0, y: 8.0), size: dotSize))
context.setFillColor(UIColor(rgb: 0x70bb23).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 28.0, y: 24.0), size: dotSize))
context.setFillColor(UIColor(rgb: 0x5396fa).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 14.0, y: 32.0), size: dotSize))
context.setFillColor(UIColor(rgb: 0x9472ee).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 24.0), size: dotSize))
context.setFillColor(UIColor(rgb: 0xeb6ca4).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: dotSize))
})
}
private final class ThemeSettingsAccentColorPickerItemNode : ListViewItemNode {
private let imageNode: ASImageNode
var item: ThemeSettingsAccentColorPickerItem?
init() {
self.imageNode = ASImageNode()
self.imageNode.displaysAsynchronously = false
self.imageNode.displayWithoutProcessing = true
self.imageNode.image = generateCustomSwatchImage()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.imageNode)
}
override func didLoad() {
super.didLoad()
self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
}
func asyncLayout() -> (ThemeSettingsAccentColorPickerItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
return { [weak self] item, params in
let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 60.0, height: 60.0), insets: UIEdgeInsets())
return (itemLayout, { animated in
if let strongSelf = self {
strongSelf.item = item
strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: 11.0, y: 9.0), size: CGSize(width: 42.0, height: 42.0))
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
super.animateInsertion(currentTimestamp, duration: duration, options: options)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
super.animateRemoved(currentTimestamp, duration: duration)
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
super.animateAdded(currentTimestamp, duration: duration)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
enum ThemeSettingsAccentColor {
case `default`
case color(PresentationThemeBaseColor)
case preset(PresentationThemeAccentColor)
case custom(PresentationThemeAccentColor)
case theme(PresentationThemeReference)
var index: Int64? {
switch self {
case .default:
return nil
case let .color(color):
return Int64(10 + color.rawValue)
case let .preset(color), let .custom(color):
return Int64(color.index)
case let .theme(theme):
return theme.index
}
}
}
class ThemeSettingsAccentColorItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId
let theme: PresentationTheme
let generalThemeReference: PresentationThemeReference
let themeReference: PresentationThemeReference
let colors: [ThemeSettingsAccentColor]
let currentColor: ThemeSettingsColorOption?
let updated: (ThemeSettingsColorOption?) -> Void
let contextAction: ((Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void)?
let openColorPicker: (Bool) -> Void
let tag: ItemListItemTag?
init(theme: PresentationTheme, sectionId: ItemListSectionId, generalThemeReference: PresentationThemeReference, themeReference: PresentationThemeReference, colors: [ThemeSettingsAccentColor], currentColor: ThemeSettingsColorOption?, updated: @escaping (ThemeSettingsColorOption?) -> Void, contextAction: ((Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void)?, openColorPicker: @escaping (Bool) -> Void, tag: ItemListItemTag? = nil) {
self.theme = theme
self.generalThemeReference = generalThemeReference
self.themeReference = themeReference
self.colors = colors
self.currentColor = currentColor
self.updated = updated
self.contextAction = contextAction
self.openColorPicker = openColorPicker
self.tag = tag
self.sectionId = sectionId
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ThemeSettingsAccentColorItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ThemeSettingsAccentColorItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private struct ThemeSettingsAccentColorItemNodeTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let updatePosition: Bool
}
private func preparedTransition(action: @escaping (ThemeSettingsColorOption?, Bool) -> Void, contextAction: ((ThemeSettingsColorOption?, Bool, ASDisplayNode, ContextGesture?) -> Void)?, openColorPicker: @escaping (Bool) -> Void, from fromEntries: [ThemeSettingsColorEntry], to toEntries: [ThemeSettingsColorEntry], updatePosition: Bool) -> ThemeSettingsAccentColorItemNodeTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(action: action, contextAction: contextAction, openColorPicker: openColorPicker), directionHint: .Down) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(action: action, contextAction: contextAction, openColorPicker: openColorPicker), directionHint: nil) }
return ThemeSettingsAccentColorItemNodeTransition(deletions: deletions, insertions: insertions, updates: updates, updatePosition: updatePosition)
}
private func ensureColorVisible(listNode: ListView, accentColor: ThemeSettingsColorOption?, animated: Bool) -> Bool {
var resultNode: ThemeSettingsAccentColorIconItemNode?
listNode.forEachItemNode { node in
if resultNode == nil, let node = node as? ThemeSettingsAccentColorIconItemNode {
if node.item?.color?.index == accentColor?.index {
resultNode = node
}
}
}
if let resultNode = resultNode {
listNode.ensureItemNodeVisible(resultNode, animated: animated, overflow: 24.0)
return true
} else {
return false
}
}
class ThemeSettingsAccentColorItemNode: ListViewItemNode, ItemListItemNode {
private let containerNode: ASDisplayNode
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private var snapshotView: UIView?
private let listNode: ListView
private var entries: [ThemeSettingsColorEntry]?
private var enqueuedTransitions: [ThemeSettingsAccentColorItemNodeTransition] = []
private var initialized = false
private var item: ThemeSettingsAccentColorItem?
private var layoutParams: ListViewItemLayoutParams?
private var tapping = false
var tag: ItemListItemTag? {
return self.item?.tag
}
init() {
self.containerNode = ASDisplayNode()
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.listNode = ListView()
self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.containerNode)
self.addSubnode(self.listNode)
}
override func didLoad() {
super.didLoad()
self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true
}
private func enqueueTransition(_ transition: ThemeSettingsAccentColorItemNodeTransition) {
self.enqueuedTransitions.append(transition)
if let _ = self.item {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
guard let item = self.item, let transition = self.enqueuedTransitions.first else {
return
}
self.enqueuedTransitions.remove(at: 0)
let options = ListViewDeleteAndInsertOptions()
var scrollToItem: ListViewScrollToItem?
if !self.initialized || transition.updatePosition || !self.tapping {
if let index = item.colors.firstIndex(where: { $0.index == item.currentColor?.index }) {
scrollToItem = ListViewScrollToItem(index: index, position: .bottom(-70.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down)
self.initialized = true
}
}
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
})
}
func asyncLayout() -> (_ item: ThemeSettingsAccentColorItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
contentSize = CGSize(width: params.width, height: 60.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
if themeUpdated {
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
}
if strongSelf.backgroundNode.supernode == nil {
strongSelf.containerNode.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.containerNode.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.containerNode.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.containerNode.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.containerNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: contentSize.height)
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
var listInsets = UIEdgeInsets()
listInsets.top += params.leftInset + 4.0
listInsets.bottom += params.rightInset + 4.0
strongSelf.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width)
strongSelf.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0)
strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
var entries: [ThemeSettingsColorEntry] = []
entries.append(.picker)
var index: Int = 0
for color in item.colors {
switch color {
case .default:
let selected = item.currentColor == nil
entries.append(.color(index, item.theme, item.generalThemeReference, nil, selected))
case let .color(color):
var selected = false
if let currentColor = item.currentColor, case let .accentColor(accentColor) = currentColor {
selected = accentColor.baseColor == color
}
let accentColor: ThemeSettingsColorOption
if let currentColor = item.currentColor, selected {
accentColor = currentColor
} else {
accentColor = .accentColor(PresentationThemeAccentColor(index: 10 + color.rawValue, baseColor: color))
}
switch accentColor {
case let .accentColor(color):
entries.append(.color(index, item.theme, item.generalThemeReference, color, selected))
case let .theme(theme):
entries.append(.theme(index, item.theme, item.generalThemeReference, theme, selected))
}
case let .preset(color), let .custom(color):
var selected = false
if let currentColor = item.currentColor {
selected = currentColor.index == Int64(color.index)
}
entries.append(.color(index, item.theme, item.themeReference, color, selected))
case let .theme(theme):
var selected = false
if let currentColor = item.currentColor {
selected = currentColor.index == theme.index
}
entries.append(.theme(index, item.theme, item.generalThemeReference, theme, selected))
}
index += 1
}
let action: (ThemeSettingsColorOption?, Bool) -> Void = { [weak self] color, selected in
if let strongSelf = self, let item = strongSelf.item {
if selected {
var create = true
if let color = color {
switch color {
case let .accentColor(color):
create = color.baseColor != .custom
case let .theme(theme):
if case let .cloud(theme) = theme {
create = !theme.theme.isCreator
}
}
}
item.openColorPicker(create)
} else {
strongSelf.tapping = true
item.updated(color)
Queue.mainQueue().after(0.4) {
strongSelf.tapping = false
}
}
let _ = ensureColorVisible(listNode: strongSelf.listNode, accentColor: color, animated: true)
}
}
let contextAction: ((ThemeSettingsColorOption?, Bool, ASDisplayNode, ContextGesture?) -> Void)? = { color, selected, node, gesture in
if let strongSelf = self, let item = strongSelf.item {
item.contextAction?(selected, item.generalThemeReference, color, node, gesture)
}
}
let openColorPicker: (Bool) -> Void = { [weak self] create in
if let strongSelf = self, let item = strongSelf.item {
item.openColorPicker(true)
}
}
let previousEntries = strongSelf.entries ?? []
let updatePosition = currentItem != nil && (previousEntries.count != entries.count || (currentItem?.generalThemeReference.index != item.generalThemeReference.index))
let transition = preparedTransition(action: action, contextAction: contextAction, openColorPicker: openColorPicker, from: previousEntries, to: entries, updatePosition: updatePosition)
strongSelf.enqueueTransition(transition)
strongSelf.entries = entries
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
func prepareCrossfadeTransition() {
self.snapshotView?.removeFromSuperview()
if let snapshotView = self.containerNode.view.snapshotView(afterScreenUpdates: false) {
self.view.insertSubview(snapshotView, aboveSubview: self.containerNode.view)
self.snapshotView = snapshotView
}
}
func animateCrossfadeTransition() {
self.snapshotView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in
self?.snapshotView?.removeFromSuperview()
})
}
}
@@ -0,0 +1,414 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AppBundle
private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selected: Bool) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.setFillColor(theme.list.itemBlocksBackgroundColor.cgColor)
context.fill(bounds)
context.setBlendMode(.clear)
context.fillEllipse(in: bounds)
context.setBlendMode(.normal)
let lineWidth: CGFloat
if selected {
var accentColor = theme.list.itemAccentColor
if accentColor.rgb == 0xffffff {
accentColor = UIColor(rgb: 0x999999)
}
context.setStrokeColor(accentColor.cgColor)
lineWidth = 2.0 - UIScreenPixel
} else {
context.setStrokeColor(theme.list.disclosureArrowColor.withAlphaComponent(0.4).cgColor)
lineWidth = 1.0 - UIScreenPixel
}
if bordered || selected {
context.setLineWidth(lineWidth)
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
}
})?.stretchableImage(withLeftCapWidth: 15, topCapHeight: 15)
}
class ThemeSettingsAppIconItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId
let theme: PresentationTheme
let strings: PresentationStrings
let systemStyle: ItemListSystemStyle
let icons: [PresentationAppIcon]
let isPremium: Bool
let currentIconName: String?
let updated: (PresentationAppIcon) -> Void
let tag: ItemListItemTag?
init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle, sectionId: ItemListSectionId, icons: [PresentationAppIcon], isPremium: Bool, currentIconName: String?, updated: @escaping (PresentationAppIcon) -> Void, tag: ItemListItemTag? = nil) {
self.theme = theme
self.strings = strings
self.systemStyle = systemStyle
self.icons = icons
self.isPremium = isPremium
self.currentIconName = currentIconName
self.updated = updated
self.tag = tag
self.sectionId = sectionId
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ThemeSettingsAppIconItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ThemeSettingsAppIconItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private let badgeSize = CGSize(width: 24.0, height: 24.0)
private let badgeStrokeSize: CGFloat = 2.0
private final class ThemeSettingsAppIconNode : ASDisplayNode {
private let iconNode: ASImageNode
private let overlayNode: ASImageNode
fileprivate let lockNode: ASImageNode
private let textNode: ImmediateTextNode
private var action: (() -> Void)?
private let activateAreaNode: AccessibilityAreaNode
private var locked = false
override init() {
self.iconNode = ASImageNode()
self.iconNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 63.0, height: 63.0))
self.iconNode.isLayerBacked = true
self.overlayNode = ASImageNode()
self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 63.0, height: 63.0))
self.overlayNode.isLayerBacked = true
self.lockNode = ASImageNode()
self.lockNode.contentMode = .scaleAspectFit
self.lockNode.displaysAsynchronously = false
self.lockNode.isUserInteractionEnabled = false
self.textNode = ImmediateTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.activateAreaNode = AccessibilityAreaNode()
self.activateAreaNode.accessibilityTraits = [.button]
super.init()
self.addSubnode(self.iconNode)
self.addSubnode(self.overlayNode)
self.addSubnode(self.textNode)
self.addSubnode(self.lockNode)
self.addSubnode(self.activateAreaNode)
}
func setup(theme: PresentationTheme, icon: UIImage, title: NSAttributedString, locked: Bool, color: UIColor, bordered: Bool, selected: Bool, action: @escaping () -> Void) {
self.locked = locked
self.iconNode.image = icon
self.textNode.attributedText = title
self.overlayNode.image = generateBorderImage(theme: theme, bordered: bordered, selected: selected)
self.lockNode.isHidden = !locked
self.action = {
action()
}
self.activateAreaNode.accessibilityLabel = title.string
if locked {
self.activateAreaNode.accessibilityTraits = [.button, .notEnabled]
} else {
self.activateAreaNode.accessibilityTraits = [.button]
}
self.setNeedsLayout()
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.action?()
}
}
override func layout() {
super.layout()
let bounds = self.bounds
let iconSize = CGSize(width: 63.0, height: 63.0)
self.iconNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - iconSize.width) / 2.0), y: 13.0), size: iconSize)
self.overlayNode.frame = self.iconNode.frame
let textSize = self.textNode.updateLayout(bounds.size)
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - textSize.width) / 2.0), y: 81.0), size: textSize)
self.textNode.frame = textFrame
let badgeFinalSize = CGSize(width: badgeSize.width + badgeStrokeSize * 2.0, height: badgeSize.height + badgeStrokeSize * 2.0)
self.lockNode.frame = CGRect(x: bounds.width - 24.0, y: 4.0, width: badgeFinalSize.width, height: badgeFinalSize.height)
self.activateAreaNode.frame = bounds
}
}
private let textFont = Font.regular(12.0)
private let selectedTextFont = Font.medium(12.0)
class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let containerNode: ASDisplayNode
private var nodes: [ThemeSettingsAppIconNode] = []
private var item: ThemeSettingsAppIconItem?
private var layoutParams: ListViewItemLayoutParams?
var tag: ItemListItemTag? {
return self.item?.tag
}
private var lockImage: UIImage?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.containerNode = ASDisplayNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.containerNode)
}
func asyncLayout() -> (_ item: ThemeSettingsAppIconItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, neighbors in
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let nodeSize = CGSize(width: 74.0, height: 102.0)
let height: CGFloat = nodeSize.height * ceil(CGFloat(item.icons.count) / 4.0) + 12.0
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
let previousItem = strongSelf.item
strongSelf.item = item
strongSelf.layoutParams = params
if previousItem?.theme !== item.theme {
strongSelf.lockImage = generateImage(CGSize(width: badgeSize.width + badgeStrokeSize, height: badgeSize.height + badgeStrokeSize), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(item.theme.list.itemBlocksBackgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: .zero, size: size))
context.addEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: badgeStrokeSize, dy: badgeStrokeSize))
context.clip()
var locations: [CGFloat] = [0.0, 1.0]
let colors: [CGColor] = [UIColor(rgb: 0x9076FF).cgColor, UIColor(rgb: 0xB86DEA).cgColor]
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())
if let icon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: .white) {
context.draw(icon.cgImage!, in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - icon.size.width) / 2.0), y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size), byTiling: false)
}
})
}
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
strongSelf.containerNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 2.0), size: CGSize(width: layoutSize.width - params.leftInset - params.rightInset, height: layoutSize.height))
let sideInset: CGFloat = 8.0
let spacing: CGFloat = floorToScreenPixels((params.width - sideInset * 2.0 - params.leftInset - params.rightInset - nodeSize.width * 4.0) / 3.0)
let verticalSpacing: CGFloat = 0.0
var x: CGFloat = sideInset
var y: CGFloat = 0.0
var i = 0
for icon in item.icons {
if i > 0 && i % 4 == 0 {
x = sideInset
y += nodeSize.height + verticalSpacing
}
let nodeFrame = CGRect(x: x, y: y, width: nodeSize.width, height: nodeSize.height)
x += nodeSize.width + spacing
let imageNode: ThemeSettingsAppIconNode
if strongSelf.nodes.count > i {
imageNode = strongSelf.nodes[i]
} else {
imageNode = ThemeSettingsAppIconNode()
strongSelf.nodes.append(imageNode)
strongSelf.containerNode.addSubnode(imageNode)
}
imageNode.lockNode.image = strongSelf.lockImage
if let image = UIImage(named: icon.imageName, in: getAppBundle(), compatibleWith: nil) {
let selected = icon.name == item.currentIconName
var name = "Icon"
var bordered = true
switch icon.name {
case "BlueIcon":
name = item.strings.Appearance_AppIconDefault
case "BlackIcon":
name = item.strings.Appearance_AppIconDefaultX
case "BlueClassicIcon":
name = item.strings.Appearance_AppIconClassic
case "BlackClassicIcon":
name = item.strings.Appearance_AppIconClassicX
case "BlueFilledIcon":
name = item.strings.Appearance_AppIconFilled
bordered = false
case "BlackFilledIcon":
name = item.strings.Appearance_AppIconFilledX
bordered = false
case "WhiteFilled":
name = " White"
case "New1":
name = item.strings.Appearance_AppIconNew1
case "New2":
name = item.strings.Appearance_AppIconNew2
case "Premium":
name = item.strings.Appearance_AppIconPremium
case "PremiumBlack":
name = item.strings.Appearance_AppIconBlack
case "PremiumTurbo":
name = item.strings.Appearance_AppIconTurbo
default:
name = icon.name
}
imageNode.setup(theme: item.theme, icon: image, title: NSAttributedString(string: name, font: selected ? selectedTextFont : textFont, textColor: selected ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center), locked: !item.isPremium && icon.isPremium, color: item.theme.list.itemPrimaryTextColor, bordered: bordered, selected: selected, action: {
item.updated(icon)
})
}
imageNode.frame = nodeFrame
i += 1
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,245 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import LegacyComponents
import ItemListUI
import PresentationDataUtils
import AppBundle
class ThemeSettingsBrightnessItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let value: Int32
let sectionId: ItemListSectionId
let updated: (Int32) -> Void
init(theme: PresentationTheme, value: Int32, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.theme = theme
self.value = value
self.sectionId = sectionId
self.updated = updated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ThemeSettingsBrightnessItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ThemeSettingsBrightnessItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
class ThemeSettingsBrightnessItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private var sliderView: TGPhotoEditorSliderView?
private let leftIconNode: ASImageNode
private let rightIconNode: ASImageNode
private var item: ThemeSettingsBrightnessItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.leftIconNode = ASImageNode()
self.leftIconNode.displaysAsynchronously = false
self.leftIconNode.displayWithoutProcessing = true
self.rightIconNode = ASImageNode()
self.rightIconNode.displaysAsynchronously = false
self.rightIconNode.displayWithoutProcessing = true
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.leftIconNode)
self.addSubnode(self.rightIconNode)
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 2.0
sliderView.minimumValue = 0.0
sliderView.startValue = 0.0
sliderView.maximumValue = 100.0
sliderView.disablesInteractiveTransitionGestureRecognizer = true
if let item = self.item, let params = self.layoutParams {
sliderView.value = CGFloat(item.value)
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 38.0, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 38.0 * 2.0, height: 44.0))
}
self.view.addSubview(sliderView)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
}
func asyncLayout() -> (_ item: ThemeSettingsBrightnessItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { item, params, neighbors in
var updatedLeftIcon: UIImage?
var updatedRightIcon: UIImage?
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
updatedLeftIcon = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsBrightnessMinIcon"), color: item.theme.list.itemPrimaryTextColor)
updatedRightIcon = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsBrightnessMaxIcon"), color: item.theme.list.itemPrimaryTextColor)
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
contentSize = CGSize(width: params.width, height: 60.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
if let updatedLeftIcon = updatedLeftIcon {
strongSelf.leftIconNode.image = updatedLeftIcon
}
if let image = strongSelf.leftIconNode.image {
strongSelf.leftIconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 25.0), size: CGSize(width: image.size.width, height: image.size.height))
}
if let updatedRightIcon = updatedRightIcon {
strongSelf.rightIconNode.image = updatedRightIcon
}
if let image = strongSelf.rightIconNode.image {
strongSelf.rightIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 14.0 - image.size.width, y: 21.0), size: CGSize(width: image.size.width, height: image.size.height))
}
if let sliderView = strongSelf.sliderView {
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSecondaryTextColor
sliderView.trackColor = item.theme.list.itemAccentColor.withAlphaComponent(0.45)
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
}
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 38.0, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 38.0 * 2.0, height: 44.0))
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func sliderValueChanged() {
guard let sliderView = self.sliderView else {
return
}
self.item?.updated(Int32(sliderView.value))
}
}
@@ -0,0 +1,313 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import Postbox
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import WallpaperBackgroundNode
struct ChatPreviewMessageItem: Equatable {
static func == (lhs: ChatPreviewMessageItem, rhs: ChatPreviewMessageItem) -> Bool {
if lhs.outgoing != rhs.outgoing {
return false
}
if let lhsReply = lhs.reply, let rhsReply = rhs.reply, lhsReply.0 != rhsReply.0 || lhsReply.1 != rhsReply.1 {
return false
} else if (lhs.reply == nil) != (rhs.reply == nil) {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.nameColor != rhs.nameColor {
return false
}
if lhs.backgroundEmojiId != rhs.backgroundEmojiId {
return false
}
return true
}
let outgoing: Bool
let reply: (String, String)?
let text: String
let nameColor: PeerColor
let backgroundEmojiId: Int64?
}
class ThemeSettingsChatPreviewItem: ListViewItem, ItemListItem {
let context: AccountContext
let systemStyle: ItemListSystemStyle
let theme: PresentationTheme
let componentTheme: PresentationTheme
let strings: PresentationStrings
let sectionId: ItemListSectionId
let fontSize: PresentationFontSize
let chatBubbleCorners: PresentationChatBubbleCorners
let wallpaper: TelegramWallpaper
let dateTimeFormat: PresentationDateTimeFormat
let nameDisplayOrder: PresentationPersonNameOrder
let messageItems: [ChatPreviewMessageItem]
init(context: AccountContext, systemStyle: ItemListSystemStyle = .legacy, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, messageItems: [ChatPreviewMessageItem]) {
self.context = context
self.systemStyle = systemStyle
self.theme = theme
self.componentTheme = componentTheme
self.strings = strings
self.sectionId = sectionId
self.fontSize = fontSize
self.chatBubbleCorners = chatBubbleCorners
self.wallpaper = wallpaper
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
self.messageItems = messageItems
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ThemeSettingsChatPreviewItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ThemeSettingsChatPreviewItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
class ThemeSettingsChatPreviewItemNode: ListViewItemNode {
private var backgroundNode: WallpaperBackgroundNode?
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let containerNode: ASDisplayNode
private var messageNodes: [ListViewItemNode]?
private var item: ThemeSettingsChatPreviewItem?
private var finalImage = true
private let disposable = MetaDisposable()
init() {
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.containerNode = ASDisplayNode()
self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
super.init(layerBacked: false, dynamicBounce: false)
self.clipsToBounds = true
self.addSubnode(self.containerNode)
}
deinit {
self.disposable.dispose()
}
func asyncLayout() -> (_ item: ThemeSettingsChatPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentNodes = self.messageNodes
var currentBackgroundNode = self.backgroundNode
return { item, params, neighbors in
if currentBackgroundNode == nil {
currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false)
}
currentBackgroundNode?.update(wallpaper: item.wallpaper, animated: false)
currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.componentTheme, bubbleCorners: item.chatBubbleCorners)
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1))
let otherPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2))
var items: [ListViewItem] = []
for messageItem in item.messageItems.reversed() {
var peers = SimpleDictionary<PeerId, Peer>()
var messages = SimpleDictionary<MessageId, Message>()
let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3)
if let (author, text) = messageItem.reply {
peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: author, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
}
let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: messageItem.outgoing ? otherPeerId : peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: messageItem.outgoing ? TelegramUser(id: otherPeerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) : nil, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
}
var nodes: [ListViewItemNode] = []
if let messageNodes = currentNodes {
nodes = messageNodes
for i in 0 ..< items.count {
let itemNode = messageNodes[i]
items[i].updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
apply(ListViewItemApply(isOnScreen: true))
})
}
} else {
var messageNodes: [ListViewItemNode] = []
for i in 0 ..< items.count {
var itemNode: ListViewItemNode?
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
itemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode!.isUserInteractionEnabled = false
messageNodes.append(itemNode!)
}
nodes = messageNodes
}
var contentSize = CGSize(width: params.width, height: 4.0 + 4.0)
for node in nodes {
contentSize.height += node.frame.size.height
}
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
strongSelf.messageNodes = nodes
var topOffset: CGFloat = 4.0
for node in nodes {
if node.supernode == nil {
strongSelf.containerNode.addSubnode(node)
}
node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: node.frame.size), within: layoutSize)
topOffset += node.frame.size.height
}
if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode {
strongSelf.backgroundNode = currentBackgroundNode
strongSelf.insertSubnode(currentBackgroundNode, at: 0)
}
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
let displayMode: WallpaperDisplayMode
if abs(params.availableHeight - params.width) < 100.0, params.availableHeight > 700.0 {
displayMode = .halfAspectFill
} else {
if backgroundFrame.width > backgroundFrame.height * 4.0 {
if params.availableHeight < 700.0 {
displayMode = .halfAspectFill
} else {
displayMode = .aspectFill
}
} else {
displayMode = .aspectFill
}
}
if let backgroundNode = strongSelf.backgroundNode {
backgroundNode.frame = backgroundFrame.insetBy(dx: 0.0, dy: -100.0)
backgroundNode.updateLayout(size: backgroundNode.bounds.size, displayMode: displayMode, transition: .immediate)
}
strongSelf.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,348 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import LegacyComponents
import ItemListUI
import PresentationDataUtils
import AppBundle
class ThemeSettingsFontSizeItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let fontSize: PresentationFontSize
let disableLeadingInset: Bool
let displayIcons: Bool
let disableDecorations: Bool
let force: Bool
let enabled: Bool
let sectionId: ItemListSectionId
let updated: (PresentationFontSize) -> Void
let tag: ItemListItemTag?
init(theme: PresentationTheme, fontSize: PresentationFontSize, enabled: Bool = true, disableLeadingInset: Bool = false, displayIcons: Bool = true, disableDecorations: Bool = false, force: Bool = false, sectionId: ItemListSectionId, updated: @escaping (PresentationFontSize) -> Void, tag: ItemListItemTag? = nil) {
self.theme = theme
self.fontSize = fontSize
self.enabled = enabled
self.disableLeadingInset = disableLeadingInset
self.displayIcons = displayIcons
self.force = force
self.disableDecorations = disableDecorations
self.sectionId = sectionId
self.updated = updated
self.tag = tag
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ThemeSettingsFontSizeItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ThemeSettingsFontSizeItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
class ThemeSettingsFontSizeItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private var sliderView: TGPhotoEditorSliderView?
private let leftIconNode: ASImageNode
private let rightIconNode: ASImageNode
private let disabledOverlayNode: ASDisplayNode
private var item: ThemeSettingsFontSizeItem?
private var layoutParams: ListViewItemLayoutParams?
var tag: ItemListItemTag? {
return self.item?.tag
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.leftIconNode = ASImageNode()
self.leftIconNode.displaysAsynchronously = false
self.leftIconNode.displayWithoutProcessing = true
self.rightIconNode = ASImageNode()
self.rightIconNode.displaysAsynchronously = false
self.rightIconNode.displayWithoutProcessing = true
self.disabledOverlayNode = ASDisplayNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.leftIconNode)
self.addSubnode(self.rightIconNode)
self.addSubnode(self.disabledOverlayNode)
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 4.0
sliderView.dotSize = 8.0
sliderView.minimumValue = 0.0
sliderView.maximumValue = 6.0
sliderView.startValue = 0.0
sliderView.positionsCount = 7
sliderView.useLinesForPositions = true
sliderView.disablesInteractiveTransitionGestureRecognizer = true
if let item = self.item, let params = self.layoutParams {
sliderView.isUserInteractionEnabled = item.enabled
let value: CGFloat
switch item.fontSize {
case .extraSmall:
value = 0.0
case .small:
value = 1.0
case .medium:
value = 2.0
case .regular:
value = 3.0
case .large:
value = 4.0
case .extraLarge:
value = 5.0
case .extraLargeX2:
value = 6.0
}
sliderView.value = value
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.enabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
let sliderInset: CGFloat = item.displayIcons ? 38.0 : 16.0
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + sliderInset, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - sliderInset * 2.0, height: 44.0))
}
self.view.insertSubview(sliderView, belowSubview: self.disabledOverlayNode.view)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
}
func asyncLayout() -> (_ item: ThemeSettingsFontSizeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { item, params, neighbors in
var updatedLeftIcon: UIImage?
var updatedRightIcon: UIImage?
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
updatedLeftIcon = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsFontMinIcon"), color: item.theme.list.itemPrimaryTextColor)
updatedRightIcon = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsFontMaxIcon"), color: item.theme.list.itemPrimaryTextColor)
}
let contentSize: CGSize
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
contentSize = CGSize(width: params.width, height: 60.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
if item.disableLeadingInset {
insets.top = 0.0
insets.bottom = 0.0
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
let firstTime = strongSelf.item == nil || item.force
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.disabledOverlayNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4)
strongSelf.disabledOverlayNode.isHidden = item.enabled
strongSelf.disabledOverlayNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: 44.0))
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params) && !item.disableDecorations
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
if let updatedLeftIcon = updatedLeftIcon {
strongSelf.leftIconNode.image = updatedLeftIcon
}
if let image = strongSelf.leftIconNode.image {
strongSelf.leftIconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 25.0), size: CGSize(width: image.size.width, height: image.size.height))
}
if let updatedRightIcon = updatedRightIcon {
strongSelf.rightIconNode.image = updatedRightIcon
}
if let image = strongSelf.rightIconNode.image {
strongSelf.rightIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 14.0 - image.size.width, y: 21.0), size: CGSize(width: image.size.width, height: image.size.height))
}
strongSelf.leftIconNode.isHidden = !item.displayIcons
strongSelf.rightIconNode.isHidden = !item.displayIcons
if let sliderView = strongSelf.sliderView {
sliderView.isUserInteractionEnabled = item.enabled
sliderView.trackColor = item.enabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
}
let value: CGFloat
switch item.fontSize {
case .extraSmall:
value = 0.0
case .small:
value = 1.0
case .medium:
value = 2.0
case .regular:
value = 3.0
case .large:
value = 4.0
case .extraLarge:
value = 5.0
case .extraLargeX2:
value = 6.0
}
if firstTime {
sliderView.value = value
}
let sliderInset: CGFloat = item.displayIcons ? 38.0 : 16.0
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + sliderInset, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - sliderInset * 2.0, height: 44.0))
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func sliderValueChanged() {
guard let sliderView = self.sliderView else {
return
}
let fontSize: PresentationFontSize
switch Int(sliderView.value) {
case 0:
fontSize = .extraSmall
case 1:
fontSize = .small
case 2:
fontSize = .medium
case 3:
fontSize = .regular
case 4:
fontSize = .large
case 5:
fontSize = .extraLarge
case 6:
fontSize = .extraLargeX2
default:
fontSize = .regular
}
self.item?.updated(fontSize)
}
}
@@ -0,0 +1,754 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import ShareController
import UndoUI
import InviteLinksUI
import TextFormat
import Postbox
private final class UsernameSetupControllerArguments {
let account: Account
let updatePublicLinkText: (String?, String) -> Void
let shareLink: () -> Void
let activateLink: (String) -> Void
let deactivateLink: (String) -> Void
let openAuction: (String) -> Void
init(account: Account, updatePublicLinkText: @escaping (String?, String) -> Void, shareLink: @escaping () -> Void, activateLink: @escaping (String) -> Void, deactivateLink: @escaping (String) -> Void, openAuction: @escaping (String) -> Void) {
self.account = account
self.updatePublicLinkText = updatePublicLinkText
self.shareLink = shareLink
self.activateLink = activateLink
self.deactivateLink = deactivateLink
self.openAuction = openAuction
}
}
private enum UsernameSetupSection: Int32 {
case link
case additional
}
public enum UsernameEntryTag: ItemListItemTag {
case username
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? UsernameEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum UsernameSetupEntryId: Hashable {
case index(Int32)
case username(String)
}
private enum UsernameSetupEntry: ItemListNodeEntry {
case publicLinkHeader(PresentationTheme, String)
case editablePublicLink(PresentationTheme, PresentationStrings, String, String?, String, Bool)
case publicLinkStatus(PresentationTheme, String, AddressNameValidationStatus, String, String)
case publicLinkInfo(PresentationTheme, String)
case additionalLinkHeader(PresentationTheme, String)
case additionalLink(PresentationTheme, TelegramPeerUsername, Int32, Bool)
case additionalLinkInfo(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .publicLinkHeader, .editablePublicLink, .publicLinkStatus, .publicLinkInfo:
return UsernameSetupSection.link.rawValue
case .additionalLinkHeader, .additionalLink, .additionalLinkInfo:
return UsernameSetupSection.additional.rawValue
}
}
var stableId: UsernameSetupEntryId {
switch self {
case .publicLinkHeader:
return .index(0)
case .editablePublicLink:
return .index(1)
case .publicLinkStatus:
return .index(2)
case .publicLinkInfo:
return .index(3)
case .additionalLinkHeader:
return .index(4)
case let .additionalLink(_, username, _, _):
return .username(username.username)
case .additionalLinkInfo:
return .index(5)
}
}
static func ==(lhs: UsernameSetupEntry, rhs: UsernameSetupEntry) -> Bool {
switch lhs {
case let .publicLinkHeader(lhsTheme, lhsText):
if case let .publicLinkHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .editablePublicLink(lhsTheme, lhsStrings, lhsPrefix, lhsCurrentText, lhsText, lhsEnabled):
if case let .editablePublicLink(rhsTheme, rhsStrings, rhsPrefix, rhsCurrentText, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPrefix == rhsPrefix, lhsCurrentText == rhsCurrentText, lhsText == rhsText, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .publicLinkInfo(lhsTheme, lhsText):
if case let .publicLinkInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .publicLinkStatus(lhsTheme, lhsAddressName, lhsStatus, lhsText, lhsUsername):
if case let .publicLinkStatus(rhsTheme, rhsAddressName, rhsStatus, rhsText, rhsUsername) = rhs, lhsTheme === rhsTheme, lhsAddressName == rhsAddressName, lhsStatus == rhsStatus, lhsText == rhsText, lhsUsername == rhsUsername {
return true
} else {
return false
}
case let .additionalLinkHeader(lhsTheme, lhsText):
if case let .additionalLinkHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .additionalLink(lhsTheme, lhsAddressName, lhsIndex, lhsCanToggleIsActive):
if case let .additionalLink(rhsTheme, rhsAddressName, rhsIndex, rhsCanToggleIsActive) = rhs, lhsTheme === rhsTheme, lhsAddressName == rhsAddressName, lhsIndex == rhsIndex, lhsCanToggleIsActive == rhsCanToggleIsActive {
return true
} else {
return false
}
case let .additionalLinkInfo(lhsTheme, lhsText):
if case let .additionalLinkInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: UsernameSetupEntry, rhs: UsernameSetupEntry) -> Bool {
switch lhs {
case .publicLinkHeader:
switch rhs {
case .publicLinkHeader:
return false
default:
return true
}
case .editablePublicLink:
switch rhs {
case .publicLinkHeader, .editablePublicLink:
return false
default:
return true
}
case .publicLinkStatus:
switch rhs {
case .publicLinkHeader, .editablePublicLink, .publicLinkStatus:
return false
default:
return true
}
case .publicLinkInfo:
switch rhs {
case .publicLinkHeader, .editablePublicLink, .publicLinkStatus, .publicLinkInfo:
return false
default:
return true
}
case .additionalLinkHeader:
switch rhs {
case .publicLinkHeader, .editablePublicLink, .publicLinkStatus, .publicLinkInfo, .additionalLinkHeader:
return false
default:
return true
}
case let .additionalLink(_, _, lhsIndex, _):
switch rhs {
case let .additionalLink(_, _, rhsIndex, _):
return lhsIndex < rhsIndex
case .publicLinkHeader, .editablePublicLink, .publicLinkStatus, .publicLinkInfo, .additionalLinkHeader:
return false
default:
return true
}
case .additionalLinkInfo:
switch rhs {
case .publicLinkHeader, .editablePublicLink, .publicLinkStatus, .publicLinkInfo, .additionalLinkHeader, .additionalLink, .additionalLinkInfo:
return false
}
}
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! UsernameSetupControllerArguments
switch self {
case let .publicLinkHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .editablePublicLink(theme, _, prefix, currentText, text, enabled):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: enabled ? prefix : "", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .username, spacing: 10.0, clearType: enabled ? .always : .none, enabled: enabled, tag: UsernameEntryTag.username, sectionId: self.section, textUpdated: { updatedText in
arguments.updatePublicLinkText(currentText, updatedText)
}, action: {
})
case let .publicLinkInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { action in
if case .tap = action {
arguments.shareLink()
}
})
case let .publicLinkStatus(_, _, status, text, username):
var displayActivity = false
let textColor: ItemListActivityTextItem.TextColor
switch status {
case .invalidFormat:
textColor = .destructive
case let .availability(availability):
switch availability {
case .available:
textColor = .constructive
case .purchaseAvailable:
textColor = .generic
case .invalid, .taken:
textColor = .destructive
}
case .checking:
textColor = .generic
displayActivity = true
}
return ItemListActivityTextItem(displayActivity: displayActivity, presentationData: presentationData, text: text, color: textColor, linkAction: { _ in
arguments.openAuction(username)
}, sectionId: self.section)
case let .additionalLinkHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .additionalLink(_, link, _, canToggleIsActive):
return AdditionalLinkItem(presentationData: presentationData, systemStyle: .glass, username: link, sectionId: self.section, style: .blocks, tapAction: {
if canToggleIsActive {
if link.isActive {
arguments.deactivateLink(link.username)
} else {
arguments.activateLink(link.username)
}
}
})
case let .additionalLinkInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
}
}
private struct UsernameSetupControllerState: Equatable {
let editingPublicLinkText: String?
let addressNameValidationStatus: AddressNameValidationStatus?
let updatingAddressName: Bool
init() {
self.editingPublicLinkText = nil
self.addressNameValidationStatus = nil
self.updatingAddressName = false
}
init(editingPublicLinkText: String?, addressNameValidationStatus: AddressNameValidationStatus?, updatingAddressName: Bool) {
self.editingPublicLinkText = editingPublicLinkText
self.addressNameValidationStatus = addressNameValidationStatus
self.updatingAddressName = updatingAddressName
}
static func ==(lhs: UsernameSetupControllerState, rhs: UsernameSetupControllerState) -> Bool {
if lhs.editingPublicLinkText != rhs.editingPublicLinkText {
return false
}
if lhs.addressNameValidationStatus != rhs.addressNameValidationStatus {
return false
}
if lhs.updatingAddressName != rhs.updatingAddressName {
return false
}
return true
}
func withUpdatedEditingPublicLinkText(_ editingPublicLinkText: String?) -> UsernameSetupControllerState {
return UsernameSetupControllerState(editingPublicLinkText: editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: self.updatingAddressName)
}
func withUpdatedAddressNameValidationStatus(_ addressNameValidationStatus: AddressNameValidationStatus?) -> UsernameSetupControllerState {
return UsernameSetupControllerState(editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: addressNameValidationStatus, updatingAddressName: self.updatingAddressName)
}
func withUpdatedUpdatingAddressName(_ updatingAddressName: Bool) -> UsernameSetupControllerState {
return UsernameSetupControllerState(editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName)
}
}
private func usernameSetupControllerEntries(presentationData: PresentationData, view: PeerView, state: UsernameSetupControllerState, temporaryOrder: [String]?, mode: UsernameSetupMode) -> [UsernameSetupEntry] {
var entries: [UsernameSetupEntry] = []
if let peer = view.peers[view.peerId] as? TelegramUser {
let currentUsername: String
if let current = state.editingPublicLinkText {
currentUsername = current
} else {
if let username = peer.editableUsername {
currentUsername = username
} else {
currentUsername = ""
}
}
entries.append(.publicLinkHeader(presentationData.theme, presentationData.strings.Username_Username))
entries.append(.editablePublicLink(presentationData.theme, presentationData.strings, presentationData.strings.Username_Title, peer.editableUsername, currentUsername, mode == .account))
if let status = state.addressNameValidationStatus {
let statusText: String
switch status {
case let .invalidFormat(error):
switch error {
case .startsWithDigit:
statusText = presentationData.strings.Username_InvalidStartsWithNumber
case .startsWithUnderscore:
statusText = presentationData.strings.Username_InvalidStartsWithUnderscore
case .endsWithUnderscore:
statusText = presentationData.strings.Username_InvalidEndsWithUnderscore
case .invalidCharacters:
statusText = presentationData.strings.Username_InvalidCharacters
case .tooShort:
statusText = presentationData.strings.Username_InvalidTooShort
}
case let .availability(availability):
switch availability {
case .available:
statusText = presentationData.strings.Username_UsernameIsAvailable(currentUsername).string
case .invalid:
statusText = presentationData.strings.Username_InvalidValue
case .taken:
statusText = presentationData.strings.Username_InvalidTaken
case .purchaseAvailable:
var markdownString = presentationData.strings.Username_UsernamePurchaseAvailable
let entities = generateTextEntities(markdownString, enabledTypes: [.mention])
if let entity = entities.first {
markdownString.insert(contentsOf: "]()", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.upperBound))
markdownString.insert(contentsOf: "[", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.lowerBound))
}
statusText = markdownString
}
case .checking:
statusText = presentationData.strings.Username_CheckingUsername
}
entries.append(.publicLinkStatus(presentationData.theme, currentUsername, status, statusText, currentUsername))
}
var isBot = false
let otherUsernames = peer.usernames.filter { !$0.flags.contains(.isEditable) }
if case .bot = mode {
isBot = true
var infoText = presentationData.strings.Username_BotLinkHint
if otherUsernames.isEmpty {
infoText = presentationData.strings.Username_BotLinkHintExtended
}
entries.append(.publicLinkInfo(presentationData.theme, infoText))
} else {
var infoText = presentationData.strings.Username_Help
if otherUsernames.isEmpty {
infoText += "\n\n"
let hintText = presentationData.strings.Username_LinkHint(currentUsername.replacingOccurrences(of: "[", with: "").replacingOccurrences(of: "]", with: "")).string.replacingOccurrences(of: "]", with: "]()")
infoText += hintText
}
entries.append(.publicLinkInfo(presentationData.theme, infoText))
}
if !otherUsernames.isEmpty {
entries.append(.additionalLinkHeader(presentationData.theme, presentationData.strings.Username_LinksOrder))
var usernames = peer.usernames
if let temporaryOrder = temporaryOrder {
var usernamesMap: [String: TelegramPeerUsername] = [:]
for username in usernames {
usernamesMap[username.username] = username
}
var sortedUsernames: [TelegramPeerUsername] = []
for username in temporaryOrder {
if let username = usernamesMap[username] {
sortedUsernames.append(username)
}
}
usernames = sortedUsernames
}
var i: Int32 = 0
for username in usernames {
var canToggleIsActive = false
if !username.flags.contains(.isEditable) || isBot {
canToggleIsActive = true
}
entries.append(.additionalLink(presentationData.theme, username, i, canToggleIsActive))
i += 1
}
let text: String
switch mode {
case .account:
text = presentationData.strings.Username_LinksOrderInfo
case .bot:
text = presentationData.strings.Username_BotLinksOrderInfo
}
entries.append(.additionalLinkInfo(presentationData.theme, text))
}
}
return entries
}
public enum UsernameSetupMode: Equatable {
case account
case bot(EnginePeer.Id)
}
public func usernameSetupController(context: AccountContext, mode: UsernameSetupMode = .account) -> ViewController {
let statePromise = ValuePromise(UsernameSetupControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: UsernameSetupControllerState())
let updateState: ((UsernameSetupControllerState) -> UsernameSetupControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
let actionsDisposable = DisposableSet()
let checkAddressNameDisposable = MetaDisposable()
actionsDisposable.add(checkAddressNameDisposable)
let updateAddressNameDisposable = MetaDisposable()
actionsDisposable.add(updateAddressNameDisposable)
let peerId: EnginePeer.Id
let domain: AddressNameDomain
switch mode {
case .account:
domain = .account
peerId = context.account.peerId
case let .bot(botPeerId):
domain = .bot(botPeerId)
peerId = botPeerId
}
let arguments = UsernameSetupControllerArguments(account: context.account, updatePublicLinkText: { currentText, text in
if text.isEmpty {
checkAddressNameDisposable.set(nil)
updateState { state in
return state.withUpdatedEditingPublicLinkText(text).withUpdatedAddressNameValidationStatus(nil)
}
} else if currentText == text {
checkAddressNameDisposable.set(nil)
updateState { state in
return state.withUpdatedEditingPublicLinkText(text).withUpdatedAddressNameValidationStatus(nil).withUpdatedAddressNameValidationStatus(nil)
}
} else {
updateState { state in
return state.withUpdatedEditingPublicLinkText(text)
}
checkAddressNameDisposable.set((context.engine.peers.validateAddressNameInteractive(domain: domain, name: text)
|> deliverOnMainQueue).start(next: { result in
updateState { state in
return state.withUpdatedAddressNameValidationStatus(result)
}
}))
}
}, shareLink: {
let _ = (context.account.postbox.loadedPeerWithId(peerId)
|> take(1)
|> deliverOnMainQueue).start(next: { peer in
if let user = peer as? TelegramUser, user.botInfo != nil {
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://fragment.com/", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {})
} else {
var currentAddressName: String = peer.addressName ?? ""
updateState { state in
if let current = state.editingPublicLinkText {
currentAddressName = current
}
return state
}
if !currentAddressName.isEmpty {
dismissInputImpl?()
let shareController = ShareController(context: context, subject: .url("https://t.me/\(currentAddressName)"))
shareController.actionCompleted = {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}
presentControllerImpl?(shareController, nil)
}
}
})
}, activateLink: { name in
dismissInputImpl?()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let alertText: String
if case .bot = mode {
alertText = presentationData.strings.Username_BotActivateAlertText
} else {
alertText = presentationData.strings.Username_ActivateAlertText
}
presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Username_ActivateAlertTitle, text: alertText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Username_ActivateAlertShow, action: {
let _ = (context.engine.peers.toggleAddressNameActive(domain: domain, name: name, active: true)
|> deliverOnMainQueue).start(error: { error in
let errorText: String
switch error {
case .activeLimitReached:
if case .bot = mode {
errorText = presentationData.strings.Username_BotActiveLimitReachedError
} else {
errorText = presentationData.strings.Username_ActiveLimitReachedError
}
default:
errorText = presentationData.strings.Login_UnknownError
}
presentControllerImpl?(textAlertController(context: context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
})
})]), nil)
}, deactivateLink: { name in
dismissInputImpl?()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let alertText: String
if case .bot = mode {
alertText = presentationData.strings.Username_BotDeactivateAlertText
} else {
alertText = presentationData.strings.Username_DeactivateAlertText
}
presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Username_DeactivateAlertTitle, text: alertText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Username_DeactivateAlertHide, action: {
let _ = context.engine.peers.toggleAddressNameActive(domain: domain, name: name, active: false).start()
})]), nil)
}, openAuction: { username in
dismissInputImpl?()
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://fragment.com/username/\(username)", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {})
})
let temporaryOrder = Promise<[String]?>(nil)
let peerView = context.account.viewTracker.peerView(peerId)
|> deliverOnMainQueue
let signal = combineLatest(
context.sharedContext.presentationData,
statePromise.get() |> deliverOnMainQueue,
peerView,
temporaryOrder.get()
)
|> map { presentationData, state, view, temporaryOrder -> (ItemListControllerState, (ItemListNodeState, Any)) in
let peer = peerViewMainPeer(view)
var rightNavigationButton: ItemListNavigationButton?
if let peer = peer as? TelegramUser {
var doneEnabled = true
if let addressNameValidationStatus = state.addressNameValidationStatus {
switch addressNameValidationStatus {
case .availability(.available):
break
default:
doneEnabled = false
}
}
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: {
var updatedAddressNameValue: String?
updateState { state in
if state.editingPublicLinkText != peer.addressName {
updatedAddressNameValue = state.editingPublicLinkText
}
if updatedAddressNameValue != nil {
return state.withUpdatedUpdatingAddressName(true)
} else {
return state
}
}
if let updatedAddressNameValue = updatedAddressNameValue {
updateAddressNameDisposable.set((context.engine.peers.updateAddressName(domain: domain, name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue)
|> deliverOnMainQueue).start(error: { _ in
updateState { state in
return state.withUpdatedUpdatingAddressName(false)
}
}, completed: {
updateState { state in
return state.withUpdatedUpdatingAddressName(false)
}
dismissImpl?()
}))
} else {
dismissImpl?()
}
})
}
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
let title: String
if case .bot = mode {
title = presentationData.strings.Username_BotTitle
} else {
title = presentationData.strings.Username_Title
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: usernameSetupControllerEntries(presentationData: presentationData, view: view, state: state, temporaryOrder: temporaryOrder, mode: mode), style: .blocks, focusItemTag: mode == .account ? UsernameEntryTag.username : nil, animateChanges: true)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
controller.navigationPresentation = .modal
controller.enableInteractiveDismiss = true
controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [UsernameSetupEntry]) -> Signal<Bool, NoError> in
let fromEntry = entries[fromIndex]
guard case let .additionalLink(_, fromUsername, _, _) = fromEntry else {
return .single(false)
}
var referenceId: String?
var beforeAll = false
var afterAll = false
var maxIndex: Int?
var currentUsernames: [String] = []
var i = 0
for entry in entries {
switch entry {
case let .additionalLink(_, link, _, _):
currentUsernames.append(link.username)
if !link.isActive && maxIndex == nil {
maxIndex = max(0, i - 1)
}
i += 1
default:
break
}
}
if toIndex < entries.count {
switch entries[toIndex] {
case let .additionalLink(_, toUsername, _, _):
if toUsername.isActive {
referenceId = toUsername.username
} else {
afterAll = true
}
default:
if entries[toIndex] < fromEntry {
beforeAll = true
} else {
afterAll = true
}
}
} else {
afterAll = true
}
var previousIndex: Int?
for i in 0 ..< currentUsernames.count {
if currentUsernames[i] == fromUsername.username {
previousIndex = i
currentUsernames.remove(at: i)
break
}
}
var didReorder = false
if let referenceId = referenceId {
var inserted = false
for i in 0 ..< currentUsernames.count {
if currentUsernames[i] == referenceId {
if fromIndex < toIndex {
didReorder = previousIndex != i + 1
currentUsernames.insert(fromUsername.username, at: i + 1)
} else {
didReorder = previousIndex != i
currentUsernames.insert(fromUsername.username, at: i)
}
inserted = true
break
}
}
if !inserted {
didReorder = previousIndex != currentUsernames.count
if let maxIndex = maxIndex {
currentUsernames.insert(fromUsername.username, at: maxIndex)
} else {
currentUsernames.append(fromUsername.username)
}
}
} else if beforeAll {
didReorder = previousIndex != 0
currentUsernames.insert(fromUsername.username, at: 0)
} else if afterAll {
didReorder = previousIndex != currentUsernames.count
if let maxIndex = maxIndex {
currentUsernames.insert(fromUsername.username, at: maxIndex)
} else {
currentUsernames.append(fromUsername.username)
}
}
temporaryOrder.set(.single(currentUsernames))
if didReorder {
DispatchQueue.main.async {
dismissInputImpl?()
}
}
return .single(didReorder)
})
controller.setReorderCompleted({ (entries: [UsernameSetupEntry]) -> Void in
var currentUsernames: [TelegramPeerUsername] = []
for entry in entries {
switch entry {
case let .additionalLink(_, username, _, _):
currentUsernames.append(username)
default:
break
}
}
let _ = (context.engine.peers.reorderAddressNames(domain: domain, names: currentUsernames)
|> deliverOnMainQueue).start(completed: {
temporaryOrder.set(.single(nil))
})
})
controller.beganInteractiveDragging = {
dismissInputImpl?()
}
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
controller?.dismiss()
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
return controller
}

Some files were not shown because too many files have changed in this diff Show More