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?)
}
public enum ResolvedUrlSettingsSection {
case theme
case devices
case autoremoveMessages
case twoStepAuth
case enableLog
case phonePrivacy
case loginEmail
}
public struct ResolvedBotChoosePeerTypes: OptionSet {
public var rawValue: UInt32
@ -315,13 +305,12 @@ public enum ResolvedUrl {
case share(url: String?, text: String?, to: String?)
case wallpaper(WallpaperUrlParameter)
case theme(String)
case settings(ResolvedUrlSettingsSection)
case joinVoiceChat(PeerId, String?)
case importStickers
case startAttach(peerId: PeerId, payload: String?, choose: ResolvedBotChoosePeerTypes?)
case invoice(slug: String, invoice: TelegramMediaInvoice?)
case premiumOffer(reference: String?)
case starsTopup(amount: Int64, purpose: String?)
case starsTopup(amount: Int64?, purpose: String?)
case chatFolder(slug: String)
case story(peerId: PeerId, id: Int32)
case boost(peerId: PeerId?, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus?)
@ -336,6 +325,53 @@ public enum ResolvedUrl {
case storyFolder(peerId: PeerId, id: Int64)
case giftCollection(peerId: PeerId, id: Int64)
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 {
@ -972,18 +1008,30 @@ public protocol MediaEditorScreenResult {
var target: Stories.PendingTarget { get }
}
public enum StoryCameraMode {
case photo
case video
case live
}
public protocol TelegramRootControllerInterface: NavigationController {
@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 getContactsController() -> ViewController?
func getChatsController() -> ViewController?
func getSettingsController() -> ViewController?
func getPrivacySettings() -> Promise<AccountPrivacySettings?>?
func openSettings()
func getTwoStepAuthData() -> Promise<TwoStepAuthData?>?
func openContacts()
func openSettings(edit: Bool)
func openBirthdaySetup()
func openPhotoSetup(completedWithUploadingImage: @escaping (UIImage, Signal<PeerInfoAvatarUploadStatus, NoError>) -> UIView?)
func openAvatars()
func startNewCall()
}
public protocol QuickReplySetupScreenInitialData: AnyObject {
@ -1185,6 +1233,7 @@ public enum ChannelMembersSearchControllerMode {
case promote
case ban
case inviteToCall
case ownershipTransfer
}
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 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 makeGiftCraftScreen(context: AccountContext, gift: StarGift.UniqueGift) -> ViewController
func makeGiftDemoScreen(context: AccountContext) -> 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)?)

View file

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

View file

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

View file

@ -779,7 +779,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
self.ignoreUpdatesUntilScrollingStopped = true
}
}
@available(iOS 13.0, *)
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
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 (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)
self.minimize()
if !url.contains("domain=oauth") {
self.minimize()
}
self.openAppUrl(url)
} 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)
if ["facetime"].contains(scheme) {
return
}
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: self.presentationData, navigationController: nil, dismissInput: {})
} else {
decisionHandler(.allow, preferences)

View file

@ -23,6 +23,20 @@ public enum CallListControllerMode {
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 let pressed: () -> Void
@ -69,6 +83,8 @@ private final class DeleteAllButtonNode: ASDisplayNode {
}
}
public final class CallListController: TelegramBaseController {
private var controllerNode: CallListControllerNode {
return self.displayNode as! CallListControllerNode
@ -81,6 +97,7 @@ public final class CallListController: TelegramBaseController {
private let context: AccountContext
private let mode: CallListControllerMode
private let focusOnItemTag: CallListEntryTag?
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
@ -96,9 +113,15 @@ public final class CallListController: TelegramBaseController {
private let clearDisposable = MetaDisposable()
private var createConferenceCallDisposable: Disposable?
public init(context: AccountContext, mode: CallListControllerMode) {
public init(
context: AccountContext,
mode: CallListControllerMode,
focusOnItemTag: CallListEntryTag? = nil
) {
self.context = context
self.mode = mode
self.focusOnItemTag = focusOnItemTag
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)
@ -409,7 +432,7 @@ public final class CallListController: TelegramBaseController {
if let strongSelf = self {
strongSelf.callPressed()
}
})
}, focusOnItemTag: self.focusOnItemTag)
if case .navigation = self.mode {
self.controllerNode.navigationBar = self.navigationBar
@ -421,6 +444,18 @@ public final class CallListController: TelegramBaseController {
}
self._ready.set(self.controllerNode.ready)
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 {

View file

@ -125,7 +125,7 @@ private func mappedInsertEntries(context: AccountContext, presentationData: Item
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
nodeInteraction.updateShowCallsTab(value)
}), directionHint: entry.directionHint)
}, tag: CallListEntryTag.showTab), directionHint: entry.directionHint)
case let .displayTabInfo(_, text):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
case .openNewCall:
@ -149,7 +149,7 @@ private func mappedUpdateEntries(context: AccountContext, presentationData: Item
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
nodeInteraction.updateShowCallsTab(value)
}), directionHint: entry.directionHint)
}, tag: CallListEntryTag.showTab), directionHint: entry.directionHint)
case let .displayTabInfo(_, text):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
case .openNewCall:
@ -184,6 +184,7 @@ final class CallListControllerNode: ASDisplayNode {
private let context: AccountContext
private let mode: CallListControllerMode
private var presentationData: PresentationData
private var focusOnItemTag: CallListEntryTag?
private var containerLayout: (ContainerViewLayout, CGFloat)?
@ -241,7 +242,7 @@ final class CallListControllerNode: ASDisplayNode {
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.context = context
self.mode = mode
@ -253,6 +254,7 @@ final class CallListControllerNode: ASDisplayNode {
self.openNewCall = openNewCall
self.currentState = CallListNodeState(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: true, editing: false, messageIdWithRevealedOptions: nil)
self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true)
self.focusOnItemTag = focusOnItemTag
self.listNode = ListView()
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
@ -861,6 +863,16 @@ final class CallListControllerNode: ASDisplayNode {
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()
}
}

View file

@ -125,6 +125,10 @@ swift_library(
"//submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent",
"//submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode",
"//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:public",

View file

@ -144,7 +144,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private var didSuggestLoginEmailSetup = false
private var didSuggestLoginPasskeySetup = false
private var presentationData: PresentationData
private(set) var presentationData: PresentationData
private let presentationDataValue = Promise<PresentationData>()
private var presentationDataDisposable: Disposable?
@ -3029,7 +3029,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
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()
}
}
@ -3511,7 +3511,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
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 case .chatList(.root) = self.chatListDisplayNode.effectiveContainerNode.location {
self.effectiveContext?.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent(
@ -5874,10 +5884,33 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
], parseMarkdown: true), in: .window(.root))
} else {
completion(true)
self.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
removed()
})
let proceed = {
completion(true)
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 {
coordinator = current
} 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 {
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
if let initialPreset {
initialName = initialPreset.title

View file

@ -45,6 +45,8 @@ private enum ChatListFilterPresetListSection: Int32 {
}
public enum ChatListFilterPresetListEntryTag: ItemListItemTag {
case edit
case addRecommended
case displayTags
public func isEqual(to other: ItemListItemTag) -> Bool {
@ -175,7 +177,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
case let .screenHeader(text):
return ChatListFilterSettingsHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animation: .folders, sectionId: self.section)
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):
return ChatListFilterPresetListSuggestedItem(presentationData: presentationData, systemStyle: .glass, title: title.text, label: label, sectionId: self.section, style: .blocks, installAction: {
arguments.addSuggestedPressed(title, preset)
@ -318,7 +320,7 @@ public enum ChatListFilterPresetListControllerMode {
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 statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
@ -326,6 +328,14 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
statePromise.set(stateValue.modify { f($0) })
}
if focusOnItemTag == .edit {
updateState { state in
var state = state
state.isEditing = true
return state
}
}
var dismissImpl: (() -> Void)?
var pushControllerImpl: ((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 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))
}
@ -797,6 +807,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
}
})
})
var didFocusOnItem = false
controller.afterTransactionCompleted = { [weak controller] in
guard let toggleDirection = animateNextShowHideTagsTransition.swap(nil) else {
return
@ -819,6 +831,15 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
}
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

View file

@ -193,7 +193,7 @@ public final class FilledRoundedRectangleComponent: Component {
}
} else {
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 {
} else {
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)
}

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 {
var dismissImpl: (() -> Void)?
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 {
func snapshotContentTree(unhide: Bool = false, keepPortals: Bool = false, keepTransform: Bool = false) -> UIView? {
let wasHidden = self.isHidden

View file

@ -35,8 +35,9 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
let noInsets: Bool
public let sectionId: ItemListSectionId
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.style = style
self.systemStyle = systemStyle
@ -52,6 +53,7 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
self.color = color
self.sectionId = sectionId
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) {
@ -133,6 +135,10 @@ public final class ItemListPeerActionItemNode: ListViewItemNode {
private let iconDisposable = MetaDisposable()
public var tag: ItemListItemTag? {
return self.item?.tag as? ItemListItemTag
}
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true

View file

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

View file

@ -42,8 +42,9 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem {
public let sectionId: ItemListSectionId
let action: () -> 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.systemStyle = systemStyle
self.icon = icon
@ -60,6 +61,7 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem {
self.sectionId = sectionId
self.action = action
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) {
@ -127,6 +129,10 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
return self.contentParentNode
}
public var tag: ItemListItemTag? {
return self.item?.tag as? ItemListItemTag
}
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true

View file

@ -173,6 +173,7 @@ private let boldBadgeFont = Font.semibold(14.0)
public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let highlightNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
@ -215,6 +216,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.highlightNode = ASDisplayNode()
self.highlightNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
@ -283,6 +287,20 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
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) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode.textNode)
let makeTitleWithEntitiesLayout = TextNodeWithEntities.asyncLayout(self.titleNode)
@ -600,6 +618,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
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 {
@ -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.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.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))

View file

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

View file

@ -147,6 +147,7 @@ extension IconSwitchNode: ItemListSwitchNodeImpl {
public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let highlightNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
@ -175,6 +176,9 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.highlightNode = ASDisplayNode()
self.highlightNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
@ -234,6 +238,20 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
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) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
@ -400,14 +418,13 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.switchNode.frameColor = item.presentationData.theme.list.itemSwitchColors.frameColor
strongSelf.switchNode.contentColor = item.presentationData.theme.list.itemSwitchColors.contentColor
strongSelf.switchNode.handleColor = item.presentationData.theme.list.itemSwitchColors.handleColor
strongSelf.switchNode.positiveContentColor = item.presentationData.theme.list.itemSwitchColors.positiveColor
strongSelf.switchNode.negativeContentColor = item.presentationData.theme.list.itemSwitchColors.negativeColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
strongSelf.highlightNode.backgroundColor = item.presentationData.theme.list.itemSearchHighlightColor
}
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
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.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)))

