Various improvements

This commit is contained in:
Ilya Laktyushin 2026-01-25 22:17:49 +04:00
parent 7f59789ffc
commit 0d12dd29ad
190 changed files with 16735 additions and 5317 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -185,16 +185,6 @@ public enum WallpaperUrlParameter {
case gradient([UInt32], Int32?) case gradient([UInt32], Int32?)
} }
public enum ResolvedUrlSettingsSection {
case theme
case devices
case autoremoveMessages
case twoStepAuth
case enableLog
case phonePrivacy
case loginEmail
}
public struct ResolvedBotChoosePeerTypes: OptionSet { public struct ResolvedBotChoosePeerTypes: OptionSet {
public var rawValue: UInt32 public var rawValue: UInt32
@ -315,13 +305,12 @@ public enum ResolvedUrl {
case share(url: String?, text: String?, to: String?) case share(url: String?, text: String?, to: String?)
case wallpaper(WallpaperUrlParameter) case wallpaper(WallpaperUrlParameter)
case theme(String) case theme(String)
case settings(ResolvedUrlSettingsSection)
case joinVoiceChat(PeerId, String?) case joinVoiceChat(PeerId, String?)
case importStickers case importStickers
case startAttach(peerId: PeerId, payload: String?, choose: ResolvedBotChoosePeerTypes?) case startAttach(peerId: PeerId, payload: String?, choose: ResolvedBotChoosePeerTypes?)
case invoice(slug: String, invoice: TelegramMediaInvoice?) case invoice(slug: String, invoice: TelegramMediaInvoice?)
case premiumOffer(reference: String?) case premiumOffer(reference: String?)
case starsTopup(amount: Int64, purpose: String?) case starsTopup(amount: Int64?, purpose: String?)
case chatFolder(slug: String) case chatFolder(slug: String)
case story(peerId: PeerId, id: Int32) case story(peerId: PeerId, id: Int32)
case boost(peerId: PeerId?, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus?) case boost(peerId: PeerId?, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus?)
@ -336,6 +325,53 @@ public enum ResolvedUrl {
case storyFolder(peerId: PeerId, id: Int64) case storyFolder(peerId: PeerId, id: Int64)
case giftCollection(peerId: PeerId, id: Int64) case giftCollection(peerId: PeerId, id: Int64)
case sendGift(peerId: PeerId?) case sendGift(peerId: PeerId?)
case unknownDeepLink(path: String)
case oauth(url: String)
public enum ChatsSection {
case search
case edit
case emojiStatus
}
case chats(ChatsSection?)
public enum ComposeSection {
case group
case channel
case contact
}
case compose(ComposeSection?)
public enum PostStorySection {
case photo
case video
case live
}
case postStory(PostStorySection?)
public enum ContactsSection {
case search
case sort
case new
case invite
case manage
}
case contacts(ContactsSection?)
public enum SettingsSection {
public enum Legacy {
case theme
case devices
case autoremoveMessages
case enableLog
case phonePrivacy
case loginEmail
}
case legacy(Legacy)
case path(String)
}
case settings(SettingsSection)
} }
public enum ResolveUrlResult { public enum ResolveUrlResult {
@ -972,18 +1008,30 @@ public protocol MediaEditorScreenResult {
var target: Stories.PendingTarget { get } var target: Stories.PendingTarget { get }
} }
public enum StoryCameraMode {
case photo
case video
case live
}
public protocol TelegramRootControllerInterface: NavigationController { public protocol TelegramRootControllerInterface: NavigationController {
@discardableResult @discardableResult
func openStoryCamera(customTarget: Stories.PendingTarget?, resumeLiveStream: Bool, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? func openStoryCamera(mode: StoryCameraMode, customTarget: Stories.PendingTarget?, resumeLiveStream: Bool, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator?
func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void)
func getContactsController() -> ViewController? func getContactsController() -> ViewController?
func getChatsController() -> ViewController? func getChatsController() -> ViewController?
func getSettingsController() -> ViewController?
func getPrivacySettings() -> Promise<AccountPrivacySettings?>? func getPrivacySettings() -> Promise<AccountPrivacySettings?>?
func openSettings() func getTwoStepAuthData() -> Promise<TwoStepAuthData?>?
func openContacts()
func openSettings(edit: Bool)
func openBirthdaySetup() func openBirthdaySetup()
func openPhotoSetup(completedWithUploadingImage: @escaping (UIImage, Signal<PeerInfoAvatarUploadStatus, NoError>) -> UIView?) func openPhotoSetup(completedWithUploadingImage: @escaping (UIImage, Signal<PeerInfoAvatarUploadStatus, NoError>) -> UIView?)
func openAvatars() func openAvatars()
func startNewCall()
} }
public protocol QuickReplySetupScreenInitialData: AnyObject { public protocol QuickReplySetupScreenInitialData: AnyObject {
@ -1185,6 +1233,7 @@ public enum ChannelMembersSearchControllerMode {
case promote case promote
case ban case ban
case inviteToCall case inviteToCall
case ownershipTransfer
} }
public enum ChannelMembersSearchFilter { public enum ChannelMembersSearchFilter {
@ -1412,6 +1461,7 @@ public protocol SharedAccountContext: AnyObject {
func makeGiftOfferScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController func makeGiftOfferScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController
func makeGiftUpgradeVariantsScreen(context: AccountContext, gift: StarGift, onlyCrafted: Bool, attributes: [StarGift.UniqueGift.Attribute], selectedAttributes: [StarGift.UniqueGift.Attribute]?, focusedAttribute: StarGift.UniqueGift.Attribute?) -> ViewController func makeGiftUpgradeVariantsScreen(context: AccountContext, gift: StarGift, onlyCrafted: Bool, attributes: [StarGift.UniqueGift.Attribute], selectedAttributes: [StarGift.UniqueGift.Attribute]?, focusedAttribute: StarGift.UniqueGift.Attribute?) -> ViewController
func makeGiftAuctionWearPreviewScreen(context: AccountContext, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?, attributes: [StarGift.UniqueGift.Attribute], completion: @escaping () -> Void) -> ViewController func makeGiftAuctionWearPreviewScreen(context: AccountContext, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?, attributes: [StarGift.UniqueGift.Attribute], completion: @escaping () -> Void) -> ViewController
func makeGiftCraftScreen(context: AccountContext, gift: StarGift.UniqueGift) -> ViewController
func makeGiftDemoScreen(context: AccountContext) -> ViewController func makeGiftDemoScreen(context: AccountContext) -> ViewController
func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController
func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?) func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?)

View file

@ -976,13 +976,19 @@ public enum PeerInfoAvatarUploadStatus {
public protocol PeerInfoScreen: ViewController { public protocol PeerInfoScreen: ViewController {
var peerId: PeerId { get } var peerId: PeerId { get }
var privacySettings: Promise<AccountPrivacySettings?> { get } var privacySettings: Promise<AccountPrivacySettings?> { get }
var twoStepAuthData: Promise<TwoStepAuthData?> { get }
func activateEdit()
func openEmojiStatusSetup()
func openBirthdaySetup() func openBirthdaySetup()
func toggleStorySelection(ids: [Int32], isSelected: Bool) func toggleStorySelection(ids: [Int32], isSelected: Bool)
func togglePaneIsReordering(isReordering: Bool) func togglePaneIsReordering(isReordering: Bool)
func cancelItemSelection() func cancelItemSelection()
func openAvatarSetup(completedWithUploadingImage: @escaping (UIImage, Signal<PeerInfoAvatarUploadStatus, NoError>) -> UIView?) func openAvatarSetup(completedWithUploadingImage: @escaping (UIImage, Signal<PeerInfoAvatarUploadStatus, NoError>) -> UIView?)
func openAvatars() func openAvatars()
func updateProfilePhoto(_ image: UIImage)
func updateProfileVideo(_ image: UIImage, video: Any?, values: Any?, markup: UploadPeerPhotoMarkup?)
} }
public extension Peer { public extension Peer {

View file

@ -28,4 +28,7 @@ public protocol ChatListController: ViewController {
func openStoriesFromNotification(peerId: EnginePeer.Id, storyId: Int32) func openStoriesFromNotification(peerId: EnginePeer.Id, storyId: Int32)
func resetForumStackIfOpen() func resetForumStackIfOpen()
func activateEdit()
func openEmojiStatusSetup()
} }

View file

@ -779,7 +779,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
self.ignoreUpdatesUntilScrollingStopped = true self.ignoreUpdatesUntilScrollingStopped = true
} }
} }
@available(iOS 13.0, *) @available(iOS 13.0, *)
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
if #available(iOS 14.5, *), navigationAction.shouldPerformDownload { if #available(iOS 14.5, *), navigationAction.shouldPerformDownload {
@ -798,11 +798,16 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
if let url = navigationAction.request.url?.absoluteString { if let url = navigationAction.request.url?.absoluteString {
if (navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == true) && (isTelegramMeLink(url) || url.hasPrefix("tg://")) && !url.contains("/auth/push?") && !self._state.url.contains("/auth/push?") { if (navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == true) && (isTelegramMeLink(url) || url.hasPrefix("tg://")) && !url.contains("/auth/push?") && !self._state.url.contains("/auth/push?") {
decisionHandler(.cancel, preferences) decisionHandler(.cancel, preferences)
self.minimize() if !url.contains("domain=oauth") {
self.minimize()
}
self.openAppUrl(url) self.openAppUrl(url)
} else { } else {
if let scheme = navigationAction.request.url?.scheme, !["http", "https", "tonsite", "about"].contains(scheme.lowercased()) { if let scheme = navigationAction.request.url?.scheme?.lowercased(), !["http", "https", "tonsite", "about"].contains(scheme) {
decisionHandler(.cancel, preferences) decisionHandler(.cancel, preferences)
if ["facetime"].contains(scheme) {
return
}
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: self.presentationData, navigationController: nil, dismissInput: {}) self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: self.presentationData, navigationController: nil, dismissInput: {})
} else { } else {
decisionHandler(.allow, preferences) decisionHandler(.allow, preferences)

View file

@ -23,6 +23,20 @@ public enum CallListControllerMode {
case navigation case navigation
} }
public enum CallListEntryTag: ItemListItemTag, Equatable {
case edit
case showTab
case missed
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? CallListEntryTag, self == other {
return true
} else {
return false
}
}
}
private final class DeleteAllButtonNode: ASDisplayNode { private final class DeleteAllButtonNode: ASDisplayNode {
private let pressed: () -> Void private let pressed: () -> Void
@ -69,6 +83,8 @@ private final class DeleteAllButtonNode: ASDisplayNode {
} }
} }
public final class CallListController: TelegramBaseController { public final class CallListController: TelegramBaseController {
private var controllerNode: CallListControllerNode { private var controllerNode: CallListControllerNode {
return self.displayNode as! CallListControllerNode return self.displayNode as! CallListControllerNode
@ -81,6 +97,7 @@ public final class CallListController: TelegramBaseController {
private let context: AccountContext private let context: AccountContext
private let mode: CallListControllerMode private let mode: CallListControllerMode
private let focusOnItemTag: CallListEntryTag?
private var presentationData: PresentationData private var presentationData: PresentationData
private var presentationDataDisposable: Disposable? private var presentationDataDisposable: Disposable?
@ -96,9 +113,15 @@ public final class CallListController: TelegramBaseController {
private let clearDisposable = MetaDisposable() private let clearDisposable = MetaDisposable()
private var createConferenceCallDisposable: Disposable? private var createConferenceCallDisposable: Disposable?
public init(context: AccountContext, mode: CallListControllerMode) { public init(
context: AccountContext,
mode: CallListControllerMode,
focusOnItemTag: CallListEntryTag? = nil
) {
self.context = context self.context = context
self.mode = mode self.mode = mode
self.focusOnItemTag = focusOnItemTag
self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.segmentedTitleView = ItemListControllerSegmentedTitleView(theme: self.presentationData.theme, segments: [self.presentationData.strings.Calls_All, self.presentationData.strings.Calls_Missed], selectedIndex: 0) self.segmentedTitleView = ItemListControllerSegmentedTitleView(theme: self.presentationData.theme, segments: [self.presentationData.strings.Calls_All, self.presentationData.strings.Calls_Missed], selectedIndex: 0)
@ -409,7 +432,7 @@ public final class CallListController: TelegramBaseController {
if let strongSelf = self { if let strongSelf = self {
strongSelf.callPressed() strongSelf.callPressed()
} }
}) }, focusOnItemTag: self.focusOnItemTag)
if case .navigation = self.mode { if case .navigation = self.mode {
self.controllerNode.navigationBar = self.navigationBar self.controllerNode.navigationBar = self.navigationBar
@ -421,6 +444,18 @@ public final class CallListController: TelegramBaseController {
} }
self._ready.set(self.controllerNode.ready) self._ready.set(self.controllerNode.ready)
self.displayNodeDidLoad() self.displayNodeDidLoad()
switch self.focusOnItemTag {
case .edit:
self.editPressed()
case .missed:
Queue.mainQueue().after(0.1) {
self.segmentedTitleView.index = 1
self.controllerNode.updateType(.missed)
}
default:
break
}
} }
override public var navigationEdgeEffectExtension: CGFloat { override public var navigationEdgeEffectExtension: CGFloat {

View file

@ -125,7 +125,7 @@ private func mappedInsertEntries(context: AccountContext, presentationData: Item
case let .displayTab(_, text, value): case let .displayTab(_, text, value):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enabled: true, noCorners: false, sectionId: 0, style: .blocks, updated: { value in return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enabled: true, noCorners: false, sectionId: 0, style: .blocks, updated: { value in
nodeInteraction.updateShowCallsTab(value) nodeInteraction.updateShowCallsTab(value)
}), directionHint: entry.directionHint) }, tag: CallListEntryTag.showTab), directionHint: entry.directionHint)
case let .displayTabInfo(_, text): case let .displayTabInfo(_, text):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint) return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
case .openNewCall: case .openNewCall:
@ -149,7 +149,7 @@ private func mappedUpdateEntries(context: AccountContext, presentationData: Item
case let .displayTab(_, text, value): case let .displayTab(_, text, value):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enabled: true, noCorners: false, sectionId: 0, style: .blocks, updated: { value in return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enabled: true, noCorners: false, sectionId: 0, style: .blocks, updated: { value in
nodeInteraction.updateShowCallsTab(value) nodeInteraction.updateShowCallsTab(value)
}), directionHint: entry.directionHint) }, tag: CallListEntryTag.showTab), directionHint: entry.directionHint)
case let .displayTabInfo(_, text): case let .displayTabInfo(_, text):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint) return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
case .openNewCall: case .openNewCall:
@ -184,6 +184,7 @@ final class CallListControllerNode: ASDisplayNode {
private let context: AccountContext private let context: AccountContext
private let mode: CallListControllerMode private let mode: CallListControllerMode
private var presentationData: PresentationData private var presentationData: PresentationData
private var focusOnItemTag: CallListEntryTag?
private var containerLayout: (ContainerViewLayout, CGFloat)? private var containerLayout: (ContainerViewLayout, CGFloat)?
@ -241,7 +242,7 @@ final class CallListControllerNode: ASDisplayNode {
private var previousContentOffset: ListViewVisibleContentOffset? private var previousContentOffset: ListViewVisibleContentOffset?
init(controller: CallListController, context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (EngineMessage) -> Void, joinGroupCall: @escaping (EnginePeer.Id, EngineGroupCallDescription) -> Void, openInfo: @escaping (EnginePeer.Id, [EngineMessage]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void, openNewCall: @escaping () -> Void) { init(controller: CallListController, context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (EngineMessage) -> Void, joinGroupCall: @escaping (EnginePeer.Id, EngineGroupCallDescription) -> Void, openInfo: @escaping (EnginePeer.Id, [EngineMessage]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void, openNewCall: @escaping () -> Void, focusOnItemTag: CallListEntryTag?) {
self.controller = controller self.controller = controller
self.context = context self.context = context
self.mode = mode self.mode = mode
@ -253,6 +254,7 @@ final class CallListControllerNode: ASDisplayNode {
self.openNewCall = openNewCall self.openNewCall = openNewCall
self.currentState = CallListNodeState(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: true, editing: false, messageIdWithRevealedOptions: nil) self.currentState = CallListNodeState(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: true, editing: false, messageIdWithRevealedOptions: nil)
self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true)
self.focusOnItemTag = focusOnItemTag
self.listNode = ListView() self.listNode = ListView()
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
@ -861,6 +863,16 @@ final class CallListControllerNode: ASDisplayNode {
strongSelf._ready.set(true) strongSelf._ready.set(true)
} }
if let focusOnItemTag = strongSelf.focusOnItemTag {
strongSelf.focusOnItemTag = nil
strongSelf.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
itemNode.displayHighlight()
}
}
}
completion() completion()
} }
} }

View file

@ -125,6 +125,10 @@ swift_library(
"//submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent", "//submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent",
"//submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode", "//submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode",
"//submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent", "//submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent",
"//submodules/TelegramUI/Components/AvatarComponent",
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View file

@ -144,7 +144,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private var didSuggestLoginEmailSetup = false private var didSuggestLoginEmailSetup = false
private var didSuggestLoginPasskeySetup = false private var didSuggestLoginPasskeySetup = false
private var presentationData: PresentationData private(set) var presentationData: PresentationData
private let presentationDataValue = Promise<PresentationData>() private let presentationDataValue = Promise<PresentationData>()
private var presentationDataDisposable: Disposable? private var presentationDataDisposable: Disposable?
@ -3029,7 +3029,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} }
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
let coordinator = rootController.openStoryCamera(customTarget: nil, resumeLiveStream: hasLiveStream, transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: self.storyCameraTransitionOut()) let coordinator = rootController.openStoryCamera(mode: .photo, customTarget: nil, resumeLiveStream: hasLiveStream, transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: self.storyCameraTransitionOut())
coordinator?.animateIn() coordinator?.animateIn()
} }
} }
@ -3511,7 +3511,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
super.navigationStackConfigurationUpdated(next: next) super.navigationStackConfigurationUpdated(next: next)
} }
@objc fileprivate func editPressed() { public func activateEdit() {
self.editPressed()
}
public func openEmojiStatusSetup() {
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
navigationBarView.openEmojiStatusSetup()
}
}
@objc func editPressed() {
if self.secondaryContext == nil { if self.secondaryContext == nil {
if case .chatList(.root) = self.chatListDisplayNode.effectiveContainerNode.location { if case .chatList(.root) = self.chatListDisplayNode.effectiveContainerNode.location {
self.effectiveContext?.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( self.effectiveContext?.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent(
@ -5874,10 +5884,33 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}) })
], parseMarkdown: true), in: .window(.root)) ], parseMarkdown: true), in: .window(.root))
} else { } else {
completion(true) let proceed = {
self.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { completion(true)
removed() self.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
}) removed()
})
}
if case let .channel(channel) = peer.peer, channel.flags.contains(.isCreator) {
let _ = (self.context.engine.peers.getFutureCreatorAfterLeave(peerId: channel.id)
|> deliverOnMainQueue).start(next: { [weak self] nextCreator in
guard let self else {
return
}
if let nextCreator, let peer = peer.peer {
self.presentLeaveChannelConfirmation(peer: peer, nextCreator: nextCreator, completion: { commit in
if commit {
proceed()
} else {
completion(false)
}
})
} else {
proceed()
}
})
} else {
proceed()
}
} }
} }
@ -6425,7 +6458,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if let current = self.storyCameraTransitionInCoordinator { if let current = self.storyCameraTransitionInCoordinator {
coordinator = current coordinator = current
} else { } else {
coordinator = rootController.openStoryCamera(customTarget: nil, resumeLiveStream: false, transitionIn: nil, transitionedIn: {}, transitionOut: { [weak self] target, _ in coordinator = rootController.openStoryCamera(mode: .photo, customTarget: nil, resumeLiveStream: false, transitionIn: nil, transitionedIn: {}, transitionOut: { [weak self] target, _ in
guard let self, let target else { guard let self, let target else {
return nil return nil
} }

View file

@ -0,0 +1,157 @@
import Foundation
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AccountContext
import ComponentFlow
import AlertComponent
import AlertTransferHeaderComponent
import AvatarComponent
import PeerInfoUI
import OwnershipTransferController
extension ChatListControllerImpl {
func presentLeaveChannelConfirmation(peer: EnginePeer, nextCreator: EnginePeer, completion: @escaping (Bool) -> Void) {
Task { @MainActor in
let accountPeer = await (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))).get()
guard let accountPeer else {
completion(false)
return
}
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "header",
component: AnyComponent(
AlertTransferHeaderComponent(
fromComponent: AnyComponentWithIdentity(id: "account", component: AnyComponent(
AvatarComponent(
context: self.context,
theme: self.presentationData.theme,
peer: accountPeer
)
)),
toComponent: AnyComponentWithIdentity(id: "user", component: AnyComponent(
AvatarComponent(
context: self.context,
theme: self.presentationData.theme,
peer: nextCreator,
icon: AnyComponent(
AvatarComponent(
context: self.context,
theme: self.presentationData.theme,
peer: peer
)
)
)
)),
type: .transfer
)
)
))
//TODO:localize
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: "Leave \(peer.compactDisplayTitle)")
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain("If you leave, **\(nextCreator.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder))** will become the owner of **\(peer.compactDisplayTitle)** in **1 week**."))
)
))
let alertController = AlertScreen(
context: self.context,
configuration: .init(actionAlignment: .vertical),
content: content,
actions: [
.init(title: "Appoint Another Owner", action: { [weak self] in
guard let self else {
return
}
self.presentOwnershipTransfer(chatPeer: peer)
}),
.init(title: "Cancel", action: {
completion(false)
}),
.init(title: "Leave Group", type: .destructive, action: {
completion(true)
})
]
)
if let topController = self.navigationController?.topViewController as? ViewController {
topController.present(alertController, in: .window(.root))
}
}
}
func presentOwnershipTransfer(chatPeer: EnginePeer) {
let presentController: (ViewController) -> Void = { [weak self] c in
if let topController = self?.navigationController?.topViewController as? ViewController {
topController.present(c, in: .window(.root))
}
}
let pushController: (ViewController) -> Void = { [weak self] c in
if let topController = self?.navigationController?.topViewController as? ViewController {
topController.push(c)
}
}
var dismissController: (() -> Void)?
let controller = ChannelMembersSearchControllerImpl(
params: ChannelMembersSearchControllerParams(
context: self.context,
peerId: chatPeer.id,
mode: .ownershipTransfer,
filters: [],
openPeer: { [weak self] peer, participant in
guard let self else {
return
}
if peer.id == self.context.account.peerId {
return
}
if let participant {
switch participant.participant {
case .creator:
return
case let .member(_, _, adminInfo, _, _, _):
let _ = adminInfo
let _ = (self.context.engine.peers.checkOwnershipTranfserAvailability(memberId: peer.id) |> deliverOnMainQueue).start(error: { [weak self] error in
guard let self, case let .user(user) = peer else {
return
}
let controller = channelOwnershipTransferController(
context: self.context,
updatedPresentationData: nil,
peer: chatPeer,
member: user,
initialError: error,
present: { c, a in
presentController(c)
},
push: { c in
pushController(c)
},
completion: { _ in
dismissController?()
}
)
presentController(controller)
})
}
}
})
)
dismissController = { [weak controller] in
controller?.dismiss()
}
pushController(controller)
}
}

View file

@ -1388,7 +1388,7 @@ private final class ChatListFilterPresetController: ItemListController {
} }
} }
func chatListFilterPresetController(context: AccountContext, currentPreset initialPreset: ChatListFilter?, updated: @escaping ([ChatListFilter]) -> Void) -> ViewController { public func chatListFilterPresetController(context: AccountContext, currentPreset initialPreset: ChatListFilter?, updated: @escaping ([ChatListFilter]) -> Void) -> ViewController {
let initialName: ChatFolderTitle let initialName: ChatFolderTitle
if let initialPreset { if let initialPreset {
initialName = initialPreset.title initialName = initialPreset.title

View file

@ -45,6 +45,8 @@ private enum ChatListFilterPresetListSection: Int32 {
} }
public enum ChatListFilterPresetListEntryTag: ItemListItemTag { public enum ChatListFilterPresetListEntryTag: ItemListItemTag {
case edit
case addRecommended
case displayTags case displayTags
public func isEqual(to other: ItemListItemTag) -> Bool { public func isEqual(to other: ItemListItemTag) -> Bool {
@ -175,7 +177,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
case let .screenHeader(text): case let .screenHeader(text):
return ChatListFilterSettingsHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animation: .folders, sectionId: self.section) return ChatListFilterSettingsHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animation: .folders, sectionId: self.section)
case let .suggestedListHeader(text): case let .suggestedListHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section, tag: ChatListFilterPresetListEntryTag.addRecommended)
case let .suggestedPreset(_, title, label, preset): case let .suggestedPreset(_, title, label, preset):
return ChatListFilterPresetListSuggestedItem(presentationData: presentationData, systemStyle: .glass, title: title.text, label: label, sectionId: self.section, style: .blocks, installAction: { return ChatListFilterPresetListSuggestedItem(presentationData: presentationData, systemStyle: .glass, title: title.text, label: label, sectionId: self.section, style: .blocks, installAction: {
arguments.addSuggestedPressed(title, preset) arguments.addSuggestedPressed(title, preset)
@ -318,7 +320,7 @@ public enum ChatListFilterPresetListControllerMode {
case modal case modal
} }
public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, scrollToTags: Bool = false, dismissed: (() -> Void)? = nil) -> ViewController { public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, scrollToTags: Bool = false, focusOnItemTag: ChatListFilterPresetListEntryTag? = nil, dismissed: (() -> Void)? = nil) -> ViewController {
let initialState = ChatListFilterPresetListControllerState() let initialState = ChatListFilterPresetListControllerState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true) let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState) let stateValue = Atomic(value: initialState)
@ -326,6 +328,14 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
statePromise.set(stateValue.modify { f($0) }) statePromise.set(stateValue.modify { f($0) })
} }
if focusOnItemTag == .edit {
updateState { state in
var state = state
state.isEditing = true
return state
}
}
var dismissImpl: (() -> Void)? var dismissImpl: (() -> Void)?
var pushControllerImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController) -> Void)?
@ -680,7 +690,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let entries = chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits) let entries = chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, initialScrollToItem: scrollToTags ? ListViewScrollToItem(index: entries.count - 1, position: .center(.bottom), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down) : nil, animateChanges: true) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, initialScrollToItem: scrollToTags ? ListViewScrollToItem(index: entries.count - 1, position: .center(.bottom), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down) : nil, animateChanges: true)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }
@ -797,6 +807,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
} }
}) })
}) })
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in controller.afterTransactionCompleted = { [weak controller] in
guard let toggleDirection = animateNextShowHideTagsTransition.swap(nil) else { guard let toggleDirection = animateNextShowHideTagsTransition.swap(nil) else {
return return
@ -819,6 +831,15 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
} }
delay += 0.02 delay += 0.02
} }
if let focusOnItemTag, !didFocusOnItem {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
} }
return controller return controller

View file