View file

@ -121,7 +121,12 @@ NSString *const kYUVVideoRangeConversionForLAFragmentShaderString = SHADER_STRIN
#pragma mark Initialization and teardown
- (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];
if (t.a == -1 && t.d == -1) {

View file

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

View file

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

View file

@ -6,6 +6,7 @@ import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import SearchUI
import CounterControllerTitleView
public final class ChannelMembersSearchControllerImpl: ViewController, ChannelMembersSearchController {
private let queue = Queue()
@ -49,7 +50,18 @@ public final class ChannelMembersSearchControllerImpl: ViewController, ChannelMe
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))
@ -74,16 +86,28 @@ public final class ChannelMembersSearchControllerImpl: ViewController, ChannelMe
}
strongSelf.presentationData = 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)
|> deliverOnMainQueue).start(next: { [weak self] peer in
if let strongSelf = self {
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
strongSelf.title = strongSelf.presentationData.strings.Channel_Subscribers_Title
guard let self, let peer else {
return
}
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 {
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
switch participant {
@ -531,7 +538,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
var label: String?
var enabled = true
switch mode {
case .ban, .promote:
case .ban, .promote, .ownershipTransfer:
if participant.peer.id == context.account.peerId {
continue participantsLoop
}

View file

@ -158,10 +158,10 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .generic,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
@ -172,7 +172,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
component.cancel(true)
}
),
availableSize: CGSize(width: 40.0, height: 40.0),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
@ -496,11 +496,11 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
)
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
.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

View file

@ -1,4 +1,5 @@
import Foundation
import Foundation
import UIKit
import Display
import ComponentFlow
@ -38,6 +39,11 @@ import PremiumStarComponent
import PremiumCoinComponent
import EdgeEffect
public enum PremiumIntroEntryTag {
case doNotHideAds
}
private let doNotHideAdsTag = GenericComponentViewTag()
public enum PremiumSource: Equatable {
public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool {
switch lhs {
@ -2677,7 +2683,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let _ = accountContext.engine.accountData.updateAdMessagesEnabled(enabled: value).startStandalone()
state?.updated(transition: .immediate)
})),
action: nil
action: nil,
tag: doNotHideAdsTag
))))
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
fileprivate let mode: Mode
private let focusOnItemTag: PremiumIntroEntryTag?
private var didSetReady = false
private let _ready = Promise<Bool>()
@ -3932,13 +3940,14 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
private let overNavigationContainer: UIView
public convenience init(context: AccountContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false) {
self.init(screenContext: .accountContext(context), mode: mode, source: source, modal: modal, forceDark: forceDark, forceHasPremium: forceHasPremium)
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, 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.mode = mode
self.focusOnItemTag = focusOnItemTag
let presentationData = screenContext.presentationData
@ -4066,6 +4075,20 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
override public func 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) {

View file

@ -857,10 +857,10 @@ private final class LimitSheetContent: CombinedComponent {
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .generic,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
@ -872,7 +872,7 @@ private final class LimitSheetContent: CombinedComponent {
component.cancel()
}
),
availableSize: CGSize(width: 40.0, height: 40.0),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
context.add(closeButton
@ -1519,7 +1519,7 @@ private final class LimitSheetContent: CombinedComponent {
}
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

View file

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

View file

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

View file

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

View file

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

View file

@ -41,6 +41,23 @@ private enum AutodownloadMediaCategorySection: Int32 {
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 {
case master(PresentationTheme, String, Bool)
case dataUsageHeader(PresentationTheme, String)
@ -155,31 +172,31 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry {
case let .master(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleMaster(value)
})
}, tag: AutodownloadMediaCategoryEntryTag.master)
case let .dataUsageHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .dataUsageItem(theme, strings, value, customPosition, enabled):
return AutodownloadDataUsagePickerItem(theme: theme, strings: strings, systemStyle: .glass, value: value, customPosition: customPosition, enabled: enabled, sectionId: self.section, updated: { preset in
arguments.changePreset(preset)
})
}, tag: AutodownloadMediaCategoryEntryTag.usage)
case let .typesHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .photos(_, text, value, enabled):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Photos")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: {
arguments.customize(.photo)
})
}, tag: AutodownloadMediaCategoryEntryTag.photos)
case let .stories(_, text, value, enabled):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Stories")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: {
arguments.customize(.story)
})
}, tag: AutodownloadMediaCategoryEntryTag.stories)
case let .videos(_, text, value, enabled):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Videos")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: {
arguments.customize(.video)
})
}, tag: AutodownloadMediaCategoryEntryTag.videos)
case let .files(_, text, value, enabled):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Files")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: {
arguments.customize(.file)
})
}, tag: AutodownloadMediaCategoryEntryTag.files)
case let .voiceMessagesInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
@ -300,7 +317,7 @@ private func autodownloadMediaConnectionTypeControllerEntries(presentationData:
return entries
}
func autodownloadMediaConnectionTypeController(context: AccountContext, connectionType: AutomaticDownloadConnectionType) -> ViewController {
func autodownloadMediaConnectionTypeController(context: AccountContext, connectionType: AutomaticDownloadConnectionType, focusOnItemTag: AutodownloadMediaCategoryEntryTag? = nil) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)?
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 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))
}
@ -379,5 +396,20 @@ func autodownloadMediaConnectionTypeController(context: AccountContext, connecti
(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
}

View file

@ -40,8 +40,9 @@ final class AutodownloadDataUsagePickerItem: ListViewItem, ItemListItem {
let enabled: Bool
let sectionId: ItemListSectionId
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.strings = strings
self.systemStyle = systemStyle
@ -50,6 +51,7 @@ final class AutodownloadDataUsagePickerItem: ListViewItem, ItemListItem {
self.enabled = enabled
self.sectionId = sectionId
self.updated = updated
self.tag = tag
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -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 topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
@ -103,6 +105,10 @@ private final class AutodownloadDataUsagePickerItemNode: ListViewItemNode {
private var item: AutodownloadDataUsagePickerItem?
private var layoutParams: ListViewItemLayoutParams?
public var tag: ItemListItemTag? {
return self.item?.tag
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true

View file

@ -58,8 +58,9 @@ final class AutodownloadSizeLimitItem: ListViewItem, ItemListItem {
let range: Range<Int64>?
let sectionId: ItemListSectionId
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.strings = strings
self.systemStyle = systemStyle
@ -69,6 +70,7 @@ final class AutodownloadSizeLimitItem: ListViewItem, ItemListItem {
self.range = range
self.sectionId = sectionId
self.updated = updated
self.tag = tag
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -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 topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
@ -119,6 +121,10 @@ private final class AutodownloadSizeLimitItemNode: ListViewItemNode {
private var item: AutodownloadSizeLimitItem?
private var layoutParams: ListViewItemLayoutParams?
public var tag: ItemListItemTag? {
return self.item?.tag
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true

View file

@ -90,6 +90,7 @@ public enum DataAndStorageEntryTag: ItemListItemTag, Equatable {
case raiseToListen
case autoSave(AutomaticSaveIncomingPeerType)
case sensitiveContent
case useLessVoiceData
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? DataAndStorageEntryTag, self == other {
@ -409,7 +410,7 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
case let .useLessVoiceData(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleVoiceUseLessData(value)
}, tag: nil)
}, tag: DataAndStorageEntryTag.useLessVoiceData)
case let .useLessVoiceDataInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .otherHeader(_, text):
@ -1012,6 +1013,20 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
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
}

View file

@ -10,7 +10,7 @@ import PresentationDataUtils
import AccountContext
import UndoUI
enum ItemType: CaseIterable {
public enum EnergySavingItemType: CaseIterable {
case autoplayVideo
case autoplayGif
case loopStickers
@ -88,10 +88,10 @@ enum ItemType: CaseIterable {
private final class EnergeSavingSettingsScreenArguments {
let updateThreshold: (Int32) -> Void
let toggleItem: (ItemType) -> Void
let toggleItem: (EnergySavingItemType) -> 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.toggleItem = toggleItem
self.displayDisabledTooltip = displayDisabledTooltip
@ -103,19 +103,31 @@ private enum EnergeSavingSettingsScreenSection: Int32 {
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 {
enum StableId: Hashable {
case allHeader
case all
case allFooter
case itemsHeader
case item(ItemType)
case item(EnergySavingItemType)
}
case allHeader(Bool?)
case all(Int32)
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
var section: ItemListSectionId {
@ -197,7 +209,7 @@ private enum EnergeSavingSettingsScreenEntry: ItemListNodeEntry {
arguments.toggleItem(type)
}, activatedWhileDisabled: {
arguments.displayDisabledTooltip()
})
}, tag: EnergySavingEntryTag.item(type))
}
}
}
@ -241,14 +253,14 @@ private func energeSavingSettingsScreenEntries(
}
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))
}
return entries
}
public func energySavingSettingsScreen(context: AccountContext) -> ViewController {
public func energySavingSettingsScreen(context: AccountContext, focusOnItemTag: EnergySavingEntryTag? = nil) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)?
let _ = pushControllerImpl
@ -300,7 +312,7 @@ public func energySavingSettingsScreen(context: AccountContext) -> ViewControlle
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
animateChanges: false
)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: energeSavingSettingsScreenEntries(presentationData: presentationData, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: energeSavingSettingsScreenEntries(presentationData: presentationData, settings: automaticMediaDownloadSettings), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: true)
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)
}
}
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
}

View file

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

View file

@ -58,6 +58,21 @@ private enum ProxySettingsControllerEntryId: Equatable, Hashable {
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 {
case enabled(PresentationTheme, String, Bool, Bool)
case serversHeader(PresentationTheme, String)
@ -207,7 +222,7 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry {
} else {
arguments.toggleEnabled(value)
}
})
}, tag: ProxySettingsEntryTag.useProxy)
case let .serversHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .addServer(_, text, _):
@ -227,11 +242,11 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry {
case let .shareProxyList(_, text):
return ProxySettingsActionItem(presentationData: presentationData, systemStyle: .glass, title: text, sectionId: self.section, editing: false, action: {
arguments.shareProxyList()
})
}, tag: ProxySettingsEntryTag.shareList)
case let .useForCalls(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleUseForCalls(value)
})
}, tag: ProxySettingsEntryTag.useForCalls)
case let .useForCallsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
@ -308,12 +323,12 @@ public enum ProxySettingsControllerMode {
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 }
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 dismissImpl: (() -> Void)?
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)?
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 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))
}
@ -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
}

View file

@ -20,8 +20,9 @@ final class ProxySettingsActionItem: ListViewItem, ItemListItem {
let editing: Bool
let sectionId: ItemListSectionId
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.systemStyle = systemStyle
self.title = title
@ -29,6 +30,7 @@ final class ProxySettingsActionItem: ListViewItem, ItemListItem {
self.editing = editing
self.sectionId = sectionId
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) {
@ -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 topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
@ -89,6 +91,10 @@ private final class ProxySettingsActionItemNode: ListViewItemNode {
private var item: ProxySettingsActionItem?
public var tag: ItemListItemTag? {
return self.item?.tag
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true

View file

@ -48,6 +48,20 @@ enum SaveIncomingMediaSection: ItemListSectionId {
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 {
enum StableId: Hashable {
case peer
@ -200,7 +214,7 @@ private enum SaveIncomingMediaEntry: ItemListNodeEntry {
case let .videoSize(decimalSeparator, text, size):
return AutodownloadSizeLimitItem(theme: presentationData.theme, strings: presentationData.strings, systemStyle: .glass, decimalSeparator: decimalSeparator, text: text, value: size, range: nil/*2 * 1024 * 1024 ..< (4 * 1024 * 1024 * 1024)*/, sectionId: self.section, updated: { value in
arguments.updateMaximumVideoSize(value)
})
}, tag: SaveIncomingMediaEntryTag.maxVideoSize)
case let .videoInfo(text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .exceptionsHeader(title):
@ -209,7 +223,7 @@ private enum SaveIncomingMediaEntry: ItemListNodeEntry {
let icon: UIImage? = PresentationResourcesItemList.createGroupIcon(presentationData.theme)
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: icon, title: title, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: {
arguments.openAddException()
})
}, tag: SaveIncomingMediaEntryTag.addException)
case let .exceptionItem(_, peer, label):
return ItemListPeerItem(
presentationData: presentationData,
@ -248,7 +262,7 @@ private enum SaveIncomingMediaEntry: ItemListNodeEntry {
case let .deleteAllExceptions(title):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.deleteAllExceptions()
})
}, tag: SaveIncomingMediaEntryTag.deleteExceptions)
}
}
}
@ -352,7 +366,7 @@ private func saveIncomingMediaControllerEntries(presentationData: PresentationDa
return entries
}
enum SaveIncomingMediaScope {
public enum SaveIncomingMediaScope {
case peer(EnginePeer.Id)
case addPeer(id: EnginePeer.Id, completion: (MediaAutoSaveConfiguration) -> Void)
case peerType(AutomaticSaveIncomingPeerType)
@ -363,7 +377,7 @@ private struct SaveIncomingMediaControllerState: Equatable {
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 statePromise = ValuePromise<SaveIncomingMediaControllerState>(stateValue.with { $0 })
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 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))
}
@ -743,6 +757,20 @@ func saveIncomingMediaController(context: AccountContext, scope: SaveIncomingMed
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
}

View file