@ -193,7 +193,7 @@ public final class FilledRoundedRectangleComponent: Component {
} }
} else { } else {
if component.smoothCorners { if component.smoothCorners {
let size = CGSize(width: cornerRadius * 2.0 + 10.0, height: cornerRadius * 2.0 + 10.0) let size = CGSize(width: cornerRadius * 2.0 + 8.0, height: cornerRadius * 2.0 + 8.0)
if let cornerImage = self.cornerImage, cornerImage.size == size { if let cornerImage = self.cornerImage, cornerImage.size == size {
} else { } else {
self.cornerImage = generateImage(size, rotatedContext: { size, context in self.cornerImage = generateImage(size, rotatedContext: { size, context in

View file

@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ResizableSheetComponent",
module_name = "ResizableSheetComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/TelegramUI/Components/DynamicCornerRadiusView",
"//submodules/TelegramUI/Components/EdgeEffect",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,678 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ViewControllerComponent
import SwiftSignalKit
import DynamicCornerRadiusView
import TelegramPresentationData
import EdgeEffect
public final class ResizableSheetComponentEnvironment: Equatable {
public let theme: PresentationTheme
public let statusBarHeight: CGFloat
public let safeInsets: UIEdgeInsets
public let metrics: LayoutMetrics
public let deviceMetrics: DeviceMetrics
public let isDisplaying: Bool
public let isCentered: Bool
public let screenSize: CGSize
public let regularMetricsSize: CGSize?
public let dismiss: (Bool) -> Void
public init(
theme: PresentationTheme,
statusBarHeight: CGFloat,
safeInsets: UIEdgeInsets,
metrics: LayoutMetrics,
deviceMetrics: DeviceMetrics,
isDisplaying: Bool,
isCentered: Bool,
screenSize: CGSize,
regularMetricsSize: CGSize?,
dismiss: @escaping (Bool) -> Void
) {
self.theme = theme
self.statusBarHeight = statusBarHeight
self.safeInsets = safeInsets
self.metrics = metrics
self.deviceMetrics = deviceMetrics
self.isDisplaying = isDisplaying
self.isCentered = isCentered
self.screenSize = screenSize
self.regularMetricsSize = regularMetricsSize
self.dismiss = dismiss
}
public static func ==(lhs: ResizableSheetComponentEnvironment, rhs: ResizableSheetComponentEnvironment) -> Bool {
if lhs.theme != rhs.theme {
return false
}
if lhs.statusBarHeight != rhs.statusBarHeight {
return false
}
if lhs.safeInsets != rhs.safeInsets {
return false
}
if lhs.metrics != rhs.metrics {
return false
}
if lhs.deviceMetrics != rhs.deviceMetrics {
return false
}
if lhs.isDisplaying != rhs.isDisplaying {
return false
}
if lhs.isCentered != rhs.isCentered {
return false
}
if lhs.screenSize != rhs.screenSize {
return false
}
if lhs.regularMetricsSize != rhs.regularMetricsSize {
return false
}
return true
}
}
public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equatable>: Component {
public typealias EnvironmentType = (ChildEnvironmentType, ResizableSheetComponentEnvironment)
public class ExternalState {
public fileprivate(set) var contentHeight: CGFloat
public init() {
self.contentHeight = 0.0
}
}
public enum BackgroundColor: Equatable {
case color(UIColor)
}
public let content: AnyComponent<ChildEnvironmentType>
public let titleItem: AnyComponent<Empty>?
public let leftItem: AnyComponent<Empty>?
public let rightItem: AnyComponent<Empty>?
public let hasTopEdgeEffect: Bool
public let bottomItem: AnyComponent<Empty>?
public let backgroundColor: BackgroundColor
public let isFullscreen: Bool
public let externalState: ExternalState?
public let animateOut: ActionSlot<Action<()>>
public init(
content: AnyComponent<ChildEnvironmentType>,
titleItem: AnyComponent<Empty>? = nil,
leftItem: AnyComponent<Empty>? = nil,
rightItem: AnyComponent<Empty>? = nil,
hasTopEdgeEffect: Bool = true,
bottomItem: AnyComponent<Empty>? = nil,
backgroundColor: BackgroundColor,
isFullscreen: Bool = false,
externalState: ExternalState? = nil,
animateOut: ActionSlot<Action<()>>,
) {
self.content = content
self.titleItem = titleItem
self.leftItem = leftItem
self.rightItem = rightItem
self.hasTopEdgeEffect = hasTopEdgeEffect
self.bottomItem = bottomItem
self.backgroundColor = backgroundColor
self.isFullscreen = isFullscreen
self.externalState = externalState
self.animateOut = animateOut
}
public static func ==(lhs: ResizableSheetComponent, rhs: ResizableSheetComponent) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.titleItem != rhs.titleItem {
return false
}
if lhs.leftItem != rhs.leftItem {
return false
}
if lhs.rightItem != rhs.rightItem {
return false
}
if lhs.hasTopEdgeEffect != rhs.hasTopEdgeEffect {
return false
}
if lhs.bottomItem != rhs.bottomItem {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.isFullscreen != rhs.isFullscreen {
return false
}
if lhs.animateOut != rhs.animateOut {
return false
}
return true
}
private struct ItemLayout: Equatable {
var containerSize: CGSize
var containerInset: CGFloat
var containerCornerRadius: CGFloat
var bottomInset: CGFloat
var topInset: CGFloat
init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat) {
self.containerSize = containerSize
self.containerInset = containerInset
self.containerCornerRadius = containerCornerRadius
self.bottomInset = bottomInset
self.topInset = topInset
}
}
private final class ScrollView: UIScrollView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
}
public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView {
public final class Tag {
public init() {
}
}
public func matches(tag: Any) -> Bool {
if let _ = tag as? Tag {
return true
}
return false
}
private let dimView: UIView
private let containerView: UIView
private let backgroundLayer: SimpleLayer
private let navigationBarContainer: SparseContainerView
private let bottomContainer: SparseContainerView
private let scrollView: ScrollView
private let scrollContentClippingView: SparseContainerView
private let scrollContentView: UIView
private let topEdgeEffectView: EdgeEffectView
private let bottomEdgeEffectView: EdgeEffectView
private let contentView: ComponentView<ChildEnvironmentType>
private var titleItemView: ComponentView<Empty>?
private var leftItemView: ComponentView<Empty>?
private var rightItemView: ComponentView<Empty>?
private var bottomItemView: ComponentView<Empty>?
private let backgroundHandleView: UIImageView
private var ignoreScrolling: Bool = false
private var component: ResizableSheetComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private var environment: ResizableSheetComponentEnvironment?
private var itemLayout: ItemLayout?
override init(frame: CGRect) {
self.dimView = UIView()
self.containerView = UIView()
self.containerView.clipsToBounds = true
self.containerView.layer.cornerRadius = 40.0
self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.backgroundLayer.cornerRadius = 40.0
self.backgroundHandleView = UIImageView()
self.navigationBarContainer = SparseContainerView()
self.bottomContainer = SparseContainerView()
self.scrollView = ScrollView()
self.scrollContentClippingView = SparseContainerView()
self.scrollContentClippingView.clipsToBounds = true
self.scrollContentView = UIView()
self.topEdgeEffectView = EdgeEffectView()
self.topEdgeEffectView.clipsToBounds = true
self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.topEdgeEffectView.layer.cornerRadius = 40.0
self.bottomEdgeEffectView = EdgeEffectView()
self.bottomEdgeEffectView.clipsToBounds = true
self.bottomEdgeEffectView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
self.bottomEdgeEffectView.layer.cornerRadius = 40.0
self.contentView = ComponentView()
super.init(frame: frame)
self.addSubview(self.dimView)
self.addSubview(self.containerView)
self.containerView.layer.addSublayer(self.backgroundLayer)
self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
self.scrollView.contentInsetAdjustmentBehavior = .never
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.alwaysBounceVertical = true
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.containerView.addSubview(self.scrollContentClippingView)
self.scrollContentClippingView.addSubview(self.scrollView)
self.scrollView.addSubview(self.scrollContentView)
self.containerView.addSubview(self.navigationBarContainer)
self.containerView.addSubview(self.bottomContainer)
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if !self.backgroundLayer.frame.contains(point) {
return self.dimView
}
if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
return result
}
if let result = self.bottomContainer.hitTest(self.convert(point, to: self.bottomContainer), with: event) {
return result
}
let result = super.hitTest(point, with: event)
return result
}
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.dismissAnimated()
}
}
public func dismissAnimated() {
guard let environment = self.environment else {
return
}
self.endEditing(true)
environment.dismiss(true)
}
private func updateScrolling(transition: ComponentTransition) {
guard let itemLayout = self.itemLayout, let component = self.component else {
return
}
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
topOffset = max(0.0, topOffset)
transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
var topOffsetFraction = self.scrollView.bounds.minY / 100.0
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
if component.isFullscreen {
topOffsetFraction = 1.0
}
let minScale: CGFloat = (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width
let minScaledTranslation: CGFloat = (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0
let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius
let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction
let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction)
let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction
var containerTransform = CATransform3DIdentity
containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0)
containerTransform = CATransform3DScale(containerTransform, scale, scale, scale)
transition.setTransform(view: self.containerView, transform: containerTransform)
transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius)
if component.isFullscreen {
transition.setBounds(view: self.scrollView, bounds: CGRect(origin: .zero, size: self.scrollView.bounds.size))
self.scrollView.isScrollEnabled = false
} else {
self.scrollView.isScrollEnabled = true
}
}
private var didPlayAppearanceAnimation = false
func animateIn() {
self.didPlayAppearanceAnimation = true
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.bottomContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
func animateOut(completion: @escaping () -> Void) {
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
completion()
})
self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.bottomContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
}
func update(component: ResizableSheetComponent<ChildEnvironmentType>, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let sheetEnvironment = environment[ResizableSheetComponentEnvironment.self].value
component.animateOut.connect { [weak self] completion in
guard let self else {
return
}
self.animateOut {
completion(Void())
}
}
let resetScrolling = self.scrollView.bounds.width != availableSize.width
let fillingSize: CGFloat
if case .regular = sheetEnvironment.metrics.widthClass {
fillingSize = min(availableSize.width, 414.0) - sheetEnvironment.safeInsets.left * 2.0
} else {
fillingSize = min(availableSize.width, sheetEnvironment.deviceMetrics.screenSize.width) - sheetEnvironment.safeInsets.left * 2.0
}
let rawSideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5)
self.component = component
self.state = state
self.environment = sheetEnvironment
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
let backgroundColor: UIColor
switch component.backgroundColor {
case let .color(color):
backgroundColor = color
self.backgroundLayer.backgroundColor = backgroundColor.cgColor
}
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
var containerSize: CGSize
if !"".isEmpty, sheetEnvironment.isCentered {
let verticalInset: CGFloat = 44.0
let maxSide = max(availableSize.width, availableSize.height)
let minSide = min(availableSize.width, availableSize.height)
containerSize = CGSize(width: min(availableSize.width - 20.0, floor(maxSide / 2.0)), height: min(availableSize.height, minSide) - verticalInset * 2.0)
if let regularMetricsSize = sheetEnvironment.regularMetricsSize {
containerSize = regularMetricsSize
}
} else {
containerSize = CGSize(width: fillingSize, height: .greatestFiniteMagnitude)
}
var containerInset: CGFloat = sheetEnvironment.statusBarHeight + 10.0
if component.isFullscreen {
containerInset = 0.0
}
let clippingY: CGFloat
self.contentView.parentState = state
let contentViewSize = self.contentView.update(
transition: transition,
component: component.content,
environment: {
environment[ChildEnvironmentType.self]
},
containerSize: containerSize
)
component.externalState?.contentHeight = contentViewSize.height
if let contentView = self.contentView.view {
if contentView.superview == nil {
self.scrollContentView.addSubview(contentView)
}
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: contentViewSize))
}
let contentHeight = contentViewSize.height
let initialContentHeight = contentHeight
let edgeEffectHeight: CGFloat = 80.0
let edgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: edgeEffectHeight))
transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame)
self.topEdgeEffectView.update(content: backgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition)
if self.topEdgeEffectView.superview == nil {
self.navigationBarContainer.insertSubview(self.topEdgeEffectView, at: 0)
}
self.topEdgeEffectView.isHidden = !component.hasTopEdgeEffect
if let titleItem = component.titleItem {
let titleItemView: ComponentView<Empty>
if let current = self.titleItemView {
titleItemView = current
} else {
titleItemView = ComponentView<Empty>()
self.titleItemView = titleItemView
}
let titleItemSize = titleItemView.update(
transition: transition,
component: titleItem,
environment: {},
containerSize: CGSize(width: containerSize.width - 66.0 * 2.0, height: 66.0)
)
let titleItemFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((containerSize.width - titleItemSize.width)) / 2.0, y: floorToScreenPixels(36.0 - titleItemSize.height * 0.5)), size: titleItemSize)
if let view = titleItemView.view {
if view.superview == nil {
self.navigationBarContainer.addSubview(view)
}
transition.setFrame(view: view, frame: titleItemFrame)
}
} else if let titleItemView = self.titleItemView {
self.titleItemView = nil
titleItemView.view?.removeFromSuperview()
}
if let leftItem = component.leftItem {
var leftItemTransition = transition
let leftItemView: ComponentView<Empty>
if let current = self.leftItemView {
leftItemView = current
} else {
leftItemTransition = .immediate
leftItemView = ComponentView<Empty>()
self.leftItemView = leftItemView
}
let leftItemSize = leftItemView.update(
transition: leftItemTransition,
component: leftItem,
environment: {},
containerSize: CGSize(width: 66.0, height: 66.0)
)
let leftItemFrame = CGRect(origin: CGPoint(x: rawSideInset + 16.0, y: 16.0), size: leftItemSize)
if let view = leftItemView.view {
if view.superview == nil {
self.navigationBarContainer.addSubview(view)
}
leftItemTransition.setFrame(view: view, frame: leftItemFrame)
}
} else if let leftItemView = self.leftItemView {
self.leftItemView = nil
if !transition.animation.isImmediate {
leftItemView.view?.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
leftItemView.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
leftItemView.view?.removeFromSuperview()
})
} else {
leftItemView.view?.removeFromSuperview()
}
}
if let rightItem = component.rightItem {
var rightItemTransition = transition
let rightItemView: ComponentView<Empty>
if let current = self.rightItemView {
rightItemView = current
} else {
rightItemTransition = .immediate
rightItemView = ComponentView<Empty>()
self.rightItemView = rightItemView
}
let rightItemSize = rightItemView.update(
transition: rightItemTransition,
component: rightItem,
environment: {},
containerSize: CGSize(width: 66.0, height: 66.0)
)
let rightItemFrame = CGRect(origin: CGPoint(x: availableSize.width - rawSideInset - 16.0 - rightItemSize.width, y: 16.0), size: rightItemSize)
if let view = rightItemView.view {
if view.superview == nil {
self.navigationBarContainer.addSubview(view)
if !transition.animation.isImmediate {
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
rightItemTransition.setFrame(view: view, frame: rightItemFrame)
}
} else if let rightItemView = self.rightItemView {
self.rightItemView = nil
if !transition.animation.isImmediate {
rightItemView.view?.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
rightItemView.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
rightItemView.view?.removeFromSuperview()
})
} else {
rightItemView.view?.removeFromSuperview()
}
}
if let bottomItem = component.bottomItem {
let bottomItemView: ComponentView<Empty>
if let current = self.bottomItemView {
bottomItemView = current
} else {
bottomItemView = ComponentView<Empty>()
self.bottomItemView = bottomItemView
}
let bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: sheetEnvironment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
let bottomItemSize = bottomItemView.update(
transition: transition,
component: bottomItem,
environment: {},
containerSize: CGSize(width: containerSize.width - bottomInsets.left - bottomInsets.right, height: 52.0)
)
let bottomItemFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((containerSize.width - bottomItemSize.width)) / 2.0, y: availableSize.height - bottomItemSize.height - bottomInsets.bottom), size: bottomItemSize)
if let view = bottomItemView.view {
if view.superview == nil {
self.bottomContainer.addSubview(view)
}
transition.setFrame(view: view, frame: bottomItemFrame)
}
} else if let bottomItemView = self.bottomItemView {
self.bottomItemView = nil
bottomItemView.view?.removeFromSuperview()
}
let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: availableSize.height - edgeEffectHeight), size: CGSize(width: fillingSize, height: edgeEffectHeight))
transition.setFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame)
self.bottomEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: transition)
if self.bottomEdgeEffectView.superview == nil {
self.bottomContainer.insertSubview(self.bottomEdgeEffectView, at: 0)
}
transition.setAlpha(view: self.bottomContainer, alpha: component.bottomItem != nil ? 1.0 : 0.0)
clippingY = availableSize.height
var topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight)
if component.isFullscreen {
topInset = 0.0
}
let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset)
self.scrollContentClippingView.layer.cornerRadius = 38.0
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: sheetEnvironment.deviceMetrics.screenCornerRadius, bottomInset: sheetEnvironment.safeInsets.bottom, topInset: topInset)
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: CGSize(width: fillingSize, height: availableSize.height)))
let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset))
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
self.ignoreScrolling = true
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
if contentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize
}
if resetScrolling {
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
if sheetEnvironment.isDisplaying && !self.didPlayAppearanceAnimation {
self.animateIn()
}
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -575,7 +575,7 @@ public class ContactsController: ViewController {
} }
} }
@objc private func sortPressed() { @objc public func sortPressed() {
self.sortButton.contextAction?(self.sortButton.containerNode, nil) self.sortButton.contextAction?(self.sortButton.containerNode, nil)
} }

View file

@ -551,7 +551,7 @@ private final class ContactContextExtractedContentSource: ContextExtractedConten
} }
} }
private func presentContactAccessPicker(context: AccountContext) { public func presentContactAccessPicker(context: AccountContext) {
if #available(iOS 18.0, *), let rootViewController = context.sharedContext.mainWindow?.viewController?.view.window?.rootViewController { if #available(iOS 18.0, *), let rootViewController = context.sharedContext.mainWindow?.viewController?.view.window?.rootViewController {
var dismissImpl: (() -> Void)? var dismissImpl: (() -> Void)?
let pickerView = ContactAccessPickerHostingView(completionHandler: { [weak rootViewController] ids in let pickerView = ContactAccessPickerHostingView(completionHandler: { [weak rootViewController] ids in

View file

@ -835,6 +835,17 @@ private func makeLayerSubtreeSnapshotAsView(layer: CALayer) -> UIView? {
} }
public func findParentScrollView(view: UIView?) -> UIScrollView? {
if let view = view {
if let view = view as? UIScrollView {
return view
}
return findParentScrollView(view: view.superview)
} else {
return nil
}
}
public extension UIView { public extension UIView {
func snapshotContentTree(unhide: Bool = false, keepPortals: Bool = false, keepTransform: Bool = false) -> UIView? { func snapshotContentTree(unhide: Bool = false, keepPortals: Bool = false, keepTransform: Bool = false) -> UIView? {
let wasHidden = self.isHidden let wasHidden = self.isHidden

View file

@ -35,8 +35,9 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
let noInsets: Bool let noInsets: Bool
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
public let action: (() -> Void)? public let action: (() -> Void)?
public let tag: Any?
public init(presentationData: ItemListPresentationData, style: ItemListStyle = .blocks, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage?, iconSignal: Signal<UIImage?, NoError>? = nil, title: String, additionalBadgeIcon: UIImage? = nil, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, noInsets: Bool = false, editing: Bool = false, action: (() -> Void)?) { public init(presentationData: ItemListPresentationData, style: ItemListStyle = .blocks, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage?, iconSignal: Signal<UIImage?, NoError>? = nil, title: String, additionalBadgeIcon: UIImage? = nil, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, noInsets: Bool = false, editing: Bool = false, action: (() -> Void)?, tag: Any? = nil) {
self.presentationData = presentationData self.presentationData = presentationData
self.style = style self.style = style
self.systemStyle = systemStyle self.systemStyle = systemStyle
@ -52,6 +53,7 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
self.color = color self.color = color
self.sectionId = sectionId self.sectionId = sectionId
self.action = action self.action = action
self.tag = tag
} }
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) { 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) {
@ -133,6 +135,10 @@ public final class ItemListPeerActionItemNode: ListViewItemNode {
private let iconDisposable = MetaDisposable() private let iconDisposable = MetaDisposable()
public var tag: ItemListItemTag? {
return self.item?.tag as? ItemListItemTag
}
public init() { public init() {
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true self.backgroundNode.isLayerBacked = true

View file

@ -33,6 +33,13 @@ public extension ItemListItem {
public protocol ItemListItemNode { public protocol ItemListItemNode {
var tag: ItemListItemTag? { get } var tag: ItemListItemTag? { get }
func displayHighlight()
}
public extension ItemListItemNode {
func displayHighlight() {
}
} }
public protocol ItemListItemFocusableNode { public protocol ItemListItemFocusableNode {

View file

@ -42,8 +42,9 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem {
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let action: () -> Void let action: () -> Void
let deleteAction: (() -> Void)? let deleteAction: (() -> Void)?
public let tag: Any?
public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage? = nil, iconSize: CGSize? = nil, iconPlacement: IconPlacement = .default, title: String, subtitle: String? = nil, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, textColor: TextColor = .primary, checked: Bool, enabled: Bool = true, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void, deleteAction: (() -> Void)? = nil) { public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage? = nil, iconSize: CGSize? = nil, iconPlacement: IconPlacement = .default, title: String, subtitle: String? = nil, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, textColor: TextColor = .primary, checked: Bool, enabled: Bool = true, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void, deleteAction: (() -> Void)? = nil, tag: Any? = nil) {
self.presentationData = presentationData self.presentationData = presentationData
self.systemStyle = systemStyle self.systemStyle = systemStyle
self.icon = icon self.icon = icon
@ -60,6 +61,7 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem {
self.sectionId = sectionId self.sectionId = sectionId
self.action = action self.action = action
self.deleteAction = deleteAction self.deleteAction = deleteAction
self.tag = tag
} }
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) { 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) {
@ -127,6 +129,10 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
return self.contentParentNode return self.contentParentNode
} }
public var tag: ItemListItemTag? {
return self.item?.tag as? ItemListItemTag
}
public init() { public init() {
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true self.backgroundNode.isLayerBacked = true

View file

@ -173,6 +173,7 @@ private let boldBadgeFont = Font.semibold(14.0)
public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private let highlightNode: ASDisplayNode
private let topStripeNode: ASDisplayNode private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode
@ -215,6 +216,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
self.backgroundNode.isLayerBacked = true self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white self.backgroundNode.backgroundColor = .white
self.highlightNode = ASDisplayNode()
self.highlightNode.isLayerBacked = true
self.maskNode = ASImageNode() self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false self.maskNode.isUserInteractionEnabled = false
@ -283,6 +287,20 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
transition.updateAlpha(node: self.arrowNode, alpha: hasContextMenu ? 0.5 : 1.0) transition.updateAlpha(node: self.arrowNode, alpha: hasContextMenu ? 0.5 : 1.0)
} }
public func displayHighlight() {
if self.backgroundNode.supernode != nil {
self.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode)
} else {
self.insertSubnode(self.highlightNode, at: 0)
}
Queue.mainQueue().after(1.2, {
self.highlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
self.highlightNode.removeFromSupernode()
})
})
}
public func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { public func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode.textNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode.textNode)
let makeTitleWithEntitiesLayout = TextNodeWithEntities.asyncLayout(self.titleNode) let makeTitleWithEntitiesLayout = TextNodeWithEntities.asyncLayout(self.titleNode)
@ -600,6 +618,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
strongSelf.highlightNode.backgroundColor = item.presentationData.theme.list.itemSearchHighlightColor
} }
if let titleWithEntitiesApply = titleWithEntitiesLayoutAndApply?.1, let context = item.context { if let titleWithEntitiesApply = titleWithEntitiesLayoutAndApply?.1, let context = item.context {
@ -671,6 +690,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil 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.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.highlightNode.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.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.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 - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight))

View file

@ -59,10 +59,11 @@ public class ItemListSectionHeaderItem: ListViewItem, ItemListItem {
let actionText: String? let actionText: String?
let action: (() -> Void)? let action: (() -> Void)?
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
public let tag: ItemListItemTag?
public let isAlwaysPlain: Bool = true public let isAlwaysPlain: Bool = true
public init(presentationData: ItemListPresentationData, text: String, badge: String? = nil, badgeStyle: BadgeStyle? = nil, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, actionText: String? = nil, action: (() -> Void)? = nil, sectionId: ItemListSectionId) { public init(presentationData: ItemListPresentationData, text: String, badge: String? = nil, badgeStyle: BadgeStyle? = nil, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, actionText: String? = nil, action: (() -> Void)? = nil, sectionId: ItemListSectionId, tag: ItemListItemTag? = nil) {
self.presentationData = presentationData self.presentationData = presentationData
self.text = text self.text = text
self.badge = badge self.badge = badge
@ -73,6 +74,7 @@ public class ItemListSectionHeaderItem: ListViewItem, ItemListItem {
self.actionText = actionText self.actionText = actionText
self.action = action self.action = action
self.sectionId = sectionId self.sectionId = sectionId
self.tag = tag
} }
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) { 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) {
@ -125,6 +127,10 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode {
private let activateArea: AccessibilityAreaNode private let activateArea: AccessibilityAreaNode
public var tag: ItemListItemTag? {
return self.item?.tag
}
public init() { public init() {
self.titleNode = TextNode() self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false self.titleNode.isUserInteractionEnabled = false

View file

@ -147,6 +147,7 @@ extension IconSwitchNode: ItemListSwitchNodeImpl {
public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private let highlightNode: ASDisplayNode
private let topStripeNode: ASDisplayNode private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode
@ -175,6 +176,9 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white self.backgroundNode.backgroundColor = .white
self.highlightNode = ASDisplayNode()
self.highlightNode.isLayerBacked = true
self.maskNode = ASImageNode() self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false self.maskNode.isUserInteractionEnabled = false
@ -234,6 +238,20 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
self.switchGestureNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) self.switchGestureNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
} }
public func displayHighlight() {
if self.backgroundNode.supernode != nil {
self.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode)
} else {
self.insertSubnode(self.highlightNode, at: 0)
}
Queue.mainQueue().after(1.2, {
self.highlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
self.highlightNode.removeFromSupernode()
})
})
}
func asyncLayout() -> (_ item: ItemListSwitchItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { func asyncLayout() -> (_ item: ItemListSwitchItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode) let makeTextLayout = TextNode.asyncLayout(self.textNode)
@ -400,14 +418,13 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.switchNode.frameColor = item.presentationData.theme.list.itemSwitchColors.frameColor strongSelf.switchNode.frameColor = item.presentationData.theme.list.itemSwitchColors.frameColor
strongSelf.switchNode.contentColor = item.presentationData.theme.list.itemSwitchColors.contentColor strongSelf.switchNode.contentColor = item.presentationData.theme.list.itemSwitchColors.contentColor
strongSelf.switchNode.handleColor = item.presentationData.theme.list.itemSwitchColors.handleColor strongSelf.switchNode.handleColor = item.presentationData.theme.list.itemSwitchColors.handleColor
strongSelf.switchNode.positiveContentColor = item.presentationData.theme.list.itemSwitchColors.positiveColor strongSelf.switchNode.positiveContentColor = item.presentationData.theme.list.itemSwitchColors.positiveColor
strongSelf.switchNode.negativeContentColor = item.presentationData.theme.list.itemSwitchColors.negativeColor strongSelf.switchNode.negativeContentColor = item.presentationData.theme.list.itemSwitchColors.negativeColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
strongSelf.highlightNode.backgroundColor = item.presentationData.theme.list.itemSearchHighlightColor
} }
let _ = titleApply() let _ = titleApply()
@ -465,6 +482,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
transition.updateFrame(node: 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)))) transition.updateFrame(node: 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))))
transition.updateFrame(node: strongSelf.highlightNode, 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))))
transition.updateFrame(node: strongSelf.maskNode, frame: strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)) transition.updateFrame(node: 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.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 - separatorHeight), size: CGSize(width: layoutSize.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight)))

View file

@ -121,7 +121,12 @@ NSString *const kYUVVideoRangeConversionForLAFragmentShaderString = SHADER_STRIN
#pragma mark Initialization and teardown #pragma mark Initialization and teardown
- (GPUImageRotationMode)rotationForTrack:(AVAsset *)asset { - (GPUImageRotationMode)rotationForTrack:(AVAsset *)asset {
AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo];
if (tracks.count == 0) {
return kGPUImageNoRotation;
}
AVAssetTrack *videoTrack = [tracks objectAtIndex:0];
CGAffineTransform t = [videoTrack preferredTransform]; CGAffineTransform t = [videoTrack preferredTransform];
if (t.a == -1 && t.d == -1) { if (t.a == -1 && t.d == -1) {

View file

@ -428,9 +428,9 @@
if (itemChanged) { if (itemChanged) {
[self _playerCleanup]; [self _playerCleanup];
if (!item.asFile) { // if (!item.asFile) {
[_facesDisposable setDisposable:[[TGPaintFaceDetector detectFacesInItem:item.editableMediaItem editingContext:item.editingContext] startStrictWithNext:nil file:__FILE_NAME__ line:__LINE__]]; // [_facesDisposable setDisposable:[[TGPaintFaceDetector detectFacesInItem:item.editableMediaItem editingContext:item.editingContext] startStrictWithNext:nil file:__FILE_NAME__ line:__LINE__]];
} // }
} }
_scrubberView.allowsTrimming = false; _scrubberView.allowsTrimming = false;

View file

@ -83,6 +83,7 @@ swift_library(
"//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/ComponentFlow", "//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters", "//submodules/Components/ComponentDisplayAdapters",
"//submodules/CounterControllerTitleView",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View file

@ -6,6 +6,7 @@ import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import AccountContext import AccountContext
import SearchUI import SearchUI
import CounterControllerTitleView
public final class ChannelMembersSearchControllerImpl: ViewController, ChannelMembersSearchController { public final class ChannelMembersSearchControllerImpl: ViewController, ChannelMembersSearchController {
private let queue = Queue() private let queue = Queue()
@ -49,7 +50,18 @@ public final class ChannelMembersSearchControllerImpl: ViewController, ChannelMe
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.title = self.presentationData.strings.Channel_Members_Title let title: String
switch params.mode {
case .ownershipTransfer:
//TODO:localize
title = "Appoint Another Owner"
let titleView = CounterControllerTitleView(theme: self.presentationData.theme)
titleView.title = CounterControllerTitle(title: title, counter: " ")
self.navigationItem.titleView = titleView
default:
title = self.presentationData.strings.Channel_Members_Title
self.title = title
}
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
@ -74,16 +86,28 @@ public final class ChannelMembersSearchControllerImpl: ViewController, ChannelMe
} }
strongSelf.presentationData = presentationData strongSelf.presentationData = presentationData
strongSelf.controllerNode.updatePresentationData(presentationData) strongSelf.controllerNode.updatePresentationData(presentationData)
if let titleView = strongSelf.navigationItem.titleView as? CounterControllerTitleView {
titleView.theme = presentationData.theme
}
}) })
let _ = (params.context.account.postbox.loadedPeerWithId(peerId) let _ = (params.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId))
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { [weak self] peer in |> deliverOnMainQueue).start(next: { [weak self] peer in
if let strongSelf = self { guard let self, let peer else {
if let peer = peer as? TelegramChannel, case .broadcast = peer.info { return
strongSelf.title = strongSelf.presentationData.strings.Channel_Subscribers_Title }
switch self.mode {
case .ownershipTransfer:
if let titleView = self.navigationItem.titleView as? CounterControllerTitleView {
titleView.title = CounterControllerTitle(title: titleView.title.title, counter: peer.compactDisplayTitle)
}
default:
if case let .channel(channel) = peer, case .broadcast = channel.info {
self.title = self.presentationData.strings.Channel_Subscribers_Title
} else { } else {
strongSelf.title = strongSelf.presentationData.strings.Channel_Members_Title self.title = self.presentationData.strings.Channel_Members_Title
} }
} }
}) })

View file

@ -390,6 +390,13 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
} }
} }
} }
case .ownershipTransfer:
if peer.id == context.account.peerId {
continue
}
if let user = peer as? TelegramUser, user.botInfo != nil || user.flags.contains(.isSupport) {
continue
}
} }
let renderedParticipant: RenderedChannelParticipant let renderedParticipant: RenderedChannelParticipant
switch participant { switch participant {
@ -531,7 +538,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
var label: String? var label: String?
var enabled = true var enabled = true
switch mode { switch mode {
case .ban, .promote: case .ban, .promote, .ownershipTransfer:
if participant.peer.id == context.account.peerId { if participant.peer.id == context.account.peerId {
continue participantsLoop continue participantsLoop
} }

View file

@ -158,10 +158,10 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
let closeButton = closeButton.update( let closeButton = closeButton.update(
component: GlassBarButtonComponent( component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0), size: CGSize(width: 44.0, height: 44.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, backgroundColor: nil,
isDark: theme.overallDarkAppearance, isDark: theme.overallDarkAppearance,
state: .generic, state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent( component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent( BundleIconComponent(
name: "Navigation/Close", name: "Navigation/Close",
@ -172,7 +172,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
component.cancel(true) component.cancel(true)
} }
), ),
availableSize: CGSize(width: 40.0, height: 40.0), availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate transition: .immediate
) )
@ -496,11 +496,11 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
) )
context.add(title context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: 36.0)) .position(CGPoint(x: context.availableSize.width / 2.0, y: 38.0))
) )
context.add(star context.add(star
.position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0)) .position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 + 6.0))
) )
var originY: CGFloat = 0.0 var originY: CGFloat = 0.0

View file