@ -8,9 +8,24 @@ import TelegramCore
import TelegramPresentationData
import AccountContext
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 {
private let context: AccountContext
private let focusOnItemTag: LocalizationListEntryTag?
private var controllerNode: LocalizationListControllerNode {
return self.displayNode as! LocalizationListControllerNode
@ -31,8 +46,9 @@ public class LocalizationListController: ViewController {
private var previousContentOffset: ListViewVisibleContentOffset?
public init(context: AccountContext) {
public init(context: AccountContext, focusOnItemTag: LocalizationListEntryTag? = nil) {
self.context = context
self.focusOnItemTag = focusOnItemTag
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -125,7 +141,7 @@ public class LocalizationListController: ViewController {
self?.present(c, in: .window(.root), with: a)
}, push: { [weak self] c in
self?.push(c)
})
}, focusOnItemTag: self.focusOnItemTag)
self.controllerNode.listNode.visibleContentOffsetChanged = { [weak self] offset in
if let strongSelf = self {

View file

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

View file

@ -77,9 +77,12 @@ private enum NotificationsPeerCategorySection: Int32 {
}
public enum NotificationsPeerCategoryEntryTag: ItemListItemTag {
case edit
case enable
case previews
case sound
case important
case deleteExceptions
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? NotificationsPeerCategoryEntryTag, self == other {
@ -284,7 +287,7 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
case let .enableImportant(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateEnabledImportant(updatedValue)
}, tag: self.tag)
}, tag: NotificationsPeerCategoryEntryTag.important)
case let .importantInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .optionsHeader(_, text):
@ -314,7 +317,7 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
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: {
arguments.removeAllExceptions()
})
}, tag: NotificationsPeerCategoryEntryTag.deleteExceptions)
}
}
}
@ -617,6 +620,12 @@ public func notificationsPeerCategoryController(context: AccountContext, categor
updatedMode(result.mode)
}
if focusOnItemTag == .edit {
updateState {
$0.withUpdatedEditing(true)
}
}
let updatePeerSound: (EnginePeer.Id, PeerMessageSound) -> Signal<Void, NoError> = { peerId, sound in
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
(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
}

View file

@ -203,13 +203,19 @@ private func blockedPeersControllerEntries(presentationData: PresentationData, s
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 stateValue = Atomic(value: BlockedPeersControllerState())
let updateState: ((BlockedPeersControllerState) -> BlockedPeersControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
if forceEdit {
updateState {
$0.withUpdatedEditing(true)
}
}
var pushControllerImpl: ((ViewController) -> Void)?
let actionsDisposable = DisposableSet()

View file

@ -36,7 +36,7 @@ private final class DataPrivacyControllerArguments {
}
}
private enum PrivacyAndSecuritySection: Int32 {
private enum DataPrivacySection: Int32 {
case contacts
case frequentContacts
case chats
@ -45,7 +45,24 @@ private enum PrivacyAndSecuritySection: Int32 {
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 deleteContacts(PresentationTheme, String, Bool)
case syncContacts(PresentationTheme, String, Bool)
@ -70,17 +87,17 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
var section: ItemListSectionId {
switch self {
case .contactsHeader, .deleteContacts, .syncContacts, .syncContactsInfo:
return PrivacyAndSecuritySection.contacts.rawValue
return DataPrivacySection.contacts.rawValue
case .frequentContacts, .frequentContactsInfo:
return PrivacyAndSecuritySection.frequentContacts.rawValue
return DataPrivacySection.frequentContacts.rawValue
case .chatsHeader, .deleteCloudDrafts:
return PrivacyAndSecuritySection.chats.rawValue
return DataPrivacySection.chats.rawValue
case .paymentHeader, .clearPaymentInfo, .paymentInfo:
return PrivacyAndSecuritySection.payments.rawValue
return DataPrivacySection.payments.rawValue
case .secretChatLinkPreviewsHeader, .secretChatLinkPreviews, .secretChatLinkPreviewsInfo:
return PrivacyAndSecuritySection.secretChats.rawValue
return DataPrivacySection.secretChats.rawValue
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 {
case let .contactsHeader(lhsTheme, lhsText):
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
}
@ -231,17 +248,17 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
case let .deleteContacts(_, text, value):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.deleteContacts()
})
}, tag: DataPrivacyEntryTag.deleteSynced)
case let .syncContacts(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateSyncContacts(updatedValue)
})
}, tag: DataPrivacyEntryTag.syncContacts)
case let .syncContactsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .frequentContacts(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: !value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateSuggestFrequentContacts(updatedValue)
})
}, tag: DataPrivacyEntryTag.suggestContacts)
case let .frequentContactsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .chatsHeader(_, text):
@ -249,13 +266,13 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
case let .deleteCloudDrafts(_, text, value):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.deleteCloudDrafts()
})
}, tag: DataPrivacyEntryTag.deleteCloudDrafts)
case let .paymentHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .clearPaymentInfo(_, text, enabled):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.clearPaymentInfo()
})
}, tag: DataPrivacyEntryTag.clearPaymentInfo)
case let .paymentInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .secretChatLinkPreviewsHeader(_, text):
@ -263,7 +280,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
case let .secretChatLinkPreviews(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateSecretChatLinkPreviews(updatedValue)
})
}, tag: DataPrivacyEntryTag.linkPreviews)
case let .secretChatLinkPreviewsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case .botList:
@ -289,8 +306,8 @@ private struct DataPrivacyControllerState: Equatable {
var deletingCloudDrafts: Bool = false
}
private func dataPrivacyControllerEntries(presentationData: PresentationData, state: DataPrivacyControllerState, secretChatLinkPreviews: Bool?, synchronizeDeviceContacts: Bool, frequentContacts: Bool, hasBotSettings: Bool) -> [PrivacyAndSecurityEntry] {
var entries: [PrivacyAndSecurityEntry] = []
private func dataPrivacyControllerEntries(presentationData: PresentationData, state: DataPrivacyControllerState, secretChatLinkPreviews: Bool?, synchronizeDeviceContacts: Bool, frequentContacts: Bool, hasBotSettings: Bool) -> [DataPrivacyEntry] {
var entries: [DataPrivacyEntry] = []
entries.append(.contactsHeader(presentationData.theme, presentationData.strings.Privacy_ContactsTitle))
entries.append(.deleteContacts(presentationData.theme, presentationData.strings.Privacy_ContactsReset, !state.deletingContacts))
@ -317,7 +334,7 @@ private func dataPrivacyControllerEntries(presentationData: PresentationData, st
return entries
}
public func dataPrivacyController(context: AccountContext) -> ViewController {
public func dataPrivacyController(context: AccountContext, focusOnItemTag: DataPrivacyEntryTag? = nil) -> ViewController {
let statePromise = ValuePromise(DataPrivacyControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: DataPrivacyControllerState())
let updateState: ((DataPrivacyControllerState) -> DataPrivacyControllerState) -> Void = { f in
@ -434,7 +451,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController {
}
if canBegin {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_ContactsResetConfirmation, actions: [TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
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
updateState { state in
var state = state
@ -461,7 +478,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_ContactsReset_ContactsDeleted, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }))
}))
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})]))
})]))
}
}, updateSyncContacts: { value in
let _ = context.engine.contacts.updateIsContactSynchronizationEnabled(isContactSynchronizationEnabled: value).start()
@ -556,7 +573,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController {
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))
}
@ -572,5 +589,19 @@ public func dataPrivacyController(context: AccountContext) -> ViewController {
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
}

View file

@ -38,6 +38,18 @@ private enum GlobalAutoremoveSection: Int32 {
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 {
case header
case sectionHeader(String)
@ -126,7 +138,7 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry {
case let .customAction(text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openCustomValue()
})
}, tag: GlobalAutoremoveEntryTag.setCustom)
case let .info(text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in
arguments.infoLinkAction()
@ -188,7 +200,7 @@ private func globalAutoremoveScreenEntries(presentationData: PresentationData, s
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(
additionalValues: Set([initialValue]),
updatedValue: initialValue
@ -430,7 +442,7 @@ public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32,
let animateChanges = false
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges, scrollEnabled: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges, scrollEnabled: true)
return (controllerState, (listState, arguments))
}
@ -461,5 +473,19 @@ public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32,
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
}

View file

@ -48,6 +48,19 @@ private enum IncomingMessagePrivacySection: Int32 {
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 {
case header
case optionEverybody(value: GlobalPrivacySettings.NonContactChatsPrivacy)
@ -166,7 +179,7 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry {
case let .exceptions(count):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: presentationData.strings.Privacy_Messages_RemoveFee, label: count > 0 ? "\(count)" : "", sectionId: self.section, style: .blocks, action: {
arguments.openExceptions()
})
}, tag: IncomingMessagePrivacyEntryTag.removeFee)
case .exceptionsInfo:
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
}
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] = [:]
if case let .enableContacts(value, _, _, _) = exceptions {
disableFor = value
@ -424,7 +437,7 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP
let animateChanges = false
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges, scrollEnabled: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges, scrollEnabled: true)
return (controllerState, (listState, arguments))
}
@ -468,5 +481,19 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP
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
}

View file

@ -35,6 +35,21 @@ private enum PasscodeOptionsSection: Int32 {
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 {
case togglePasscode(PresentationTheme, String, Bool)
case changePasscode(PresentationTheme, String)
@ -114,21 +129,21 @@ private enum PasscodeOptionsEntry: ItemListNodeEntry {
if value {
arguments.turnPasscodeOff()
}
})
}, tag: PasscodeOptionsEntryTag.togglePasscode)
case let .changePasscode(_, title):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.changePasscode()
})
}, tag: PasscodeOptionsEntryTag.changePasscode)
case let .settingInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .autoLock(_, title, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, label: value, sectionId: self.section, style: .blocks, action: {
arguments.changePasscodeTimeout()
})
}, tag: PasscodeOptionsEntryTag.autolock)
case let .touchId(_, title, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.changeTouchId(value)
})
}, tag: PasscodeOptionsEntryTag.touchId)
}
}
}
@ -206,7 +221,7 @@ private func passcodeOptionsControllerEntries(presentationData: PresentationData
return entries
}
func passcodeOptionsController(context: AccountContext) -> ViewController {
func passcodeOptionsController(context: AccountContext, focusOnItemTag: PasscodeOptionsEntryTag? = nil) -> ViewController {
let initialState = PasscodeOptionsControllerState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
@ -360,7 +375,7 @@ func passcodeOptionsController(context: AccountContext) -> ViewController {
|> map { presentationData, state, passcodeOptionsData -> (ItemListControllerState, (ItemListNodeState, Any)) in
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PasscodeSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: passcodeOptionsControllerEntries(presentationData: presentationData, state: state, passcodeOptionsData: passcodeOptionsData), style: .blocks, emptyStateItem: nil, animateChanges: false)
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))
} |> afterDisposed {
@ -383,6 +398,20 @@ func passcodeOptionsController(context: AccountContext) -> ViewController {
(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
}

View file

@ -1629,5 +1629,19 @@ public func privacyAndSecurityController(
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
}

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 {
case header(SortIndex, String)
case currentSessionHeader(SortIndex, String)
@ -341,7 +355,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
case let .terminateOtherSessions(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.blockDestructiveIcon(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: {
arguments.terminateOtherSessions()
})
}, tag: RecentSessionsEntryTag.terminateOtherSessions)
case let .terminateAllWebSessions(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.blockDestructiveIcon(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: {
arguments.terminateAllWebSessions()
@ -403,7 +417,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
case let .ttlTimeout(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, action: {
arguments.setupAuthorizationTTL()
}, tag: PrivacyAndSecurityEntryTag.accountTimeout)
}, tag: RecentSessionsEntryTag.autoTerminate)
}
}
}
@ -560,13 +574,19 @@ private func recentSessionsControllerEntries(presentationData: PresentationData,
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 stateValue = Atomic(value: RecentSessionsControllerState())
let updateState: ((RecentSessionsControllerState) -> RecentSessionsControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
if focusOnItemTag == .edit {
updateState {
$0.withUpdatedEditing(true)
}
}
activeSessionsContext.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 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))
} |> afterDisposed {
@ -891,5 +911,19 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
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
}

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 {
case forwardsPreviewHeader(PresentationTheme, 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):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, enabled: isEnabled, label: value, sectionId: self.section, style: .blocks, action: {
arguments.openSelective(.main, false)
})
}, tag: SelectivePrivacyEntryTag.neverAllow)
case let .enableFor(_, title, value, isEnabled):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, enabled: isEnabled, label: value, sectionId: self.section, style: .blocks, action: {
arguments.openSelective(.main, true)
})
}, tag: SelectivePrivacyEntryTag.alwaysAllow)
case let .peersInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .callsP2PHeader(_, text):
@ -632,17 +656,17 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry {
case let .callsP2PDisableFor(_, title, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, label: value, sectionId: self.section, style: .blocks, action: {
arguments.openSelective(.callP2P, false)
})
}, tag: SelectivePrivacyEntryTag.callsP2PNeverAllow)
case let .callsP2PEnableFor(_, title, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, label: value, sectionId: self.section, style: .blocks, action: {
arguments.openSelective(.callP2P, true)
})
}, tag: SelectivePrivacyEntryTag.callsP2PAlwaysAllow)
case let .callsP2PPeersInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .callsIntegrationEnabled(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateCallIntegrationEnabled?(value)
})
}, tag: SelectivePrivacyEntryTag.callsIntegration)
case let .callsIntegrationInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .phoneDiscoveryHeader(_, text):
@ -662,18 +686,18 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry {
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: {
arguments.setPublicPhoto?()
})
}, tag: SelectivePrivacyEntryTag.photoSetPublic)
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: {
arguments.removePublicPhoto?()
})
}, tag: SelectivePrivacyEntryTag.photoRemovePublic)
case let .publicPhotoInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in
})
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
arguments.updateHideReadTime?(value)
})
}, tag: SelectivePrivacyEntryTag.lastSeenHideReadTime)
case let .hideReadTimeInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .subscribeToPremium(_, text):
@ -683,7 +707,7 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry {
case let .subscribeToPremiumInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
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):
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 {
@ -747,7 +771,7 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry {
if available {
arguments.displayLockedGiftsInfo()
}
})
}, tag: SelectivePrivacyEntryTag.giftsShowButton)
case let .showGiftButtonInfo(_, text):
let attributedString = NSMutableAttributedString(string: text, font: Font.regular(presentationData.fontSize.itemListBaseHeaderFontSize), textColor: presentationData.theme.list.freeTextColor)
if let range = attributedString.string.range(of: "#") {
@ -1216,6 +1240,7 @@ public func selectivePrivacySettingsController(
requestPublicPhotoSetup: ((@escaping (UIImage?) -> Void) -> Void)? = nil,
requestPublicPhotoRemove: ((@escaping () -> Void) -> Void)? = nil,
openedFromBirthdayScreen: Bool = false,
focusOnItemTag: SelectivePrivacyEntryTag? = nil,
updated: @escaping (SelectivePrivacySettings, (SelectivePrivacySettings, VoiceCallSettings)?, Bool?, GlobalPrivacySettings?) -> Void
) -> ViewController {
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
@ -1910,5 +1935,20 @@ public func selectivePrivacySettingsController(
dismissImpl = { [weak controller] in
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
}

View file

@ -50,19 +50,15 @@ private enum TwoStepVerificationUnlockSettingsSection: Int32 {
case email
}
private enum TwoStepVerificationUnlockSettingsEntryTag: ItemListItemTag {
public enum TwoStepVerificationUnlockSettingsEntryTag: ItemListItemTag {
case password
case change
case disable
case changeEmail
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? TwoStepVerificationUnlockSettingsEntryTag {
switch self {
case .password:
if case .password = other {
return true
} else {
return false
}
}
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? TwoStepVerificationUnlockSettingsEntryTag, self == other {
return true
} else {
return false
}
@ -159,15 +155,15 @@ private enum TwoStepVerificationUnlockSettingsEntry: ItemListNodeEntry {
case let .changePassword(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openSetupPassword()
})
}, tag: TwoStepVerificationUnlockSettingsEntryTag.change)
case let .turnPasswordOff(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openDisablePassword()
})
}, tag: TwoStepVerificationUnlockSettingsEntryTag.disable)
case let .setupRecoveryEmail(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openSetupEmail()
})
}, tag: TwoStepVerificationUnlockSettingsEntryTag.changeEmail)
case let .passwordInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .pendingEmailConfirmInfo(_, text):
@ -283,7 +279,7 @@ public enum TwoStepVerificationUnlockSettingsControllerData: Equatable {
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 statePromise = ValuePromise(initialState, ignoreRepeated: true)
@ -463,7 +459,7 @@ public func twoStepVerificationUnlockSettingsController(context: AccountContext,
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
updateState { state in
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 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))
}
@ -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
}

View file