@ -1,4 +1,5 @@
import Foundation import Foundation
import Foundation
import UIKit import UIKit
import Display import Display
import ComponentFlow import ComponentFlow
@ -38,6 +39,11 @@ import PremiumStarComponent
import PremiumCoinComponent import PremiumCoinComponent
import EdgeEffect import EdgeEffect
public enum PremiumIntroEntryTag {
case doNotHideAds
}
private let doNotHideAdsTag = GenericComponentViewTag()
public enum PremiumSource: Equatable { public enum PremiumSource: Equatable {
public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool { public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool {
switch lhs { switch lhs {
@ -2677,7 +2683,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let _ = accountContext.engine.accountData.updateAdMessagesEnabled(enabled: value).startStandalone() let _ = accountContext.engine.accountData.updateAdMessagesEnabled(enabled: value).startStandalone()
state?.updated(transition: .immediate) state?.updated(transition: .immediate)
})), })),
action: nil action: nil,
tag: doNotHideAdsTag
)))) ))))
let adsInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Business_AdsInfo, attributes: termsMarkdownAttributes, textAlignment: .natural let adsInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Business_AdsInfo, attributes: termsMarkdownAttributes, textAlignment: .natural
@ -3918,6 +3925,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
} }
private let screenContext: ScreenContext private let screenContext: ScreenContext
fileprivate let mode: Mode fileprivate let mode: Mode
private let focusOnItemTag: PremiumIntroEntryTag?
private var didSetReady = false private var didSetReady = false
private let _ready = Promise<Bool>() private let _ready = Promise<Bool>()
@ -3932,13 +3940,14 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
private let overNavigationContainer: UIView private let overNavigationContainer: UIView
public convenience init(context: AccountContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false) { public convenience init(context: AccountContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false, focusOnItemTag: PremiumIntroEntryTag? = nil) {
self.init(screenContext: .accountContext(context), mode: mode, source: source, modal: modal, forceDark: forceDark, forceHasPremium: forceHasPremium) self.init(screenContext: .accountContext(context), mode: mode, source: source, modal: modal, forceDark: forceDark, forceHasPremium: forceHasPremium, focusOnItemTag: focusOnItemTag)
} }
public init(screenContext: ScreenContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false) { public init(screenContext: ScreenContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false, focusOnItemTag: PremiumIntroEntryTag? = nil) {
self.screenContext = screenContext self.screenContext = screenContext
self.mode = mode self.mode = mode
self.focusOnItemTag = focusOnItemTag
let presentationData = screenContext.presentationData let presentationData = screenContext.presentationData
@ -4066,6 +4075,20 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
override public func viewDidLoad() { override public func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
if let focusOnItemTag = self.focusOnItemTag {
Queue.mainQueue().after(0.1, {
switch focusOnItemTag {
case .doNotHideAds:
if let view = self.node.hostView.findTaggedView(tag: doNotHideAdsTag) as? ListActionItemComponent.View {
if let scrollView = findParentScrollView(view: view) {
view.displayHighlight()
scrollView.setContentOffset(CGPoint(x: 0.0, y: scrollView.contentSize.height - scrollView.bounds.height), animated: true)
}
}
}
})
}
} }
public override func viewWillDisappear(_ animated: Bool) { public override func viewWillDisappear(_ animated: Bool) {

View file

@ -857,10 +857,10 @@ private final class LimitSheetContent: CombinedComponent {
let closeButton = closeButton.update( let closeButton = closeButton.update(
component: GlassBarButtonComponent( component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0), size: CGSize(width: 44.0, height: 44.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, backgroundColor: nil,
isDark: theme.overallDarkAppearance, isDark: theme.overallDarkAppearance,
state: .generic, state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent( component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent( BundleIconComponent(
name: "Navigation/Close", name: "Navigation/Close",
@ -872,7 +872,7 @@ private final class LimitSheetContent: CombinedComponent {
component.cancel() component.cancel()
} }
), ),
availableSize: CGSize(width: 40.0, height: 40.0), availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate transition: .immediate
) )
context.add(closeButton context.add(closeButton
@ -1519,7 +1519,7 @@ private final class LimitSheetContent: CombinedComponent {
} }
context.add(title context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: 36.0)) .position(CGPoint(x: context.availableSize.width / 2.0, y: 38.0))
) )
var textSize: CGSize var textSize: CGSize

View file

@ -163,10 +163,10 @@ private final class SheetContent: CombinedComponent {
let closeButton = closeButton.update( let closeButton = closeButton.update(
component: GlassBarButtonComponent( component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0), size: CGSize(width: 44.0, height: 44.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, backgroundColor: nil,
isDark: theme.overallDarkAppearance, isDark: theme.overallDarkAppearance,
state: .generic, state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent( component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent( BundleIconComponent(
name: "Navigation/Close", name: "Navigation/Close",
@ -177,7 +177,7 @@ private final class SheetContent: CombinedComponent {
component.dismiss() component.dismiss()
} }
), ),
availableSize: CGSize(width: 40.0, height: 40.0), availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate transition: .immediate
) )
context.add(closeButton context.add(closeButton

View file

@ -159,25 +159,25 @@ private final class SheetContent: CombinedComponent {
textString = "" textString = ""
} }
var contentSize = CGSize(width: context.availableSize.width, height: 36.0) var contentSize = CGSize(width: context.availableSize.width, height: 38.0)
let closeButton = closeButton.update( let closeButton = closeButton.update(
component: GlassBarButtonComponent( component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0), size: CGSize(width: 44.0, height: 44.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, backgroundColor: nil,
isDark: theme.overallDarkAppearance, isDark: theme.overallDarkAppearance,
state: .generic, state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent( component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent( BundleIconComponent(
name: "Navigation/Close", name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor tintColor: theme.chat.inputPanel.panelControlColor
) )
)), )),
action: { _ in action: { _ in
component.dismiss() component.dismiss()
} }
), ),
availableSize: CGSize(width: 40.0, height: 40.0), availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate transition: .immediate
) )
context.add(closeButton context.add(closeButton

View file

@ -135,6 +135,10 @@ swift_library(
"//submodules/TelegramUI/Components/AlertComponent", "//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent",
"//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/EdgeEffect",
"//submodules/TelegramUI/Components/AvatarEditorScreen",
"//submodules/TelegramUI/Components/Settings/PeerSelectionScreen",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View file

@ -40,13 +40,22 @@ func faqSearchableItems(context: AccountContext, resolvedUrl: Signal<ResolvedUrl
for item in items { for item in items {
if case let .text(itemText, _) = item, case let .url(text, url, _) = itemText { if case let .text(itemText, _) = item, case let .url(text, url, _) = itemText {
let (_, anchor) = extractAnchor(string: url) let (_, anchor) = extractAnchor(string: url)
guard let anchor else {
continue
}
var index = nextIndex var index = nextIndex
if suggestAccountDeletion && (anchor?.contains("delete-my-account") ?? false) { if suggestAccountDeletion && anchor.contains("delete-my-account") {
index = 1 index = 1
} else { } else {
nextIndex += 1 nextIndex += 1
} }
let item = SettingsSearchableItem(id: .faq(index), title: text.plainText, alternate: [], icon: .faq, breadcrumbs: [strings.SettingsSearch_FAQ, currentSection], present: { context, _, present in let item = SettingsSearchableItem(
id: "faq/\(anchor)",
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)) let controller = context.sharedContext.makeInstantPageController(context: context, webPage: webPage, anchor: anchor, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .channel))
present(.push, controller) present(.push, controller)
}) })

View file

@ -41,6 +41,23 @@ private enum AutodownloadMediaCategorySection: Int32 {
case types case types
} }
public enum AutodownloadMediaCategoryEntryTag: ItemListItemTag, Equatable {
case master
case usage
case photos
case stories
case videos
case files
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? AutodownloadMediaCategoryEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry { private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry {
case master(PresentationTheme, String, Bool) case master(PresentationTheme, String, Bool)
case dataUsageHeader(PresentationTheme, String) case dataUsageHeader(PresentationTheme, String)
@ -155,31 +172,31 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry {
case let .master(_, text, value): 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 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) arguments.toggleMaster(value)
}) }, tag: AutodownloadMediaCategoryEntryTag.master)
case let .dataUsageHeader(_, text): case let .dataUsageHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .dataUsageItem(theme, strings, value, customPosition, enabled): 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 return AutodownloadDataUsagePickerItem(theme: theme, strings: strings, systemStyle: .glass, value: value, customPosition: customPosition, enabled: enabled, sectionId: self.section, updated: { preset in
arguments.changePreset(preset) arguments.changePreset(preset)
}) }, tag: AutodownloadMediaCategoryEntryTag.usage)
case let .typesHeader(_, text): case let .typesHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .photos(_, text, value, enabled): 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: { 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) arguments.customize(.photo)
}) }, tag: AutodownloadMediaCategoryEntryTag.photos)
case let .stories(_, text, value, enabled): 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: { 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) arguments.customize(.story)
}) }, tag: AutodownloadMediaCategoryEntryTag.stories)
case let .videos(_, text, value, enabled): 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: { 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) arguments.customize(.video)
}) }, tag: AutodownloadMediaCategoryEntryTag.videos)
case let .files(_, text, value, enabled): 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: { 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) arguments.customize(.file)
}) }, tag: AutodownloadMediaCategoryEntryTag.files)
case let .voiceMessagesInfo(_, text): case let .voiceMessagesInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
} }
@ -300,7 +317,7 @@ private func autodownloadMediaConnectionTypeControllerEntries(presentationData:
return entries return entries
} }
func autodownloadMediaConnectionTypeController(context: AccountContext, connectionType: AutomaticDownloadConnectionType) -> ViewController { func autodownloadMediaConnectionTypeController(context: AccountContext, connectionType: AutomaticDownloadConnectionType, focusOnItemTag: AutodownloadMediaCategoryEntryTag? = nil) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)?
let arguments = AutodownloadMediaConnectionTypeControllerArguments(toggleMaster: { value in let arguments = AutodownloadMediaConnectionTypeControllerArguments(toggleMaster: { value in
@ -368,7 +385,7 @@ func autodownloadMediaConnectionTypeController(context: AccountContext, connecti
} }
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) 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) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: autodownloadMediaConnectionTypeControllerEntries(presentationData: presentationData, connectionType: connectionType, settings: automaticMediaDownloadSettings), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }
@ -379,5 +396,20 @@ func autodownloadMediaConnectionTypeController(context: AccountContext, connecti
(controller.navigationController as? NavigationController)?.pushViewController(c) (controller.navigationController as? NavigationController)?.pushViewController(c)
} }
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -40,8 +40,9 @@ final class AutodownloadDataUsagePickerItem: ListViewItem, ItemListItem {
let enabled: Bool let enabled: Bool
let sectionId: ItemListSectionId let sectionId: ItemListSectionId
let updated: (AutomaticDownloadDataUsage) -> Void let updated: (AutomaticDownloadDataUsage) -> Void
let tag: ItemListItemTag?
init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle = .legacy, value: AutomaticDownloadDataUsage, customPosition: Int?, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (AutomaticDownloadDataUsage) -> Void) { init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle = .legacy, value: AutomaticDownloadDataUsage, customPosition: Int?, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (AutomaticDownloadDataUsage) -> Void, tag: ItemListItemTag? = nil) {
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.systemStyle = systemStyle self.systemStyle = systemStyle
@ -50,6 +51,7 @@ final class AutodownloadDataUsagePickerItem: ListViewItem, ItemListItem {
self.enabled = enabled self.enabled = enabled
self.sectionId = sectionId self.sectionId = sectionId
self.updated = updated 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) { 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) {
@ -86,7 +88,7 @@ final class AutodownloadDataUsagePickerItem: ListViewItem, ItemListItem {
} }
} }
private final class AutodownloadDataUsagePickerItemNode: ListViewItemNode { private final class AutodownloadDataUsagePickerItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode
@ -103,6 +105,10 @@ private final class AutodownloadDataUsagePickerItemNode: ListViewItemNode {
private var item: AutodownloadDataUsagePickerItem? private var item: AutodownloadDataUsagePickerItem?
private var layoutParams: ListViewItemLayoutParams? private var layoutParams: ListViewItemLayoutParams?
public var tag: ItemListItemTag? {
return self.item?.tag
}
init() { init() {
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true self.backgroundNode.isLayerBacked = true

View file

@ -58,8 +58,9 @@ final class AutodownloadSizeLimitItem: ListViewItem, ItemListItem {
let range: Range<Int64>? let range: Range<Int64>?
let sectionId: ItemListSectionId let sectionId: ItemListSectionId
let updated: (Int64) -> Void let updated: (Int64) -> Void
let tag: ItemListItemTag?
init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle = .legacy, decimalSeparator: String, text: String, value: Int64, range: Range<Int64>?, sectionId: ItemListSectionId, updated: @escaping (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, tag: ItemListItemTag? = nil) {
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.systemStyle = systemStyle self.systemStyle = systemStyle
@ -69,6 +70,7 @@ final class AutodownloadSizeLimitItem: ListViewItem, ItemListItem {
self.range = range self.range = range
self.sectionId = sectionId self.sectionId = sectionId
self.updated = updated 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) { 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) {
@ -105,7 +107,7 @@ final class AutodownloadSizeLimitItem: ListViewItem, ItemListItem {
} }
} }
private final class AutodownloadSizeLimitItemNode: ListViewItemNode { private final class AutodownloadSizeLimitItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode
@ -119,6 +121,10 @@ private final class AutodownloadSizeLimitItemNode: ListViewItemNode {
private var item: AutodownloadSizeLimitItem? private var item: AutodownloadSizeLimitItem?
private var layoutParams: ListViewItemLayoutParams? private var layoutParams: ListViewItemLayoutParams?
public var tag: ItemListItemTag? {
return self.item?.tag
}
init() { init() {
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true self.backgroundNode.isLayerBacked = true

View file

@ -90,6 +90,7 @@ public enum DataAndStorageEntryTag: ItemListItemTag, Equatable {
case raiseToListen case raiseToListen
case autoSave(AutomaticSaveIncomingPeerType) case autoSave(AutomaticSaveIncomingPeerType)
case sensitiveContent case sensitiveContent
case useLessVoiceData
public func isEqual(to other: ItemListItemTag) -> Bool { public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? DataAndStorageEntryTag, self == other { if let other = other as? DataAndStorageEntryTag, self == other {
@ -409,7 +410,7 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
case let .useLessVoiceData(_, text, value): case let .useLessVoiceData(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleVoiceUseLessData(value) arguments.toggleVoiceUseLessData(value)
}, tag: nil) }, tag: DataAndStorageEntryTag.useLessVoiceData)
case let .useLessVoiceDataInfo(_, text): case let .useLessVoiceDataInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .otherHeader(_, text): case let .otherHeader(_, text):
@ -1012,6 +1013,20 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
update() update()
}) })
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -10,7 +10,7 @@ import PresentationDataUtils
import AccountContext import AccountContext
import UndoUI import UndoUI
enum ItemType: CaseIterable { public enum EnergySavingItemType: CaseIterable {
case autoplayVideo case autoplayVideo
case autoplayGif case autoplayGif
case loopStickers case loopStickers
@ -88,10 +88,10 @@ enum ItemType: CaseIterable {
private final class EnergeSavingSettingsScreenArguments { private final class EnergeSavingSettingsScreenArguments {
let updateThreshold: (Int32) -> Void let updateThreshold: (Int32) -> Void
let toggleItem: (ItemType) -> Void let toggleItem: (EnergySavingItemType) -> Void
let displayDisabledTooltip: () -> Void let displayDisabledTooltip: () -> Void
init(updateThreshold: @escaping (Int32) -> Void, toggleItem: @escaping (ItemType) -> Void, displayDisabledTooltip: @escaping () -> Void) { init(updateThreshold: @escaping (Int32) -> Void, toggleItem: @escaping (EnergySavingItemType) -> Void, displayDisabledTooltip: @escaping () -> Void) {
self.updateThreshold = updateThreshold self.updateThreshold = updateThreshold
self.toggleItem = toggleItem self.toggleItem = toggleItem
self.displayDisabledTooltip = displayDisabledTooltip self.displayDisabledTooltip = displayDisabledTooltip
@ -103,19 +103,31 @@ private enum EnergeSavingSettingsScreenSection: Int32 {
case items case items
} }
public enum EnergySavingEntryTag: ItemListItemTag, Equatable {
case item(EnergySavingItemType)
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? EnergySavingEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum EnergeSavingSettingsScreenEntry: ItemListNodeEntry { private enum EnergeSavingSettingsScreenEntry: ItemListNodeEntry {
enum StableId: Hashable { enum StableId: Hashable {
case allHeader case allHeader
case all case all
case allFooter case allFooter
case itemsHeader case itemsHeader
case item(ItemType) case item(EnergySavingItemType)
} }
case allHeader(Bool?) case allHeader(Bool?)
case all(Int32) case all(Int32)
case allFooter(String) case allFooter(String)
case item(index: Int, type: ItemType, value: Bool, enabled: Bool) case item(index: Int, type: EnergySavingItemType, value: Bool, enabled: Bool)
case itemsHeader case itemsHeader
var section: ItemListSectionId { var section: ItemListSectionId {
@ -197,7 +209,7 @@ private enum EnergeSavingSettingsScreenEntry: ItemListNodeEntry {
arguments.toggleItem(type) arguments.toggleItem(type)
}, activatedWhileDisabled: { }, activatedWhileDisabled: {
arguments.displayDisabledTooltip() arguments.displayDisabledTooltip()
}) }, tag: EnergySavingEntryTag.item(type))
} }
} }
} }
@ -241,14 +253,14 @@ private func energeSavingSettingsScreenEntries(
} }
entries.append(.itemsHeader) entries.append(.itemsHeader)
for type in ItemType.allCases { for type in EnergySavingItemType.allCases {
entries.append(.item(index: entries.count, type: type, value: settings.energyUsageSettings[keyPath: type.settingsKeyPath] && itemsEnabled, enabled: itemsEnabled)) entries.append(.item(index: entries.count, type: type, value: settings.energyUsageSettings[keyPath: type.settingsKeyPath] && itemsEnabled, enabled: itemsEnabled))
} }
return entries return entries
} }
public func energySavingSettingsScreen(context: AccountContext) -> ViewController { public func energySavingSettingsScreen(context: AccountContext, focusOnItemTag: EnergySavingEntryTag? = nil) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)?
let _ = pushControllerImpl let _ = pushControllerImpl
@ -300,7 +312,7 @@ public func energySavingSettingsScreen(context: AccountContext) -> ViewControlle
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
animateChanges: false animateChanges: false
) )
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: energeSavingSettingsScreenEntries(presentationData: presentationData, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: true) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: energeSavingSettingsScreenEntries(presentationData: presentationData, settings: automaticMediaDownloadSettings), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: true)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }
@ -317,5 +329,20 @@ public func energySavingSettingsScreen(context: AccountContext) -> ViewControlle
controller.present(UndoOverlayController(presentationData: presentationData, content: c, elevatedLayout: false, action: { _ in return false }), in: .current) controller.present(UndoOverlayController(presentationData: presentationData, content: c, elevatedLayout: false, action: { _ in return false }), in: .current)
} }
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -12,6 +12,20 @@ import AccountContext
import TelegramIntents import TelegramIntents
import AccountUtils import AccountUtils
public enum IntentsEntryTag: ItemListItemTag, Equatable {
case suggested
case suggestBy
case reset
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? IntentsEntryTag, self == other {
return true
} else {
return false
}
}
}
private final class IntentsSettingsControllerArguments { private final class IntentsSettingsControllerArguments {
let context: AccountContext let context: AccountContext
let updateSettings: (@escaping (IntentsSettings) -> IntentsSettings) -> Void let updateSettings: (@escaping (IntentsSettings) -> IntentsSettings) -> Void
@ -197,7 +211,7 @@ private enum IntentsSettingsControllerEntry: ItemListNodeEntry {
case let .contacts(_, text, value): 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 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) } arguments.updateSettings { $0.withUpdatedContacts(value) }
}) }, tag: IntentsEntryTag.suggested)
case let .savedMessages(_, text, 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 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) } arguments.updateSettings { $0.withUpdatedSavedMessages(value) }
@ -217,7 +231,7 @@ private enum IntentsSettingsControllerEntry: ItemListNodeEntry {
case let .suggestAll(_, text, value): case let .suggestAll(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateSettings { $0.withUpdatedOnlyShared(false) } arguments.updateSettings { $0.withUpdatedOnlyShared(false) }
}) }, tag: IntentsEntryTag.suggestBy)
case let .suggestOnlyShared(_, text, value): case let .suggestOnlyShared(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateSettings { $0.withUpdatedOnlyShared(true) } arguments.updateSettings { $0.withUpdatedOnlyShared(true) }
@ -226,7 +240,7 @@ private enum IntentsSettingsControllerEntry: ItemListNodeEntry {
case let .resetAll(_, text): case let .resetAll(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.resetAll() arguments.resetAll()
}) }, tag: IntentsEntryTag.reset)
} }
} }
} }
@ -261,7 +275,7 @@ private func intentsSettingsControllerEntries(context: AccountContext, presentat
return entries return entries
} }
public func intentsSettingsController(context: AccountContext) -> ViewController { public func intentsSettingsController(context: AccountContext, focusOnItemTag: IntentsEntryTag? = nil) -> ViewController {
var presentControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController) -> Void)?
let arguments = IntentsSettingsControllerArguments(context: context, updateSettings: { f in let arguments = IntentsSettingsControllerArguments(context: context, updateSettings: { f in
@ -317,5 +331,20 @@ public func intentsSettingsController(context: AccountContext) -> ViewController
presentControllerImpl = { [weak controller] c in presentControllerImpl = { [weak controller] c in
controller?.present(c, in: .window(.root)) controller?.present(c, in: .window(.root))
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -58,6 +58,21 @@ private enum ProxySettingsControllerEntryId: Equatable, Hashable {
case server(String, Int32, ProxyServerConnection) case server(String, Int32, ProxyServerConnection)
} }
public enum ProxySettingsEntryTag: ItemListItemTag, Equatable {
case edit
case useProxy
case shareList
case useForCalls
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? ProxySettingsEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum ProxySettingsControllerEntry: ItemListNodeEntry { private enum ProxySettingsControllerEntry: ItemListNodeEntry {
case enabled(PresentationTheme, String, Bool, Bool) case enabled(PresentationTheme, String, Bool, Bool)
case serversHeader(PresentationTheme, String) case serversHeader(PresentationTheme, String)
@ -207,7 +222,7 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry {
} else { } else {
arguments.toggleEnabled(value) arguments.toggleEnabled(value)
} }
}) }, tag: ProxySettingsEntryTag.useProxy)
case let .serversHeader(_, text): case let .serversHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .addServer(_, text, _): case let .addServer(_, text, _):
@ -227,11 +242,11 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry {
case let .shareProxyList(_, text): case let .shareProxyList(_, text):
return ProxySettingsActionItem(presentationData: presentationData, systemStyle: .glass, title: text, sectionId: self.section, editing: false, action: { return ProxySettingsActionItem(presentationData: presentationData, systemStyle: .glass, title: text, sectionId: self.section, editing: false, action: {
arguments.shareProxyList() arguments.shareProxyList()
}) }, tag: ProxySettingsEntryTag.shareList)
case let .useForCalls(_, text, value): 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 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) arguments.toggleUseForCalls(value)
}) }, tag: ProxySettingsEntryTag.useForCalls)
case let .useForCallsInfo(_, text): case let .useForCallsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
} }
@ -308,12 +323,12 @@ public enum ProxySettingsControllerMode {
case modal case modal
} }
public func proxySettingsController(context: AccountContext, mode: ProxySettingsControllerMode = .default) -> ViewController { public func proxySettingsController(context: AccountContext, mode: ProxySettingsControllerMode = .default, focusOnItemTag: ProxySettingsEntryTag? = nil) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } 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) 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, focusOnItemTag: focusOnItemTag)
} }
public func proxySettingsController(accountManager: AccountManager<TelegramAccountManagerTypes>, sharedContext: SharedAccountContext, context: AccountContext? = nil, postbox: Postbox, network: Network, mode: ProxySettingsControllerMode, presentationData: PresentationData, updatedPresentationData: Signal<PresentationData, NoError>) -> ViewController { public func proxySettingsController(accountManager: AccountManager<TelegramAccountManagerTypes>, sharedContext: SharedAccountContext, context: AccountContext? = nil, postbox: Postbox, network: Network, mode: ProxySettingsControllerMode, presentationData: PresentationData, updatedPresentationData: Signal<PresentationData, NoError>, focusOnItemTag: ProxySettingsEntryTag? = nil) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)? var dismissImpl: (() -> Void)?
let stateValue = Atomic(value: ProxySettingsControllerState()) let stateValue = Atomic(value: ProxySettingsControllerState())
@ -332,6 +347,14 @@ public func proxySettingsController(accountManager: AccountManager<TelegramAccou
} }
} }
if focusOnItemTag == ProxySettingsEntryTag.edit {
updateState { state in
var state = state
state.editing = true
return state
}
}
var shareProxyListImpl: (() -> Void)? var shareProxyListImpl: (() -> Void)?
let arguments = ProxySettingsControllerArguments(toggleEnabled: { value in let arguments = ProxySettingsControllerArguments(toggleEnabled: { value in
@ -431,7 +454,7 @@ public func proxySettingsController(accountManager: AccountManager<TelegramAccou
} }
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.SocksProxySetup_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) 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) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: proxySettingsControllerEntries(theme: presentationData.theme, strings: presentationData.strings, state: state, proxySettings: proxySettings, statuses: statuses, connectionStatus: connectionStatus), style: .blocks, ensureVisibleItemTag: focusOnItemTag)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }
@ -530,5 +553,19 @@ public func proxySettingsController(accountManager: AccountManager<TelegramAccou
}) })
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -20,8 +20,9 @@ final class ProxySettingsActionItem: ListViewItem, ItemListItem {
let editing: Bool let editing: Bool
let sectionId: ItemListSectionId let sectionId: ItemListSectionId
let action: () -> Void let action: () -> Void
let tag: ItemListItemTag?
init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, title: String, icon: ProxySettingsActionIcon = .none, sectionId: ItemListSectionId, editing: Bool, action: @escaping () -> Void) { init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, title: String, icon: ProxySettingsActionIcon = .none, sectionId: ItemListSectionId, editing: Bool, action: @escaping () -> Void, tag: ItemListItemTag? = nil) {
self.presentationData = presentationData self.presentationData = presentationData
self.systemStyle = systemStyle self.systemStyle = systemStyle
self.title = title self.title = title
@ -29,6 +30,7 @@ final class ProxySettingsActionItem: ListViewItem, ItemListItem {
self.editing = editing self.editing = editing
self.sectionId = sectionId self.sectionId = sectionId
self.action = action self.action = action
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) { 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) {
@ -77,7 +79,7 @@ final class ProxySettingsActionItem: ListViewItem, ItemListItem {
} }
} }
private final class ProxySettingsActionItemNode: ListViewItemNode { private final class ProxySettingsActionItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode
@ -89,6 +91,10 @@ private final class ProxySettingsActionItemNode: ListViewItemNode {
private var item: ProxySettingsActionItem? private var item: ProxySettingsActionItem?
public var tag: ItemListItemTag? {
return self.item?.tag
}
init() { init() {
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true self.backgroundNode.isLayerBacked = true

View file

@ -48,6 +48,20 @@ enum SaveIncomingMediaSection: ItemListSectionId {
case deleteAllExceptions case deleteAllExceptions
} }
public enum SaveIncomingMediaEntryTag: ItemListItemTag, Equatable {
case maxVideoSize
case addException
case deleteExceptions
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? SaveIncomingMediaEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum SaveIncomingMediaEntry: ItemListNodeEntry { private enum SaveIncomingMediaEntry: ItemListNodeEntry {
enum StableId: Hashable { enum StableId: Hashable {
case peer case peer
@ -200,7 +214,7 @@ private enum SaveIncomingMediaEntry: ItemListNodeEntry {
case let .videoSize(decimalSeparator, text, size): 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 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) arguments.updateMaximumVideoSize(value)
}) }, tag: SaveIncomingMediaEntryTag.maxVideoSize)
case let .videoInfo(text): case let .videoInfo(text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .exceptionsHeader(title): case let .exceptionsHeader(title):
@ -209,7 +223,7 @@ private enum SaveIncomingMediaEntry: ItemListNodeEntry {
let icon: UIImage? = PresentationResourcesItemList.createGroupIcon(presentationData.theme) 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: { return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: icon, title: title, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: {
arguments.openAddException() arguments.openAddException()
}) }, tag: SaveIncomingMediaEntryTag.addException)
case let .exceptionItem(_, peer, label): case let .exceptionItem(_, peer, label):
return ItemListPeerItem( return ItemListPeerItem(
presentationData: presentationData, presentationData: presentationData,
@ -248,7 +262,7 @@ private enum SaveIncomingMediaEntry: ItemListNodeEntry {
case let .deleteAllExceptions(title): case let .deleteAllExceptions(title):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.deleteAllExceptions() arguments.deleteAllExceptions()
}) }, tag: SaveIncomingMediaEntryTag.deleteExceptions)
} }
} }
} }
@ -352,7 +366,7 @@ private func saveIncomingMediaControllerEntries(presentationData: PresentationDa
return entries return entries
} }
enum SaveIncomingMediaScope { public enum SaveIncomingMediaScope {
case peer(EnginePeer.Id) case peer(EnginePeer.Id)
case addPeer(id: EnginePeer.Id, completion: (MediaAutoSaveConfiguration) -> Void) case addPeer(id: EnginePeer.Id, completion: (MediaAutoSaveConfiguration) -> Void)
case peerType(AutomaticSaveIncomingPeerType) case peerType(AutomaticSaveIncomingPeerType)
@ -363,7 +377,7 @@ private struct SaveIncomingMediaControllerState: Equatable {
var peerIdWithOptions: EnginePeer.Id? var peerIdWithOptions: EnginePeer.Id?
} }
func saveIncomingMediaController(context: AccountContext, scope: SaveIncomingMediaScope) -> ViewController { public func saveIncomingMediaController(context: AccountContext, scope: SaveIncomingMediaScope, focusOnItemTag: SaveIncomingMediaEntryTag? = nil) -> ViewController {
let stateValue = Atomic(value: SaveIncomingMediaControllerState()) let stateValue = Atomic(value: SaveIncomingMediaControllerState())
let statePromise = ValuePromise<SaveIncomingMediaControllerState>(stateValue.with { $0 }) let statePromise = ValuePromise<SaveIncomingMediaControllerState>(stateValue.with { $0 })
let updateState: ((SaveIncomingMediaControllerState) -> SaveIncomingMediaControllerState) -> Void = { f in let updateState: ((SaveIncomingMediaControllerState) -> SaveIncomingMediaControllerState) -> Void = { f in
@ -719,7 +733,7 @@ func saveIncomingMediaController(context: AccountContext, scope: SaveIncomingMed
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) 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) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: animateChanges)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }
@ -743,6 +757,20 @@ func saveIncomingMediaController(context: AccountContext, scope: SaveIncomingMed
controller?.dismiss() controller?.dismiss()
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -8,9 +8,24 @@ import TelegramCore
import TelegramPresentationData import TelegramPresentationData
import AccountContext import AccountContext
import SearchUI import SearchUI
import ItemListUI
public enum LocalizationListEntryTag: ItemListItemTag, Equatable {
case showButton
case translateChats
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? LocalizationListEntryTag, self == other {
return true
} else {
return false
}
}
}
public class LocalizationListController: ViewController { public class LocalizationListController: ViewController {
private let context: AccountContext private let context: AccountContext
private let focusOnItemTag: LocalizationListEntryTag?
private var controllerNode: LocalizationListControllerNode { private var controllerNode: LocalizationListControllerNode {
return self.displayNode as! LocalizationListControllerNode return self.displayNode as! LocalizationListControllerNode
@ -31,8 +46,9 @@ public class LocalizationListController: ViewController {
private var previousContentOffset: ListViewVisibleContentOffset? private var previousContentOffset: ListViewVisibleContentOffset?
public init(context: AccountContext) { public init(context: AccountContext, focusOnItemTag: LocalizationListEntryTag? = nil) {
self.context = context self.context = context
self.focusOnItemTag = focusOnItemTag
self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -125,7 +141,7 @@ public class LocalizationListController: ViewController {
self?.present(c, in: .window(.root), with: a) self?.present(c, in: .window(.root), with: a)
}, push: { [weak self] c in }, push: { [weak self] c in
self?.push(c) self?.push(c)
}) }, focusOnItemTag: self.focusOnItemTag)
self.controllerNode.listNode.visibleContentOffsetChanged = { [weak self] offset in self.controllerNode.listNode.visibleContentOffsetChanged = { [weak self] offset in
if let strongSelf = self { if let strongSelf = self {

View file

@ -95,7 +95,7 @@ private enum LanguageListEntry: Comparable, Identifiable {
case let .translate(text, value): 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 return ItemListSwitchItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, title: text, value: value, sectionId: LanguageListSection.translate.rawValue, style: .blocks, updated: { value in
toggleShowTranslate(value) toggleShowTranslate(value)
}) }, tag: LocalizationListEntryTag.showButton)
case let .translateEntire(text, value, locked): 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 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 { if !locked {
@ -103,7 +103,7 @@ private enum LanguageListEntry: Comparable, Identifiable {
} }
}, activatedWhileDisabled: { }, activatedWhileDisabled: {
showPremiumInfo() showPremiumInfo()
}) }, tag: LocalizationListEntryTag.translateChats)
case let .doNotTranslate(text, value): case let .doNotTranslate(text, value):
return ItemListDisclosureItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, title: text, label: value, sectionId: LanguageListSection.translate.rawValue, style: .blocks, action: { return ItemListDisclosureItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, title: text, label: value, sectionId: LanguageListSection.translate.rawValue, style: .blocks, action: {
openDoNotTranslate() openDoNotTranslate()
@ -342,6 +342,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
private let requestDeactivateSearch: () -> Void private let requestDeactivateSearch: () -> Void
private let present: (ViewController, Any?) -> Void private let present: (ViewController, Any?) -> Void
private let push: (ViewController) -> Void private let push: (ViewController) -> Void
private var focusOnItemTag: LocalizationListEntryTag?
private var didSetReady = false private var didSetReady = false
let _ready = ValuePromise<Bool>() let _ready = ValuePromise<Bool>()
@ -367,7 +368,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
} }
} }
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) { 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, focusOnItemTag: LocalizationListEntryTag?) {
self.context = context self.context = context
self.presentationData = presentationData self.presentationData = presentationData
self.presentationDataValue.set(.single(presentationData)) self.presentationDataValue.set(.single(presentationData))
@ -376,6 +377,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
self.requestDeactivateSearch = requestDeactivateSearch self.requestDeactivateSearch = requestDeactivateSearch
self.present = present self.present = present
self.push = push self.push = push
self.focusOnItemTag = focusOnItemTag
self.listNode = ListView() self.listNode = ListView()
self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.list.blocksBackgroundColor, direction: true) self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.list.blocksBackgroundColor, direction: true)
@ -741,6 +743,16 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
if !strongSelf.didSetReady { if !strongSelf.didSetReady {
strongSelf.didSetReady = true strongSelf.didSetReady = true
strongSelf._ready.set(true) strongSelf._ready.set(true)
if let focusOnItemTag = strongSelf.focusOnItemTag {
strongSelf.focusOnItemTag = nil
strongSelf.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
itemNode.displayHighlight()
}
}
}
} }
} }
}) })

View file

@ -897,5 +897,20 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions
pushControllerImpl = { [weak controller] c in pushControllerImpl = { [weak controller] c in
(controller?.navigationController as? NavigationController)?.pushViewController(c) (controller?.navigationController as? NavigationController)?.pushViewController(c)
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -77,9 +77,12 @@ private enum NotificationsPeerCategorySection: Int32 {
} }
public enum NotificationsPeerCategoryEntryTag: ItemListItemTag { public enum NotificationsPeerCategoryEntryTag: ItemListItemTag {
case edit
case enable case enable
case previews case previews
case sound case sound
case important
case deleteExceptions
public func isEqual(to other: ItemListItemTag) -> Bool { public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? NotificationsPeerCategoryEntryTag, self == other { if let other = other as? NotificationsPeerCategoryEntryTag, self == other {
@ -284,7 +287,7 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
case let .enableImportant(_, text, value): case let .enableImportant(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateEnabledImportant(updatedValue) arguments.updateEnabledImportant(updatedValue)
}, tag: self.tag) }, tag: NotificationsPeerCategoryEntryTag.important)
case let .importantInfo(_, text): case let .importantInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .optionsHeader(_, text): case let .optionsHeader(_, text):
@ -314,7 +317,7 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
case let .removeAllExceptions(theme, text): case let .removeAllExceptions(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.deleteIconImage(theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: { return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.deleteIconImage(theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: {
arguments.removeAllExceptions() arguments.removeAllExceptions()
}) }, tag: NotificationsPeerCategoryEntryTag.deleteExceptions)
} }
} }
} }
@ -617,6 +620,12 @@ public func notificationsPeerCategoryController(context: AccountContext, categor
updatedMode(result.mode) updatedMode(result.mode)
} }
if focusOnItemTag == .edit {
updateState {
$0.withUpdatedEditing(true)
}
}
let updatePeerSound: (EnginePeer.Id, PeerMessageSound) -> Signal<Void, NoError> = { peerId, sound in let updatePeerSound: (EnginePeer.Id, PeerMessageSound) -> Signal<Void, NoError> = { peerId, sound in
return context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: nil, sound: sound) |> deliverOnMainQueue return context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: nil, sound: sound) |> deliverOnMainQueue
} }
@ -1096,5 +1105,20 @@ public func notificationsPeerCategoryController(context: AccountContext, categor
pushControllerImpl = { [weak controller] c in pushControllerImpl = { [weak controller] c in
(controller?.navigationController as? NavigationController)?.pushViewController(c) (controller?.navigationController as? NavigationController)?.pushViewController(c)
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -203,13 +203,19 @@ private func blockedPeersControllerEntries(presentationData: PresentationData, s
return entries return entries
} }
public func blockedPeersController(context: AccountContext, blockedPeersContext: BlockedPeersContext) -> ViewController { public func blockedPeersController(context: AccountContext, blockedPeersContext: BlockedPeersContext, forceEdit: Bool = false) -> ViewController {
let statePromise = ValuePromise(BlockedPeersControllerState(), ignoreRepeated: true) let statePromise = ValuePromise(BlockedPeersControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: BlockedPeersControllerState()) let stateValue = Atomic(value: BlockedPeersControllerState())
let updateState: ((BlockedPeersControllerState) -> BlockedPeersControllerState) -> Void = { f in let updateState: ((BlockedPeersControllerState) -> BlockedPeersControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) }) statePromise.set(stateValue.modify { f($0) })
} }
if forceEdit {
updateState {
$0.withUpdatedEditing(true)
}
}
var pushControllerImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)?
let actionsDisposable = DisposableSet() let actionsDisposable = DisposableSet()

View file

@ -36,7 +36,7 @@ private final class DataPrivacyControllerArguments {
} }
} }
private enum PrivacyAndSecuritySection: Int32 { private enum DataPrivacySection: Int32 {
case contacts case contacts
case frequentContacts case frequentContacts
case chats case chats
@ -45,7 +45,24 @@ private enum PrivacyAndSecuritySection: Int32 {
case bots case bots
} }
private enum PrivacyAndSecurityEntry: ItemListNodeEntry { public enum DataPrivacyEntryTag: ItemListItemTag, Equatable {
case deleteSynced
case syncContacts
case suggestContacts
case deleteCloudDrafts
case clearPaymentInfo
case linkPreviews
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? DataPrivacyEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum DataPrivacyEntry: ItemListNodeEntry {
case contactsHeader(PresentationTheme, String) case contactsHeader(PresentationTheme, String)
case deleteContacts(PresentationTheme, String, Bool) case deleteContacts(PresentationTheme, String, Bool)
case syncContacts(PresentationTheme, String, Bool) case syncContacts(PresentationTheme, String, Bool)
@ -70,17 +87,17 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .contactsHeader, .deleteContacts, .syncContacts, .syncContactsInfo: case .contactsHeader, .deleteContacts, .syncContacts, .syncContactsInfo:
return PrivacyAndSecuritySection.contacts.rawValue return DataPrivacySection.contacts.rawValue
case .frequentContacts, .frequentContactsInfo: case .frequentContacts, .frequentContactsInfo:
return PrivacyAndSecuritySection.frequentContacts.rawValue return DataPrivacySection.frequentContacts.rawValue
case .chatsHeader, .deleteCloudDrafts: case .chatsHeader, .deleteCloudDrafts:
return PrivacyAndSecuritySection.chats.rawValue return DataPrivacySection.chats.rawValue
case .paymentHeader, .clearPaymentInfo, .paymentInfo: case .paymentHeader, .clearPaymentInfo, .paymentInfo:
return PrivacyAndSecuritySection.payments.rawValue return DataPrivacySection.payments.rawValue
case .secretChatLinkPreviewsHeader, .secretChatLinkPreviews, .secretChatLinkPreviewsInfo: case .secretChatLinkPreviewsHeader, .secretChatLinkPreviews, .secretChatLinkPreviewsInfo:
return PrivacyAndSecuritySection.secretChats.rawValue return DataPrivacySection.secretChats.rawValue
case .botList: case .botList:
return PrivacyAndSecuritySection.bots.rawValue return DataPrivacySection.bots.rawValue
} }
} }
@ -124,7 +141,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
} }
} }
static func ==(lhs: PrivacyAndSecurityEntry, rhs: PrivacyAndSecurityEntry) -> Bool { static func ==(lhs: DataPrivacyEntry, rhs: DataPrivacyEntry) -> Bool {
switch lhs { switch lhs {
case let .contactsHeader(lhsTheme, lhsText): case let .contactsHeader(lhsTheme, lhsText):
if case let .contactsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .contactsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
@ -219,7 +236,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
} }
} }
static func <(lhs: PrivacyAndSecurityEntry, rhs: PrivacyAndSecurityEntry) -> Bool { static func <(lhs: DataPrivacyEntry, rhs: DataPrivacyEntry) -> Bool {
return lhs.stableId < rhs.stableId return lhs.stableId < rhs.stableId
} }
@ -231,17 +248,17 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
case let .deleteContacts(_, text, value): 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: { return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.deleteContacts() arguments.deleteContacts()
}) }, tag: DataPrivacyEntryTag.deleteSynced)
case let .syncContacts(_, text, value): case let .syncContacts(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateSyncContacts(updatedValue) arguments.updateSyncContacts(updatedValue)
}) }, tag: DataPrivacyEntryTag.syncContacts)
case let .syncContactsInfo(_, text): case let .syncContactsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .frequentContacts(_, text, value): 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 return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: !value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateSuggestFrequentContacts(updatedValue) arguments.updateSuggestFrequentContacts(updatedValue)
}) }, tag: DataPrivacyEntryTag.suggestContacts)
case let .frequentContactsInfo(_, text): case let .frequentContactsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .chatsHeader(_, text): case let .chatsHeader(_, text):
@ -249,13 +266,13 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
case let .deleteCloudDrafts(_, text, value): 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: { return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.deleteCloudDrafts() arguments.deleteCloudDrafts()
}) }, tag: DataPrivacyEntryTag.deleteCloudDrafts)
case let .paymentHeader(_, text): case let .paymentHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .clearPaymentInfo(_, text, enabled): 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: { return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.clearPaymentInfo() arguments.clearPaymentInfo()
}) }, tag: DataPrivacyEntryTag.clearPaymentInfo)
case let .paymentInfo(_, text): case let .paymentInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .secretChatLinkPreviewsHeader(_, text): case let .secretChatLinkPreviewsHeader(_, text):
@ -263,7 +280,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
case let .secretChatLinkPreviews(_, text, value): case let .secretChatLinkPreviews(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateSecretChatLinkPreviews(updatedValue) arguments.updateSecretChatLinkPreviews(updatedValue)
}) }, tag: DataPrivacyEntryTag.linkPreviews)
case let .secretChatLinkPreviewsInfo(_, text): case let .secretChatLinkPreviewsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case .botList: case .botList:
@ -289,8 +306,8 @@ private struct DataPrivacyControllerState: Equatable {
var deletingCloudDrafts: Bool = false var deletingCloudDrafts: Bool = false
} }
private func dataPrivacyControllerEntries(presentationData: PresentationData, state: DataPrivacyControllerState, secretChatLinkPreviews: Bool?, synchronizeDeviceContacts: Bool, frequentContacts: Bool, hasBotSettings: Bool) -> [PrivacyAndSecurityEntry] { private func dataPrivacyControllerEntries(presentationData: PresentationData, state: DataPrivacyControllerState, secretChatLinkPreviews: Bool?, synchronizeDeviceContacts: Bool, frequentContacts: Bool, hasBotSettings: Bool) -> [DataPrivacyEntry] {
var entries: [PrivacyAndSecurityEntry] = [] var entries: [DataPrivacyEntry] = []
entries.append(.contactsHeader(presentationData.theme, presentationData.strings.Privacy_ContactsTitle)) entries.append(.contactsHeader(presentationData.theme, presentationData.strings.Privacy_ContactsTitle))
entries.append(.deleteContacts(presentationData.theme, presentationData.strings.Privacy_ContactsReset, !state.deletingContacts)) entries.append(.deleteContacts(presentationData.theme, presentationData.strings.Privacy_ContactsReset, !state.deletingContacts))
@ -317,7 +334,7 @@ private func dataPrivacyControllerEntries(presentationData: PresentationData, st
return entries return entries
} }
public func dataPrivacyController(context: AccountContext) -> ViewController { public func dataPrivacyController(context: AccountContext, focusOnItemTag: DataPrivacyEntryTag? = nil) -> ViewController {
let statePromise = ValuePromise(DataPrivacyControllerState(), ignoreRepeated: true) let statePromise = ValuePromise(DataPrivacyControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: DataPrivacyControllerState()) let stateValue = Atomic(value: DataPrivacyControllerState())
let updateState: ((DataPrivacyControllerState) -> DataPrivacyControllerState) -> Void = { f in let updateState: ((DataPrivacyControllerState) -> DataPrivacyControllerState) -> Void = { f in
@ -434,7 +451,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController {
} }
if canBegin { if canBegin {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } 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: { presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_ContactsResetConfirmation, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultDestructiveAction, title: presentationData.strings.Common_Delete, action: {
var begin = false var begin = false
updateState { state in updateState { state in
var state = state var state = state
@ -461,7 +478,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } 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 })) 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 }, updateSyncContacts: { value in
let _ = context.engine.contacts.updateIsContactSynchronizationEnabled(isContactSynchronizationEnabled: value).start() let _ = context.engine.contacts.updateIsContactSynchronizationEnabled(isContactSynchronizationEnabled: value).start()
@ -556,7 +573,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController {
let 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) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataPrivacyControllerEntries(presentationData: presentationData, state: state, secretChatLinkPreviews: secretChatLinkPreviews, synchronizeDeviceContacts: synchronizeDeviceContacts, frequentContacts: suggestRecentPeers, hasBotSettings: hasBotSettings), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: animateChanges)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }
@ -572,5 +589,19 @@ public func dataPrivacyController(context: AccountContext) -> ViewController {
controller?.push(c) controller?.push(c)
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -38,6 +38,18 @@ private enum GlobalAutoremoveSection: Int32 {
case general case general
} }
public enum GlobalAutoremoveEntryTag: ItemListItemTag, Equatable {
case setCustom
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? GlobalAutoremoveEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum GlobalAutoremoveEntry: ItemListNodeEntry { private enum GlobalAutoremoveEntry: ItemListNodeEntry {
case header case header
case sectionHeader(String) case sectionHeader(String)
@ -126,7 +138,7 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry {
case let .customAction(text): case let .customAction(text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openCustomValue() arguments.openCustomValue()
}) }, tag: GlobalAutoremoveEntryTag.setCustom)
case let .info(text): case let .info(text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in
arguments.infoLinkAction() arguments.infoLinkAction()
@ -188,7 +200,7 @@ private func globalAutoremoveScreenEntries(presentationData: PresentationData, s
return entries return entries
} }
public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32, updated: @escaping (Int32) -> Void) -> ViewController { public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32, updated: @escaping (Int32) -> Void, focusOnItemTag: GlobalAutoremoveEntryTag? = nil) -> ViewController {
let initialState = GlobalAutoremoveScreenState( let initialState = GlobalAutoremoveScreenState(
additionalValues: Set([initialValue]), additionalValues: Set([initialValue]),
updatedValue: initialValue updatedValue: initialValue
@ -430,7 +442,7 @@ public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32,
let animateChanges = false 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 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) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges, scrollEnabled: true)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }
@ -461,5 +473,19 @@ public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32,
controller?.dismiss() controller?.dismiss()
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -48,6 +48,19 @@ private enum IncomingMessagePrivacySection: Int32 {
case exceptions case exceptions
} }
public enum IncomingMessagePrivacyEntryTag: ItemListItemTag, Equatable {
case setPrice
case removeFee
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? IncomingMessagePrivacyEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum GlobalAutoremoveEntry: ItemListNodeEntry { private enum GlobalAutoremoveEntry: ItemListNodeEntry {
case header case header
case optionEverybody(value: GlobalPrivacySettings.NonContactChatsPrivacy) case optionEverybody(value: GlobalPrivacySettings.NonContactChatsPrivacy)
@ -166,7 +179,7 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry {
case let .exceptions(count): 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: { return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: presentationData.strings.Privacy_Messages_RemoveFee, label: count > 0 ? "\(count)" : "", sectionId: self.section, style: .blocks, action: {
arguments.openExceptions() arguments.openExceptions()
}) }, tag: IncomingMessagePrivacyEntryTag.removeFee)
case .exceptionsInfo: case .exceptionsInfo:
return ItemListTextItem(presentationData: presentationData, text: .markdown(presentationData.strings.Privacy_Messages_RemoveFeeInfo), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .markdown(presentationData.strings.Privacy_Messages_RemoveFeeInfo), sectionId: self.section)
} }
@ -212,7 +225,7 @@ private func incomingMessagePrivacyScreenEntries(presentationData: PresentationD
return entries return entries
} }
public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void) -> ViewController { public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void, focusOnItemTag: IncomingMessagePrivacyEntryTag? = nil) -> ViewController {
var disableFor: [EnginePeer.Id: SelectivePrivacyPeer] = [:] var disableFor: [EnginePeer.Id: SelectivePrivacyPeer] = [:]
if case let .enableContacts(value, _, _, _) = exceptions { if case let .enableContacts(value, _, _, _) = exceptions {
disableFor = value disableFor = value
@ -424,7 +437,7 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP
let animateChanges = false 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 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) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges, scrollEnabled: true)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }
@ -468,5 +481,19 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP
controller?.dismiss() controller?.dismiss()
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -35,6 +35,21 @@ private enum PasscodeOptionsSection: Int32 {
case options case options
} }
public enum PasscodeOptionsEntryTag: ItemListItemTag, Equatable {
case togglePasscode
case changePasscode
case autolock
case touchId
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? PasscodeOptionsEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum PasscodeOptionsEntry: ItemListNodeEntry { private enum PasscodeOptionsEntry: ItemListNodeEntry {
case togglePasscode(PresentationTheme, String, Bool) case togglePasscode(PresentationTheme, String, Bool)
case changePasscode(PresentationTheme, String) case changePasscode(PresentationTheme, String)
@ -114,21 +129,21 @@ private enum PasscodeOptionsEntry: ItemListNodeEntry {
if value { if value {
arguments.turnPasscodeOff() arguments.turnPasscodeOff()
} }
}) }, tag: PasscodeOptionsEntryTag.togglePasscode)
case let .changePasscode(_, title): case let .changePasscode(_, title):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.changePasscode() arguments.changePasscode()
}) }, tag: PasscodeOptionsEntryTag.changePasscode)
case let .settingInfo(_, text): case let .settingInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .autoLock(_, title, value): case let .autoLock(_, title, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, label: value, sectionId: self.section, style: .blocks, action: { return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, label: value, sectionId: self.section, style: .blocks, action: {
arguments.changePasscodeTimeout() arguments.changePasscodeTimeout()
}) }, tag: PasscodeOptionsEntryTag.autolock)
case let .touchId(_, title, value): case let .touchId(_, title, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.changeTouchId(value) arguments.changeTouchId(value)
}) }, tag: PasscodeOptionsEntryTag.touchId)
} }
} }
} }
@ -206,7 +221,7 @@ private func passcodeOptionsControllerEntries(presentationData: PresentationData
return entries return entries
} }
func passcodeOptionsController(context: AccountContext) -> ViewController { func passcodeOptionsController(context: AccountContext, focusOnItemTag: PasscodeOptionsEntryTag? = nil) -> ViewController {
let initialState = PasscodeOptionsControllerState() let initialState = PasscodeOptionsControllerState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true) let statePromise = ValuePromise(initialState, ignoreRepeated: true)
@ -360,7 +375,7 @@ func passcodeOptionsController(context: AccountContext) -> ViewController {
|> map { presentationData, state, passcodeOptionsData -> (ItemListControllerState, (ItemListNodeState, Any)) in |> 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 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) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: passcodeOptionsControllerEntries(presentationData: presentationData, state: state, passcodeOptionsData: passcodeOptionsData), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} |> afterDisposed { } |> afterDisposed {
@ -383,6 +398,20 @@ func passcodeOptionsController(context: AccountContext) -> ViewController {
(controller?.navigationController as? NavigationController)?.replaceTopController(c, animated: animated) (controller?.navigationController as? NavigationController)?.replaceTopController(c, animated: animated)
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -1629,5 +1629,19 @@ public func privacyAndSecurityController(
pushControllerImpl?(controller, true) pushControllerImpl?(controller, true)
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -89,6 +89,20 @@ private struct SortIndex: Comparable {
} }
} }
public enum RecentSessionsEntryTag: ItemListItemTag, Equatable {
case edit
case terminateOtherSessions
case autoTerminate
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? RecentSessionsEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum RecentSessionsEntry: ItemListNodeEntry { private enum RecentSessionsEntry: ItemListNodeEntry {
case header(SortIndex, String) case header(SortIndex, String)
case currentSessionHeader(SortIndex, String) case currentSessionHeader(SortIndex, String)
@ -341,7 +355,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
case let .terminateOtherSessions(_, text): 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: { 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() arguments.terminateOtherSessions()
}) }, tag: RecentSessionsEntryTag.terminateOtherSessions)
case let .terminateAllWebSessions(_, text): 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: { 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() arguments.terminateAllWebSessions()
@ -403,7 +417,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
case let .ttlTimeout(_, text, value): case let .ttlTimeout(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, action: { return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, action: {
arguments.setupAuthorizationTTL() arguments.setupAuthorizationTTL()
}, tag: PrivacyAndSecurityEntryTag.accountTimeout) }, tag: RecentSessionsEntryTag.autoTerminate)
} }
} }
} }
@ -560,13 +574,19 @@ private func recentSessionsControllerEntries(presentationData: PresentationData,
private final class RecentSessionsControllerImpl: ItemListController, RecentSessionsController { private final class RecentSessionsControllerImpl: ItemListController, RecentSessionsController {
} }
public func recentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext, webSessionsContext: WebSessionsContext, websitesOnly: Bool) -> ViewController & RecentSessionsController { public func recentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext, webSessionsContext: WebSessionsContext, websitesOnly: Bool, focusOnItemTag: RecentSessionsEntryTag? = nil) -> ViewController & RecentSessionsController {
let statePromise = ValuePromise(RecentSessionsControllerState(), ignoreRepeated: true) let statePromise = ValuePromise(RecentSessionsControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: RecentSessionsControllerState()) let stateValue = Atomic(value: RecentSessionsControllerState())
let updateState: ((RecentSessionsControllerState) -> RecentSessionsControllerState) -> Void = { f in let updateState: ((RecentSessionsControllerState) -> RecentSessionsControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) }) statePromise.set(stateValue.modify { f($0) })
} }
if focusOnItemTag == .edit {
updateState {
$0.withUpdatedEditing(true)
}
}
activeSessionsContext.loadMore() activeSessionsContext.loadMore()
webSessionsContext.loadMore() webSessionsContext.loadMore()
@ -865,7 +885,7 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
} }
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) 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) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: emptyStateItem, crossfadeState: crossfadeState, animateChanges: animateChanges, scrollEnabled: emptyStateItem == nil)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} |> afterDisposed { } |> afterDisposed {
@ -891,5 +911,19 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
controller?.dismiss() controller?.dismiss()
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -136,6 +136,30 @@ private func stringForUserCount(_ peers: [EnginePeer.Id: SelectivePrivacyPeer],
} }
} }
public enum SelectivePrivacyEntryTag: ItemListItemTag, Equatable {
case neverAllow
case alwaysAllow
case lastSeenHideReadTime
case birthdaySetup
case giftsShowButton
case giftsAcceptedTypes
case photoSetPublic
case photoUpdatePublic
case photoRemovePublic
case callsP2PNeverAllow
case callsP2PAlwaysAllow
case callsP2P
case callsIntegration
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? SelectivePrivacyEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { private enum SelectivePrivacySettingsEntry: ItemListNodeEntry {
case forwardsPreviewHeader(PresentationTheme, String) case forwardsPreviewHeader(PresentationTheme, String)
case forwardsPreview(PresentationTheme, TelegramWallpaper, PresentationFontSize, PresentationChatBubbleCorners, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, String, Bool, String) case forwardsPreview(PresentationTheme, TelegramWallpaper, PresentationFontSize, PresentationChatBubbleCorners, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, String, Bool, String)
@ -606,11 +630,11 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry {
case let .disableFor(_, title, value, isEnabled): case let .disableFor(_, title, value, isEnabled):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, enabled: isEnabled, label: value, sectionId: self.section, style: .blocks, action: { return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, enabled: isEnabled, label: value, sectionId: self.section, style: .blocks, action: {
arguments.openSelective(.main, false) arguments.openSelective(.main, false)
}) }, tag: SelectivePrivacyEntryTag.neverAllow)
case let .enableFor(_, title, value, isEnabled): case let .enableFor(_, title, value, isEnabled):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, enabled: isEnabled, label: value, sectionId: self.section, style: .blocks, action: { return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, enabled: isEnabled, label: value, sectionId: self.section, style: .blocks, action: {
arguments.openSelective(.main, true) arguments.openSelective(.main, true)
}) }, tag: SelectivePrivacyEntryTag.alwaysAllow)
case let .peersInfo(_, text): case let .peersInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .callsP2PHeader(_, text): case let .callsP2PHeader(_, text):
@ -632,17 +656,17 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry {
case let .callsP2PDisableFor(_, title, value): case let .callsP2PDisableFor(_, title, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, label: value, sectionId: self.section, style: .blocks, action: { return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, label: value, sectionId: self.section, style: .blocks, action: {
arguments.openSelective(.callP2P, false) arguments.openSelective(.callP2P, false)
}) }, tag: SelectivePrivacyEntryTag.callsP2PNeverAllow)
case let .callsP2PEnableFor(_, title, value): case let .callsP2PEnableFor(_, title, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, label: value, sectionId: self.section, style: .blocks, action: { return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, label: value, sectionId: self.section, style: .blocks, action: {
arguments.openSelective(.callP2P, true) arguments.openSelective(.callP2P, true)
}) }, tag: SelectivePrivacyEntryTag.callsP2PAlwaysAllow)
case let .callsP2PPeersInfo(_, text): case let .callsP2PPeersInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .callsIntegrationEnabled(_, text, value): case let .callsIntegrationEnabled(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateCallIntegrationEnabled?(value) arguments.updateCallIntegrationEnabled?(value)
}) }, tag: SelectivePrivacyEntryTag.callsIntegration)
case let .callsIntegrationInfo(_, text): case let .callsIntegrationInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .phoneDiscoveryHeader(_, text): case let .phoneDiscoveryHeader(_, text):
@ -662,18 +686,18 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry {
case let .setPublicPhoto(theme, text): case let .setPublicPhoto(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.addPhotoIcon(theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: { return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.addPhotoIcon(theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
arguments.setPublicPhoto?() arguments.setPublicPhoto?()
}) }, tag: SelectivePrivacyEntryTag.photoSetPublic)
case let .removePublicPhoto(_, text, peer, image, completeImage): case let .removePublicPhoto(_, text, peer, image, completeImage):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: completeImage, iconSignal: completeImage == nil ? peerAvatarCompleteImage(account: arguments.context.account, peer: peer, forceProvidedRepresentation: true, representation: image?.representationForDisplayAtSize(PixelDimensions(width: 28, height: 28)), size: CGSize(width: 28.0, height: 28.0)) : nil, title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: { return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: completeImage, iconSignal: completeImage == nil ? peerAvatarCompleteImage(account: arguments.context.account, peer: peer, forceProvidedRepresentation: true, representation: image?.representationForDisplayAtSize(PixelDimensions(width: 28, height: 28)), size: CGSize(width: 28.0, height: 28.0)) : nil, title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: {
arguments.removePublicPhoto?() arguments.removePublicPhoto?()
}) }, tag: SelectivePrivacyEntryTag.photoRemovePublic)
case let .publicPhotoInfo(_, text): case let .publicPhotoInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in
}) })
case let .hideReadTime(_, text, enabled, value): case let .hideReadTime(_, text, enabled, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateHideReadTime?(value) arguments.updateHideReadTime?(value)
}) }, tag: SelectivePrivacyEntryTag.lastSeenHideReadTime)
case let .hideReadTimeInfo(_, text): case let .hideReadTimeInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .subscribeToPremium(_, text): case let .subscribeToPremium(_, text):
@ -683,7 +707,7 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry {
case let .subscribeToPremiumInfo(_, text): case let .subscribeToPremiumInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .disallowedGiftsHeader(_, text): case let .disallowedGiftsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section, tag: SelectivePrivacyEntryTag.giftsAcceptedTypes)
case let .disallowedGiftsUnlimited(_, text, isLocked, value): case let .disallowedGiftsUnlimited(_, text, isLocked, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: !isLocked, enabled: true, displayLocked: isLocked, sectionId: self.section, style: .blocks, updated: { updatedValue in return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: !isLocked, enabled: true, displayLocked: isLocked, sectionId: self.section, style: .blocks, updated: { updatedValue in
if !isLocked { if !isLocked {
@ -747,7 +771,7 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry {
if available { if available {
arguments.displayLockedGiftsInfo() arguments.displayLockedGiftsInfo()
} }
}) }, tag: SelectivePrivacyEntryTag.giftsShowButton)
case let .showGiftButtonInfo(_, text): case let .showGiftButtonInfo(_, text):
let attributedString = NSMutableAttributedString(string: text, font: Font.regular(presentationData.fontSize.itemListBaseHeaderFontSize), textColor: presentationData.theme.list.freeTextColor) let attributedString = NSMutableAttributedString(string: text, font: Font.regular(presentationData.fontSize.itemListBaseHeaderFontSize), textColor: presentationData.theme.list.freeTextColor)
if let range = attributedString.string.range(of: "#") { if let range = attributedString.string.range(of: "#") {
@ -1216,6 +1240,7 @@ public func selectivePrivacySettingsController(
requestPublicPhotoSetup: ((@escaping (UIImage?) -> Void) -> Void)? = nil, requestPublicPhotoSetup: ((@escaping (UIImage?) -> Void) -> Void)? = nil,
requestPublicPhotoRemove: ((@escaping () -> Void) -> Void)? = nil, requestPublicPhotoRemove: ((@escaping () -> Void) -> Void)? = nil,
openedFromBirthdayScreen: Bool = false, openedFromBirthdayScreen: Bool = false,
focusOnItemTag: SelectivePrivacyEntryTag? = nil,
updated: @escaping (SelectivePrivacySettings, (SelectivePrivacySettings, VoiceCallSettings)?, Bool?, GlobalPrivacySettings?) -> Void updated: @escaping (SelectivePrivacySettings, (SelectivePrivacySettings, VoiceCallSettings)?, Bool?, GlobalPrivacySettings?) -> Void
) -> ViewController { ) -> ViewController {
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
@ -1910,5 +1935,20 @@ public func selectivePrivacySettingsController(
dismissImpl = { [weak controller] in dismissImpl = { [weak controller] in
controller?.dismiss() controller?.dismiss()
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -50,19 +50,15 @@ private enum TwoStepVerificationUnlockSettingsSection: Int32 {
case email case email
} }
private enum TwoStepVerificationUnlockSettingsEntryTag: ItemListItemTag { public enum TwoStepVerificationUnlockSettingsEntryTag: ItemListItemTag {
case password case password
case change
case disable
case changeEmail
func isEqual(to other: ItemListItemTag) -> Bool { public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? TwoStepVerificationUnlockSettingsEntryTag { if let other = other as? TwoStepVerificationUnlockSettingsEntryTag, self == other {
switch self { return true
case .password:
if case .password = other {
return true
} else {
return false
}
}
} else { } else {
return false return false
} }
@ -159,15 +155,15 @@ private enum TwoStepVerificationUnlockSettingsEntry: ItemListNodeEntry {
case let .changePassword(_, text): case let .changePassword(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openSetupPassword() arguments.openSetupPassword()
}) }, tag: TwoStepVerificationUnlockSettingsEntryTag.change)
case let .turnPasswordOff(_, text): case let .turnPasswordOff(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openDisablePassword() arguments.openDisablePassword()
}) }, tag: TwoStepVerificationUnlockSettingsEntryTag.disable)
case let .setupRecoveryEmail(_, text): case let .setupRecoveryEmail(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openSetupEmail() arguments.openSetupEmail()
}) }, tag: TwoStepVerificationUnlockSettingsEntryTag.changeEmail)
case let .passwordInfo(_, text): case let .passwordInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .pendingEmailConfirmInfo(_, text): case let .pendingEmailConfirmInfo(_, text):
@ -283,7 +279,7 @@ public enum TwoStepVerificationUnlockSettingsControllerData: Equatable {
case manage(password: String, emailSet: Bool, pendingEmail: TwoStepVerificationPendingEmail?, hasSecureValues: Bool) case manage(password: String, emailSet: Bool, pendingEmail: TwoStepVerificationPendingEmail?, hasSecureValues: Bool)
} }
public func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: TwoStepVerificationUnlockSettingsControllerMode, openSetupPasswordImmediately: Bool = false) -> ViewController { public func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: TwoStepVerificationUnlockSettingsControllerMode, openSetupPasswordImmediately: Bool = false, focusOnItemTag: TwoStepVerificationUnlockSettingsEntryTag? = nil) -> ViewController {
let initialState = TwoStepVerificationUnlockSettingsControllerState() let initialState = TwoStepVerificationUnlockSettingsControllerState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true) let statePromise = ValuePromise(initialState, ignoreRepeated: true)
@ -463,7 +459,7 @@ public func twoStepVerificationUnlockSettingsController(context: AccountContext,
return state return state
} }
replaceControllerImpl?(twoStepVerificationUnlockSettingsController(context: context, mode: .manage(password: password, email: settings.email, pendingEmail: pendingEmail, hasSecureValues: settings.secureSecret != nil)), true) replaceControllerImpl?(twoStepVerificationUnlockSettingsController(context: context, mode: .manage(password: password, email: settings.email, pendingEmail: pendingEmail, hasSecureValues: settings.secureSecret != nil), focusOnItemTag: focusOnItemTag), true)
}, error: { error in }, error: { error in
updateState { state in updateState { state in
var state = state var state = state
@ -969,7 +965,7 @@ public func twoStepVerificationUnlockSettingsController(context: AccountContext,
} }
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: twoStepVerificationUnlockSettingsControllerEntries(presentationData: presentationData, state: state, data: data), style: .blocks, focusItemTag: didAppear ? TwoStepVerificationUnlockSettingsEntryTag.password : nil, emptyStateItem: emptyStateItem, animateChanges: false) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: twoStepVerificationUnlockSettingsControllerEntries(presentationData: presentationData, state: state, data: data), style: .blocks, focusItemTag: focusOnItemTag ?? (didAppear ? TwoStepVerificationUnlockSettingsEntryTag.password : nil), emptyStateItem: emptyStateItem, animateChanges: false)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }
@ -1041,5 +1037,19 @@ public func twoStepVerificationUnlockSettingsController(context: AccountContext,
})) }))
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -53,6 +53,22 @@ private enum ReactionNotificationSettingsSection: Int32 {
case options case options
} }
public enum ReactionNotificationSettingsEntryTag: ItemListItemTag {
case messages
case stories
case showSender
case sound
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? ReactionNotificationSettingsEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum ReactionNotificationSettingsEntry: ItemListNodeEntry { private enum ReactionNotificationSettingsEntry: ItemListNodeEntry {
enum StableId: Hashable { enum StableId: Hashable {
case categoriesHeader case categoriesHeader
@ -169,23 +185,23 @@ private enum ReactionNotificationSettingsEntry: ItemListNodeEntry {
arguments.toggleMessages(value) arguments.toggleMessages(value)
}, action: { }, action: {
arguments.openMessages() arguments.openMessages()
}) }, tag: ReactionNotificationSettingsEntryTag.messages)
case let .stories(title, text, value): 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 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) arguments.toggleStories(value)
}, action: { }, action: {
arguments.openStories() arguments.openStories()
}) }, tag: ReactionNotificationSettingsEntryTag.stories)
case let .optionsHeader(text): case let .optionsHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .previews(text, value): case let .previews(text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updatePreviews(value) arguments.updatePreviews(value)
}) }, tag: ReactionNotificationSettingsEntryTag.showSender)
case let .sound(text, value, sound): 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: { return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openSound(sound) arguments.openSound(sound)
}, tag: self.tag) }, tag: ReactionNotificationSettingsEntryTag.sound)
} }
} }
} }
@ -255,7 +271,8 @@ private struct ReactionNotificationSettingsState: Equatable {
} }
public func reactionNotificationSettingsController( public func reactionNotificationSettingsController(
context: AccountContext context: AccountContext,
focusOnItemTag: ReactionNotificationSettingsEntryTag? = nil
) -> ViewController { ) -> ViewController {
var presentControllerImpl: ((ViewController, Any?) -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)?
@ -399,7 +416,7 @@ public func reactionNotificationSettingsController(
let title: String = presentationData.strings.Notifications_Reactions_Title 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 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) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }
@ -411,5 +428,20 @@ public func reactionNotificationSettingsController(
pushControllerImpl = { [weak controller] c in pushControllerImpl = { [weak controller] c in
(controller?.navigationController as? NavigationController)?.pushViewController(c) (controller?.navigationController as? NavigationController)?.pushViewController(c)
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -48,6 +48,8 @@ extension SettingsSearchableItemIcon {
return PresentationResourcesSettings.support return PresentationResourcesSettings.support
case .faq: case .faq:
return PresentationResourcesSettings.faq return PresentationResourcesSettings.faq
case .tips:
return PresentationResourcesSettings.tips
case .chatFolders: case .chatFolders:
return PresentationResourcesSettings.chatFolders return PresentationResourcesSettings.chatFolders
case .deleteAccount: case .deleteAccount:
@ -56,24 +58,42 @@ extension SettingsSearchableItemIcon {
return PresentationResourcesSettings.devices return PresentationResourcesSettings.devices
case .premium: case .premium:
return PresentationResourcesSettings.premium return PresentationResourcesSettings.premium
case .business:
return PresentationResourcesSettings.business
case .stars:
return PresentationResourcesSettings.stars
case .ton:
return PresentationResourcesSettings.ton
case .stories: case .stories:
return PresentationResourcesSettings.stories return PresentationResourcesSettings.stories
case .myProfile:
return PresentationResourcesSettings.myProfile
case .gift:
return PresentationResourcesSettings.premiumGift
case .powerSaving:
return PresentationResourcesSettings.powerSaving
} }
} }
} }
final class SettingsSearchInteraction { final class SettingsSearchInteraction {
let openItem: (SettingsSearchableItem) -> Void let openItem: (SettingsSearchableItem) -> Void
let deleteRecentItem: (SettingsSearchableItemId) -> Void let openItemContextMenu: (SettingsSearchableItem, ContextExtractedContentContainingNode, CGRect, UIGestureRecognizer?) -> Void
let deleteRecentItem: (AnyHashable) -> Void
init(openItem: @escaping (SettingsSearchableItem) -> Void, deleteRecentItem: @escaping (SettingsSearchableItemId) -> Void) { init(
openItem: @escaping (SettingsSearchableItem) -> Void,
openItemContextMenu: @escaping (SettingsSearchableItem, ContextExtractedContentContainingNode, CGRect, UIGestureRecognizer?) -> Void,
deleteRecentItem: @escaping (AnyHashable) -> Void
) {
self.openItem = openItem self.openItem = openItem
self.openItemContextMenu = openItemContextMenu
self.deleteRecentItem = deleteRecentItem self.deleteRecentItem = deleteRecentItem
} }
} }
private enum SettingsSearchEntryStableId: Hashable { private enum SettingsSearchEntryStableId: Hashable {
case result(SettingsSearchableItemId) case result(AnyHashable)
} }
private enum SettingsSearchEntry: Comparable, Identifiable { private enum SettingsSearchEntry: Comparable, Identifiable {
@ -132,7 +152,7 @@ private func preparedSettingsSearchContainerTransition(theme: PresentationTheme,
} }
private enum SettingsSearchRecentEntryStableId: Hashable { private enum SettingsSearchRecentEntryStableId: Hashable {
case recent(SettingsSearchableItemId) case recent(AnyHashable)
} }
private enum SettingsSearchRecentEntry: Comparable, Identifiable { private enum SettingsSearchRecentEntry: Comparable, Identifiable {
@ -192,7 +212,11 @@ private enum SettingsSearchRecentEntry: Comparable, Identifiable {
func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: SettingsSearchInteraction) -> ListViewItem { func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: SettingsSearchInteraction) -> ListViewItem {
switch self { switch self {
case let .recent(_, item, header): case let .recent(_, item, header):
return SettingsSearchRecentItem(account: account, theme: theme, strings: strings, title: item.title, breadcrumbs: item.breadcrumbs, isFaq: false, action: { var title = item.title
if title.isEmpty, let id = item.id.base as? String {
title = id
}
return SettingsSearchRecentItem(account: account, theme: theme, strings: strings, title: title, breadcrumbs: item.breadcrumbs, isFaq: false, action: {
interaction.openItem(item) interaction.openItem(item)
}, deleted: { }, deleted: {
interaction.deleteRecentItem(item.id) interaction.deleteRecentItem(item.id)
@ -244,7 +268,19 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo
private var presentationDataDisposable: Disposable? private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<PresentationData> 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>) { public init(
context: AccountContext,
openResult: @escaping (SettingsSearchableItem) -> Void,
openContextMenu: @escaping (SettingsSearchableItem, ContextExtractedContentContainingNode, CGRect, UIGestureRecognizer?) -> 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 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData self.presentationData = presentationData
self.presentationDataPromise = Promise(self.presentationData) self.presentationDataPromise = Promise(self.presentationData)
@ -273,15 +309,30 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo
self.addSubnode(self.listNode) self.addSubnode(self.listNode)
self.view.addSubview(self.edgeEffectView) self.view.addSubview(self.edgeEffectView)
let interaction = SettingsSearchInteraction(openItem: { result in let interaction = SettingsSearchInteraction(
addRecentSettingsSearchItem(engine: context.engine, item: result.id) openItem: { item in
openResult(result) addRecentSettingsSearchItem(engine: context.engine, item: item.id)
}, deleteRecentItem: { id in openResult(item)
removeRecentSettingsSearchItem(engine: context.engine, item: id) },
}) openItemContextMenu: { item, node, rect, gesture in
openContextMenu(item, node, rect, gesture)
},
deleteRecentItem: { id in
removeRecentSettingsSearchItem(engine: context.engine, item: id)
}
)
let searchableItems = Promise<[SettingsSearchableItem]>() let searchableItems = Promise<[SettingsSearchableItem]>()
searchableItems.set(settingsSearchableItems(context: context, notificationExceptionsList: exceptionsList, archivedStickerPacks: archivedStickerPacks, privacySettings: privacySettings, hasTwoStepAuth: hasTwoStepAuth, twoStepAuthData: twoStepAuthData, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext)) searchableItems.set(settingsSearchableItems(
context: context,
notificationExceptionsList: exceptionsList,
archivedStickerPacks: archivedStickerPacks,
privacySettings: privacySettings,
hasTwoStepAuth: hasTwoStepAuth,
twoStepAuthData: twoStepAuthData,
activeSessionsContext: activeSessionsContext,
webSessionsContext: webSessionsContext
))
let faqItems = Promise<[SettingsSearchableItem]>() let faqItems = Promise<[SettingsSearchableItem]>()
faqItems.set(faqSearchableItems(context: context, resolvedUrl: resolvedFaqUrl, suggestAccountDeletion: false)) faqItems.set(faqSearchableItems(context: context, resolvedUrl: resolvedFaqUrl, suggestAccountDeletion: false))
@ -294,11 +345,11 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo
let results = searchSettingsItems(items: searchableItems, query: query) let results = searchSettingsItems(items: searchableItems, query: query)
let faqResults = searchSettingsItems(items: faqSearchableItems, query: query) let faqResults = searchSettingsItems(items: faqSearchableItems, query: query)
let finalResults: [SettingsSearchableItem] let finalResults: [SettingsSearchableItem]
if faqResults.first?.id == .faq(1) { //if faqResults.first?.id == .faq(1) {
finalResults = faqResults + results // finalResults = faqResults + results
} else { //} else {
finalResults = results + faqResults finalResults = results + faqResults
} //}
return .single((query, finalResults)) return .single((query, finalResults))
} else { } else {
return .single(nil) return .single(nil)
@ -308,12 +359,12 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo
self.recentListNode.isHidden = false self.recentListNode.isHidden = false
let previousRecentlySearchedItemOrder = Atomic<[SettingsSearchableItemId]>(value: []) let previousRecentlySearchedItemOrder = Atomic<[AnyHashable]>(value: [])
let fixedRecentlySearchedItems = settingsSearchRecentItems(engine: context.engine) let fixedRecentlySearchedItems = settingsSearchRecentItems(engine: context.engine)
|> map { recentIds -> [SettingsSearchableItemId] in |> map { recentIds -> [AnyHashable] in
var result: [SettingsSearchableItemId] = [] var result: [AnyHashable] = []
let _ = previousRecentlySearchedItemOrder.modify { current in let _ = previousRecentlySearchedItemOrder.modify { current in
var updated: [SettingsSearchableItemId] = [] var updated: [AnyHashable] = []
for id in current { for id in current {
inner: for recentId in recentIds { inner: for recentId in recentIds {
if recentId == id { if recentId == id {
@ -336,7 +387,7 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo
let recentSearchItems = combineLatest(searchableItems.get(), fixedRecentlySearchedItems) let recentSearchItems = combineLatest(searchableItems.get(), fixedRecentlySearchedItems)
|> map { searchableItems, recentItems -> [SettingsSearchableItem] in |> map { searchableItems, recentItems -> [SettingsSearchableItem] in
let searchableItemsMap = searchableItems.reduce([SettingsSearchableItemId : SettingsSearchableItem]()) { (map, item) -> [SettingsSearchableItemId: SettingsSearchableItem] in let searchableItemsMap = searchableItems.reduce([AnyHashable : SettingsSearchableItem]()) { (map, item) -> [AnyHashable: SettingsSearchableItem] in
var map = map var map = map
map[item.id] = item map[item.id] = item
return map return map
@ -344,10 +395,10 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo
var result: [SettingsSearchableItem] = [] var result: [SettingsSearchableItem] = []
for itemId in recentItems { for itemId in recentItems {
if let searchItem = searchableItemsMap[itemId] { if let searchItem = searchableItemsMap[itemId] {
if case let .language(id) = searchItem.id, id > 0 { //if case let .language(id) = searchItem.id, id > 0 {
} else { //} else {
result.append(searchItem) result.append(searchItem)
} //}
} }
} }
return result return result
@ -620,7 +671,7 @@ private final class SettingsSearchItemNode: ItemListControllerSearchNode {
} }
}) })
} }
}, 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 }, openContextMenu: { _, _, _, _ in }, 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?.cancel()
}, fieldStyle: placeholderNode.fieldStyle) }, fieldStyle: placeholderNode.fieldStyle)

View file

@ -187,14 +187,14 @@ class SettingsSearchRecentItemNode: ItemListRevealOptionsItemNode {
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())) 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 var height: CGFloat = 30.0 + titleLayout.size.height
if subtitle.isEmpty { if !subtitle.isEmpty {
height += 22.0 height += 9.0
} else {
height += 39.0
} }
let contentSize = CGSize(width: params.width, height: height) 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)) 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 return (nodeLayout, { [weak self] in
var updatedTheme: PresentationTheme? var updatedTheme: PresentationTheme?
if currentItem?.theme !== item.theme { if currentItem?.theme !== item.theme {
@ -215,7 +215,7 @@ class SettingsSearchRecentItemNode: ItemListRevealOptionsItemNode {
let _ = titleApply() let _ = titleApply()
let _ = subtitleApply() let _ = subtitleApply()
let titleY: CGFloat = subtitle.isEmpty ? 11.0 : 11.0 let titleY: CGFloat = subtitle.isEmpty ? 16.0 - UIScreenPixel : 11.0
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: titleY), size: titleLayout.size) 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) strongSelf.subtitleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: titleY + titleLayout.size.height + 1.0), size: subtitleLayout.size)
@ -223,7 +223,7 @@ class SettingsSearchRecentItemNode: ItemListRevealOptionsItemNode {
let topHighlightInset: CGFloat = (firstWithHeader || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight 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.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.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.contentSize.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.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight))
strongSelf.separatorNode.isHidden = last strongSelf.separatorNode.isHidden = last

View file

@ -38,28 +38,34 @@ public final class RecentSettingsSearchQueryItem: Codable {
} }
} }
func addRecentSettingsSearchItem(engine: TelegramEngine, item: SettingsSearchableItemId) { func addRecentSettingsSearchItem(engine: TelegramEngine, item: AnyHashable) {
let itemId = SettingsSearchRecentQueryItemId(item.index) guard let id = item.base as? String, let data = id.data(using: .ascii) else {
let _ = engine.orderedLists.addOrMoveToFirstPosition(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems, id: itemId.rawValue, item: RecentSettingsSearchQueryItem(), removeTailIfCountExceeds: 100).start() return
}
let itemId = MemoryBuffer(data: data)
let _ = engine.orderedLists.addOrMoveToFirstPosition(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems, id: itemId, item: RecentSettingsSearchQueryItem(), removeTailIfCountExceeds: 100).start()
} }
func removeRecentSettingsSearchItem(engine: TelegramEngine, item: SettingsSearchableItemId) { func removeRecentSettingsSearchItem(engine: TelegramEngine, item: AnyHashable) {
let itemId = SettingsSearchRecentQueryItemId(item.index) guard let id = item.base as? String, let data = id.data(using: .ascii) else {
let _ = engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems, id: itemId.rawValue).start() return
}
let itemId = MemoryBuffer(data: data)
let _ = engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems, id: itemId).start()
} }
func clearRecentSettingsSearchItems(engine: TelegramEngine) { func clearRecentSettingsSearchItems(engine: TelegramEngine) {
let _ = engine.orderedLists.clear(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems).start() let _ = engine.orderedLists.clear(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems).start()
} }
func settingsSearchRecentItems(engine: TelegramEngine) -> Signal<[SettingsSearchableItemId], NoError> { func settingsSearchRecentItems(engine: TelegramEngine) -> Signal<[AnyHashable], NoError> {
return engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems)) return engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems))
|> map { items -> [SettingsSearchableItemId] in |> map { items -> [AnyHashable] in
var result: [SettingsSearchableItemId] = [] var result: [AnyHashable] = []
for item in items { for item in items {
let index = SettingsSearchRecentQueryItemId(item.id).value let data = item.id.makeData()
if let itemId = SettingsSearchableItemId(index: index) { if let id = String(data: data, encoding: .utf8) {
result.append(itemId) result.append(id)
} }
} }
return result return result

View file

@ -77,6 +77,15 @@ private let titleFont = Font.regular(17.0)
private let subtitleFont = Font.regular(13.0) private let subtitleFont = Font.regular(13.0)
class SettingsSearchResultItemNode: ListViewItemNode { class SettingsSearchResultItemNode: ListViewItemNode {
private let contextSourceNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let extractedBackgroundImageNode: ASImageNode
private var extractedRect: CGRect?
private var nonExtractedRect: CGRect?
private let offsetContainerNode: ASDisplayNode
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode
@ -90,6 +99,15 @@ class SettingsSearchResultItemNode: ListViewItemNode {
private var layoutParams: (ListViewItemLayoutParams, ItemListNeighbors)? private var layoutParams: (ListViewItemLayoutParams, ItemListNeighbors)?
init() { init() {
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.extractedBackgroundImageNode = ASImageNode()
self.extractedBackgroundImageNode.displaysAsynchronously = false
self.extractedBackgroundImageNode.alpha = 0.0
self.offsetContainerNode = ASDisplayNode()
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true self.backgroundNode.isLayerBacked = true
@ -119,9 +137,48 @@ class SettingsSearchResultItemNode: ListViewItemNode {
super.init(layerBacked: false, rotated: false, seeThrough: false) super.init(layerBacked: false, rotated: false, seeThrough: false)
self.addSubnode(self.iconNode) self.containerNode.addSubnode(self.contextSourceNode)
self.addSubnode(self.titleNode) self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.addSubnode(self.subtitleNode) self.addSubnode(self.containerNode)
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode)
self.offsetContainerNode.addSubnode(self.iconNode)
self.offsetContainerNode.addSubnode(self.titleNode)
self.offsetContainerNode.addSubnode(self.subtitleNode)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
cancelParentGestures(view: strongSelf.view)
item.interaction.openItemContextMenu(item.item, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
if isExtracted {
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 52.0, color: item.theme.list.plainBackgroundColor)
}
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
let rect = isExtracted ? extractedRect : nonExtractedRect
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
}
transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0))
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
if !isExtracted {
self?.extractedBackgroundImageNode.image = nil
}
})
}
} }
func asyncLayout() -> (_ item: SettingsSearchResultItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { func asyncLayout() -> (_ item: SettingsSearchResultItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
@ -132,7 +189,7 @@ class SettingsSearchResultItemNode: ListViewItemNode {
return { item, params, neighbors in return { item, params, neighbors in
var leftInset: CGFloat = params.leftInset var leftInset: CGFloat = params.leftInset
let contentInset: CGFloat = 58.0 let contentInset: CGFloat = 60.0
let insets = itemListNeighborsGroupedInsets(neighbors, params) let insets = itemListNeighborsGroupedInsets(neighbors, params)
@ -157,12 +214,11 @@ class SettingsSearchResultItemNode: ListViewItemNode {
updateIconImage = item.icon updateIconImage = item.icon
} }
var height = titleLayout.size.height var height: CGFloat = 30.0 + titleLayout.size.height
if subtitle.isEmpty { if !subtitle.isEmpty {
height += 22.0 height += 9.0
} else {
height += 39.0
} }
let contentSize = CGSize(width: params.width, height: height) let contentSize = CGSize(width: params.width, height: height)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] animated in return (layout, { [weak self] animated in
@ -219,11 +275,29 @@ class SettingsSearchResultItemNode: ListViewItemNode {
default: default:
bottomStripeInset = 0.0 bottomStripeInset = 0.0
} }
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width - 16.0, height: layout.contentSize.height))
let extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0)
strongSelf.extractedRect = extractedRect
strongSelf.nonExtractedRect = nonExtractedRect
if strongSelf.contextSourceNode.isExtractedToContextPreview {
strongSelf.extractedBackgroundImageNode.frame = extractedRect
} else {
strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect
}
strongSelf.contextSourceNode.contentRect = extractedRect
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.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.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)) 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 let titleY: CGFloat = subtitle.isEmpty ? 16.0 - UIScreenPixel : 11.0
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: titleY), size: titleLayout.size)) 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)) transition.updateFrame(node: strongSelf.subtitleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: titleY + titleLayout.size.height + 1.0), size: subtitleLayout.size))