@ -53,6 +53,22 @@ private enum ReactionNotificationSettingsSection: Int32 {
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 {
enum StableId: Hashable {
case categoriesHeader
@ -169,23 +185,23 @@ private enum ReactionNotificationSettingsEntry: ItemListNodeEntry {
arguments.toggleMessages(value)
}, action: {
arguments.openMessages()
})
}, tag: ReactionNotificationSettingsEntryTag.messages)
case let .stories(title, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, text: text, textColor: .accent, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleStories(value)
}, action: {
arguments.openStories()
})
}, tag: ReactionNotificationSettingsEntryTag.stories)
case let .optionsHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .previews(text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updatePreviews(value)
})
}, tag: ReactionNotificationSettingsEntryTag.showSender)
case let .sound(text, value, sound):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openSound(sound)
}, tag: self.tag)
}, tag: ReactionNotificationSettingsEntryTag.sound)
}
}
}
@ -255,7 +271,8 @@ private struct ReactionNotificationSettingsState: Equatable {
}
public func reactionNotificationSettingsController(
context: AccountContext
context: AccountContext,
focusOnItemTag: ReactionNotificationSettingsEntryTag? = nil
) -> ViewController {
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
@ -399,7 +416,7 @@ public func reactionNotificationSettingsController(
let title: String = presentationData.strings.Notifications_Reactions_Title
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag)
return (controllerState, (listState, arguments))
}
@ -411,5 +428,20 @@ public func reactionNotificationSettingsController(
pushControllerImpl = { [weak controller] c in
(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
}

View file

@ -48,6 +48,8 @@ extension SettingsSearchableItemIcon {
return PresentationResourcesSettings.support
case .faq:
return PresentationResourcesSettings.faq
case .tips:
return PresentationResourcesSettings.tips
case .chatFolders:
return PresentationResourcesSettings.chatFolders
case .deleteAccount:
@ -56,24 +58,42 @@ extension SettingsSearchableItemIcon {
return PresentationResourcesSettings.devices
case .premium:
return PresentationResourcesSettings.premium
case .business:
return PresentationResourcesSettings.business
case .stars:
return PresentationResourcesSettings.stars
case .ton:
return PresentationResourcesSettings.ton
case .stories:
return PresentationResourcesSettings.stories
case .myProfile:
return PresentationResourcesSettings.myProfile
case .gift:
return PresentationResourcesSettings.premiumGift
case .powerSaving:
return PresentationResourcesSettings.powerSaving
}
}
}
final class SettingsSearchInteraction {
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.openItemContextMenu = openItemContextMenu
self.deleteRecentItem = deleteRecentItem
}
}
private enum SettingsSearchEntryStableId: Hashable {
case result(SettingsSearchableItemId)
case result(AnyHashable)
}
private enum SettingsSearchEntry: Comparable, Identifiable {
@ -132,7 +152,7 @@ private func preparedSettingsSearchContainerTransition(theme: PresentationTheme,
}
private enum SettingsSearchRecentEntryStableId: Hashable {
case recent(SettingsSearchableItemId)
case recent(AnyHashable)
}
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 {
switch self {
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)
}, deleted: {
interaction.deleteRecentItem(item.id)
@ -244,7 +268,19 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo
private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<PresentationData>
public init(context: AccountContext, openResult: @escaping (SettingsSearchableItem) -> Void, resolvedFaqUrl: Signal<ResolvedUrl?, NoError>, exceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, hasTwoStepAuth: Signal<Bool?, NoError>, twoStepAuthData: Signal<TwoStepVerificationAccessConfiguration?, NoError>, activeSessionsContext: Signal<ActiveSessionsContext?, NoError>, webSessionsContext: Signal<WebSessionsContext?, NoError>) {
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 }
self.presentationData = presentationData
self.presentationDataPromise = Promise(self.presentationData)
@ -273,15 +309,30 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo
self.addSubnode(self.listNode)
self.view.addSubview(self.edgeEffectView)
let interaction = SettingsSearchInteraction(openItem: { result in
addRecentSettingsSearchItem(engine: context.engine, item: result.id)
openResult(result)
}, deleteRecentItem: { id in
removeRecentSettingsSearchItem(engine: context.engine, item: id)
})
let interaction = SettingsSearchInteraction(
openItem: { item in
addRecentSettingsSearchItem(engine: context.engine, item: item.id)
openResult(item)
},
openItemContextMenu: { item, node, rect, gesture in
openContextMenu(item, node, rect, gesture)
},
deleteRecentItem: { id in
removeRecentSettingsSearchItem(engine: context.engine, item: id)
}
)
let searchableItems = Promise<[SettingsSearchableItem]>()
searchableItems.set(settingsSearchableItems(context: context, notificationExceptionsList: exceptionsList, archivedStickerPacks: archivedStickerPacks, privacySettings: privacySettings, hasTwoStepAuth: hasTwoStepAuth, twoStepAuthData: twoStepAuthData, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext))
searchableItems.set(settingsSearchableItems(
context: context,
notificationExceptionsList: exceptionsList,
archivedStickerPacks: archivedStickerPacks,
privacySettings: privacySettings,
hasTwoStepAuth: hasTwoStepAuth,
twoStepAuthData: twoStepAuthData,
activeSessionsContext: activeSessionsContext,
webSessionsContext: webSessionsContext
))
let faqItems = Promise<[SettingsSearchableItem]>()
faqItems.set(faqSearchableItems(context: context, resolvedUrl: resolvedFaqUrl, suggestAccountDeletion: false))
@ -294,11 +345,11 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo
let results = searchSettingsItems(items: searchableItems, query: query)
let faqResults = searchSettingsItems(items: faqSearchableItems, query: query)
let finalResults: [SettingsSearchableItem]
if faqResults.first?.id == .faq(1) {
finalResults = faqResults + results
} else {
//if faqResults.first?.id == .faq(1) {
// finalResults = faqResults + results
//} else {
finalResults = results + faqResults
}
//}
return .single((query, finalResults))
} else {
return .single(nil)
@ -308,12 +359,12 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo
self.recentListNode.isHidden = false
let previousRecentlySearchedItemOrder = Atomic<[SettingsSearchableItemId]>(value: [])
let previousRecentlySearchedItemOrder = Atomic<[AnyHashable]>(value: [])
let fixedRecentlySearchedItems = settingsSearchRecentItems(engine: context.engine)
|> map { recentIds -> [SettingsSearchableItemId] in
var result: [SettingsSearchableItemId] = []
|> map { recentIds -> [AnyHashable] in
var result: [AnyHashable] = []
let _ = previousRecentlySearchedItemOrder.modify { current in
var updated: [SettingsSearchableItemId] = []
var updated: [AnyHashable] = []
for id in current {
inner: for recentId in recentIds {
if recentId == id {
@ -336,7 +387,7 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo
let recentSearchItems = combineLatest(searchableItems.get(), fixedRecentlySearchedItems)
|> 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
map[item.id] = item
return map
@ -344,10 +395,10 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo
var result: [SettingsSearchableItem] = []
for itemId in recentItems {
if let searchItem = searchableItemsMap[itemId] {
if case let .language(id) = searchItem.id, id > 0 {
} else {
//if case let .language(id) = searchItem.id, id > 0 {
//} else {
result.append(searchItem)
}
//}
}
}
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()
}, 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()))
var height = titleLayout.size.height
if subtitle.isEmpty {
height += 22.0
} else {
height += 39.0
var height: CGFloat = 30.0 + titleLayout.size.height
if !subtitle.isEmpty {
height += 9.0
}
let contentSize = CGSize(width: params.width, height: height)
let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0))
return (nodeLayout, { [weak self] in
var updatedTheme: PresentationTheme?
if currentItem?.theme !== item.theme {
@ -215,7 +215,7 @@ class SettingsSearchRecentItemNode: ItemListRevealOptionsItemNode {
let _ = titleApply()
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.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
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.isHidden = last

View file

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

View file

@ -77,6 +77,15 @@ private let titleFont = Font.regular(17.0)
private let subtitleFont = Font.regular(13.0)
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 topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
@ -90,6 +99,15 @@ class SettingsSearchResultItemNode: ListViewItemNode {
private var layoutParams: (ListViewItemLayoutParams, ItemListNeighbors)?
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.isLayerBacked = true
@ -119,9 +137,48 @@ class SettingsSearchResultItemNode: ListViewItemNode {
super.init(layerBacked: false, rotated: false, seeThrough: false)
self.addSubnode(self.iconNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
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) {
@ -132,7 +189,7 @@ class SettingsSearchResultItemNode: ListViewItemNode {
return { item, params, neighbors in
var leftInset: CGFloat = params.leftInset
let contentInset: CGFloat = 58.0
let contentInset: CGFloat = 60.0
let insets = itemListNeighborsGroupedInsets(neighbors, params)
@ -157,12 +214,11 @@ class SettingsSearchResultItemNode: ListViewItemNode {
updateIconImage = item.icon
}
var height = titleLayout.size.height
if subtitle.isEmpty {
height += 22.0
} else {
height += 39.0
var height: CGFloat = 30.0 + titleLayout.size.height
if !subtitle.isEmpty {
height += 9.0
}
let contentSize = CGSize(width: params.width, height: height)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] animated in
@ -219,11 +275,29 @@ class SettingsSearchResultItemNode: ListViewItemNode {
default:
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.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
let titleY: CGFloat = subtitle.isEmpty ? 11.0 : 11.0
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.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
}
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 stateValue = Atomic(value: ArchivedStickerPacksControllerState())
let updateState: ((ArchivedStickerPacksControllerState) -> ArchivedStickerPacksControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
if forceEdit {
updateState {
$0.withUpdatedEditing(true)
}
}
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var navigationControllerImpl: (() -> NavigationController?)?

View file

@ -67,7 +67,10 @@ private enum InstalledStickerPacksSection: Int32 {
}
public enum InstalledStickerPacksEntryTag: ItemListItemTag {
case edit
case suggestOptions
case largeEmoji
case dynamicOrder
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? InstalledStickerPacksEntryTag, self == other {
@ -397,7 +400,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
case let .largeEmoji(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleLargeEmoji(value)
})
}, tag: InstalledStickerPacksEntryTag.largeEmoji)
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: {
arguments.openFeatured()
@ -421,7 +424,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
case let .packOrder(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleDynamicPackOrder(value)
})
}, tag: InstalledStickerPacksEntryTag.dynamicOrder)
case let .packOrderInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .suggestAnimatedEmoji(text, value):
@ -651,6 +654,12 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta
statePromise.set(stateValue.modify { f($0) })
}
if focusOnItemTag == InstalledStickerPacksEntryTag.edit {
updateState {
$0.withUpdatedEditing(true)
}
}
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
if let forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme)
@ -1299,6 +1308,20 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta
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
}

View file

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

View file

@ -84,6 +84,8 @@ public enum ThemeSettingsEntryTag: ItemListItemTag {
case powerSaving
case stickersAndEmoji
case animations
case tapForNextMedia
case nightMode
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? ThemeSettingsEntryTag, self == other {
@ -318,7 +320,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
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
arguments.toggleNightTheme(value)
}, tag: nil)
}, tag: ThemeSettingsEntryTag.nightMode)
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: {
arguments.openAutoNightTheme()
@ -352,7 +354,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
case let .showNextMediaOnTap(_, title, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleShowNextMediaOnTap(value)
}, tag: ThemeSettingsEntryTag.animations)
}, tag: ThemeSettingsEntryTag.tapForNextMedia)
case let .showNextMediaOnTapInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
@ -1309,6 +1311,21 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
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
}

View file

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

View file

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

View file

@ -23,9 +23,13 @@ import MultiAnimationRenderer
import Pasteboard
import StickerPackEditTitleController
import EntityKeyboard
//import CameraScreen
import ComponentFlow
import EmojiStatusComponent
import EdgeEffect
import GlassBarButtonComponent
import BundleIconComponent
import LottieComponent
import ButtonComponent
private let maxStickersCount = 120
@ -143,18 +147,20 @@ private final class StickerPackContainer: ASDisplayNode {
private let previewIconFile: TelegramMediaFile?
private var mainPreviewIcon: ComponentView<Empty>?
private let gridNode: GridNode
private let actionAreaBackgroundNode: NavigationBackgroundNode
private let actionAreaSeparatorNode: ASDisplayNode
private let buttonNode: HighlightableButtonNode
private let titleBackgroundnode: NavigationBackgroundNode
private let titleNode: ImmediateTextNode
private var titlePlaceholderNode: ShimmerEffectNode?
private let titleContainer: ASDisplayNode
private let titleSeparatorNode: ASDisplayNode
private let topContainerNode: ASDisplayNode
private let cancelButtonNode: HighlightableButtonNode
private let moreButtonNode: MoreButtonNode
private let bottomContainerNode: ASDisplayNode
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)?
@ -176,6 +182,8 @@ private final class StickerPackContainer: ASDisplayNode {
return self.isReadyValue.get()
}
private var itemCount: Int32 = 0
var expandProgress: CGFloat = 0.0
var expandScrollProgress: CGFloat = 0.0
var modalProgress: CGFloat = 0.0
@ -225,7 +233,7 @@ private final class StickerPackContainer: ASDisplayNode {
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = 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
if self.previewIconFile != nil {
@ -235,14 +243,9 @@ private final class StickerPackContainer: ASDisplayNode {
self.gridNode = GridNode()
self.gridNode.scrollView.alwaysBounceVertical = true
self.gridNode.scrollView.showsVerticalScrollIndicator = false
self.titleBackgroundnode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor)
self.actionAreaBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.tabBar.backgroundColor)
self.actionAreaSeparatorNode = ASDisplayNode()
self.actionAreaSeparatorNode.backgroundColor = self.presentationData.theme.rootController.tabBar.separatorColor
self.bottomContainerNode = ASDisplayNode()
self.buttonNode = HighlightableButtonNode()
self.titleNode = ImmediateTextNode()
self.titleNode.textAlignment = .center
@ -261,13 +264,7 @@ private final class StickerPackContainer: ASDisplayNode {
}
self.titleContainer = ASDisplayNode()
self.titleSeparatorNode = ASDisplayNode()
self.titleSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
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 removeStickerPackImpl: ((StickerPackCollectionInfo) -> Void)?
@ -290,18 +287,13 @@ private final class StickerPackContainer: ASDisplayNode {
self.addSubnode(self.backgroundNode)
self.addSubnode(self.gridNode)
self.addSubnode(self.actionAreaBackgroundNode)
self.addSubnode(self.actionAreaSeparatorNode)
self.addSubnode(self.buttonNode)
self.titleContainer.addSubnode(self.titleNode)
self.addSubnode(self.titleContainer)
self.addSubnode(self.titleSeparatorNode)
self.addSubnode(self.topContainerNode)
self.topContainerNode.addSubnode(self.cancelButtonNode)
self.topContainerNode.addSubnode(self.moreButtonNode)
self.addSubnode(self.bottomContainerNode)
self.gridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in
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
guard let strongSelf = self, !strongSelf.isDismissed else {
return targetOffset
@ -431,27 +416,27 @@ private final class StickerPackContainer: ASDisplayNode {
strongSelf.updateStickerPackContents(contents, hasPremium: hasPremium)
})
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonNode.alpha = 0.8
} else {
strongSelf.buttonNode.alpha = 1.0
strongSelf.buttonNode.layer.animateAlpha(from: 0.8, to: 1.0, duration: 0.3)
}
}
}
// self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
// self.buttonNode.highligthedChanged = { [weak self] highlighted in
// if let strongSelf = self {
// if highlighted {
// strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity")
// strongSelf.buttonNode.alpha = 0.8
// } else {
// strongSelf.buttonNode.alpha = 1.0
// 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.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
//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.moreButtonNode.action = { [weak self] _, gesture in
if let strongSelf = self {
strongSelf.morePressed(node: strongSelf.moreButtonNode.contextSourceNode, gesture: gesture)
}
}
// self.moreButtonNode.action = { [weak self] _, gesture in
// if let strongSelf = self {
// strongSelf.morePressed(node: strongSelf.moreButtonNode.contextSourceNode, gesture: gesture)
// }
// }
self.titleNode.linkHighlightColor = self.presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.2)
@ -510,7 +495,7 @@ private final class StickerPackContainer: ASDisplayNode {
private var reorderingGestureRecognizer: ReorderingGestureRecognizer?
override func didLoad() {
super.didLoad()
let peekGestureRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>? in
if let strongSelf = self {
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
self.reorderingGestureRecognizer = reorderingGestureRecognizer
self.gridNode.view.addGestureRecognizer(reorderingGestureRecognizer)
self.topContainerNode.view.addSubview(self.topEdgeEffectView)
self.bottomContainerNode.view.addSubview(self.bottomEdgeEffectView)
}
private func hideMainPreviewIcon() {
@ -1005,15 +993,7 @@ private final class StickerPackContainer: ASDisplayNode {
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.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.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 76.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor)
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)
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)
}
}
@ -1084,7 +1064,6 @@ private final class StickerPackContainer: ASDisplayNode {
private func updateIsEditing(_ isEditing: Bool) {
self.isEditing = isEditing
self.updateEntries(reload: true)
self.updateButton()
self.peekGestureRecognizer?.longPressEnabled = !isEditing
self.reorderingGestureRecognizer?.isEnabled = isEditing
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 {
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)
}
@ -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) {
self.currentContents = contents
self.didReceiveStickerPackResult = true
@ -1864,7 +1751,8 @@ private final class StickerPackContainer: ASDisplayNode {
} else {
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 {
@ -2032,43 +1920,15 @@ private final class StickerPackContainer: ASDisplayNode {
insets.top += 10.0
}
var buttonHeight: CGFloat = 50.0
var actionAreaTopInset: CGFloat = 8.0
var actionAreaBottomInset: CGFloat = 16.0
if let _ = self.controller?.mainActionTitle {
} 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 buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: layout.intrinsicInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
let titleAreaInset: CGFloat = 76.0
let buttonHeight: CGFloat = 52.0
let buttonSideInset: CGFloat = 30.0
let actionAreaHeight: CGFloat = buttonHeight + buttonInsets.bottom
let buttonSideInset: CGFloat = 16.0
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.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)))
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)))
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)))
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)))
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)
}
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)
//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)
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.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.moreButtonNode, 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)
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 {
while !self.enqueuedTransactions.isEmpty {
@ -2194,7 +2208,6 @@ private final class StickerPackContainer: ASDisplayNode {
if !transition.isAnimated {
self.backgroundNode.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))
@ -2203,7 +2216,7 @@ private final class StickerPackContainer: ASDisplayNode {
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())
} 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)
@ -2239,14 +2252,8 @@ private final class StickerPackContainer: ASDisplayNode {
}
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)))
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
transition.updateAlpha(node: self.titleSeparatorNode, alpha: unclippedBackgroundY < minBackgroundY ? 1.0 : 0.0)
transition.updateFrame(node: self.topContainerNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.width, height: 76.0)))
}
private func enqueueTransaction(_ transaction: StickerPackPreviewGridTransaction) {
@ -2423,7 +2430,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
let containerInsets: UIEdgeInsets
if case .regular = layout.metrics.widthClass {
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))
var contentRect: CGRect
@ -2476,8 +2483,15 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
let shadowFrame = containerContainingFrame.insetBy(dx: -60.0, dy: -60.0)
transition.updateFrame(node: self.shadowNode, frame: shadowFrame)
let expandProgress: CGFloat = 1.0
let scaledInset: CGFloat = 12.0
let expandProgress: CGFloat
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 minScale = (layout.size.width - scaledInset * 2.0) / layout.size.width
let containerScale = expandProgress * 1.0 + (1.0 - expandProgress) * minScale
@ -3101,15 +3115,15 @@ public func StickerPackScreen(
private final class StickerPackContextReferenceContentSource: ContextReferenceContentSource {
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.sourceNode = sourceNode
self.sourceView = sourceView
}
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 ip: 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`

View file

@ -96,7 +96,7 @@ public extension TelegramEngine {
}
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> {

View file

@ -3186,6 +3186,12 @@ private final class CraftGiftsContextImpl {
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]) {
let referencesSet = Set(references)
self.gifts.removeAll { gift in
@ -3318,6 +3324,12 @@ public final class CraftGiftsContext {
impl.reload()
}
}
public func addGift(gift: ProfileGiftsContext.State.StarGift) {
self.impl.with { impl in
impl.addGift(gift: gift)
}
}
public func removeGifts(references: [StarGiftReference]) {
self.impl.with { impl in
@ -3838,6 +3850,7 @@ private final class ResaleGiftsContextImpl {
private let queue: Queue
private let account: Account
private let giftId: Int64
private let forCrafting: Bool
private let disposable = MetaDisposable()
@ -3861,11 +3874,13 @@ private final class ResaleGiftsContextImpl {
init(
queue: Queue,
account: Account,
giftId: Int64
giftId: Int64,
forCrafting: Bool
) {
self.queue = queue
self.account = account
self.giftId = giftId
self.forCrafting = forCrafting
self.loadMore()
}
@ -3898,6 +3913,10 @@ private final class ResaleGiftsContextImpl {
}
var flags: Int32 = 0
if self.forCrafting {
flags |= (1 << 4)
}
switch sorting {
case .date:
break
@ -4169,11 +4188,12 @@ public final class ResaleGiftsContext {
public init(
account: Account,
giftId: Int64
giftId: Int64,
forCrafting: Bool
) {
let queue = self.queue
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 globalPostsSearch = 83
case giftAuctionTips = 84
case giftCraftingTips = 85
var key: ValueBoxKey {
let v = ValueBoxKey(length: 4)
@ -574,6 +575,10 @@ private struct ApplicationSpecificNoticeKeys {
static func giftAuctionTips() -> NoticeEntryKey {
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 {
@ -2514,4 +2519,31 @@ public struct ApplicationSpecificNotice {
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 {
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 {

View file

@ -96,7 +96,7 @@ public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDate
}
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
if fraction > 0.0 {
balanceText.append(dateTimeFormat.decimalSeparator)

View file

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

View file

@ -489,10 +489,10 @@ private final class RecentActionsSettingsSheetComponent: Component {
let leftButtonSize = self.leftButton.update(
transition: transition,
component: AnyComponent(GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: environment.theme.overallDarkAppearance,
state: .generic,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
@ -507,7 +507,7 @@ private final class RecentActionsSettingsSheetComponent: Component {
}
)),
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)
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)
var availableHeight = availableSize.height
availableHeight -= environment.statusBarHeight
if component.configuration.allowInputInset, environment.inputHeight > 0.0 {
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)
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
public final class AvatarComponent: Component {
public enum ClipStyle {
case round
case roundedRect
}
let context: AccountContext
let theme: PresentationTheme
let peer: EnginePeer
let clipStyle: ClipStyle
let icon: AnyComponent<Empty>?
public init(
context: AccountContext,
theme: PresentationTheme,
peer: EnginePeer,
clipStyle: ClipStyle = .round,
icon: AnyComponent<Empty>? = nil
) {
self.context = context
self.theme = theme
self.peer = peer
self.clipStyle = clipStyle
self.icon = icon
}
@ -35,6 +43,9 @@ public final class AvatarComponent: Component {
if lhs.peer != rhs.peer {
return false
}
if lhs.clipStyle != rhs.clipStyle {
return false
}
if lhs.icon != rhs.icon {
return false
}
@ -81,7 +92,7 @@ public final class AvatarComponent: Component {
transition: .immediate,
component: icon,
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)
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)
}
var clipStyle: AvatarNodeClipStyle = .round
if case .roundedRect = component.clipStyle {
clipStyle = .roundedRect
}
self.avatarNode.frame = CGRect(origin: .zero, size: availableSize)
self.avatarNode.setPeer(
context: component.context,
theme: component.theme,
peer: component.peer,
clipStyle: clipStyle,
synchronousLoad: true,
cutoutRect: cutoutRect
)

View file

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

View file

@ -28,6 +28,8 @@ import AvatarBackground
import LottieComponent
import UndoUI
import PremiumAlertController
import GlassBarButtonComponent
import BundleIconComponent
public struct AvatarKeyboardInputData: Equatable {
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(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: state.expanded && !backgroundIsBright ? .white : environment.theme.rootController.navigationBar.accentTextColor)),
action: { [weak self] in
guard let self else {
return
component: AnyComponent(
GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
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: {},
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 navigationCancelButtonView.superview == nil {
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)
}
let navigationDoneButtonSize = self.navigationDoneButton.update(
transition: transition,
component: AnyComponent(Button(
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)),
action: { [weak self] in
guard let self else {
return
component: AnyComponent(
GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: environment.theme.list.itemCheckColors.fillColor,
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: {},
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 navigationDoneButtonView.superview == nil {
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)
}
@ -1190,9 +1205,10 @@ final class AvatarEditorScreenComponent: Component {
contentHeight += keyboardTitleSize.height
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 {
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))
@ -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(
transition: transition,
component: AnyComponent(
@ -1340,13 +1355,13 @@ final class AvatarEditorScreenComponent: Component {
)
),
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 buttonView.superview == nil {
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))

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

View file

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

View file

@ -1428,8 +1428,6 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
break
case .theme:
break
case .settings:
break
case .premiumOffer:
break
case .starsTopup:
@ -1465,6 +1463,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
break
case .sendGift:
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))
}
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) {
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) {
}
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 {
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]) {
guard let component = self.component else {
return

View file

@ -172,10 +172,10 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
particleColor: statusParticleColor,
isVisibleForAnimations: true,
action: { [weak self] in
guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else {
guard let self else {
return
}
strongSelf.openStatusSetup?(titleCredibilityIconView)
self.openEmojiStatusSetup()
}
)),
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 {
let _ = self.updateLayoutInternal(size: availableSize, transition: transition)
return availableSize
@ -414,10 +421,10 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
content: statusContent,
isVisibleForAnimations: true,
action: { [weak self] in
guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else {
guard let self else {
return
}
strongSelf.openStatusSetup?(titleCredibilityIconView)
self.openEmojiStatusSetup()
}
)),
environment: {},

View file

@ -167,15 +167,15 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
var contentHeight: CGFloat = 0.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(
transition: transition,
component: AnyComponent(
GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
backgroundColor: nil,
isDark: environment.theme.overallDarkAppearance,
state: .generic,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",

View file

@ -52,6 +52,8 @@ private final class CocoonInfoSheetContent: CombinedComponent {
fileprivate let playButtonAnimation = ActionSlot<Void>()
private var didPlayAnimation = false
var cachedDescription: String?
init(
context: AccountContext,
@ -91,16 +93,17 @@ private final class CocoonInfoSheetContent: CombinedComponent {
}
static var body: Body {
let background = Child(GradientBackgroundComponent.self)
let closeButton = Child(GlassBarButtonComponent.self)
let icon = Child(BundleIconComponent.self)
let title = Child(BalancedTextComponent.self)
let logo = Child(BundleIconComponent.self)
let text = Child(BalancedTextComponent.self)
let list = Child(List<Empty>.self)
let additionalText = Child(MultilineTextComponent.self)
let button = Child(ButtonComponent.self)
let navigateDisposable = MetaDisposable()
return { context in
let component = context.component
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 textSideInset: CGFloat = 30.0 + environment.safeInsets.left
let titleFont = Font.bold(24.0)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
@ -122,42 +124,48 @@ private final class CocoonInfoSheetContent: CombinedComponent {
let spacing: CGFloat = 16.0
var contentSize = CGSize(width: context.availableSize.width, height: 28.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 title = title.update(
component: BalancedTextComponent(
text: .plain(NSAttributedString(string: strings.CocoonInfo_Title, font: titleFont, textColor: textColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
)
contentSize.height += title.size.height
contentSize.height += spacing - 8.0
let descriptionText: String
if let cachedDescription = state.cachedDescription {
descriptionText = cachedDescription
} else {
func updateText(_ input: String) -> String {
let pattern = #"\(([^()]*)\)"#
let boldPattern = #"\*\*(.*?)\*\*"#
let regex = try! NSRegularExpression(pattern: pattern)
let boldRegex = try! NSRegularExpression(pattern: boldPattern)
let nsInput = input as NSString
var result = input
let matches = regex.matches(in: input, range: NSRange(location: 0, length: nsInput.length))
for match in matches.reversed() {
let range = match.range(at: 1)
let inner = nsInput.substring(with: range)
let replacedInner = boldRegex.stringByReplacingMatches(
in: inner,
range: NSRange(location: 0, length: (inner as NSString).length),
withTemplate: "[$1]()"
)
result = (result as NSString).replacingCharacters(in: range, with: replacedInner)
}
return result
}
descriptionText = updateText(strings.CocoonInfo_Description)
state.cachedDescription = descriptionText
}
let attributedText = parseMarkdownIntoAttributedString(
strings.CocoonInfo_Description,
descriptionText,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: linkColor),
body: MarkdownAttributeSet(font: textFont, textColor: UIColor(rgb: 0xb8c9ef)),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: UIColor(rgb: 0xb8c9ef)),
link: MarkdownAttributeSet(font: boldTextFont, textColor: UIColor(rgb: 0xffffff)),
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),
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
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0))
)
contentSize.height += text.size.height
contentSize.height += spacing + 9.0
contentSize.height += spacing + 31.0
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
@ -292,14 +341,14 @@ private final class CocoonInfoSheetContent: CombinedComponent {
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: theme.overallDarkAppearance,
state: .generic,
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: UIColor(rgb: 0x071533),
isDark: true,
state: .tintedGlass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor
tintColor: .white
)
)),
action: { [weak state] _ in
@ -309,7 +358,7 @@ private final class CocoonInfoSheetContent: CombinedComponent {
state.dismiss(animated: true)
}
),
availableSize: CGSize(width: 40.0, height: 40.0),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
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)
}
let barButtonSize = CGSize(width: 40.0, height: 40.0)
let barButtonSize = CGSize(width: 44.0, height: 44.0)
let cancelButtonSize = self.cancelButton.update(
transition: transition,
component: AnyComponent(GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: environment.theme.rootController.navigationBar.opaqueBackgroundColor,
backgroundColor: nil,
isDark: environment.theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(

View file

@ -519,13 +519,13 @@ private final class SheetContent: CombinedComponent {
contentSize.height += navigation.size.height
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(
component: GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
backgroundColor: nil,
isDark: environment.theme.overallDarkAppearance,
state: .generic,
state: .glass,
component: AnyComponentWithIdentity(id: isBack ? "back" : "close", component: AnyComponent(
BundleIconComponent(
name: isBack ? "Navigation/Back" : "Navigation/Close",

View file

@ -40,6 +40,7 @@ struct Particle {
kernel void dustEffectInitializeParticle(
device Particle *particles [[ buffer(0) ]],
const device float &verticalDirection [[ buffer(1) ]],
uint gid [[ thread_position_in_grid ]]
) {
Loki rng = Loki(gid);
@ -49,7 +50,9 @@ kernel void dustEffectInitializeParticle(
float direction = rng.rand() * (3.14159265 * 2.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);
@ -107,6 +110,7 @@ kernel void dustEffectUpdateParticle(
const device uint2 &size [[ buffer(1) ]],
const device float &phase [[ buffer(2) ]],
const device float &timeStep [[ buffer(3) ]],
const device float &verticalDirection [[ buffer(4) ]],
uint gid [[ thread_position_in_grid ]]
) {
uint count = size.x * size.y;
@ -124,7 +128,7 @@ kernel void dustEffectUpdateParticle(
Particle particle = particles[gid];
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);
particles[gid] = particle;
}

View file

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

View file

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

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