View file

@ -249,13 +249,19 @@ private func archivedStickerPacksControllerEntries(context: AccountContext, mode
return entries return entries
} }
public func archivedStickerPacksController(context: AccountContext, mode: ArchivedStickerPacksControllerMode, archived: [ArchivedStickerPackItem]?, forceTheme: PresentationTheme? = nil, updatedPacks: @escaping ([ArchivedStickerPackItem]?) -> Void) -> ViewController { public func archivedStickerPacksController(context: AccountContext, mode: ArchivedStickerPacksControllerMode, archived: [ArchivedStickerPackItem]?, forceTheme: PresentationTheme? = nil, updatedPacks: @escaping ([ArchivedStickerPackItem]?) -> Void, forceEdit: Bool = false) -> ViewController {
let statePromise = ValuePromise(ArchivedStickerPacksControllerState(), ignoreRepeated: true) let statePromise = ValuePromise(ArchivedStickerPacksControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: ArchivedStickerPacksControllerState()) let stateValue = Atomic(value: ArchivedStickerPacksControllerState())
let updateState: ((ArchivedStickerPacksControllerState) -> ArchivedStickerPacksControllerState) -> Void = { f in let updateState: ((ArchivedStickerPacksControllerState) -> ArchivedStickerPacksControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) }) statePromise.set(stateValue.modify { f($0) })
} }
if forceEdit {
updateState {
$0.withUpdatedEditing(true)
}
}
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var navigationControllerImpl: (() -> NavigationController?)? var navigationControllerImpl: (() -> NavigationController?)?

View file

@ -67,7 +67,10 @@ private enum InstalledStickerPacksSection: Int32 {
} }
public enum InstalledStickerPacksEntryTag: ItemListItemTag { public enum InstalledStickerPacksEntryTag: ItemListItemTag {
case edit
case suggestOptions case suggestOptions
case largeEmoji
case dynamicOrder
public func isEqual(to other: ItemListItemTag) -> Bool { public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? InstalledStickerPacksEntryTag, self == other { if let other = other as? InstalledStickerPacksEntryTag, self == other {
@ -397,7 +400,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
case let .largeEmoji(_, text, value): case let .largeEmoji(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleLargeEmoji(value) arguments.toggleLargeEmoji(value)
}) }, tag: InstalledStickerPacksEntryTag.largeEmoji)
case let .trending(theme, text, count): case let .trending(theme, text, count):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Trending")?.precomposed(), title: text, label: count == 0 ? "" : "\(count)", labelStyle: .badge(theme.list.itemAccentColor), sectionId: self.section, style: .blocks, action: { return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Trending")?.precomposed(), title: text, label: count == 0 ? "" : "\(count)", labelStyle: .badge(theme.list.itemAccentColor), sectionId: self.section, style: .blocks, action: {
arguments.openFeatured() arguments.openFeatured()
@ -421,7 +424,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
case let .packOrder(_, text, value): case let .packOrder(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleDynamicPackOrder(value) arguments.toggleDynamicPackOrder(value)
}) }, tag: InstalledStickerPacksEntryTag.dynamicOrder)
case let .packOrderInfo(_, text): case let .packOrderInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .suggestAnimatedEmoji(text, value): case let .suggestAnimatedEmoji(text, value):
@ -651,6 +654,12 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta
statePromise.set(stateValue.modify { f($0) }) statePromise.set(stateValue.modify { f($0) })
} }
if focusOnItemTag == InstalledStickerPacksEntryTag.edit {
updateState {
$0.withUpdatedEditing(true)
}
}
var presentationData = context.sharedContext.currentPresentationData.with { $0 } var presentationData = context.sharedContext.currentPresentationData.with { $0 }
if let forceTheme { if let forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme) presentationData = presentationData.withUpdated(theme: forceTheme)
@ -1299,6 +1308,20 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta
controller?.dismiss() controller?.dismiss()
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -16,6 +16,18 @@ import WallpaperBackgroundNode
import AnimationCache import AnimationCache
import MultiAnimationRenderer import MultiAnimationRenderer
public enum TextSizeSelectionEntryTag: ItemListItemTag, Equatable {
case useSystem
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? TextSizeSelectionEntryTag, self == other {
return true
} else {
return false
}
}
}
private func generateMaskImage(color: UIColor) -> UIImage? { private func generateMaskImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size) let bounds = CGRect(origin: CGPoint(), size: size)
@ -33,6 +45,8 @@ private func generateMaskImage(color: UIColor) -> UIImage? {
private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollViewDelegate { private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollViewDelegate {
private let context: AccountContext private let context: AccountContext
private let focusOnItemTag: TextSizeSelectionEntryTag?
private var presentationThemeSettings: PresentationThemeSettings private var presentationThemeSettings: PresentationThemeSettings
private var presentationData: PresentationData private var presentationData: PresentationData
@ -57,8 +71,9 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView
private var validLayout: (ContainerViewLayout, CGFloat)? private var validLayout: (ContainerViewLayout, CGFloat)?
init(context: AccountContext, presentationThemeSettings: PresentationThemeSettings, dismiss: @escaping () -> Void, apply: @escaping (Bool, PresentationFontSize, PresentationFontSize) -> Void) { init(context: AccountContext, presentationThemeSettings: PresentationThemeSettings, focusOnItemTag: TextSizeSelectionEntryTag?, dismiss: @escaping () -> Void, apply: @escaping (Bool, PresentationFontSize, PresentationFontSize) -> Void) {
self.context = context self.context = context
self.focusOnItemTag = focusOnItemTag
self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationThemeSettings = presentationThemeSettings self.presentationThemeSettings = presentationThemeSettings
@ -92,7 +107,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView
self.chatBackgroundNode.update(wallpaper: self.presentationData.chatWallpaper, animated: false) self.chatBackgroundNode.update(wallpaper: self.presentationData.chatWallpaper, animated: false)
self.chatBackgroundNode.updateBubbleTheme(bubbleTheme: self.presentationData.theme, bubbleCorners: self.presentationData.chatBubbleCorners) self.chatBackgroundNode.updateBubbleTheme(bubbleTheme: self.presentationData.theme, bubbleCorners: self.presentationData.chatBubbleCorners)
self.toolbarNode = TextSelectionToolbarNode(presentationThemeSettings: self.presentationThemeSettings, presentationData: self.presentationData) self.toolbarNode = TextSelectionToolbarNode(presentationThemeSettings: self.presentationThemeSettings, presentationData: self.presentationData, focusOnItemTag: focusOnItemTag)
self.maskNode = ASImageNode() self.maskNode = ASImageNode()
self.maskNode.displaysAsynchronously = false self.maskNode.displaysAsynchronously = false
@ -602,6 +617,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView
final class TextSizeSelectionController: ViewController { final class TextSizeSelectionController: ViewController {
private let context: AccountContext private let context: AccountContext
private let focusOnItemTag: TextSizeSelectionEntryTag?
private var controllerNode: TextSizeSelectionControllerNode { private var controllerNode: TextSizeSelectionControllerNode {
return self.displayNode as! TextSizeSelectionControllerNode return self.displayNode as! TextSizeSelectionControllerNode
@ -618,8 +634,9 @@ final class TextSizeSelectionController: ViewController {
private var disposable: Disposable? private var disposable: Disposable?
private var applyDisposable = MetaDisposable() private var applyDisposable = MetaDisposable()
public init(context: AccountContext, presentationThemeSettings: PresentationThemeSettings) { public init(context: AccountContext, presentationThemeSettings: PresentationThemeSettings, focusOnItemTag: TextSizeSelectionEntryTag? = nil) {
self.context = context self.context = context
self.focusOnItemTag = focusOnItemTag
self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationThemeSettings = presentationThemeSettings self.presentationThemeSettings = presentationThemeSettings
@ -670,7 +687,7 @@ final class TextSizeSelectionController: ViewController {
override public func loadDisplayNode() { override public func loadDisplayNode() {
super.loadDisplayNode() super.loadDisplayNode()
self.displayNode = TextSizeSelectionControllerNode(context: self.context, presentationThemeSettings: self.presentationThemeSettings, dismiss: { [weak self] in self.displayNode = TextSizeSelectionControllerNode(context: self.context, presentationThemeSettings: self.presentationThemeSettings, focusOnItemTag: self.focusOnItemTag, dismiss: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.dismiss() strongSelf.dismiss()
} }
@ -727,7 +744,7 @@ private final class TextSelectionToolbarNode: ASDisplayNode {
var updateUseSystemFont: ((Bool) -> Void)? var updateUseSystemFont: ((Bool) -> Void)?
var updateCustomFontSize: ((PresentationFontSize) -> Void)? var updateCustomFontSize: ((PresentationFontSize) -> Void)?
init(presentationThemeSettings: PresentationThemeSettings, presentationData: PresentationData) { init(presentationThemeSettings: PresentationThemeSettings, presentationData: PresentationData, focusOnItemTag: TextSizeSelectionEntryTag?) {
self.presentationThemeSettings = presentationThemeSettings self.presentationThemeSettings = presentationThemeSettings
self.presentationData = presentationData self.presentationData = presentationData
@ -774,6 +791,10 @@ private final class TextSelectionToolbarNode: ASDisplayNode {
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
self.doneButton.addTarget(self, action: #selector(self.donePressed), forControlEvents: .touchUpInside) self.doneButton.addTarget(self, action: #selector(self.donePressed), forControlEvents: .touchUpInside)
if focusOnItemTag == .useSystem {
self.switchItemNode.displayHighlight()
}
} }
func setDoneEnabled(_ enabled: Bool) { func setDoneEnabled(_ enabled: Bool) {

View file

@ -84,6 +84,8 @@ public enum ThemeSettingsEntryTag: ItemListItemTag {
case powerSaving case powerSaving
case stickersAndEmoji case stickersAndEmoji
case animations case animations
case tapForNextMedia
case nightMode
public func isEqual(to other: ItemListItemTag) -> Bool { public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? ThemeSettingsEntryTag, self == other { if let other = other as? ThemeSettingsEntryTag, self == other {
@ -318,7 +320,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
case let .autoNight(_, title, value, enabled): case let .autoNight(_, title, value, enabled):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleNightTheme(value) arguments.toggleNightTheme(value)
}, tag: nil) }, tag: ThemeSettingsEntryTag.nightMode)
case let .autoNightTheme(_, text, value): case let .autoNightTheme(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: nil, title: text, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: nil, title: text, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openAutoNightTheme() arguments.openAutoNightTheme()
@ -352,7 +354,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
case let .showNextMediaOnTap(_, title, value): case let .showNextMediaOnTap(_, title, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleShowNextMediaOnTap(value) arguments.toggleShowNextMediaOnTap(value)
}, tag: ThemeSettingsEntryTag.animations) }, tag: ThemeSettingsEntryTag.tapForNextMedia)
case let .showNextMediaOnTapInfo(_, text): case let .showNextMediaOnTapInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
} }
@ -1309,6 +1311,21 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
presentCrossfadeControllerImpl?(true) presentCrossfadeControllerImpl?(true)
}) })
} }
if let focusOnItemTag {
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
if !didFocusOnItem, let controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) {
didFocusOnItem = true
itemNode.displayHighlight()
}
}
}
}
}
return controller return controller
} }

View file

@ -91,10 +91,10 @@ private final class SheetContent: CombinedComponent {
let closeButton = closeButton.update( let closeButton = closeButton.update(
component: GlassBarButtonComponent( component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0), size: CGSize(width: 44.0, height: 44.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, backgroundColor: nil,
isDark: theme.overallDarkAppearance, isDark: theme.overallDarkAppearance,
state: .generic, state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent( component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent( BundleIconComponent(
name: "Navigation/Close", name: "Navigation/Close",
@ -105,7 +105,7 @@ private final class SheetContent: CombinedComponent {
component.dismiss() component.dismiss()
} }
), ),
availableSize: CGSize(width: 40.0, height: 40.0), availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate transition: .immediate
) )
context.add(closeButton context.add(closeButton

View file

@ -41,6 +41,9 @@ swift_library(
"//submodules/Pasteboard:Pasteboard", "//submodules/Pasteboard:Pasteboard",
"//submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController", "//submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController",
"//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/TelegramUI/Components/EdgeEffect",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View file

@ -23,9 +23,13 @@ import MultiAnimationRenderer
import Pasteboard import Pasteboard
import StickerPackEditTitleController import StickerPackEditTitleController
import EntityKeyboard import EntityKeyboard
//import CameraScreen
import ComponentFlow import ComponentFlow
import EmojiStatusComponent import EmojiStatusComponent
import EdgeEffect
import GlassBarButtonComponent
import BundleIconComponent
import LottieComponent
import ButtonComponent
private let maxStickersCount = 120 private let maxStickersCount = 120
@ -143,18 +147,20 @@ private final class StickerPackContainer: ASDisplayNode {
private let previewIconFile: TelegramMediaFile? private let previewIconFile: TelegramMediaFile?
private var mainPreviewIcon: ComponentView<Empty>? private var mainPreviewIcon: ComponentView<Empty>?
private let gridNode: GridNode private let gridNode: GridNode
private let actionAreaBackgroundNode: NavigationBackgroundNode
private let actionAreaSeparatorNode: ASDisplayNode
private let buttonNode: HighlightableButtonNode private let buttonNode: HighlightableButtonNode
private let titleBackgroundnode: NavigationBackgroundNode
private let titleNode: ImmediateTextNode private let titleNode: ImmediateTextNode
private var titlePlaceholderNode: ShimmerEffectNode? private var titlePlaceholderNode: ShimmerEffectNode?
private let titleContainer: ASDisplayNode private let titleContainer: ASDisplayNode
private let titleSeparatorNode: ASDisplayNode
private let topContainerNode: ASDisplayNode private let topContainerNode: ASDisplayNode
private let cancelButtonNode: HighlightableButtonNode private let bottomContainerNode: ASDisplayNode
private let moreButtonNode: MoreButtonNode
private let topEdgeEffectView = EdgeEffectView()
private let bottomEdgeEffectView = EdgeEffectView()
private let button = ComponentView<Empty>()
private let cancelButton = ComponentView<Empty>()
private let moreButton = ComponentView<Empty>()
private let moreButtonPlayOnce = ActionSlot<Void>()
private(set) var validLayout: (ContainerViewLayout, CGRect, CGFloat, UIEdgeInsets)? private(set) var validLayout: (ContainerViewLayout, CGRect, CGFloat, UIEdgeInsets)?
@ -176,6 +182,8 @@ private final class StickerPackContainer: ASDisplayNode {
return self.isReadyValue.get() return self.isReadyValue.get()
} }
private var itemCount: Int32 = 0
var expandProgress: CGFloat = 0.0 var expandProgress: CGFloat = 0.0
var expandScrollProgress: CGFloat = 0.0 var expandScrollProgress: CGFloat = 0.0
var modalProgress: CGFloat = 0.0 var modalProgress: CGFloat = 0.0
@ -225,7 +233,7 @@ private final class StickerPackContainer: ASDisplayNode {
self.backgroundNode = ASImageNode() self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = true self.backgroundNode.displaysAsynchronously = true
self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 76.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor)
self.previewIconFile = previewIconFile self.previewIconFile = previewIconFile
if self.previewIconFile != nil { if self.previewIconFile != nil {
@ -235,14 +243,9 @@ private final class StickerPackContainer: ASDisplayNode {
self.gridNode = GridNode() self.gridNode = GridNode()
self.gridNode.scrollView.alwaysBounceVertical = true self.gridNode.scrollView.alwaysBounceVertical = true
self.gridNode.scrollView.showsVerticalScrollIndicator = false self.gridNode.scrollView.showsVerticalScrollIndicator = false
self.titleBackgroundnode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor) self.bottomContainerNode = ASDisplayNode()
self.actionAreaBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.tabBar.backgroundColor)
self.actionAreaSeparatorNode = ASDisplayNode()
self.actionAreaSeparatorNode.backgroundColor = self.presentationData.theme.rootController.tabBar.separatorColor
self.buttonNode = HighlightableButtonNode() self.buttonNode = HighlightableButtonNode()
self.titleNode = ImmediateTextNode() self.titleNode = ImmediateTextNode()
self.titleNode.textAlignment = .center self.titleNode.textAlignment = .center
@ -261,13 +264,7 @@ private final class StickerPackContainer: ASDisplayNode {
} }
self.titleContainer = ASDisplayNode() self.titleContainer = ASDisplayNode()
self.titleSeparatorNode = ASDisplayNode()
self.titleSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.topContainerNode = ASDisplayNode() self.topContainerNode = ASDisplayNode()
self.cancelButtonNode = HighlightableButtonNode()
self.moreButtonNode = MoreButtonNode(theme: self.presentationData.theme)
self.moreButtonNode.iconNode.enqueueState(.more, animated: false)
var addStickerPackImpl: ((StickerPackCollectionInfo, [StickerPackItem]) -> Void)? var addStickerPackImpl: ((StickerPackCollectionInfo, [StickerPackItem]) -> Void)?
var removeStickerPackImpl: ((StickerPackCollectionInfo) -> Void)? var removeStickerPackImpl: ((StickerPackCollectionInfo) -> Void)?
@ -290,18 +287,13 @@ private final class StickerPackContainer: ASDisplayNode {
self.addSubnode(self.backgroundNode) self.addSubnode(self.backgroundNode)
self.addSubnode(self.gridNode) self.addSubnode(self.gridNode)
self.addSubnode(self.actionAreaBackgroundNode)
self.addSubnode(self.actionAreaSeparatorNode)
self.addSubnode(self.buttonNode)
self.titleContainer.addSubnode(self.titleNode) self.titleContainer.addSubnode(self.titleNode)
self.addSubnode(self.titleContainer) self.addSubnode(self.titleContainer)
self.addSubnode(self.titleSeparatorNode)
self.addSubnode(self.topContainerNode) self.addSubnode(self.topContainerNode)
self.topContainerNode.addSubnode(self.cancelButtonNode) self.addSubnode(self.bottomContainerNode)
self.topContainerNode.addSubnode(self.moreButtonNode)
self.gridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in self.gridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in
self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition)
} }
@ -329,14 +321,7 @@ private final class StickerPackContainer: ASDisplayNode {
} }
} }
} }
self.gridNode.visibleContentOffsetChanged = { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.updateButtonBackgroundAlpha()
}
self.gridNode.interactiveScrollingWillBeEnded = { [weak self] contentOffset, velocity, targetOffset -> CGPoint in self.gridNode.interactiveScrollingWillBeEnded = { [weak self] contentOffset, velocity, targetOffset -> CGPoint in
guard let strongSelf = self, !strongSelf.isDismissed else { guard let strongSelf = self, !strongSelf.isDismissed else {
return targetOffset return targetOffset
@ -431,27 +416,27 @@ private final class StickerPackContainer: ASDisplayNode {
strongSelf.updateStickerPackContents(contents, hasPremium: hasPremium) strongSelf.updateStickerPackContents(contents, hasPremium: hasPremium)
}) })
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) // self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in // self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self { // if let strongSelf = self {
if highlighted { // if highlighted {
strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity") // strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonNode.alpha = 0.8 // strongSelf.buttonNode.alpha = 0.8
} else { // } else {
strongSelf.buttonNode.alpha = 1.0 // strongSelf.buttonNode.alpha = 1.0
strongSelf.buttonNode.layer.animateAlpha(from: 0.8, to: 1.0, duration: 0.3) // strongSelf.buttonNode.layer.animateAlpha(from: 0.8, to: 1.0, duration: 0.3)
} // }
} // }
} // }
self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) //self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
self.cancelButtonNode.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) //self.cancelButtonNode.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
self.moreButtonNode.action = { [weak self] _, gesture in // self.moreButtonNode.action = { [weak self] _, gesture in
if let strongSelf = self { // if let strongSelf = self {
strongSelf.morePressed(node: strongSelf.moreButtonNode.contextSourceNode, gesture: gesture) // strongSelf.morePressed(node: strongSelf.moreButtonNode.contextSourceNode, gesture: gesture)
} // }
} // }
self.titleNode.linkHighlightColor = self.presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.2) self.titleNode.linkHighlightColor = self.presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.2)
@ -510,7 +495,7 @@ private final class StickerPackContainer: ASDisplayNode {
private var reorderingGestureRecognizer: ReorderingGestureRecognizer? private var reorderingGestureRecognizer: ReorderingGestureRecognizer?
override func didLoad() { override func didLoad() {
super.didLoad() super.didLoad()
let peekGestureRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>? in let peekGestureRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>? in
if let strongSelf = self { if let strongSelf = self {
if let itemNode = strongSelf.gridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem { if let itemNode = strongSelf.gridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem {
@ -690,6 +675,9 @@ private final class StickerPackContainer: ASDisplayNode {
reorderingGestureRecognizer.isEnabled = self.isEditing reorderingGestureRecognizer.isEnabled = self.isEditing
self.reorderingGestureRecognizer = reorderingGestureRecognizer self.reorderingGestureRecognizer = reorderingGestureRecognizer
self.gridNode.view.addGestureRecognizer(reorderingGestureRecognizer) self.gridNode.view.addGestureRecognizer(reorderingGestureRecognizer)
self.topContainerNode.view.addSubview(self.topEdgeEffectView)
self.bottomContainerNode.view.addSubview(self.bottomEdgeEffectView)
} }
private func hideMainPreviewIcon() { private func hideMainPreviewIcon() {
@ -1005,15 +993,7 @@ private final class StickerPackContainer: ASDisplayNode {
func updatePresentationData(_ presentationData: PresentationData) { func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData self.presentationData = presentationData
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 76.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor)
self.titleBackgroundnode.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.actionAreaBackgroundNode.updateColor(color: self.presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate)
self.actionAreaSeparatorNode.backgroundColor = self.presentationData.theme.rootController.tabBar.separatorColor
self.titleSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
self.moreButtonNode.theme = self.presentationData.theme
self.titleNode.linkHighlightColor = self.presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.5) self.titleNode.linkHighlightColor = self.presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.5)
@ -1056,7 +1036,7 @@ private final class StickerPackContainer: ASDisplayNode {
self.titleNode.attributedText = stringWithAppliedEntities(title, entities: entities, baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: titleFont, linkFont: titleFont, boldFont: titleFont, italicFont: titleFont, boldItalicFont: titleFont, fixedFont: titleFont, blockQuoteFont: titleFont, message: nil) self.titleNode.attributedText = stringWithAppliedEntities(title, entities: entities, baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: titleFont, linkFont: titleFont, boldFont: titleFont, italicFont: titleFont, boldItalicFont: titleFont, fixedFont: titleFont, blockQuoteFont: titleFont, message: nil)
if let (layout, _, _, _) = self.validLayout { if let (layout, _, _, _) = self.validLayout {
let _ = self.titleNode.updateLayout(CGSize(width: layout.size.width - max(12.0, self.cancelButtonNode.frame.width) * 2.0 - 40.0, height: .greatestFiniteMagnitude)) let _ = self.titleNode.updateLayout(CGSize(width: layout.size.width - 44.0 * 2.0 - 40.0, height: .greatestFiniteMagnitude))
self.updateLayout(layout: layout, transition: .immediate) self.updateLayout(layout: layout, transition: .immediate)
} }
} }
@ -1084,7 +1064,6 @@ private final class StickerPackContainer: ASDisplayNode {
private func updateIsEditing(_ isEditing: Bool) { private func updateIsEditing(_ isEditing: Bool) {
self.isEditing = isEditing self.isEditing = isEditing
self.updateEntries(reload: true) self.updateEntries(reload: true)
self.updateButton()
self.peekGestureRecognizer?.longPressEnabled = !isEditing self.peekGestureRecognizer?.longPressEnabled = !isEditing
self.reorderingGestureRecognizer?.isEnabled = isEditing self.reorderingGestureRecognizer?.isEnabled = isEditing
if let (layout, _, _, _) = self.validLayout { if let (layout, _, _, _) = self.validLayout {
@ -1096,7 +1075,7 @@ private final class StickerPackContainer: ASDisplayNode {
} }
} }
@objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) { @objc private func morePressed(view: UIView, gesture: ContextGesture?) {
guard let controller = self.controller else { guard let controller = self.controller else {
return return
} }
@ -1228,7 +1207,13 @@ private final class StickerPackContainer: ASDisplayNode {
}))) })))
} }
let contextController = makeContextController(presentationData: self.presentationData, source: .reference(StickerPackContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) let contextController = makeContextController(
presentationData: presentationData,
source: .reference(StickerPackContextReferenceContentSource(controller: controller, sourceView: view)),
items: .single(ContextController.Items(content: .list(items))),
recognizer: nil,
gesture: gesture
)
self.presentInGlobalOverlay(contextController, nil) self.presentInGlobalOverlay(contextController, nil)
} }
@ -1562,104 +1547,6 @@ private final class StickerPackContainer: ASDisplayNode {
} }
} }
private func updateButtonBackgroundAlpha() {
let offset = self.gridNode.visibleContentOffset()
let backgroundAlpha: CGFloat
switch offset {
case .known:
let topPosition = self.view.convert(self.topContainerNode.frame, to: self.view).minY
let bottomPosition = self.actionAreaBackgroundNode.view.convert(self.actionAreaBackgroundNode.bounds, to: self.view).minY
let bottomEdgePosition = topPosition + self.topContainerNode.frame.height + self.gridNode.scrollView.contentSize.height
let bottomOffset = bottomPosition - bottomEdgePosition
backgroundAlpha = min(10.0, max(0.0, -1.0 * bottomOffset)) / 10.0
case .unknown, .none:
backgroundAlpha = 1.0
}
let transition: ContainedViewLayoutTransition
var delay: Double = 0.0
if backgroundAlpha >= self.actionAreaBackgroundNode.alpha || abs(backgroundAlpha - self.actionAreaBackgroundNode.alpha) < 0.01 {
transition = .immediate
} else {
transition = .animated(duration: 0.2, curve: .linear)
if abs(backgroundAlpha - self.actionAreaBackgroundNode.alpha) > 0.9 {
delay = 0.2
}
}
transition.updateAlpha(node: self.actionAreaBackgroundNode, alpha: backgroundAlpha, delay: delay)
transition.updateAlpha(node: self.actionAreaSeparatorNode, alpha: backgroundAlpha, delay: delay)
}
private func updateButton(count: Int32 = 0) {
if let currentContents = self.currentContents, currentContents.count == 1, let content = currentContents.first, case let .result(info, _, installed) = content {
if installed {
let text: String
if info.flags.contains(.isCreator) && !info.flags.contains(.isEmoji) {
if self.isEditing {
var updated = false
if let current = self.buttonNode.attributedTitle(for: .normal)?.string, !current.isEmpty && current != self.presentationData.strings.Common_Done {
updated = true
}
if updated, let snapshotView = self.buttonNode.view.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = self.buttonNode.view.frame
self.buttonNode.view.superview?.insertSubview(snapshotView, belowSubview: self.buttonNode.view)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
self.buttonNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
self.buttonNode.setTitle(self.presentationData.strings.Common_Done, with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal)
self.buttonNode.setBackgroundImage(generateStretchableFilledCircleImage(radius: 11, color: self.presentationData.theme.list.itemCheckColors.fillColor), for: [])
} else {
let buttonTitle = self.presentationData.strings.StickerPack_EditStickers
var updated = false
if let current = self.buttonNode.attributedTitle(for: .normal)?.string, !current.isEmpty && current != buttonTitle {
updated = true
}
if updated, let snapshotView = self.buttonNode.view.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = self.buttonNode.view.frame
self.buttonNode.view.superview?.insertSubview(snapshotView, belowSubview: self.buttonNode.view)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
self.buttonNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
text = buttonTitle
self.buttonNode.setTitle(text, with: Font.regular(17.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal)
self.buttonNode.setBackgroundImage(nil, for: [])
}
} else {
if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
text = self.presentationData.strings.StickerPack_RemoveStickerCount(count)
} else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks {
text = self.presentationData.strings.StickerPack_RemoveEmojiCount(count)
} else {
text = self.presentationData.strings.StickerPack_RemoveMaskCount(count)
}
self.buttonNode.setTitle(text, with: Font.regular(17.0), with: self.presentationData.theme.list.itemDestructiveColor, for: .normal)
self.buttonNode.setBackgroundImage(nil, for: [])
}
} else {
let text: String
if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
text = self.presentationData.strings.StickerPack_AddStickerCount(count)
} else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks {
text = self.presentationData.strings.StickerPack_AddEmojiCount(count)
} else {
text = self.presentationData.strings.StickerPack_AddMaskCount(count)
}
self.buttonNode.setTitle(text, with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal)
self.buttonNode.setBackgroundImage(generateStretchableFilledCircleImage(radius: 11, color: self.presentationData.theme.list.itemCheckColors.fillColor), for: [])
}
}
}
private func updateStickerPackContents(_ contents: [LoadedStickerPack], hasPremium: Bool) { private func updateStickerPackContents(_ contents: [LoadedStickerPack], hasPremium: Bool) {
self.currentContents = contents self.currentContents = contents
self.didReceiveStickerPackResult = true self.didReceiveStickerPackResult = true
@ -1864,7 +1751,8 @@ private final class StickerPackContainer: ASDisplayNode {
} else { } else {
count = Int32(entries.count) count = Int32(entries.count)
} }
self.updateButton(count: count) self.itemCount = count
updateLayout = true
} }
if info.flags.contains(.isCreator) && !info.flags.contains(.isEmoji) && entries.count < maxStickersCount { if info.flags.contains(.isCreator) && !info.flags.contains(.isEmoji) && entries.count < maxStickersCount {
@ -2032,43 +1920,15 @@ private final class StickerPackContainer: ASDisplayNode {
insets.top += 10.0 insets.top += 10.0
} }
var buttonHeight: CGFloat = 50.0 let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: layout.intrinsicInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
var actionAreaTopInset: CGFloat = 8.0 let titleAreaInset: CGFloat = 76.0
var actionAreaBottomInset: CGFloat = 16.0 let buttonHeight: CGFloat = 52.0
if let _ = self.controller?.mainActionTitle { let buttonSideInset: CGFloat = 30.0
let actionAreaHeight: CGFloat = buttonHeight + buttonInsets.bottom
} else {
if !self.currentStickerPacks.isEmpty {
var installedCount = 0
for (_, _, isInstalled) in self.currentStickerPacks {
if isInstalled {
installedCount += 1
}
}
if installedCount == self.currentStickerPacks.count {
buttonHeight = 42.0
actionAreaTopInset = 1.0
actionAreaBottomInset = 2.0
}
}
if let (info, _, isInstalled) = self.currentStickerPack, isInstalled, (!info.flags.contains(.isCreator) || info.flags.contains(.isEmoji)) {
buttonHeight = 42.0
actionAreaTopInset = 1.0
actionAreaBottomInset = 2.0
}
}
let buttonSideInset: CGFloat = 16.0 transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonSideInset, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width - buttonSideInset * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: buttonHeight)))
let titleAreaInset: CGFloat = 56.0
var actionAreaHeight: CGFloat = buttonHeight
actionAreaHeight += insets.bottom + actionAreaBottomInset
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonSideInset, y: layout.size.height - actionAreaHeight + actionAreaTopInset), size: CGSize(width: layout.size.width - buttonSideInset * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: buttonHeight)))
transition.updateFrame(node: self.actionAreaBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: actionAreaHeight))) transition.updateFrame(node: self.bottomContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: 90.0)))
self.actionAreaBackgroundNode.update(size: CGSize(width: layout.size.width, height: actionAreaHeight), transition: .immediate)
transition.updateFrame(node: self.actionAreaSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
let gridFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top + titleAreaInset), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - titleAreaInset)) let gridFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top + titleAreaInset), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - titleAreaInset))
@ -2131,17 +1991,171 @@ private final class StickerPackContainer: ASDisplayNode {
titlePlaceholderNode.updateAbsoluteRect(titlePlaceholderNode.frame.offsetBy(dx: self.titleContainer.frame.minX, dy: self.titleContainer.frame.minY - gridInsets.top - gridFrame.minY), within: gridFrame.size) titlePlaceholderNode.updateAbsoluteRect(titlePlaceholderNode.frame.offsetBy(dx: self.titleContainer.frame.minX, dy: self.titleContainer.frame.minY - gridInsets.top - gridFrame.minY), within: gridFrame.size)
} }
let cancelSize = self.cancelButtonNode.measure(CGSize(width: layout.size.width, height: .greatestFiniteMagnitude)) //let cancelSize = self.cancelButtonNode.measure(CGSize(width: layout.size.width, height: .greatestFiniteMagnitude))
self.cancelButtonNode.frame = CGRect(origin: CGPoint(x: layout.safeInsets.left + 16.0, y: 18.0), size: cancelSize) //self.cancelButtonNode.frame = CGRect(origin: CGPoint(x: layout.safeInsets.left + 16.0, y: 18.0), size: cancelSize)
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - cancelSize.width * 2.0 - 40.0, height: .greatestFiniteMagnitude)) let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - 44.0 * 2.0 - 40.0, height: .greatestFiniteMagnitude))
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((-titleSize.width) / 2.0), y: floor((-titleSize.height) / 2.0)), size: titleSize) self.titleNode.frame = CGRect(origin: CGPoint(x: floor((-titleSize.width) / 2.0), y: floor((-titleSize.height) / 2.0)), size: titleSize)
self.moreButtonNode.frame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - 46.0, y: 5.0), size: CGSize(width: 44.0, height: 44.0)) //self.moreButtonNode.frame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - 46.0, y: 5.0), size: CGSize(width: 44.0, height: 44.0))
transition.updateAlpha(node: self.cancelButtonNode, alpha: self.isEditing ? 0.0 : 1.0) //transition.updateAlpha(node: self.cancelButtonNode, alpha: self.isEditing ? 0.0 : 1.0)
transition.updateAlpha(node: self.moreButtonNode, alpha: self.isEditing ? 0.0 : 1.0) //transition.updateAlpha(node: self.moreButtonNode, alpha: self.isEditing ? 0.0 : 1.0)
let cancelButtonSize = self.cancelButton.update(
transition: .immediate,
component: AnyComponent(GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: false,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: self.presentationData.theme.chat.inputPanel.panelControlColor
)
)),
action: { [weak self] _ in
guard let self else {
return
}
self.cancelPressed()
}
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0),
)
let cancelButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelButtonSize)
if let cancelButtonView = self.cancelButton.view {
if cancelButtonView.superview == nil {
self.topContainerNode.view.addSubview(cancelButtonView)
}
transition.updateFrame(view: cancelButtonView, frame: cancelButtonFrame)
transition.updateAlpha(layer: cancelButtonView.layer, alpha: self.isEditing ? 0.0 : 1.0)
}
let moreButtonSize = self.moreButton.update(
transition: .immediate,
component: AnyComponent(GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: false,
state: .glass,
component: AnyComponentWithIdentity(id: "more", component: AnyComponent(
LottieComponent(
content: LottieComponent.AppBundleContent(
name: "anim_morewide"
),
color: self.presentationData.theme.chat.inputPanel.panelControlColor,
size: CGSize(width: 34.0, height: 34.0),
playOnce: self.moreButtonPlayOnce
)
)),
action: { [weak self] view in
guard let self else {
return
}
self.morePressed(view: view, gesture: nil)
self.moreButtonPlayOnce.invoke(Void())
}
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0),
)
let moreButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - 16.0 - moreButtonSize.width, y: 16.0), size: moreButtonSize)
if let moreButtonView = self.moreButton.view {
if moreButtonView.superview == nil {
self.topContainerNode.view.addSubview(moreButtonView)
}
transition.updateFrame(view: moreButtonView, frame: moreButtonFrame)
transition.updateAlpha(layer: moreButtonView.layer, alpha: self.isEditing ? 0.0 : 1.0)
}
var buttonTitle: String = ""
var buttonForegroundColor = self.presentationData.theme.list.itemCheckColors.foregroundColor
var buttonBackgroundColor = self.presentationData.theme.list.itemCheckColors.fillColor
if let currentContents = self.currentContents, currentContents.count == 1, let content = currentContents.first, case let .result(info, _, installed) = content {
if installed {
if info.flags.contains(.isCreator) && !info.flags.contains(.isEmoji) {
if self.isEditing {
var updated = false
if let current = self.buttonNode.attributedTitle(for: .normal)?.string, !current.isEmpty && current != self.presentationData.strings.Common_Done {
updated = true
}
if updated, let snapshotView = self.buttonNode.view.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = self.buttonNode.view.frame
self.buttonNode.view.superview?.insertSubview(snapshotView, belowSubview: self.buttonNode.view)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
self.buttonNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
buttonTitle = self.presentationData.strings.Common_Done
} else {
buttonTitle = self.presentationData.strings.StickerPack_EditStickers
buttonBackgroundColor = self.presentationData.theme.list.plainBackgroundColor
buttonBackgroundColor = self.presentationData.theme.list.itemAccentColor
}
} else {
if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
buttonTitle = self.presentationData.strings.StickerPack_RemoveStickerCount(self.itemCount)
} else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks {
buttonTitle = self.presentationData.strings.StickerPack_RemoveEmojiCount(self.itemCount)
} else {
buttonTitle = self.presentationData.strings.StickerPack_RemoveMaskCount(self.itemCount)
}
buttonBackgroundColor = self.presentationData.theme.list.plainBackgroundColor
buttonForegroundColor = self.presentationData.theme.list.itemDestructiveColor
}
} else {
if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
buttonTitle = self.presentationData.strings.StickerPack_AddStickerCount(self.itemCount)
} else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks {
buttonTitle = self.presentationData.strings.StickerPack_AddEmojiCount(self.itemCount)
} else {
buttonTitle = self.presentationData.strings.StickerPack_AddMaskCount(self.itemCount)
}
}
}
let buttonSize = self.button.update(
transition: .immediate,
component: AnyComponent(
ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: buttonBackgroundColor,
foreground: buttonForegroundColor,
pressedColor: buttonBackgroundColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(Text(text: buttonTitle, font: Font.semibold(17.0), color: buttonForegroundColor))
),
action: { [weak self] in
self?.buttonPressed()
}
)
),
environment: {},
containerSize: CGSize(width: layout.size.width - buttonInsets.left - buttonInsets.right, height: 52.0)
)
let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: 0.0), size: buttonSize)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.bottomContainerNode.view.addSubview(buttonView)
}
buttonView.frame = buttonFrame
}
let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: actionAreaHeight - 90.0), size: CGSize(width: layout.size.width, height: 90.0))
transition.updateFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame)
self.bottomEdgeEffectView.update(content: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 0.65, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition))
if firstTime { if firstTime {
while !self.enqueuedTransactions.isEmpty { while !self.enqueuedTransactions.isEmpty {
@ -2194,7 +2208,6 @@ private final class StickerPackContainer: ASDisplayNode {
if !transition.isAnimated { if !transition.isAnimated {
self.backgroundNode.layer.removeAllAnimations() self.backgroundNode.layer.removeAllAnimations()
self.titleContainer.layer.removeAllAnimations() self.titleContainer.layer.removeAllAnimations()
self.titleSeparatorNode.layer.removeAllAnimations()
} }
var backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: max(minBackgroundY, unclippedBackgroundY)), size: CGSize(width: layout.size.width, height: layout.size.height)) var backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: max(minBackgroundY, unclippedBackgroundY)), size: CGSize(width: layout.size.width, height: layout.size.height))
@ -2203,7 +2216,7 @@ private final class StickerPackContainer: ASDisplayNode {
backgroundFrame.origin.y = min(0.0, backgroundFrame.origin.y) backgroundFrame.origin.y = min(0.0, backgroundFrame.origin.y)
titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: floor((56.0) / 2.0)), size: CGSize()) titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: floor((56.0) / 2.0)), size: CGSize())
} else { } else {
titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: backgroundFrame.minY + floor((56.0) / 2.0)), size: CGSize()) titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: backgroundFrame.minY + floor((56.0) / 2.0) + 10.0), size: CGSize())
} }
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
@ -2239,14 +2252,8 @@ private final class StickerPackContainer: ASDisplayNode {
} }
transition.updateFrame(node: self.titleContainer, frame: titleContainerFrame) transition.updateFrame(node: self.titleContainer, frame: titleContainerFrame)
transition.updateFrame(node: self.titleSeparatorNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY + 56.0 - UIScreenPixel), size: CGSize(width: backgroundFrame.width, height: UIScreenPixel)))
transition.updateFrame(node: self.titleBackgroundnode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.width, height: 56.0)))
self.titleBackgroundnode.update(size: CGSize(width: layout.size.width, height: 56.0), transition: .immediate)
transition.updateFrame(node: self.topContainerNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.width, height: 56.0))) transition.updateFrame(node: self.topContainerNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.width, height: 76.0)))
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
transition.updateAlpha(node: self.titleSeparatorNode, alpha: unclippedBackgroundY < minBackgroundY ? 1.0 : 0.0)
} }
private func enqueueTransaction(_ transaction: StickerPackPreviewGridTransaction) { private func enqueueTransaction(_ transaction: StickerPackPreviewGridTransaction) {
@ -2423,7 +2430,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
let containerInsets: UIEdgeInsets let containerInsets: UIEdgeInsets
if case .regular = layout.metrics.widthClass { if case .regular = layout.metrics.widthClass {
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.01) self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.01)
self.containerContainingNode.cornerRadius = 10.0 self.containerContainingNode.cornerRadius = 38.0
let size = CGSize(width: 390.0, height: min(560.0, layout.size.height - 60.0)) let size = CGSize(width: 390.0, height: min(560.0, layout.size.height - 60.0))
var contentRect: CGRect var contentRect: CGRect
@ -2476,8 +2483,15 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
let shadowFrame = containerContainingFrame.insetBy(dx: -60.0, dy: -60.0) let shadowFrame = containerContainingFrame.insetBy(dx: -60.0, dy: -60.0)
transition.updateFrame(node: self.shadowNode, frame: shadowFrame) transition.updateFrame(node: self.shadowNode, frame: shadowFrame)
let expandProgress: CGFloat = 1.0 let expandProgress: CGFloat
let scaledInset: CGFloat = 12.0 if case .regular = layout.metrics.widthClass {
expandProgress = 1.0
} else if let selectedContainer = self.containers[self.selectedStickerPackIndex] {
expandProgress = selectedContainer.expandProgress
} else {
expandProgress = 0.0
}
let scaledInset: CGFloat = 6.0
let scaledDistance: CGFloat = 4.0 let scaledDistance: CGFloat = 4.0
let minScale = (layout.size.width - scaledInset * 2.0) / layout.size.width let minScale = (layout.size.width - scaledInset * 2.0) / layout.size.width
let containerScale = expandProgress * 1.0 + (1.0 - expandProgress) * minScale let containerScale = expandProgress * 1.0 + (1.0 - expandProgress) * minScale
@ -3101,15 +3115,15 @@ public func StickerPackScreen(
private final class StickerPackContextReferenceContentSource: ContextReferenceContentSource { private final class StickerPackContextReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController private let controller: ViewController
private let sourceNode: ContextReferenceContentNode private let sourceView: UIView
init(controller: ViewController, sourceNode: ContextReferenceContentNode) { init(controller: ViewController, sourceView: UIView) {
self.controller = controller self.controller = controller
self.sourceNode = sourceNode self.sourceView = sourceView
} }
func transitionInfo() -> ContextControllerReferenceViewInfo? { func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
} }
} }

View file

@ -185,6 +185,13 @@ public enum MessageActionUrlAuthResult {
public let platform: String public let platform: String
public let ip: String public let ip: String
public let region: String public let region: String
public init(browser: String, platform: String, ip: String, region: String) {
self.browser = browser
self.platform = platform
self.ip = ip
self.region = region
}
} }
case `default` case `default`

View file

@ -96,7 +96,7 @@ public extension TelegramEngine {
} }
public func requestMessageActionUrlAuth(subject: MessageActionUrlSubject) -> Signal<MessageActionUrlAuthResult, NoError> { public func requestMessageActionUrlAuth(subject: MessageActionUrlSubject) -> Signal<MessageActionUrlAuthResult, NoError> {
_internal_requestMessageActionUrlAuth(account: self.account, subject: subject) return _internal_requestMessageActionUrlAuth(account: self.account, subject: subject)
} }
public func acceptMessageActionUrlAuth(subject: MessageActionUrlSubject, allowWriteAccess: Bool, sharePhoneNumber: Bool) -> Signal<MessageActionUrlAuthResult, NoError> { public func acceptMessageActionUrlAuth(subject: MessageActionUrlSubject, allowWriteAccess: Bool, sharePhoneNumber: Bool) -> Signal<MessageActionUrlAuthResult, NoError> {

View file

@ -3186,6 +3186,12 @@ private final class CraftGiftsContextImpl {
return _internal_craftStarGift(account: self.account, references: references) return _internal_craftStarGift(account: self.account, references: references)
} }
func addGift(gift: ProfileGiftsContext.State.StarGift) {
self.gifts.insert(gift, at: 0)
self.count = self.count + 1
self.pushState()
}
func removeGifts(references: [StarGiftReference]) { func removeGifts(references: [StarGiftReference]) {
let referencesSet = Set(references) let referencesSet = Set(references)
self.gifts.removeAll { gift in self.gifts.removeAll { gift in
@ -3318,6 +3324,12 @@ public final class CraftGiftsContext {
impl.reload() impl.reload()
} }
} }
public func addGift(gift: ProfileGiftsContext.State.StarGift) {
self.impl.with { impl in
impl.addGift(gift: gift)
}
}
public func removeGifts(references: [StarGiftReference]) { public func removeGifts(references: [StarGiftReference]) {
self.impl.with { impl in self.impl.with { impl in
@ -3838,6 +3850,7 @@ private final class ResaleGiftsContextImpl {
private let queue: Queue private let queue: Queue
private let account: Account private let account: Account
private let giftId: Int64 private let giftId: Int64
private let forCrafting: Bool
private let disposable = MetaDisposable() private let disposable = MetaDisposable()
@ -3861,11 +3874,13 @@ private final class ResaleGiftsContextImpl {
init( init(
queue: Queue, queue: Queue,
account: Account, account: Account,
giftId: Int64 giftId: Int64,
forCrafting: Bool
) { ) {
self.queue = queue self.queue = queue
self.account = account self.account = account
self.giftId = giftId self.giftId = giftId
self.forCrafting = forCrafting
self.loadMore() self.loadMore()
} }
@ -3898,6 +3913,10 @@ private final class ResaleGiftsContextImpl {
} }
var flags: Int32 = 0 var flags: Int32 = 0
if self.forCrafting {
flags |= (1 << 4)
}
switch sorting { switch sorting {
case .date: case .date:
break break
@ -4169,11 +4188,12 @@ public final class ResaleGiftsContext {
public init( public init(
account: Account, account: Account,
giftId: Int64 giftId: Int64,
forCrafting: Bool
) { ) {
let queue = self.queue let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: { self.impl = QueueLocalObject(queue: queue, generate: {
return ResaleGiftsContextImpl(queue: queue, account: account, giftId: giftId) return ResaleGiftsContextImpl(queue: queue, account: account, giftId: giftId, forCrafting: forCrafting)
}) })
} }

View file

@ -205,6 +205,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
case voiceMessagesResumeTrimWarning = 82 case voiceMessagesResumeTrimWarning = 82
case globalPostsSearch = 83 case globalPostsSearch = 83
case giftAuctionTips = 84 case giftAuctionTips = 84
case giftCraftingTips = 85
var key: ValueBoxKey { var key: ValueBoxKey {
let v = ValueBoxKey(length: 4) let v = ValueBoxKey(length: 4)
@ -574,6 +575,10 @@ private struct ApplicationSpecificNoticeKeys {
static func giftAuctionTips() -> NoticeEntryKey { static func giftAuctionTips() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.giftAuctionTips.key) return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.giftAuctionTips.key)
} }
static func giftCraftingTips() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.giftCraftingTips.key)
}
} }
public struct ApplicationSpecificNotice { public struct ApplicationSpecificNotice {
@ -2514,4 +2519,31 @@ public struct ApplicationSpecificNotice {
return Int(previousValue) return Int(previousValue)
} }
} }
public static func getGiftCraftingTips(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Int32, NoError> {
return accountManager.transaction { transaction -> Int32 in
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.giftCraftingTips())?.get(ApplicationSpecificCounterNotice.self) {
return value.value
} else {
return 0
}
}
}
public static func incrementGiftCraftingTips(accountManager: AccountManager<TelegramAccountManagerTypes>, count: Int = 1) -> Signal<Int, NoError> {
return accountManager.transaction { transaction -> Int in
var currentValue: Int32 = 0
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.giftCraftingTips())?.get(ApplicationSpecificCounterNotice.self) {
currentValue = value.value
}
let previousValue = currentValue
currentValue += Int32(count)
if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) {
transaction.setNotice(ApplicationSpecificNoticeKeys.giftCraftingTips(), entry)
}
return Int(previousValue)
}
}
} }

View file

@ -555,6 +555,10 @@ public final class PresentationThemeList {
public func withUpdated(blocksBackgroundColor: UIColor? = nil, modalBlocksBackgroundColor: UIColor? = nil, plainBackgroundColor: UIColor? = nil, modalPlainBackgroundColor: UIColor? = nil, itemPrimaryTextColor: UIColor? = nil, itemSecondaryTextColor: UIColor? = nil, itemDisabledTextColor: UIColor? = nil, itemAccentColor: UIColor? = nil, itemHighlightedColor: UIColor? = nil, itemDestructiveColor: UIColor? = nil, itemPlaceholderTextColor: UIColor? = nil, itemBlocksBackgroundColor: UIColor? = nil, itemModalBlocksBackgroundColor: UIColor? = nil, itemHighlightedBackgroundColor: UIColor? = nil, itemBlocksSeparatorColor: UIColor? = nil, itemPlainSeparatorColor: UIColor? = nil, disclosureArrowColor: UIColor? = nil, sectionHeaderTextColor: UIColor? = nil, freeTextColor: UIColor? = nil, freeTextErrorColor: UIColor? = nil, freeTextSuccessColor: UIColor? = nil, freeMonoIconColor: UIColor? = nil, itemSwitchColors: PresentationThemeSwitch? = nil, itemDisclosureActions: PresentationThemeItemDisclosureActions? = nil, itemCheckColors: PresentationThemeFillStrokeForeground? = nil, controlSecondaryColor: UIColor? = nil, freeInputField: PresentationInputFieldTheme? = nil, freePlainInputField: PresentationInputFieldTheme? = nil, mediaPlaceholderColor: UIColor? = nil, scrollIndicatorColor: UIColor? = nil, pageIndicatorInactiveColor: UIColor? = nil, inputClearButtonColor: UIColor? = nil, itemBarChart: PresentationThemeItemBarChart? = nil, itemInputField: PresentationInputFieldTheme? = nil, paymentOption: PaymentOption? = nil) -> PresentationThemeList { public func withUpdated(blocksBackgroundColor: UIColor? = nil, modalBlocksBackgroundColor: UIColor? = nil, plainBackgroundColor: UIColor? = nil, modalPlainBackgroundColor: UIColor? = nil, itemPrimaryTextColor: UIColor? = nil, itemSecondaryTextColor: UIColor? = nil, itemDisabledTextColor: UIColor? = nil, itemAccentColor: UIColor? = nil, itemHighlightedColor: UIColor? = nil, itemDestructiveColor: UIColor? = nil, itemPlaceholderTextColor: UIColor? = nil, itemBlocksBackgroundColor: UIColor? = nil, itemModalBlocksBackgroundColor: UIColor? = nil, itemHighlightedBackgroundColor: UIColor? = nil, itemBlocksSeparatorColor: UIColor? = nil, itemPlainSeparatorColor: UIColor? = nil, disclosureArrowColor: UIColor? = nil, sectionHeaderTextColor: UIColor? = nil, freeTextColor: UIColor? = nil, freeTextErrorColor: UIColor? = nil, freeTextSuccessColor: UIColor? = nil, freeMonoIconColor: UIColor? = nil, itemSwitchColors: PresentationThemeSwitch? = nil, itemDisclosureActions: PresentationThemeItemDisclosureActions? = nil, itemCheckColors: PresentationThemeFillStrokeForeground? = nil, controlSecondaryColor: UIColor? = nil, freeInputField: PresentationInputFieldTheme? = nil, freePlainInputField: PresentationInputFieldTheme? = nil, mediaPlaceholderColor: UIColor? = nil, scrollIndicatorColor: UIColor? = nil, pageIndicatorInactiveColor: UIColor? = nil, inputClearButtonColor: UIColor? = nil, itemBarChart: PresentationThemeItemBarChart? = nil, itemInputField: PresentationInputFieldTheme? = nil, paymentOption: PaymentOption? = nil) -> PresentationThemeList {
return PresentationThemeList(blocksBackgroundColor: blocksBackgroundColor ?? self.blocksBackgroundColor, modalBlocksBackgroundColor: modalBlocksBackgroundColor ?? self.modalBlocksBackgroundColor, plainBackgroundColor: plainBackgroundColor ?? self.plainBackgroundColor, modalPlainBackgroundColor: modalPlainBackgroundColor ?? self.modalPlainBackgroundColor, itemPrimaryTextColor: itemPrimaryTextColor ?? self.itemPrimaryTextColor, itemSecondaryTextColor: itemSecondaryTextColor ?? self.itemSecondaryTextColor, itemDisabledTextColor: itemDisabledTextColor ?? self.itemDisabledTextColor, itemAccentColor: itemAccentColor ?? self.itemAccentColor, itemHighlightedColor: itemHighlightedColor ?? self.itemHighlightedColor, itemDestructiveColor: itemDestructiveColor ?? self.itemDestructiveColor, itemPlaceholderTextColor: itemPlaceholderTextColor ?? self.itemPlaceholderTextColor, itemBlocksBackgroundColor: itemBlocksBackgroundColor ?? self.itemBlocksBackgroundColor, itemModalBlocksBackgroundColor: itemModalBlocksBackgroundColor ?? self.itemModalBlocksBackgroundColor, itemHighlightedBackgroundColor: itemHighlightedBackgroundColor ?? self.itemHighlightedBackgroundColor, itemBlocksSeparatorColor: itemBlocksSeparatorColor ?? self.itemBlocksSeparatorColor, itemPlainSeparatorColor: itemPlainSeparatorColor ?? self.itemPlainSeparatorColor, disclosureArrowColor: disclosureArrowColor ?? self.disclosureArrowColor, sectionHeaderTextColor: sectionHeaderTextColor ?? self.sectionHeaderTextColor, freeTextColor: freeTextColor ?? self.freeTextColor, freeTextErrorColor: freeTextErrorColor ?? self.freeTextErrorColor, freeTextSuccessColor: freeTextSuccessColor ?? self.freeTextSuccessColor, freeMonoIconColor: freeMonoIconColor ?? self.freeMonoIconColor, itemSwitchColors: itemSwitchColors ?? self.itemSwitchColors, itemDisclosureActions: itemDisclosureActions ?? self.itemDisclosureActions, itemCheckColors: itemCheckColors ?? self.itemCheckColors, controlSecondaryColor: controlSecondaryColor ?? self.controlSecondaryColor, freeInputField: freeInputField ?? self.freeInputField, freePlainInputField: freePlainInputField ?? self.freePlainInputField, mediaPlaceholderColor: mediaPlaceholderColor ?? self.mediaPlaceholderColor, scrollIndicatorColor: scrollIndicatorColor ?? self.scrollIndicatorColor, pageIndicatorInactiveColor: pageIndicatorInactiveColor ?? self.pageIndicatorInactiveColor, inputClearButtonColor: inputClearButtonColor ?? self.inputClearButtonColor, itemBarChart: itemBarChart ?? self.itemBarChart, itemInputField: itemInputField ?? self.itemInputField, paymentOption: paymentOption ?? self.paymentOption) return PresentationThemeList(blocksBackgroundColor: blocksBackgroundColor ?? self.blocksBackgroundColor, modalBlocksBackgroundColor: modalBlocksBackgroundColor ?? self.modalBlocksBackgroundColor, plainBackgroundColor: plainBackgroundColor ?? self.plainBackgroundColor, modalPlainBackgroundColor: modalPlainBackgroundColor ?? self.modalPlainBackgroundColor, itemPrimaryTextColor: itemPrimaryTextColor ?? self.itemPrimaryTextColor, itemSecondaryTextColor: itemSecondaryTextColor ?? self.itemSecondaryTextColor, itemDisabledTextColor: itemDisabledTextColor ?? self.itemDisabledTextColor, itemAccentColor: itemAccentColor ?? self.itemAccentColor, itemHighlightedColor: itemHighlightedColor ?? self.itemHighlightedColor, itemDestructiveColor: itemDestructiveColor ?? self.itemDestructiveColor, itemPlaceholderTextColor: itemPlaceholderTextColor ?? self.itemPlaceholderTextColor, itemBlocksBackgroundColor: itemBlocksBackgroundColor ?? self.itemBlocksBackgroundColor, itemModalBlocksBackgroundColor: itemModalBlocksBackgroundColor ?? self.itemModalBlocksBackgroundColor, itemHighlightedBackgroundColor: itemHighlightedBackgroundColor ?? self.itemHighlightedBackgroundColor, itemBlocksSeparatorColor: itemBlocksSeparatorColor ?? self.itemBlocksSeparatorColor, itemPlainSeparatorColor: itemPlainSeparatorColor ?? self.itemPlainSeparatorColor, disclosureArrowColor: disclosureArrowColor ?? self.disclosureArrowColor, sectionHeaderTextColor: sectionHeaderTextColor ?? self.sectionHeaderTextColor, freeTextColor: freeTextColor ?? self.freeTextColor, freeTextErrorColor: freeTextErrorColor ?? self.freeTextErrorColor, freeTextSuccessColor: freeTextSuccessColor ?? self.freeTextSuccessColor, freeMonoIconColor: freeMonoIconColor ?? self.freeMonoIconColor, itemSwitchColors: itemSwitchColors ?? self.itemSwitchColors, itemDisclosureActions: itemDisclosureActions ?? self.itemDisclosureActions, itemCheckColors: itemCheckColors ?? self.itemCheckColors, controlSecondaryColor: controlSecondaryColor ?? self.controlSecondaryColor, freeInputField: freeInputField ?? self.freeInputField, freePlainInputField: freePlainInputField ?? self.freePlainInputField, mediaPlaceholderColor: mediaPlaceholderColor ?? self.mediaPlaceholderColor, scrollIndicatorColor: scrollIndicatorColor ?? self.scrollIndicatorColor, pageIndicatorInactiveColor: pageIndicatorInactiveColor ?? self.pageIndicatorInactiveColor, inputClearButtonColor: inputClearButtonColor ?? self.inputClearButtonColor, itemBarChart: itemBarChart ?? self.itemBarChart, itemInputField: itemInputField ?? self.itemInputField, paymentOption: paymentOption ?? self.paymentOption)
} }
public var itemSearchHighlightColor: UIColor {
return self.itemAccentColor.withMultipliedAlpha(0.2)
}
} }
public final class PresentationThemeArchiveAvatarColors { public final class PresentationThemeArchiveAvatarColors {

View file

@ -96,7 +96,7 @@ public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDate
} }
public func formatStarsAmountText(_ amount: StarsAmount, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false) -> String { public func formatStarsAmountText(_ amount: StarsAmount, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false) -> String {
var balanceText = presentationStringsFormattedNumber(Int32(amount.value), dateTimeFormat.groupingSeparator) var balanceText = presentationStringsFormattedNumber(Int32(clamping: amount.value), dateTimeFormat.groupingSeparator)
let fraction = abs(Double(amount.nanos)) / 10e6 let fraction = abs(Double(amount.nanos)) / 10e6
if fraction > 0.0 { if fraction > 0.0 {
balanceText.append(dateTimeFormat.decimalSeparator) balanceText.append(dateTimeFormat.decimalSeparator)

View file

@ -465,6 +465,7 @@ swift_library(
"//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen", "//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen",
"//submodules/TelegramUI/Components/Gifts/GiftStoreScreen", "//submodules/TelegramUI/Components/Gifts/GiftStoreScreen",
"//submodules/TelegramUI/Components/Gifts/GiftSetupScreen", "//submodules/TelegramUI/Components/Gifts/GiftSetupScreen",
"//submodules/TelegramUI/Components/Gifts/GiftCraftScreen",
"//submodules/TelegramUI/Components/ContentReportScreen", "//submodules/TelegramUI/Components/ContentReportScreen",
"//submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen", "//submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen",
"//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent", "//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent",
@ -513,7 +514,9 @@ swift_library(
"//submodules/TelegramUI/Components/AlertComponent", "//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController", "//submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController",
"//submodules/TelegramUI/Components/CocoonInfoScreen", "//submodules/TelegramUI/Components/CocoonInfoScreen",
"//submodules/TelegramUI/Components/ProxyServerPreviewScreen",
"//submodules/TelegramUI/Components/ContextControllerImpl", "//submodules/TelegramUI/Components/ContextControllerImpl",
"//submodules/TelegramUI/Components/AuthConfirmationScreen",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [], "//build-system:ios_sim_arm64": [],

View file

@ -489,10 +489,10 @@ private final class RecentActionsSettingsSheetComponent: Component {
let leftButtonSize = self.leftButton.update( let leftButtonSize = self.leftButton.update(
transition: transition, transition: transition,
component: AnyComponent(GlassBarButtonComponent( component: AnyComponent(GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0), size: CGSize(width: 44.0, height: 44.0),
backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, backgroundColor: nil,
isDark: environment.theme.overallDarkAppearance, isDark: environment.theme.overallDarkAppearance,
state: .generic, state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent( component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent( BundleIconComponent(
name: "Navigation/Close", name: "Navigation/Close",
@ -507,7 +507,7 @@ private final class RecentActionsSettingsSheetComponent: Component {
} }
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: 40.0, height: 40.0) containerSize: CGSize(width: 44.0, height: 44.0)
) )
let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0 + environment.safeInsets.left, y: 16.0), size: leftButtonSize) let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0 + environment.safeInsets.left, y: 16.0), size: leftButtonSize)
if let leftButtonView = self.leftButton.view { if let leftButtonView = self.leftButton.view {

View file

@ -547,11 +547,12 @@ private final class AlertScreenComponent: Component {
self.containerView.update(size: availableSize, isDark: environment.theme.overallDarkAppearance, transition: transition) self.containerView.update(size: availableSize, isDark: environment.theme.overallDarkAppearance, transition: transition)
var availableHeight = availableSize.height var availableHeight = availableSize.height
availableHeight -= environment.statusBarHeight
if component.configuration.allowInputInset, environment.inputHeight > 0.0 { if component.configuration.allowInputInset, environment.inputHeight > 0.0 {
availableHeight -= environment.inputHeight availableHeight -= environment.inputHeight
} }
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - alertSize.width) / 2.0), y: floorToScreenPixels((availableHeight - alertSize.height) / 2.0)), size: alertSize)) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - alertSize.width) / 2.0), y: environment.statusBarHeight + floorToScreenPixels((availableHeight - alertSize.height) / 2.0)), size: alertSize))
self.backgroundView.update(size: alertSize, cornerRadius: 35.0, isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition) self.backgroundView.update(size: alertSize, cornerRadius: 35.0, isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition)
return availableSize return availableSize

View file

@ -0,0 +1,38 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AuthConfirmationScreen",
module_name = "AuthConfirmationScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/Components/SheetComponent",
"//submodules/PresentationDataUtils",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/AvatarComponent",
"//submodules/Markdown",
"//submodules/PhoneNumberFormat",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,588 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import GlassBarButtonComponent
import ButtonComponent
import PresentationDataUtils
import BundleIconComponent
import ListSectionComponent
import ListActionItemComponent
import AvatarComponent
import Markdown
import PhoneNumberFormat
private final class AuthConfirmationSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let subject: MessageActionUrlAuthResult
let completion: (Bool, Bool) -> Void
let cancel: (Bool) -> Void
init(
context: AccountContext,
subject: MessageActionUrlAuthResult,
completion: @escaping (Bool, Bool) -> Void,
cancel: @escaping (Bool) -> Void
) {
self.context = context
self.subject = subject
self.completion = completion
self.cancel = cancel
}
static func ==(lhs: AuthConfirmationSheetContent, rhs: AuthConfirmationSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private let subject: MessageActionUrlAuthResult
fileprivate var inProgress = false
var allowWrite = true
weak var controller: ViewController?
init(context: AccountContext, subject: MessageActionUrlAuthResult) {
self.context = context
self.subject = subject
super.init()
}
func displayPhoneNumberConfirmation(commit: @escaping (Bool) -> Void) {
guard case let .request(domain, _, _, _) = self.subject else {
return
}
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, case let .user(user) = peer, let phone = user.phone else {
return
}
let phoneNumber = formatPhoneNumber(context: self.context, number: phone)
//TODO:localize
let alertController = textAlertController(
context: self.context,
title: "Phone Number",
text: "**\(domain)** wants to access your phone number **\(phoneNumber)**. Allow access?",
actions: [
TextAlertAction(type: .genericAction, title: "Deny", action: {
commit(false)
}),
TextAlertAction(type: .defaultAction, title: "Allow", action: {
commit(true)
})
]
)
self.controller?.present(alertController, in: .window(.root))
})
}
}
func makeState() -> State {
return State(context: self.context, subject: self.subject)
}
static var body: Body {
let closeButton = Child(GlassBarButtonComponent.self)
let avatar = Child(AvatarComponent.self)
let title = Child(MultilineTextComponent.self)
let description = Child(MultilineTextComponent.self)
let clientSection = Child(ListSectionComponent.self)
let optionsSection = Child(ListSectionComponent.self)
let cancelButton = Child(ButtonComponent.self)
let doneButton = Child(ButtonComponent.self)
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
let theme = environment.theme
let strings = environment.strings
let state = context.state
if state.controller == nil {
state.controller = environment.controller()
}
let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 }
let _ = strings
guard case let .request(domain, bot, clientData, flags) = component.subject else {
fatalError()
}
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
component.cancel(true)
}
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
var contentHeight: CGFloat = 32.0
let avatar = avatar.update(
component: AvatarComponent(
context: component.context,
theme: environment.theme,
peer: EnginePeer(bot),
clipStyle: .roundedRect
),
availableSize: CGSize(width: 92.0, height: 92.0),
transition: .immediate
)
context.add(avatar
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + avatar.size.height / 2.0))
)
contentHeight += avatar.size.height
contentHeight += 18.0
let titleFont = Font.bold(24.0)
let title = title.update(
component: MultilineTextComponent(
text: .markdown(text: "Log in to **\(domain)**", attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.controlAccentColor), link: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })),
horizontalAlignment: .center,
maximumNumberOfLines: 2
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + title.size.height / 2.0))
)
contentHeight += title.size.height
contentHeight += 16.0
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let description = description.update(
component: MultilineTextComponent(
text: .markdown(text: "This site will receive your **name**,\n**username** and **profile photo**.", attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.actionSheet.primaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })),
horizontalAlignment: .center,
maximumNumberOfLines: 3,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
context.add(description
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + description.size.height / 2.0))
)
contentHeight += description.size.height
contentHeight += 16.0
var clientSectionItems: [AnyComponentWithIdentity<Empty>] = []
clientSectionItems.append(
AnyComponentWithIdentity(id: "device", component: AnyComponent(
ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Device",
font: Font.regular(17.0),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
contentInsets: UIEdgeInsets(top: 19.0, left: 0.0, bottom: 19.0, right: 0.0),
accessory: .custom(ListActionItemComponent.CustomAccessory(
component: AnyComponentWithIdentity(
id: "info",
component: AnyComponent(
VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: clientData?.platform ?? "",
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: clientData?.browser ?? "",
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 15.0),
textColor: theme.list.itemSecondaryTextColor
)),
horizontalAlignment: .left,
truncationType: .middle,
maximumNumberOfLines: 1
)))
], alignment: .right, spacing: 3.0)
)
),
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0),
isInteractive: true
)),
action: nil
)
))
)
clientSectionItems.append(
AnyComponentWithIdentity(id: "region", component: AnyComponent(
ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "IP Address",
font: Font.regular(17.0),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
contentInsets: UIEdgeInsets(top: 19.0, left: 0.0, bottom: 19.0, right: 0.0),
accessory: .custom(ListActionItemComponent.CustomAccessory(
component: AnyComponentWithIdentity(
id: "info",
component: AnyComponent(
VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: clientData?.ip ?? "",
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: clientData?.region ?? "",
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 15.0),
textColor: theme.list.itemSecondaryTextColor
)),
horizontalAlignment: .left,
truncationType: .middle,
maximumNumberOfLines: 1
)))
], alignment: .right, spacing: 3.0)
)
),
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0),
isInteractive: true
)),
action: nil
)
))
)
let clientSection = clientSection.update(
component: ListSectionComponent(
theme: theme,
style: .glass,
header: nil,
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "This login attempt came from the device above.",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
items: clientSectionItems
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: context.transition
)
context.add(clientSection
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + clientSection.size.height / 2.0))
)
contentHeight += clientSection.size.height
if flags.contains(.requestWriteAccess) {
contentHeight += 22.0
var optionsSectionItems: [AnyComponentWithIdentity<Empty>] = []
optionsSectionItems.append(AnyComponentWithIdentity(id: "allowWrite", component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Allow Messages",
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: state.allowWrite, action: { [weak state] _ in
guard let state else {
return
}
state.allowWrite = !state.allowWrite
state.updated()
})),
action: nil
))))
let optionsSection = optionsSection.update(
component: ListSectionComponent(
theme: theme,
style: .glass,
header: nil,
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "This will allow \(EnginePeer(bot).compactDisplayTitle) to message you.",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
items: optionsSectionItems
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: context.transition
)
context.add(optionsSection
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + optionsSection.size.height / 2.0))
)
contentHeight += optionsSection.size.height
}
contentHeight += 32.0
let buttonSpacing: CGFloat = 10.0
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
let buttonWidth = (context.availableSize.width - buttonInsets.left - buttonInsets.right - buttonSpacing) / 2.0
let cancelButton = cancelButton.update(
component: ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1),
foreground: theme.list.itemPrimaryTextColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: "Cancel", font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center))))
),
action: {
component.cancel(true)
}
),
availableSize: CGSize(width: buttonWidth, height: 52.0),
transition: .immediate
)
context.add(cancelButton
.position(CGPoint(x: context.availableSize.width / 2.0 - buttonSpacing / 2.0 - cancelButton.size.width / 2.0, y: contentHeight + cancelButton.size.height / 2.0))
)
let doneButton = doneButton.update(
component: ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
cornerRadius: 10.0,
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: "Log In", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
),
displaysProgress: state.inProgress,
action: { [weak state] in
guard let state else {
return
}
var allowWrite = false
if flags.contains(.requestWriteAccess) && state.allowWrite {
allowWrite = true
}
if flags.contains(.requestPhoneNumber) {
state.displayPhoneNumberConfirmation(commit: { sharePhoneNumber in
component.completion(allowWrite, sharePhoneNumber)
state.inProgress = true
state.updated()
})
} else {
component.completion(allowWrite, false)
state.inProgress = true
state.updated()
}
}
),
availableSize: CGSize(width: buttonWidth, height: 52.0),
transition: .immediate
)
context.add(doneButton
.position(CGPoint(x: context.availableSize.width / 2.0 + buttonSpacing / 2.0 + doneButton.size.width / 2.0, y: contentHeight + doneButton.size.height / 2.0))
)
contentHeight += doneButton.size.height
contentHeight += buttonInsets.bottom
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
return CGSize(width: context.availableSize.width, height: contentHeight)
}
}
}
private final class AuthConfirmationSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let subject: MessageActionUrlAuthResult
let completion: (Bool, Bool) -> Void
init(
context: AccountContext,
subject: MessageActionUrlAuthResult,
completion: @escaping (Bool, Bool) -> Void
) {
self.context = context
self.subject = subject
self.completion = completion
}
static func ==(lhs: AuthConfirmationSheetComponent, rhs: AuthConfirmationSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(AuthConfirmationSheetContent(
context: context.component.context,
subject: context.component.subject,
completion: context.component.completion,
cancel: { animate in
if animate {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else if let controller = controller() {
controller.dismiss(animated: false, completion: nil)
}
}
)),
style: .glass,
backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
return context.availableSize
}
}
}
public class AuthConfirmationScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let subject: MessageActionUrlAuthResult
fileprivate let completion: (Bool, Bool) -> Void
public init(
context: AccountContext,
subject: MessageActionUrlAuthResult,
completion: @escaping (Bool, Bool) -> Void
) {
self.context = context
self.subject = subject
self.completion = completion
super.init(
context: context,
component: AuthConfirmationSheetComponent(
context: context,
subject: subject,
completion: completion
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}

View file

@ -8,20 +8,28 @@ import AvatarNode
import AccountContext import AccountContext
public final class AvatarComponent: Component { public final class AvatarComponent: Component {
public enum ClipStyle {
case round
case roundedRect
}
let context: AccountContext let context: AccountContext
let theme: PresentationTheme let theme: PresentationTheme
let peer: EnginePeer let peer: EnginePeer
let clipStyle: ClipStyle
let icon: AnyComponent<Empty>? let icon: AnyComponent<Empty>?
public init( public init(
context: AccountContext, context: AccountContext,
theme: PresentationTheme, theme: PresentationTheme,
peer: EnginePeer, peer: EnginePeer,
clipStyle: ClipStyle = .round,
icon: AnyComponent<Empty>? = nil icon: AnyComponent<Empty>? = nil
) { ) {
self.context = context self.context = context
self.theme = theme self.theme = theme
self.peer = peer self.peer = peer
self.clipStyle = clipStyle
self.icon = icon self.icon = icon
} }
@ -35,6 +43,9 @@ public final class AvatarComponent: Component {
if lhs.peer != rhs.peer { if lhs.peer != rhs.peer {
return false return false
} }
if lhs.clipStyle != rhs.clipStyle {
return false
}
if lhs.icon != rhs.icon { if lhs.icon != rhs.icon {
return false return false
} }
@ -81,7 +92,7 @@ public final class AvatarComponent: Component {
transition: .immediate, transition: .immediate,
component: icon, component: icon,
environment: {}, environment: {},
containerSize: availableSize containerSize: CGSize(width: 24.0, height: 24.0)
) )
let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - iconSize.width + 2.0, y: availableSize.height - iconSize.height + 2.0), size: iconSize) let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - iconSize.width + 2.0, y: availableSize.height - iconSize.height + 2.0), size: iconSize)
if let iconView = iconView.view { if let iconView = iconView.view {
@ -93,11 +104,17 @@ public final class AvatarComponent: Component {
cutoutRect = CGRect(origin: CGPoint(x: iconFrame.minX, y: availableSize.height - iconFrame.maxY), size: iconFrame.size).insetBy(dx: -2.0 + UIScreenPixel, dy: -2.0 + UIScreenPixel) cutoutRect = CGRect(origin: CGPoint(x: iconFrame.minX, y: availableSize.height - iconFrame.maxY), size: iconFrame.size).insetBy(dx: -2.0 + UIScreenPixel, dy: -2.0 + UIScreenPixel)
} }
var clipStyle: AvatarNodeClipStyle = .round
if case .roundedRect = component.clipStyle {
clipStyle = .roundedRect
}
self.avatarNode.frame = CGRect(origin: .zero, size: availableSize) self.avatarNode.frame = CGRect(origin: .zero, size: availableSize)
self.avatarNode.setPeer( self.avatarNode.setPeer(
context: component.context, context: component.context,
theme: component.theme, theme: component.theme,
peer: component.peer, peer: component.peer,
clipStyle: clipStyle,
synchronousLoad: true, synchronousLoad: true,
cutoutRect: cutoutRect cutoutRect: cutoutRect
) )

View file

@ -42,6 +42,8 @@ swift_library(
"//submodules/TelegramUI/Components/AvatarBackground", "//submodules/TelegramUI/Components/AvatarBackground",
"//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PremiumAlertController", "//submodules/TelegramUI/Components/PremiumAlertController",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/Components/BundleIconComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View file

@ -28,6 +28,8 @@ import AvatarBackground
import LottieComponent import LottieComponent
import UndoUI import UndoUI
import PremiumAlertController import PremiumAlertController
import GlassBarButtonComponent
import BundleIconComponent
public struct AvatarKeyboardInputData: Equatable { public struct AvatarKeyboardInputData: Equatable {
var emoji: EmojiPagerContentComponent var emoji: EmojiPagerContentComponent
@ -837,50 +839,63 @@ final class AvatarEditorScreenComponent: Component {
} }
} }
let backgroundIsBright = UIColor(rgb: state.selectedBackground.colors.first ?? 0).lightness > 0.8 //let backgroundIsBright = UIColor(rgb: state.selectedBackground.colors.first ?? 0).lightness > 0.8
//state.expanded && !backgroundIsBright ? .white : environment.theme.rootController.navigationBar.accentTextColor
let navigationCancelButtonSize = self.navigationCancelButton.update( let navigationCancelButtonSize = self.navigationCancelButton.update(
transition: transition, transition: transition,
component: AnyComponent(Button( component: AnyComponent(
content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: state.expanded && !backgroundIsBright ? .white : environment.theme.rootController.navigationBar.accentTextColor)), GlassBarButtonComponent(
action: { [weak self] in size: CGSize(width: 44.0, height: 44.0),
guard let self else { backgroundColor: nil,
return isDark: environment.theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(BundleIconComponent(name: "Navigation/Close", tintColor: environment.theme.chat.inputPanel.panelControlColor))),
action: { [weak self] _ in
guard let self else {
return
}
self.controller?()?.dismiss()
} }
self.controller?()?.dismiss() )
} ),
).minSize(CGSize(width: 16.0, height: environment.navigationHeight - environment.statusBarHeight))),
environment: {}, environment: {},
containerSize: CGSize(width: 150.0, height: environment.navigationHeight - environment.statusBarHeight) containerSize: CGSize(width: 44.0, height: 44.0)
) )
if let navigationCancelButtonView = self.navigationCancelButton.view { if let navigationCancelButtonView = self.navigationCancelButton.view {
if navigationCancelButtonView.superview == nil { if navigationCancelButtonView.superview == nil {
self.addSubview(navigationCancelButtonView) self.addSubview(navigationCancelButtonView)
} }
transition.setFrame(view: navigationCancelButtonView, frame: CGRect(origin: CGPoint(x: 16.0 + environment.safeInsets.left, y: environment.statusBarHeight), size: navigationCancelButtonSize)) transition.setFrame(view: navigationCancelButtonView, frame: CGRect(origin: CGPoint(x: 16.0 + environment.safeInsets.left, y: 16.0), size: navigationCancelButtonSize))
transition.setAlpha(view: navigationCancelButtonView, alpha: !state.editingColor ? 1.0 : 0.0) transition.setAlpha(view: navigationCancelButtonView, alpha: !state.editingColor ? 1.0 : 0.0)
} }
let navigationDoneButtonSize = self.navigationDoneButton.update( let navigationDoneButtonSize = self.navigationDoneButton.update(
transition: transition, transition: transition,
component: AnyComponent(Button( component: AnyComponent(
content: AnyComponent(Text(text: component.peerType == .suggest ? strings.AvatarEditor_Suggest : strings.AvatarEditor_Set, font: Font.semibold(17.0), color: state.expanded && !backgroundIsBright ? .white : environment.theme.rootController.navigationBar.accentTextColor)), GlassBarButtonComponent(
action: { [weak self] in size: CGSize(width: 44.0, height: 44.0),
guard let self else { backgroundColor: environment.theme.list.itemCheckColors.fillColor,
return isDark: environment.theme.overallDarkAppearance,
state: .tintedGlass,
component: AnyComponentWithIdentity(id: "done", component: AnyComponent(BundleIconComponent(name: "Navigation/Done", tintColor: environment.theme.list.itemCheckColors.foregroundColor))),
action: { [weak self] _ in
guard let self else {
return
}
self.complete()
} }
self.complete() )
} ),
).minSize(CGSize(width: 16.0, height: environment.navigationHeight - environment.statusBarHeight))),
environment: {}, environment: {},
containerSize: CGSize(width: 150.0, height: environment.navigationHeight - environment.statusBarHeight) containerSize: CGSize(width: 44.0, height: 44.0)
) )
if let navigationDoneButtonView = self.navigationDoneButton.view { if let navigationDoneButtonView = self.navigationDoneButton.view {
if navigationDoneButtonView.superview == nil { if navigationDoneButtonView.superview == nil {
self.addSubview(navigationDoneButtonView) self.addSubview(navigationDoneButtonView)
} }
transition.setFrame(view: navigationDoneButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - navigationDoneButtonSize.width, y: environment.statusBarHeight), size: navigationDoneButtonSize)) transition.setFrame(view: navigationDoneButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - navigationDoneButtonSize.width, y: 16.0), size: navigationDoneButtonSize))
transition.setAlpha(view: navigationDoneButtonView, alpha: (state.expanded || environment.inputHeight > 0.0) && !state.editingColor ? 1.0 : 0.0) transition.setAlpha(view: navigationDoneButtonView, alpha: (state.expanded || environment.inputHeight > 0.0) && !state.editingColor ? 1.0 : 0.0)
} }
@ -1190,9 +1205,10 @@ final class AvatarEditorScreenComponent: Component {
contentHeight += keyboardTitleSize.height contentHeight += keyboardTitleSize.height
contentHeight += 8.0 contentHeight += 8.0
var bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom : 16.0 let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
var bottomInset: CGFloat = buttonInsets.bottom
if !effectiveIsExpanded { if !effectiveIsExpanded {
bottomInset += 50.0 + 16.0 bottomInset += 52.0 + buttonInsets.bottom - 14.0
} }
let keyboardContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - contentHeight - bottomInset)) let keyboardContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - contentHeight - bottomInset))
@ -1318,7 +1334,6 @@ final class AvatarEditorScreenComponent: Component {
)))) ))))
} }
let bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
let buttonSize = self.buttonView.update( let buttonSize = self.buttonView.update(
transition: transition, transition: transition,
component: AnyComponent( component: AnyComponent(
@ -1340,13 +1355,13 @@ final class AvatarEditorScreenComponent: Component {
) )
), ),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width - bottomInsets.left - bottomInsets.right, height: 52.0) containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0)
) )
if let buttonView = self.buttonView.view { if let buttonView = self.buttonView.view {
if buttonView.superview == nil { if buttonView.superview == nil {
self.addSubview(buttonView) self.addSubview(buttonView)
} }
transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: bottomInsets.left, y: contentHeight), size: buttonSize)) transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: buttonSize))
} }
let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight - 4.0), size: CGSize(width: availableSize.width, height: availableSize.height - contentHeight + 4.0)) let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight - 4.0), size: CGSize(width: availableSize.width, height: availableSize.height - contentHeight + 4.0))

View file

@ -46,13 +46,13 @@ let collageGrids: [Camera.CollageGrid] = [
Camera.CollageGrid(rows: [Camera.CollageGrid.Row(columns: 2), Camera.CollageGrid.Row(columns: 2), Camera.CollageGrid.Row(columns: 2)]) Camera.CollageGrid(rows: [Camera.CollageGrid.Row(columns: 2), Camera.CollageGrid.Row(columns: 2), Camera.CollageGrid.Row(columns: 2)])
] ]
enum CameraMode: Int32, Equatable {
case photo
case video
case live
}
struct CameraState: Equatable { struct CameraState: Equatable {
enum CameraMode: Int32, Equatable {
case photo
case video
case live
}
enum Recording: Equatable { enum Recording: Equatable {
case none case none
case holding case holding
@ -548,7 +548,7 @@ private final class CameraScreenComponent: CombinedComponent {
self.buttonPressTimestamp = nil self.buttonPressTimestamp = nil
} }
func updateCameraMode(_ mode: CameraMode) { func updateCameraMode(_ mode: CameraState.CameraMode) {
guard let controller = self.getController(), let camera = controller.camera else { guard let controller = self.getController(), let camera = controller.camera else {
return return
} }
@ -2102,7 +2102,7 @@ private final class CameraScreenComponent: CombinedComponent {
availableModeControlSize = availableSize availableModeControlSize = availableSize
} }
var availableModes: [CameraMode] = [.photo, .video] var availableModes: [CameraState.CameraMode] = [.photo, .video]
if !isTablet && state.canLivestream { if !isTablet && state.canLivestream {
availableModes.append(.live) availableModes.append(.live)
} }
@ -2218,6 +2218,12 @@ public class CameraScreenImpl: ViewController, CameraScreen {
case avatar case avatar
} }
public enum CameraMode {
case photo
case video
case live
}
public enum PIPPosition: Int32 { public enum PIPPosition: Int32 {
case topLeft case topLeft
case topRight case topRight
@ -2528,8 +2534,18 @@ public class CameraScreenImpl: ViewController, CameraScreen {
self.mainPreviewView.resetPlaceholder(front: cameraFrontPosition) self.mainPreviewView.resetPlaceholder(front: cameraFrontPosition)
} }
let cameraMode: CameraState.CameraMode
switch controller.cameraMode {
case .photo:
cameraMode = .photo
case .video:
cameraMode = .video
case .live:
cameraMode = .live
}
self.cameraState = CameraState( self.cameraState = CameraState(
mode: .photo, mode: cameraMode,
position: cameraFrontPosition ? .front : .back, position: cameraFrontPosition ? .front : .back,
flashMode: .off, flashMode: .off,
flashModeDidChange: false, flashModeDidChange: false,
@ -4002,6 +4018,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
private let context: AccountContext private let context: AccountContext
fileprivate let mode: Mode fileprivate let mode: Mode
fileprivate let cameraMode: CameraMode
fileprivate let customTarget: EnginePeer.Id? fileprivate let customTarget: EnginePeer.Id?
fileprivate let resumeLiveStream: Bool fileprivate let resumeLiveStream: Bool
fileprivate let holder: CameraHolder? fileprivate let holder: CameraHolder?
@ -4071,6 +4088,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
public init( public init(
context: AccountContext, context: AccountContext,
mode: Mode, mode: Mode,
cameraMode: CameraMode = .photo,
customTarget: EnginePeer.Id? = nil, customTarget: EnginePeer.Id? = nil,
resumeLiveStream: Bool = false, resumeLiveStream: Bool = false,
holder: CameraHolder? = nil, holder: CameraHolder? = nil,
@ -4080,6 +4098,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
) { ) {
self.context = context self.context = context
self.mode = mode self.mode = mode
self.cameraMode = cameraMode
self.customTarget = customTarget self.customTarget = customTarget
self.resumeLiveStream = resumeLiveStream self.resumeLiveStream = resumeLiveStream
self.holder = holder self.holder = holder

View file

@ -8,7 +8,7 @@ import GlassBackgroundComponent
import LiquidLens import LiquidLens
import TabSelectionRecognizer import TabSelectionRecognizer
extension CameraMode { extension CameraState.CameraMode {
func title(strings: PresentationStrings) -> String { func title(strings: PresentationStrings) -> String {
switch self { switch self {
case .photo: case .photo:
@ -28,18 +28,18 @@ final class ModeComponent: Component {
let isTablet: Bool let isTablet: Bool
let strings: PresentationStrings let strings: PresentationStrings
let tintColor: UIColor let tintColor: UIColor
let availableModes: [CameraMode] let availableModes: [CameraState.CameraMode]
let currentMode: CameraMode let currentMode: CameraState.CameraMode
let updatedMode: (CameraMode) -> Void let updatedMode: (CameraState.CameraMode) -> Void
let tag: AnyObject? let tag: AnyObject?
init( init(
isTablet: Bool, isTablet: Bool,
strings: PresentationStrings, strings: PresentationStrings,
tintColor: UIColor, tintColor: UIColor,
availableModes: [CameraMode], availableModes: [CameraState.CameraMode],
currentMode: CameraMode, currentMode: CameraState.CameraMode,
updatedMode: @escaping (CameraMode) -> Void, updatedMode: @escaping (CameraState.CameraMode) -> Void,
tag: AnyObject? tag: AnyObject?
) { ) {
self.isTablet = isTablet self.isTablet = isTablet

View file

@ -1428,8 +1428,6 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
break break
case .theme: case .theme:
break break
case .settings:
break
case .premiumOffer: case .premiumOffer:
break break
case .starsTopup: case .starsTopup:
@ -1465,6 +1463,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
break break
case .sendGift: case .sendGift:
break break
case .chats, .contacts, .compose, .postStory, .settings, .unknownDeepLink, .oauth:
break
} }
} }
})) }))

View file

@ -443,6 +443,10 @@ public final class ChatListHeaderComponent: Component {
alphaTransition.setAlpha(view: self.rightButtonsContainer, alpha: pow(fraction, 2.0)) alphaTransition.setAlpha(view: self.rightButtonsContainer, alpha: pow(fraction, 2.0))
} }
func openEmojiStatusSetup() {
self.chatListTitleView?.openEmojiStatusSetup()
}
func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, displayBackButton: Bool, sideInset: CGFloat, sideContentWidth: CGFloat, sideContentFraction: CGFloat, size: CGSize, transition: ComponentTransition) { func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, displayBackButton: Bool, sideInset: CGFloat, sideContentWidth: CGFloat, sideContentFraction: CGFloat, size: CGSize, transition: ComponentTransition) {
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3)
@ -815,6 +819,14 @@ public final class ChatListHeaderComponent: Component {
private func updateContentStoryOffsets(transition: ComponentTransition) { private func updateContentStoryOffsets(transition: ComponentTransition) {
} }
func openEmojiStatusSetup() {
if let storyPeerListView = self.storyPeerList?.view as? StoryPeerListComponent.View {
storyPeerListView.openEmojiStatusSetup()
} else {
self.primaryContentView?.openEmojiStatusSetup()
}
}
func update(component: ChatListHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize { func update(component: ChatListHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.state = state self.state = state

View file

@ -615,6 +615,12 @@ public final class ChatListNavigationBar: Component {
} }
} }
public func openEmojiStatusSetup() {
if let headerContentView = self.headerContent.view as? ChatListHeaderComponent.View {
headerContentView.openEmojiStatusSetup()
}
}
public func updateStoryUploadProgress(storyUploadProgress: [EnginePeer.Id: Float]) { public func updateStoryUploadProgress(storyUploadProgress: [EnginePeer.Id: Float]) {
guard let component = self.component else { guard let component = self.component else {
return return

View file

@ -172,10 +172,10 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
particleColor: statusParticleColor, particleColor: statusParticleColor,
isVisibleForAnimations: true, isVisibleForAnimations: true,
action: { [weak self] in action: { [weak self] in
guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else { guard let self else {
return return
} }
strongSelf.openStatusSetup?(titleCredibilityIconView) self.openEmojiStatusSetup()
} }
)), )),
environment: {}, environment: {},
@ -327,6 +327,13 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
} }
} }
public func openEmojiStatusSetup() {
guard let titleCredibilityIconView = self.titleCredibilityIconView else {
return
}
self.openStatusSetup?(titleCredibilityIconView)
}
public func updateLayout(availableSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { public func updateLayout(availableSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let _ = self.updateLayoutInternal(size: availableSize, transition: transition) let _ = self.updateLayoutInternal(size: availableSize, transition: transition)
return availableSize return availableSize
@ -414,10 +421,10 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
content: statusContent, content: statusContent,
isVisibleForAnimations: true, isVisibleForAnimations: true,
action: { [weak self] in action: { [weak self] in
guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else { guard let self else {
return return
} }
strongSelf.openStatusSetup?(titleCredibilityIconView) self.openEmojiStatusSetup()
} }
)), )),
environment: {}, environment: {},

View file

@ -167,15 +167,15 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
var contentHeight: CGFloat = 0.0 var contentHeight: CGFloat = 0.0
contentHeight += 30.0 contentHeight += 30.0
let barButtonSize = CGSize(width: 40.0, height: 40.0) let barButtonSize = CGSize(width: 44.0, height: 44.0)
let cancelSize = self.cancel.update( let cancelSize = self.cancel.update(
transition: transition, transition: transition,
component: AnyComponent( component: AnyComponent(
GlassBarButtonComponent( GlassBarButtonComponent(
size: barButtonSize, size: barButtonSize,
backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, backgroundColor: nil,
isDark: environment.theme.overallDarkAppearance, isDark: environment.theme.overallDarkAppearance,
state: .generic, state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent( component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent( BundleIconComponent(
name: "Navigation/Close", name: "Navigation/Close",

View file

@ -52,6 +52,8 @@ private final class CocoonInfoSheetContent: CombinedComponent {
fileprivate let playButtonAnimation = ActionSlot<Void>() fileprivate let playButtonAnimation = ActionSlot<Void>()
private var didPlayAnimation = false private var didPlayAnimation = false
var cachedDescription: String?
init( init(
context: AccountContext, context: AccountContext,
@ -91,16 +93,17 @@ private final class CocoonInfoSheetContent: CombinedComponent {
} }
static var body: Body { static var body: Body {
let background = Child(GradientBackgroundComponent.self)
let closeButton = Child(GlassBarButtonComponent.self) let closeButton = Child(GlassBarButtonComponent.self)
let icon = Child(BundleIconComponent.self) let icon = Child(BundleIconComponent.self)
let title = Child(BalancedTextComponent.self) let logo = Child(BundleIconComponent.self)
let text = Child(BalancedTextComponent.self) let text = Child(BalancedTextComponent.self)
let list = Child(List<Empty>.self) let list = Child(List<Empty>.self)
let additionalText = Child(MultilineTextComponent.self) let additionalText = Child(MultilineTextComponent.self)
let button = Child(ButtonComponent.self) let button = Child(ButtonComponent.self)
let navigateDisposable = MetaDisposable() let navigateDisposable = MetaDisposable()
return { context in return { context in
let component = context.component let component = context.component
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
@ -112,7 +115,6 @@ private final class CocoonInfoSheetContent: CombinedComponent {
let sideInset: CGFloat = 30.0 + environment.safeInsets.left let sideInset: CGFloat = 30.0 + environment.safeInsets.left
let textSideInset: CGFloat = 30.0 + environment.safeInsets.left let textSideInset: CGFloat = 30.0 + environment.safeInsets.left
let titleFont = Font.bold(24.0)
let textFont = Font.regular(15.0) let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0) let boldTextFont = Font.semibold(15.0)
@ -122,42 +124,48 @@ private final class CocoonInfoSheetContent: CombinedComponent {
let spacing: CGFloat = 16.0 let spacing: CGFloat = 16.0
var contentSize = CGSize(width: context.availableSize.width, height: 28.0) var contentSize = CGSize(width: context.availableSize.width, height: 28.0)
let icon = icon.update( let descriptionText: String
component: BundleIconComponent( if let cachedDescription = state.cachedDescription {
name: "Premium/Cocoon", tintColor: nil descriptionText = cachedDescription
), } else {
availableSize: context.availableSize, func updateText(_ input: String) -> String {
transition: context.transition let pattern = #"\(([^()]*)\)"#
) let boldPattern = #"\*\*(.*?)\*\*"#
context.add(icon
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + icon.size.height / 2.0)) let regex = try! NSRegularExpression(pattern: pattern)
) let boldRegex = try! NSRegularExpression(pattern: boldPattern)
contentSize.height += icon.size.height
contentSize.height += 14.0 let nsInput = input as NSString
var result = input
let title = title.update(
component: BalancedTextComponent( let matches = regex.matches(in: input, range: NSRange(location: 0, length: nsInput.length))
text: .plain(NSAttributedString(string: strings.CocoonInfo_Title, font: titleFont, textColor: textColor)),
horizontalAlignment: .center, for match in matches.reversed() {
maximumNumberOfLines: 0, let range = match.range(at: 1)
lineSpacing: 0.1 let inner = nsInput.substring(with: range)
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), let replacedInner = boldRegex.stringByReplacingMatches(
transition: .immediate in: inner,
) range: NSRange(location: 0, length: (inner as NSString).length),
context.add(title withTemplate: "[$1]()"
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) )
)
contentSize.height += title.size.height result = (result as NSString).replacingCharacters(in: range, with: replacedInner)
contentSize.height += spacing - 8.0 }
return result
}
descriptionText = updateText(strings.CocoonInfo_Description)
state.cachedDescription = descriptionText
}
let attributedText = parseMarkdownIntoAttributedString( let attributedText = parseMarkdownIntoAttributedString(
strings.CocoonInfo_Description, descriptionText,
attributes: MarkdownAttributes( attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor), body: MarkdownAttributeSet(font: textFont, textColor: UIColor(rgb: 0xb8c9ef)),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: UIColor(rgb: 0xb8c9ef)),
link: MarkdownAttributeSet(font: textFont, textColor: linkColor), link: MarkdownAttributeSet(font: boldTextFont, textColor: UIColor(rgb: 0xffffff)),
linkAttribute: { _ in return nil } linkAttribute: { _ in return nil }
) )
) )
@ -171,11 +179,52 @@ private final class CocoonInfoSheetContent: CombinedComponent {
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate transition: .immediate
) )
let background = background.update(
component: GradientBackgroundComponent(
colors: [
UIColor(rgb: 0x061129),
UIColor(rgb: 0x08153d)
]
),
availableSize: CGSize(width: context.availableSize.width, height: text.size.height + 220.0),
transition: context.transition
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
)
let icon = icon.update(
component: BundleIconComponent(
name: "Premium/Cocoon", tintColor: nil
),
availableSize: context.availableSize,
transition: context.transition
)
context.add(icon
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + icon.size.height / 2.0))
)
contentSize.height += icon.size.height
contentSize.height += 14.0
let logo = logo.update(
component: BundleIconComponent(
name: "Premium/CocoonLogo", tintColor: nil
),
availableSize: context.availableSize,
transition: .immediate
)
context.add(logo
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + logo.size.height / 2.0))
)
contentSize.height += logo.size.height
contentSize.height += spacing - 8.0
context.add(text context.add(text
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)) .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0))
) )
contentSize.height += text.size.height contentSize.height += text.size.height
contentSize.height += spacing + 9.0 contentSize.height += spacing + 31.0
var items: [AnyComponentWithIdentity<Empty>] = [] var items: [AnyComponentWithIdentity<Empty>] = []
items.append( items.append(
@ -292,14 +341,14 @@ private final class CocoonInfoSheetContent: CombinedComponent {
let closeButton = closeButton.update( let closeButton = closeButton.update(
component: GlassBarButtonComponent( component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0), size: CGSize(width: 44.0, height: 44.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, backgroundColor: UIColor(rgb: 0x071533),
isDark: theme.overallDarkAppearance, isDark: true,
state: .generic, state: .tintedGlass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent( component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent( BundleIconComponent(
name: "Navigation/Close", name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor tintColor: .white
) )
)), )),
action: { [weak state] _ in action: { [weak state] _ in
@ -309,7 +358,7 @@ private final class CocoonInfoSheetContent: CombinedComponent {
state.dismiss(animated: true) state.dismiss(animated: true)
} }
), ),
availableSize: CGSize(width: 40.0, height: 40.0), availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate transition: .immediate
) )
context.add(closeButton context.add(closeButton
@ -667,3 +716,66 @@ private final class ParagraphComponent: CombinedComponent {
} }
} }
} }
private final class GradientBackgroundComponent: Component {
let colors: [UIColor]
init(
colors: [UIColor]
) {
self.colors = colors
}
static func ==(lhs: GradientBackgroundComponent, rhs: GradientBackgroundComponent) -> Bool {
if lhs.colors != rhs.colors {
return false
}
return true
}
public final class View: UIView {
private let gradientLayer: CAGradientLayer
private var component: GradientBackgroundComponent?
override init(frame: CGRect) {
self.gradientLayer = CAGradientLayer()
super.init(frame: frame)
self.layer.addSublayer(self.gradientLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: GradientBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.gradientLayer.frame = CGRect(origin: .zero, size: availableSize)
var locations: [NSNumber] = []
let delta = 1.0 / CGFloat(component.colors.count - 1)
for i in 0 ..< component.colors.count {
locations.append((delta * CGFloat(i)) as NSNumber)
}
self.gradientLayer.locations = locations
self.gradientLayer.colors = component.colors.reversed().map { $0.cgColor }
self.gradientLayer.type = .radial
self.gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
self.gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
self.component = component
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -886,12 +886,12 @@ final class NewContactScreenComponent: Component {
transition.setFrame(view: titleView, frame: titleFrame) transition.setFrame(view: titleView, frame: titleFrame)
} }
let barButtonSize = CGSize(width: 40.0, height: 40.0) let barButtonSize = CGSize(width: 44.0, height: 44.0)
let cancelButtonSize = self.cancelButton.update( let cancelButtonSize = self.cancelButton.update(
transition: transition, transition: transition,
component: AnyComponent(GlassBarButtonComponent( component: AnyComponent(GlassBarButtonComponent(
size: barButtonSize, size: barButtonSize,
backgroundColor: environment.theme.rootController.navigationBar.opaqueBackgroundColor, backgroundColor: nil,
isDark: environment.theme.overallDarkAppearance, isDark: environment.theme.overallDarkAppearance,
state: .glass, state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent( component: AnyComponentWithIdentity(id: "close", component: AnyComponent(

View file

@ -519,13 +519,13 @@ private final class SheetContent: CombinedComponent {
contentSize.height += navigation.size.height contentSize.height += navigation.size.height
let isBack = items.count > 1 let isBack = items.count > 1
let barButtonSize = CGSize(width: 40.0, height: 40.0) let barButtonSize = CGSize(width: 44.0, height: 44.0)
let backButton = backButton.update( let backButton = backButton.update(
component: GlassBarButtonComponent( component: GlassBarButtonComponent(
size: barButtonSize, size: barButtonSize,
backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, backgroundColor: nil,
isDark: environment.theme.overallDarkAppearance, isDark: environment.theme.overallDarkAppearance,
state: .generic, state: .glass,
component: AnyComponentWithIdentity(id: isBack ? "back" : "close", component: AnyComponent( component: AnyComponentWithIdentity(id: isBack ? "back" : "close", component: AnyComponent(
BundleIconComponent( BundleIconComponent(
name: isBack ? "Navigation/Back" : "Navigation/Close", name: isBack ? "Navigation/Back" : "Navigation/Close",

View file

@ -40,6 +40,7 @@ struct Particle {
kernel void dustEffectInitializeParticle( kernel void dustEffectInitializeParticle(
device Particle *particles [[ buffer(0) ]], device Particle *particles [[ buffer(0) ]],
const device float &verticalDirection [[ buffer(1) ]],
uint gid [[ thread_position_in_grid ]] uint gid [[ thread_position_in_grid ]]
) { ) {
Loki rng = Loki(gid); Loki rng = Loki(gid);
@ -49,7 +50,9 @@ kernel void dustEffectInitializeParticle(
float direction = rng.rand() * (3.14159265 * 2.0); float direction = rng.rand() * (3.14159265 * 2.0);
float velocity = (0.1 + rng.rand() * (0.2 - 0.1)) * 420.0; float velocity = (0.1 + rng.rand() * (0.2 - 0.1)) * 420.0;
particle.velocity = packed_float2(cos(direction) * velocity, sin(direction) * velocity); float2 initialVelocity = float2(cos(direction) * velocity, sin(direction) * velocity);
initialVelocity.y *= verticalDirection;
particle.velocity = packed_float2(initialVelocity);
particle.lifetime = 0.7 + rng.rand() * (1.5 - 0.7); particle.lifetime = 0.7 + rng.rand() * (1.5 - 0.7);
@ -107,6 +110,7 @@ kernel void dustEffectUpdateParticle(
const device uint2 &size [[ buffer(1) ]], const device uint2 &size [[ buffer(1) ]],
const device float &phase [[ buffer(2) ]], const device float &phase [[ buffer(2) ]],
const device float &timeStep [[ buffer(3) ]], const device float &timeStep [[ buffer(3) ]],
const device float &verticalDirection [[ buffer(4) ]],
uint gid [[ thread_position_in_grid ]] uint gid [[ thread_position_in_grid ]]
) { ) {
uint count = size.x * size.y; uint count = size.x * size.y;
@ -124,7 +128,7 @@ kernel void dustEffectUpdateParticle(
Particle particle = particles[gid]; Particle particle = particles[gid];
particle.offsetFromBasePosition += (particle.velocity * timeStep) * particleFraction; particle.offsetFromBasePosition += (particle.velocity * timeStep) * particleFraction;
particle.velocity += float2(0.0, timeStep * 120.0) * particleFraction; particle.velocity += float2(0.0, timeStep * 120.0 * verticalDirection) * particleFraction;
particle.lifetime = max(0.0, particle.lifetime - timeStep * particleFraction); particle.lifetime = max(0.0, particle.lifetime - timeStep * particleFraction);
particles[gid] = particle; particles[gid] = particle;
} }

View file

@ -146,6 +146,7 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject
public var animationSpeed: Float = 1.0 public var animationSpeed: Float = 1.0
public var playsBackwards: Bool = false public var playsBackwards: Bool = false
public var animateDown: Bool = false
public var becameEmpty: (() -> Void)? public var becameEmpty: (() -> Void)?
@ -280,6 +281,8 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject
return return
} }
var verticalDirection: Float = self.animateDown ? -1.0 : 1.0
for item in self.items { for item in self.items {
guard let particleBuffer = item.particleBuffer else { guard let particleBuffer = item.particleBuffer else {
continue continue
@ -297,6 +300,7 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject
if !item.particleBufferIsInitialized { if !item.particleBufferIsInitialized {
item.particleBufferIsInitialized = true item.particleBufferIsInitialized = true
computeEncoder.setComputePipelineState(state.computePipelineStateInitializeParticle) computeEncoder.setComputePipelineState(state.computePipelineStateInitializeParticle)
computeEncoder.setBytes(&verticalDirection, length: 4, index: 1)
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
} }
@ -309,6 +313,7 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject
var timeStep: Float = Float(lastTimeStep) / Float(UIView.animationDurationFactor()) var timeStep: Float = Float(lastTimeStep) / Float(UIView.animationDurationFactor())
timeStep *= 2.0 timeStep *= 2.0
computeEncoder.setBytes(&timeStep, length: 4, index: 3) computeEncoder.setBytes(&timeStep, length: 4, index: 3)
computeEncoder.setBytes(&verticalDirection, length: 4, index: 4)
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
} }
} }

View file

@ -48,6 +48,7 @@ swift_library(
"//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/EdgeEffect",
"//submodules/Components/MultilineTextWithEntitiesComponent", "//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/Components/ResizableSheetComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

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