From 142d59cf8f3100d99a609c8b6ebe29424ce2b01a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 27 Jan 2026 22:24:31 +0400 Subject: [PATCH 01/12] Various improvements --- Telegram/Telegram-iOS/Resources/CraftFail.tgs | Bin 6404 -> 0 bytes .../Resources/CraftFailOverlay.tgs | Bin 7259 -> 0 bytes .../Telegram-iOS/en.lproj/Localizable.strings | 4 + .../Sources/AccountContext.swift | 1 + .../AttachmentTextInputPanelNode.swift | 4 +- .../Sources/ResizableSheetComponent.swift | 25 +- .../Sources/Items/ItemListActionItem.swift | 22 +- submodules/SettingsUI/BUILD | 1 + .../Sources/LogoutOptionsController.swift | 2 +- .../RecentSessionsController.swift | 53 +- .../RecentSessionScreen.swift | 1507 +++++++---------- .../Search/SettingsSearchableItems.swift | 22 +- .../Themes/ThemeSettingsAppIconItem.swift | 29 +- .../Themes/ThemeSettingsController.swift | 86 +- .../Sources/StickerPackScreen.swift | 62 +- .../ApiUtils/TelegramMediaAction.swift | 2 +- .../SyncCore_TelegramMediaAction.swift | 10 +- .../TelegramEngine/Payments/StarGifts.swift | 21 +- .../TelegramEngine/Payments/Stars.swift | 2 +- .../Sources/PresentationData.swift | 22 +- .../Sources/ServiceMessageStrings.swift | 8 +- .../Sources/AuthConfirmationScreen.swift | 40 +- .../ChatInputTextViewImpl.h | 2 +- .../Sources/ChatInputTextViewImpl.m | 15 +- .../Sources/ChatInputTextNode.swift | 6 +- .../ChatMessageGiftBubbleContentNode.swift | 4 +- .../Sources/ChatTextInputPanelNode.swift | 22 +- .../Sources/GiftCompositionComponent.swift | 2 + .../Sources/CraftTableComponent.swift | 132 +- .../Sources/CubeAnimationView.swift | 10 +- .../Sources/DialIndicatorComponent.swift | 49 +- .../Sources/GiftCraftScreen.swift | 461 +++-- .../Sources/SelectCraftGiftScreen.swift | 5 +- .../Components/Gifts/GiftStoreScreen/BUILD | 1 + .../Sources/BalanceComponent.swift | 174 ++ .../Sources/GiftStoreScreen.swift | 162 +- .../Sources/GiftAuctionViewScreen.swift | 4 +- .../GiftAuctionWearPreviewScreen.swift | 4 +- .../Sources/GiftUpgradeVariantsScreen.swift | 2 +- .../Sources/GiftViewScreen.swift | 19 +- .../Sources/MediaEditorScreen.swift | 2 +- .../Sources/MessagePriceItem.swift | 21 + .../Sources/PeerInfoCoverComponent.swift | 3 +- .../Sources/TextFieldComponent.swift | 2 +- .../Context Menu/Stars.imageset/Contents.json | 12 + .../Context Menu/Stars.imageset/star (3).pdf | Bin 0 -> 8084 bytes .../Context Menu/Ton.imageset/Contents.json | 12 + .../Context Menu/Ton.imageset/ton (3).pdf | Bin 0 -> 4185 bytes .../Premium/GiftCrash.imageset/Contents.json | 12 + .../Premium/GiftCrash.imageset/giftcrash.pdf | Bin 0 -> 4778 bytes .../Sources/ChatControllerKeyShortcuts.swift | 15 +- .../ChatInterfaceStateContextMenus.swift | 2 +- .../Sources/SharedAccountContext.swift | 15 + .../Sources/ChatSettings.swift | 49 + .../Sources/PostboxKeys.swift | 2 + 55 files changed, 1851 insertions(+), 1293 deletions(-) delete mode 100644 Telegram/Telegram-iOS/Resources/CraftFail.tgs delete mode 100644 Telegram/Telegram-iOS/Resources/CraftFailOverlay.tgs create mode 100644 submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/BalanceComponent.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Stars.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Stars.imageset/star (3).pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ton.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ton.imageset/ton (3).pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/giftcrash.pdf create mode 100644 submodules/TelegramUIPreferences/Sources/ChatSettings.swift diff --git a/Telegram/Telegram-iOS/Resources/CraftFail.tgs b/Telegram/Telegram-iOS/Resources/CraftFail.tgs deleted file mode 100644 index eaf5e1db3fa4a026c69607c360772124b1b81906..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6404 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 diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 16b14dc7b2..38e8db2492 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15669,3 +15669,7 @@ Error: %8$@"; "Conversation.Summary.Limit.Title" = "AI Summary"; "Conversation.Summary.Limit.Text" = "Summarize large messages with AI – unlimited with Telegram Premium."; + +"Appearance.SendWithCmdEnter" = "Send Messages with ⌘+Enter"; + +"Notification.StarsGift.Crafted" = "You crafted a new unique collectible"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index efab15b20f..c621d5458e 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1294,6 +1294,7 @@ public protocol SharedAccountContext: AnyObject { var currentMediaInputSettings: Atomic { get } var currentStickerSettings: Atomic { get } var currentMediaDisplaySettings: Atomic { get } + var currentChatSettings: Atomic { get } var energyUsageSettings: EnergyUsageSettings { get } diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index f74fde4062..ca3e2e75dd 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -1489,7 +1489,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } - public func chatInputTextNodeShouldReturn() -> Bool { + public func chatInputTextNodeShouldReturn(modifierFlags: UIKeyModifierFlags) -> Bool { if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero { self.sendButtonPressed() } @@ -1497,7 +1497,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } @objc public func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { - return self.chatInputTextNodeShouldReturn() + return self.chatInputTextNodeShouldReturn(modifierFlags: []) } private func applyUpdateSendButtonIcon() { diff --git a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift index e638c51cb7..dbb51b9746 100644 --- a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift +++ b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift @@ -530,6 +530,11 @@ public final class ResizableSheetComponent if let current = self.bottomItemView { bottomItemView = current } else { + bottomItemTransition = .immediate bottomItemView = ComponentView() self.bottomItemView = bottomItemView } let bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: sheetEnvironment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let bottomItemSize = bottomItemView.update( - transition: transition, + transition: bottomItemTransition, component: bottomItem, environment: {}, containerSize: CGSize(width: containerSize.width - bottomInsets.left - bottomInsets.right, height: 52.0) @@ -606,12 +613,24 @@ public final class ResizableSheetComponent (_ item: ItemListActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) @@ -211,6 +229,7 @@ public class ItemListActionItemNode: 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 } let _ = titleApply() @@ -273,6 +292,7 @@ public class ItemListActionItemNode: 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 + bottomStripeOffset), size: CGSize(width: params.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight)) diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 8a40750407..b0ecc4368e 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -139,6 +139,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/PeerSelectionScreen", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/Utils/DeviceModel", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/LogoutOptionsController.swift b/submodules/SettingsUI/Sources/LogoutOptionsController.swift index 862a3054cd..51c2e728fc 100644 --- a/submodules/SettingsUI/Sources/LogoutOptionsController.swift +++ b/submodules/SettingsUI/Sources/LogoutOptionsController.swift @@ -224,7 +224,7 @@ public func logoutOptionsController(context: AccountContext, navigationControlle context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in - pushControllerImpl?(controller) + presentControllerImpl?(controller, nil) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) }) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift index d7d5c84346..c20613b23e 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift @@ -623,15 +623,12 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont let removeSessionImpl: (Int64, @escaping () -> Void) -> Void = { sessionId, completion in let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: presentationData.strings.AuthSessions_TerminateSessionText), - ActionSheetButtonItem(title: presentationData.strings.AuthSessions_TerminateSession, color: .destructive, action: { - dismissAction() + let controller = textAlertController( + context: context, + title: nil, + text: presentationData.strings.AuthSessions_TerminateSessionText, + actions: [ + TextAlertAction(type: .defaultDestructiveAction, title: presentationData.strings.AuthSessions_TerminateSession, action: { completion() updateState { @@ -649,11 +646,12 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont } context.sharedContext.updateNotificationTokensRegistration() })) - }) - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}) + ], + actionLayout: .vertical + ) + presentControllerImpl?(controller, nil) } let removeWebSessionImpl: (Int64) -> Void = { sessionId in @@ -690,16 +688,12 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont removeSessionImpl(sessionId, {}) }, terminateOtherSessions: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: presentationData.strings.AuthSessions_TerminateOtherSessionsText), - ActionSheetButtonItem(title: presentationData.strings.AuthSessions_TerminateOtherSessions, color: .destructive, action: { - dismissAction() - + let controller = textAlertController( + context: context, + title: nil, + text: presentationData.strings.AuthSessions_TerminateOtherSessionsText, + actions: [ + TextAlertAction(type: .defaultDestructiveAction, title: presentationData.strings.AuthSessions_TerminateOtherSessions, action: { updateState { return $0.withUpdatedTerminatingOtherSessions(true) } @@ -715,11 +709,12 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont } context.sharedContext.updateNotificationTokensRegistration() })) - }) - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}) + ], + actionLayout: .vertical + ) + presentControllerImpl?(controller, nil) }, openSession: { session in let controller = RecentSessionScreen(context: context, subject: .session(session), updateAcceptSecretChats: { value in updateSessionDisposable.set(activeSessionsContext.updateSessionAcceptsSecretChats(session, accepts: value).start()) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift index ef326ba14f..d79684a91d 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift @@ -5,359 +5,272 @@ import AsyncDisplayKit import TelegramCore import SwiftSignalKit import AccountContext -import SolidRoundedButtonNode import TelegramPresentationData -import TelegramUIPreferences -import TelegramStringFormatting +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import MultilineTextComponent +import BalancedTextComponent +import GlassBarButtonComponent +import ButtonComponent +import TableComponent import PresentationDataUtils -import AnimationUI -import MergeLists -import MediaResources -import StickerResources -import AnimatedStickerNode -import TelegramAnimatedStickerNode -import AvatarNode -import UndoUI +import BundleIconComponent +import LottieAnimationComponent +import ListSectionComponent +import ListActionItemComponent +import AvatarComponent +import TelegramStringFormatting +import Markdown -private func closeButtonImage(theme: PresentationTheme) -> UIImage? { - return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setFillColor(UIColor(rgb: 0x808084, alpha: 0.1).cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - - context.setLineWidth(2.0) - context.setLineCap(.round) - context.setStrokeColor(theme.actionSheet.inputClearButtonColor.cgColor) - - context.move(to: CGPoint(x: 10.0, y: 10.0)) - context.addLine(to: CGPoint(x: 20.0, y: 20.0)) - context.strokePath() - - context.move(to: CGPoint(x: 20.0, y: 10.0)) - context.addLine(to: CGPoint(x: 10.0, y: 20.0)) - context.strokePath() - }) -} - -final class RecentSessionScreen: ViewController { - enum Subject { - case session(RecentAccountSession) - case website(WebAuthorization, EnginePeer?) - } - private var controllerNode: RecentSessionScreenNode { - return self.displayNode as! RecentSessionScreenNode - } +private final class RecentSessionSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment - private var animatedIn = false + let context: AccountContext + let subject: RecentSessionScreen.Subject + let cancel: (Bool) -> Void - private let context: AccountContext - private let subject: RecentSessionScreen.Subject - private let remove: (@escaping () -> Void) -> Void - private let updateAcceptSecretChats: (Bool) -> Void - private let updateAcceptIncomingCalls: (Bool) -> Void - - private var presentationData: PresentationData - private var presentationDataDisposable: Disposable? - - var dismissed: (() -> Void)? - - var passthroughHitTestImpl: ((CGPoint) -> UIView?)? { - didSet { - if self.isNodeLoaded { - self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl - } - } - } - - init(context: AccountContext, subject: RecentSessionScreen.Subject, updateAcceptSecretChats: @escaping (Bool) -> Void, updateAcceptIncomingCalls: @escaping (Bool) -> Void, remove: @escaping (@escaping () -> Void) -> Void) { + init( + context: AccountContext, + subject: RecentSessionScreen.Subject, + cancel: @escaping (Bool) -> Void + ) { self.context = context - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.subject = subject - self.remove = remove - self.updateAcceptSecretChats = updateAcceptSecretChats - self.updateAcceptIncomingCalls = updateAcceptIncomingCalls + self.cancel = cancel + } + + static func ==(lhs: RecentSessionSheetContent, rhs: RecentSessionSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + var allowSecretChats: Bool? + var allowIncomingCalls: Bool? - super.init(navigationBarPresentationData: nil) + weak var controller: RecentSessionScreen? - self.statusBar.statusBarStyle = .Ignore - - self.blocksBackgroundWhenInOverlay = true - - self.presentationDataDisposable = (context.sharedContext.presentationData - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.presentationData = presentationData - strongSelf.controllerNode.updatePresentationData(presentationData) + init(subject: RecentSessionScreen.Subject) { + super.init() + + switch subject { + case let .session(session): + if !session.flags.contains(.passwordPending) && session.apiId != 22 { + self.allowIncomingCalls = session.flags.contains(.acceptsIncomingCalls) + + if ![2040, 2496].contains(session.apiId) { + self.allowSecretChats = session.flags.contains(.acceptsSecretChats) + } + } + case .website: + break } - }) + } - self.statusBar.statusBarStyle = .Ignore - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDataDisposable?.dispose() - } - - override public func loadDisplayNode() { - self.displayNode = RecentSessionScreenNode(context: self.context, presentationData: self.presentationData, controller: self, subject: self.subject) - self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl - self.controllerNode.present = { [weak self] c in - self?.present(c, in: .current) + func toggleAllowSecretChats() { + guard let controller = self.controller else { + return + } + + if let allowSecretChats = self.allowSecretChats { + let newValue = !allowSecretChats + self.allowSecretChats = newValue + controller.updateAcceptSecretChats(newValue) + } + + self.updated() } - self.controllerNode.dismiss = { [weak self] in - self?.presentingViewController?.dismiss(animated: false, completion: nil) + + func toggleAllowIncomingCalls() { + guard let controller = self.controller else { + return + } + + if let allowIncomingCalls = self.allowIncomingCalls { + let newValue = !allowIncomingCalls + self.allowIncomingCalls = newValue + controller.updateAcceptIncomingCalls(newValue) + } + + self.updated() } - self.controllerNode.remove = { [weak self] in - self?.remove({ - self?.controllerNode.animateOut() + + func terminate() { + guard let controller = self.controller else { + return + } + self.updated() + + controller.remove({ [weak controller] in + controller?.dismissAnimated() }) } - self.controllerNode.updateAcceptSecretChats = { [weak self] value in - self?.updateAcceptSecretChats(value) - } - self.controllerNode.updateAcceptIncomingCalls = { [weak self] value in - self?.updateAcceptIncomingCalls(value) - } } - override public func loadView() { - super.loadView() - - self.view.disablesInteractiveTransitionGestureRecognizer = true + func makeState() -> State { + return State(subject: self.subject) } - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) + static var body: Body { + let closeButton = Child(GlassBarButtonComponent.self) + let icon = Child(ZStack.self) + let avatar = Child(AvatarComponent.self) + let title = Child(BalancedTextComponent.self) + let description = Child(MultilineTextComponent.self) + let clientSection = Child(ListSectionComponent.self) + let optionsSection = Child(ListSectionComponent.self) + let button = Child(ButtonComponent.self) - if !self.animatedIn { - self.animatedIn = true - self.controllerNode.animateIn() - } - } - - override public func dismiss(completion: (() -> Void)? = nil) { - self.controllerNode.animateOut(completion: completion) - - self.dismissed?() - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) - } -} - -private class RecentSessionScreenNode: ViewControllerTracingNode, ASScrollViewDelegate { - private let context: AccountContext - private var presentationData: PresentationData - private weak var controller: RecentSessionScreen? - private let subject: RecentSessionScreen.Subject - - private let dimNode: ASDisplayNode - private let wrappingScrollNode: ASScrollNode - private let contentContainerNode: ASDisplayNode - private let topContentContainerNode: SparseNode - private let backgroundNode: ASDisplayNode - private let contentBackgroundNode: ASDisplayNode - private var iconNode: ASImageNode? - private var animationBackgroundNode: ASDisplayNode? - private var animationNode: AnimationNode? - private var avatarNode: AvatarNode? - private let titleNode: ImmediateTextNode - private let textNode: ImmediateTextNode - private let fieldBackgroundNode: ASDisplayNode - private let deviceTitleNode: ImmediateTextNode - private let deviceValueNode: ImmediateTextNode - private let firstSeparatorNode: ASDisplayNode - private let ipTitleNode: ImmediateTextNode - private let ipValueNode: ImmediateTextNode - private let secondSeparatorNode: ASDisplayNode - private let locationTitleNode: ImmediateTextNode - private let locationValueNode: ImmediateTextNode - private let locationInfoNode: ImmediateTextNode - - private let acceptBackgroundNode: ASDisplayNode - private let acceptHeaderNode: ImmediateTextNode - private let secretChatsTitleNode: ImmediateTextNode - private let secretChatsSwitchNode: SwitchNode - private let secretChatsActivateAreaNode: AccessibilityAreaNode - private let incomingCallsTitleNode: ImmediateTextNode - private let incomingCallsSwitchNode: SwitchNode - private let incomingCallsActivateAreaNode: AccessibilityAreaNode - private let acceptSeparatorNode: ASDisplayNode - - private let cancelButton: HighlightableButtonNode - private let terminateButton: SolidRoundedButtonNode - - private var containerLayout: (ContainerViewLayout, CGFloat)? - - var present: ((ViewController) -> Void)? - var remove: (() -> Void)? - var dismiss: (() -> Void)? - var updateAcceptSecretChats: ((Bool) -> Void)? - var updateAcceptIncomingCalls: ((Bool) -> Void)? - - init(context: AccountContext, presentationData: PresentationData, controller: RecentSessionScreen, subject: RecentSessionScreen.Subject) { - self.context = context - self.controller = controller - self.presentationData = presentationData - self.subject = subject - - self.wrappingScrollNode = ASScrollNode() - self.wrappingScrollNode.view.alwaysBounceVertical = true - self.wrappingScrollNode.view.delaysContentTouches = false - self.wrappingScrollNode.view.showsVerticalScrollIndicator = false - self.wrappingScrollNode.view.canCancelContentTouches = true - - self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - - self.contentContainerNode = ASDisplayNode() - self.contentContainerNode.isOpaque = false - - self.topContentContainerNode = SparseNode() - self.topContentContainerNode.isOpaque = false - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.clipsToBounds = true - self.backgroundNode.cornerRadius = 16.0 - - let backgroundColor = self.presentationData.theme.list.blocksBackgroundColor - let textColor = self.presentationData.theme.list.itemPrimaryTextColor - let accentColor = self.presentationData.theme.list.itemAccentColor - let secondaryTextColor = self.presentationData.theme.list.itemSecondaryTextColor - - self.contentBackgroundNode = ASDisplayNode() - self.contentBackgroundNode.backgroundColor = backgroundColor - - self.titleNode = ImmediateTextNode() - self.titleNode.maximumNumberOfLines = 2 - self.titleNode.textAlignment = .center - - self.textNode = ImmediateTextNode() - self.textNode.maximumNumberOfLines = 1 - self.textNode.textAlignment = .center - - self.fieldBackgroundNode = ASDisplayNode() - self.fieldBackgroundNode.clipsToBounds = true - self.fieldBackgroundNode.cornerRadius = 11 - self.fieldBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor - - self.deviceTitleNode = ImmediateTextNode() - self.deviceValueNode = ImmediateTextNode() - - self.ipTitleNode = ImmediateTextNode() - self.ipValueNode = ImmediateTextNode() - - self.locationTitleNode = ImmediateTextNode() - self.locationValueNode = ImmediateTextNode() - self.locationInfoNode = ImmediateTextNode() - - self.acceptHeaderNode = ImmediateTextNode() - self.secretChatsTitleNode = ImmediateTextNode() - self.secretChatsSwitchNode = SwitchNode() - self.incomingCallsTitleNode = ImmediateTextNode() - self.incomingCallsSwitchNode = SwitchNode() - - self.secretChatsActivateAreaNode = AccessibilityAreaNode() - self.incomingCallsActivateAreaNode = AccessibilityAreaNode() - - self.cancelButton = HighlightableButtonNode() - self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) - self.cancelButton.accessibilityLabel = presentationData.strings.Common_Close - self.cancelButton.accessibilityTraits = [.button] - - self.terminateButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: self.presentationData.theme.list.itemDestructiveColor), font: .regular, height: 44.0, cornerRadius: 11.0) - - var hasSecretChats = false - var hasIncomingCalls = false - - let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - let title: String - let subtitle: String - let subtitleActive: Bool - let device: String - let deviceTitle: String - let location: String - let ip: String - switch subject { + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + let theme = environment.theme + let strings = environment.strings + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let state = context.state + if state.controller == nil { + state.controller = environment.controller() as? RecentSessionScreen + } + + 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 + ) + context.add(closeButton + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) + ) + + var contentHeight: CGFloat = 32.0 + switch component.subject { case let .session(session): - self.terminateButton.title = self.presentationData.strings.AuthSessions_View_TerminateSession - var appVersion = session.appVersion - appVersion = appVersion.replacingOccurrences(of: "APPSTORE", with: "").replacingOccurrences(of: "BETA", with: "Beta").trimmingTrailingSpaces() + let (image, backgroundColor, animationName, colorsArray) = iconForSession(session) - if session.isCurrent { - subtitle = presentationData.strings.Presence_online - subtitleActive = true - } else { - subtitle = stringForRelativeActivityTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, relativeTimestamp: session.activityDate, relativeTo: timestamp) - subtitleActive = false - } - deviceTitle = presentationData.strings.AuthSessions_View_Application - - var deviceString = "" - if !session.deviceModel.isEmpty { - deviceString = session.deviceModel - } - title = deviceString - device = "\(session.appName) \(appVersion)" - location = session.country - ip = session.ip - - let (icon, backgroundColor, animationName, colorsArray) = iconForSession(session) - if let animationName = animationName { + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: "background", + component: AnyComponent( + FilledRoundedRectangleComponent( + color: backgroundColor ?? .clear, + cornerRadius: .value(20.0), + smoothCorners: true + ) + ) + ) + ) + if let animationName { var colors: [String: UIColor] = [:] - if let colorsArray = colorsArray { + if let colorsArray { for color in colorsArray { colors[color] = backgroundColor } } - let animationNode = AnimationNode(animation: animationName, colors: colors, scale: 1.0) - self.animationNode = animationNode - - let animationBackgroundNode = ASDisplayNode() - animationBackgroundNode.cornerRadius = 20.0 - animationBackgroundNode.backgroundColor = backgroundColor - self.animationBackgroundNode = animationBackgroundNode - } else if let icon = icon { - let iconNode = ASImageNode() - iconNode.displaysAsynchronously = false - iconNode.image = icon - self.iconNode = iconNode + items.append( + AnyComponentWithIdentity( + id: "animation", + component: AnyComponent( + LottieAnimationComponent( + animation: .init(name: animationName, mode: .animating(loop: false)), + colors: colors, + size: CGSize(width: 92.0, height: 92.0) + ) + ) + ) + ) + } else if let image { + items.append( + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + Image(image: image) + ) + ) + ) } - - self.secretChatsSwitchNode.isOn = session.flags.contains(.acceptsSecretChats) - self.incomingCallsSwitchNode.isOn = session.flags.contains(.acceptsIncomingCalls) - - self.secretChatsActivateAreaNode.accessibilityValue = self.secretChatsSwitchNode.isOn ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off - self.incomingCallsActivateAreaNode.accessibilityValue = self.incomingCallsSwitchNode.isOn ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off - - if !session.flags.contains(.passwordPending) && session.apiId != 22 { - hasIncomingCalls = true - if ![2040, 2496].contains(session.apiId) { - hasSecretChats = true - } + + let icon = icon.update( + component: ZStack(items), + availableSize: CGSize(width: 92.0, height: 92.0), + transition: .immediate + ) + context.add(icon + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + icon.size.height / 2.0)) + ) + contentHeight += icon.size.height + contentHeight += 18.0 + case let .website(_, peer): + if let peer { + let avatar = avatar.update( + component: AvatarComponent( + context: component.context, + theme: environment.theme, + peer: peer, + 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 } - case let .website(website, peer): - self.terminateButton.title = self.presentationData.strings.AuthSessions_View_Logout + } - if let peer = peer { - title = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let titleString: String + let subtitleString: String + let subtitleActive: Bool + let applicationTitle: String + let applicationString: String + let ipString: String? + let locationString: String + let buttonString: String? + + switch component.subject { + case let .session(session): + titleString = session.deviceModel + if session.isCurrent { + subtitleString = strings.Presence_online + subtitleActive = true } else { - title = "" + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + subtitleString = stringForRelativeActivityTimestamp(strings: strings, dateTimeFormat: presentationData.dateTimeFormat, relativeTimestamp: session.activityDate, relativeTo: timestamp) + subtitleActive = false } - - subtitle = website.domain + var appVersion = session.appVersion + appVersion = appVersion.replacingOccurrences(of: "APPSTORE", with: "").replacingOccurrences(of: "BETA", with: "Beta").trimmingTrailingSpaces() + applicationTitle = strings.AuthSessions_View_Application + applicationString = "\(session.appName) \(appVersion)" + ipString = nil + locationString = session.country + + buttonString = !session.isCurrent ? strings.AuthSessions_View_TerminateSession : nil + case let .website(website, peer): + titleString = peer?.compactDisplayTitle ?? "" + subtitleString = website.domain subtitleActive = false - - deviceTitle = presentationData.strings.AuthSessions_View_Browser - + var deviceString = "" if !website.browser.isEmpty { deviceString += website.browser @@ -368,558 +281,418 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, ASScrollViewDe } deviceString += website.platform } - device = deviceString - location = website.region - ip = website.ip - - let avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 12.0)) - avatarNode.clipsToBounds = true - avatarNode.cornerRadius = 17.0 - if let peer { - avatarNode.setPeer(context: context, theme: presentationData.theme, peer: peer, authorOfMessage: nil, overrideImage: nil, emptyColor: nil, clipStyle: .none, synchronousLoad: false, displayDimensions: CGSize(width: 72.0, height: 72.0), storeUnrounded: false) - } - self.avatarNode = avatarNode - } - - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.regular(30.0), textColor: textColor) - self.titleNode.accessibilityLabel = title - self.titleNode.isAccessibilityElement = true - - self.textNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(17.0), textColor: subtitleActive ? accentColor : secondaryTextColor) - self.textNode.accessibilityLabel = subtitle - self.textNode.isAccessibilityElement = true - - self.deviceTitleNode.attributedText = NSAttributedString(string: deviceTitle, font: Font.regular(17.0), textColor: textColor) - self.deviceValueNode.attributedText = NSAttributedString(string: device, font: Font.regular(17.0), textColor: secondaryTextColor) - self.deviceValueNode.accessibilityLabel = deviceTitle - self.deviceValueNode.accessibilityValue = device - self.deviceValueNode.isAccessibilityElement = true - - self.firstSeparatorNode = ASDisplayNode() - self.firstSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor - - self.ipTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_IP, font: Font.regular(17.0), textColor: textColor) - self.ipValueNode.attributedText = NSAttributedString(string: ip, font: Font.regular(17.0), textColor: secondaryTextColor) - self.ipValueNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_IP - self.ipValueNode.accessibilityValue = ip - self.ipValueNode.isAccessibilityElement = true - - self.secondSeparatorNode = ASDisplayNode() - self.secondSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor - - self.locationTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_Location, font: Font.regular(17.0), textColor: textColor) - - self.locationValueNode.attributedText = NSAttributedString(string: location, font: Font.regular(17.0), textColor: secondaryTextColor) - self.locationValueNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_Location - self.locationValueNode.accessibilityValue = location - self.locationValueNode.isAccessibilityElement = true - - self.locationInfoNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_LocationInfo, font: Font.regular(13.0), textColor: secondaryTextColor) - self.locationInfoNode.maximumNumberOfLines = 4 - self.locationInfoNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_LocationInfo - self.locationInfoNode.isAccessibilityElement = true - - self.acceptBackgroundNode = ASDisplayNode() - self.acceptBackgroundNode.clipsToBounds = true - self.acceptBackgroundNode.cornerRadius = 11 - self.acceptBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor - - self.acceptHeaderNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptTitle.uppercased(), font: Font.regular(17.0), textColor: textColor) - self.acceptHeaderNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_AcceptTitle - self.acceptHeaderNode.isAccessibilityElement = true - - self.secretChatsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptSecretChats, font: Font.regular(17.0), textColor: textColor) - self.incomingCallsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptIncomingCalls, font: Font.regular(17.0), textColor: textColor) - - self.secretChatsActivateAreaNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_AcceptSecretChats - self.secretChatsActivateAreaNode.accessibilityHint = self.presentationData.strings.VoiceOver_Common_SwitchHint - - self.incomingCallsActivateAreaNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_AcceptIncomingCalls - self.incomingCallsActivateAreaNode.accessibilityHint = self.presentationData.strings.VoiceOver_Common_SwitchHint - - self.acceptSeparatorNode = ASDisplayNode() - self.acceptSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor - - super.init() - - self.backgroundColor = nil - self.isOpaque = false - - self.addSubnode(self.dimNode) - - self.wrappingScrollNode.view.delegate = self.wrappedScrollViewDelegate - self.addSubnode(self.wrappingScrollNode) - - self.wrappingScrollNode.addSubnode(self.backgroundNode) - self.wrappingScrollNode.addSubnode(self.contentContainerNode) - self.wrappingScrollNode.addSubnode(self.topContentContainerNode) - - self.backgroundNode.addSubnode(self.contentBackgroundNode) - self.contentContainerNode.addSubnode(self.titleNode) - self.contentContainerNode.addSubnode(self.textNode) - - self.contentContainerNode.addSubnode(self.fieldBackgroundNode) - - self.contentContainerNode.addSubnode(self.deviceTitleNode) - self.contentContainerNode.addSubnode(self.deviceValueNode) - - self.contentContainerNode.addSubnode(self.ipTitleNode) - self.contentContainerNode.addSubnode(self.ipValueNode) - - self.contentContainerNode.addSubnode(self.locationTitleNode) - self.contentContainerNode.addSubnode(self.locationValueNode) - self.contentContainerNode.addSubnode(self.locationInfoNode) - - self.contentContainerNode.addSubnode(self.firstSeparatorNode) - self.contentContainerNode.addSubnode(self.secondSeparatorNode) - - self.contentContainerNode.addSubnode(self.terminateButton) - self.topContentContainerNode.addSubnode(self.cancelButton) - - self.iconNode.flatMap { self.contentContainerNode.addSubnode($0) } - self.animationBackgroundNode.flatMap { self.contentContainerNode.addSubnode($0) } - self.animationNode.flatMap { self.contentContainerNode.addSubnode($0) } - self.avatarNode.flatMap { self.contentContainerNode.addSubnode($0) } - - if hasIncomingCalls { - self.contentContainerNode.addSubnode(self.acceptBackgroundNode) - self.contentContainerNode.addSubnode(self.acceptHeaderNode) - if hasSecretChats { - self.contentContainerNode.addSubnode(self.secretChatsTitleNode) - self.contentContainerNode.addSubnode(self.secretChatsSwitchNode) - self.contentContainerNode.addSubnode(self.secretChatsActivateAreaNode) + applicationTitle = strings.AuthSessions_View_Browser + applicationString = deviceString + ipString = website.ip + locationString = website.region - self.secretChatsSwitchNode.valueUpdated = { [weak self] value in - if let strongSelf = self { - strongSelf.updateAcceptSecretChats?(value) - - strongSelf.secretChatsActivateAreaNode.accessibilityValue = value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off - } - } - - self.secretChatsActivateAreaNode.activate = { [weak self] in - guard let strongSelf = self else { - return false - } - let value = !strongSelf.secretChatsSwitchNode.isOn - strongSelf.updateAcceptSecretChats?(value) - strongSelf.secretChatsActivateAreaNode.accessibilityValue = value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off - return true - } - - self.contentContainerNode.addSubnode(self.acceptSeparatorNode) - } - self.contentContainerNode.addSubnode(self.incomingCallsTitleNode) - self.contentContainerNode.addSubnode(self.incomingCallsSwitchNode) - self.contentContainerNode.addSubnode(self.incomingCallsActivateAreaNode) - - self.incomingCallsSwitchNode.valueUpdated = { [weak self] value in - if let strongSelf = self { - strongSelf.updateAcceptIncomingCalls?(value) - - strongSelf.incomingCallsActivateAreaNode.accessibilityValue = value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off - } + buttonString = strings.AuthSessions_View_Logout } - self.incomingCallsActivateAreaNode.activate = { [weak self] in - guard let strongSelf = self else { - return false - } - let value = !strongSelf.incomingCallsSwitchNode.isOn - strongSelf.updateAcceptIncomingCalls?(value) - strongSelf.incomingCallsActivateAreaNode.accessibilityValue = value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off - return true - } - } - - self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) - self.terminateButton.pressed = { [weak self] in - if let strongSelf = self { - strongSelf.remove?() - } - } - } - - override func didLoad() { - super.didLoad() - - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never - } - - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture))) - - let titleGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleTitleLongPress(_:))) - self.titleNode.view.addGestureRecognizer(titleGestureRecognizer) - - let deviceGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleDeviceLongPress(_:))) - self.deviceValueNode.view.addGestureRecognizer(deviceGestureRecognizer) - - let locationGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLocationLongPress(_:))) - self.locationValueNode.view.addGestureRecognizer(locationGestureRecognizer) - - let ipGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleIpLongPress(_:))) - self.ipValueNode.view.addGestureRecognizer(ipGestureRecognizer) - - if let animationNode = self.animationNode { - animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.animationPressed))) - } - } - - @objc private func handleTitleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { - if gestureRecognizer.state == .began { - self.displayCopyContextMenu(self.titleNode, self.titleNode.attributedText?.string ?? "") - } - } - - @objc private func handleDeviceLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { - if gestureRecognizer.state == .began { - self.displayCopyContextMenu(self.deviceValueNode, self.deviceValueNode.attributedText?.string ?? "") - } - } - - @objc private func handleLocationLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { - if gestureRecognizer.state == .began { - self.displayCopyContextMenu(self.locationValueNode, self.locationValueNode.attributedText?.string ?? "") - } - } - - @objc private func handleIpLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { - if gestureRecognizer.state == .began { - self.displayCopyContextMenu(self.ipValueNode, self.ipValueNode.attributedText?.string ?? "") - } - } - - private func displayCopyContextMenu(_ node: ASDisplayNode, _ string: String) { - if !string.isEmpty { - var actions: [ContextMenuAction] = [] - actions.append(ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in - UIPasteboard.general.string = string - - if let strongSelf = self { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } - })) - let contextMenuController = makeContextMenuController(actions: actions) - self.controller?.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in - if let strongSelf = self { - return (node, node.bounds.insetBy(dx: 0.0, dy: -2.0), strongSelf, strongSelf.view.bounds) - } else { - return nil - } - })) - } - } - - func updatePresentationData(_ presentationData: PresentationData) { - guard !self.animatedOut else { - return - } - let previousTheme = self.presentationData.theme - self.presentationData = presentationData - - self.contentBackgroundNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor - - self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.regular(30.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - - let subtitleColor: UIColor - if case let .session(session) = self.subject, session.isCurrent { - subtitleColor = self.presentationData.theme.list.itemAccentColor - } else { - subtitleColor = self.presentationData.theme.list.itemSecondaryTextColor - } - self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: subtitleColor) - - self.fieldBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor - self.firstSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor - self.secondSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor - self.acceptSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor - - self.deviceTitleNode.attributedText = NSAttributedString(string: self.deviceTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - self.locationTitleNode.attributedText = NSAttributedString(string: self.locationTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - self.ipTitleNode.attributedText = NSAttributedString(string: self.ipTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - - self.deviceValueNode.attributedText = NSAttributedString(string: self.deviceValueNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) - self.locationValueNode.attributedText = NSAttributedString(string: self.locationValueNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) - self.ipValueNode.attributedText = NSAttributedString(string: self.ipValueNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) - self.locationInfoNode.attributedText = NSAttributedString(string: self.locationInfoNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) - - self.acceptHeaderNode.attributedText = NSAttributedString(string: self.acceptHeaderNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) - self.secretChatsTitleNode.attributedText = NSAttributedString(string: self.secretChatsTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - self.incomingCallsTitleNode.attributedText = NSAttributedString(string: self.incomingCallsTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - self.acceptBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor - - if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - - self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) - self.terminateButton.updateTheme(SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: self.presentationData.theme.list.itemDestructiveColor)) - } - - @objc func animationPressed() { - if let animationNode = self.animationNode, !animationNode.isPlaying { - animationNode.playOnce() - } - } - - @objc func cancelButtonPressed() { - self.animateOut() - } - - @objc func dimTapGesture() { - self.cancelButtonPressed() - } - - private var animatedOut = false - func animateIn() { - self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - - let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) - let targetBounds = self.bounds - self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) - self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) - transition.animateView({ - self.bounds = targetBounds - self.dimNode.position = dimPosition - }) - } - - func animateOut(completion: (() -> Void)? = nil) { - self.animatedOut = true - - var dimCompleted = false - var offsetCompleted = false - - let internalCompletion: () -> Void = { [weak self] in - if let strongSelf = self, dimCompleted && offsetCompleted { - strongSelf.dismiss?() - } - completion?() - } - - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in - dimCompleted = true - internalCompletion() - }) - - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - self.wrappingScrollNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in - offsetCompleted = true - internalCompletion() - }) - - - self.controller?.window?.forEachController { c in - if let c = c as? UndoOverlayController { - c.dismiss() - } - } - } - - var passthroughHitTestImpl: ((CGPoint) -> UIView?)? - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.bounds.contains(point) { - if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) { - return self.dimNode.view - } - } - return super.hitTest(point, with: event) - } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - let contentOffset = scrollView.contentOffset - let additionalTopHeight = max(0.0, -contentOffset.y) - - if additionalTopHeight >= 30.0 { - self.cancelButtonPressed() - } - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - let isFirstTime = self.containerLayout == nil - self.containerLayout = (layout, navigationBarHeight) - - var insets = layout.insets(options: [.statusBar, .input]) - let cleanInsets = layout.insets(options: [.statusBar]) - insets.top = max(10.0, insets.top) - - let bottomInset: CGFloat = 10.0 + cleanInsets.bottom - - let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) - - transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - let iconSize = CGSize(width: 72.0, height: 72.0) - let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - iconSize.width) / 2.0), y: 36.0), size: iconSize) - - if let iconNode = self.iconNode { - transition.updateFrame(node: iconNode, frame: iconFrame) - } else if let animationNode = self.animationNode, let animationBackgroundNode = self.animationBackgroundNode { - transition.updateFrame(node: animationNode, frame: iconFrame) - transition.updateFrame(node: animationBackgroundNode, frame: iconFrame) - if #available(iOS 13.0, *) { - animationBackgroundNode.layer.cornerCurve = .continuous - } - if isFirstTime { - Queue.mainQueue().after(0.5) { - animationNode.playOnce() - } - } - } else if let avatarNode = self.avatarNode { - transition.updateFrame(node: avatarNode, frame: iconFrame) - } - - let inset: CGFloat = 16.0 - let titleSize = self.titleNode.updateLayout(CGSize(width: width - inset * 2.0, height: 100.0)) - let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - titleSize.width) / 2.0), y: 120.0), size: titleSize) - transition.updateFrame(node: self.titleNode, frame: titleFrame) - - let textSize = self.textNode.updateLayout(CGSize(width: width - inset * 2.0, height: 60.0)) - let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - textSize.width) / 2.0), y: titleFrame.maxY), size: textSize) - transition.updateFrame(node: self.textNode, frame: textFrame) - - let cancelSize = CGSize(width: 44.0, height: 44.0) - let cancelFrame = CGRect(origin: CGPoint(x: width - cancelSize.width - 3.0, y: 6.0), size: cancelSize) - transition.updateFrame(node: self.cancelButton, frame: cancelFrame) - - let fieldItemHeight: CGFloat = 44.0 - var fieldFrame = CGRect(x: inset, y: textFrame.maxY + 24.0, width: width - inset * 2.0, height: fieldItemHeight * 2.0) - if !(self.ipValueNode.attributedText?.string ?? "").isEmpty { - fieldFrame.size.height += fieldItemHeight + let titleFont = Font.bold(24.0) + let title = title.update( + component: BalancedTextComponent( + text: .markdown(text: titleString, 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 += 2.0 - self.ipTitleNode.isHidden = false - self.ipValueNode.isHidden = false - self.secondSeparatorNode.isHidden = false - } else { - self.ipTitleNode.isHidden = true - self.ipValueNode.isHidden = true - self.secondSeparatorNode.isHidden = true - } - transition.updateFrame(node: self.fieldBackgroundNode, frame: fieldFrame) - - let maxFieldTitleWidth = (width - inset * 4.0) * 0.4 - - let deviceTitleTextSize = self.deviceTitleNode.updateLayout(CGSize(width: maxFieldTitleWidth, height: fieldItemHeight)) - let deviceTitleTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.minY + floorToScreenPixels((fieldItemHeight - deviceTitleTextSize.height) / 2.0)), size: deviceTitleTextSize) - transition.updateFrame(node: self.deviceTitleNode, frame: deviceTitleTextFrame) - - let deviceValueTextSize = self.deviceValueNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0 - deviceTitleTextSize.width - 10.0, height: fieldItemHeight)) - let deviceValueTextFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - deviceValueTextSize.width - inset, y: fieldFrame.minY + floorToScreenPixels((fieldItemHeight - deviceValueTextSize.height) / 2.0)), size: deviceValueTextSize) - transition.updateFrame(node: self.deviceValueNode, frame: deviceValueTextFrame) - - transition.updateFrame(node: self.firstSeparatorNode, frame: CGRect(x: fieldFrame.minX + inset, y: fieldFrame.minY + fieldItemHeight, width: fieldFrame.width - inset, height: UIScreenPixel)) - - let ipTitleTextSize = self.ipTitleNode.updateLayout(CGSize(width: maxFieldTitleWidth, height: fieldItemHeight)) - let ipTitleTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.minY + fieldItemHeight + floorToScreenPixels((fieldItemHeight - ipTitleTextSize.height) / 2.0)), size: ipTitleTextSize) - transition.updateFrame(node: self.ipTitleNode, frame: ipTitleTextFrame) - - let ipValueTextSize = self.ipValueNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0 - ipTitleTextSize.width - 10.0, height: fieldItemHeight)) - let ipValueTextFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - ipValueTextSize.width - inset, y: fieldFrame.minY + fieldItemHeight + floorToScreenPixels((fieldItemHeight - ipValueTextSize.height) / 2.0)), size: ipValueTextSize) - transition.updateFrame(node: self.ipValueNode, frame: ipValueTextFrame) - - transition.updateFrame(node: self.secondSeparatorNode, frame: CGRect(x: fieldFrame.minX + inset, y: fieldFrame.minY + fieldItemHeight + fieldItemHeight, width: fieldFrame.width - inset, height: UIScreenPixel)) - - let locationTitleTextSize = self.locationTitleNode.updateLayout(CGSize(width: maxFieldTitleWidth, height: fieldItemHeight)) - let locationTitleTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - locationTitleTextSize.height) / 2.0)), size: locationTitleTextSize) - transition.updateFrame(node: self.locationTitleNode, frame: locationTitleTextFrame) - - let locationValueTextSize = self.locationValueNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0 - locationTitleTextSize.width - 10.0, height: fieldItemHeight)) - let locationValueTextFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - locationValueTextSize.width - inset, y: fieldFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - locationValueTextSize.height) / 2.0)), size: locationValueTextSize) - transition.updateFrame(node: self.locationValueNode, frame: locationValueTextFrame) - - let locationInfoTextSize = self.locationInfoNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0, height: fieldItemHeight * 2.0)) - let locationInfoTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.maxY + 6.0), size: locationInfoTextSize) - transition.updateFrame(node: self.locationInfoNode, frame: locationInfoTextFrame) - - var contentHeight = locationInfoTextFrame.maxY + bottomInset + 64.0 - - var secretFrame = CGRect(x: inset, y: locationInfoTextFrame.maxY + 59.0, width: width - inset * 2.0, height: fieldItemHeight) - if let _ = self.secretChatsTitleNode.supernode { - secretFrame.size.height += fieldItemHeight - } - transition.updateFrame(node: self.acceptBackgroundNode, frame: secretFrame) - - let secretChatsHeaderTextSize = self.acceptHeaderNode.updateLayout(CGSize(width: secretFrame.width - inset * 2.0, height: fieldItemHeight)) - let secretChatsHeaderTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.minY - secretChatsHeaderTextSize.height - 6.0), size: secretChatsHeaderTextSize) - transition.updateFrame(node: self.acceptHeaderNode, frame: secretChatsHeaderTextFrame) - - if let _ = self.secretChatsTitleNode.supernode { - let secretChatsTitleTextSize = self.secretChatsTitleNode.updateLayout(CGSize(width: width - inset * 4.0 - 80.0, height: fieldItemHeight)) - let secretChatsTitleTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.minY + floorToScreenPixels((fieldItemHeight - secretChatsTitleTextSize.height) / 2.0)), size: secretChatsTitleTextSize) - transition.updateFrame(node: self.secretChatsTitleNode, frame: secretChatsTitleTextFrame) - - if let switchView = self.secretChatsSwitchNode.view as? UISwitch { - if self.secretChatsSwitchNode.bounds.size.width.isZero { - switchView.sizeToFit() - } - let switchSize = switchView.bounds.size - - self.secretChatsSwitchNode.frame = CGRect(origin: CGPoint(x: fieldFrame.maxX - switchSize.width - inset, y: secretFrame.minY + floorToScreenPixels((fieldItemHeight - switchSize.height) / 2.0)), size: switchSize) - self.secretChatsActivateAreaNode.frame = CGRect(origin: CGPoint(x: secretFrame.minX, y: secretFrame.minY), size: CGSize(width: fieldFrame.width, height: fieldItemHeight)) - } - } - - let incomingCallsTitleTextSize = self.incomingCallsTitleNode.updateLayout(CGSize(width: width - inset * 4.0 - 80.0, height: fieldItemHeight)) - let incomingCallsTitleTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - incomingCallsTitleTextSize.height) / 2.0)), size: incomingCallsTitleTextSize) - transition.updateFrame(node: self.incomingCallsTitleNode, frame: incomingCallsTitleTextFrame) - - transition.updateFrame(node: self.acceptSeparatorNode, frame: CGRect(x: secretFrame.minX + inset, y: secretFrame.minY + fieldItemHeight, width: fieldFrame.width - inset, height: UIScreenPixel)) - - if let switchView = self.incomingCallsSwitchNode.view as? UISwitch { - if self.incomingCallsSwitchNode.bounds.size.width.isZero { - switchView.sizeToFit() - } - let switchSize = switchView.bounds.size + let textFont = Font.regular(15.0) + let description = description.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: subtitleString, font: textFont, textColor: subtitleActive ? theme.actionSheet.controlAccentColor : theme.actionSheet.secondaryTextColor)), + 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 += 22.0 - self.incomingCallsSwitchNode.frame = CGRect(origin: CGPoint(x: fieldFrame.maxX - switchSize.width - inset, y: secretFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - switchSize.height) / 2.0)), size: switchSize) - self.incomingCallsActivateAreaNode.frame = CGRect(origin: CGPoint(x: secretFrame.minX, y: secretFrame.maxY - fieldItemHeight), size: CGSize(width: fieldFrame.width, height: fieldItemHeight)) + var clientSectionItems: [AnyComponentWithIdentity] = [] + clientSectionItems.append( + AnyComponentWithIdentity(id: "application", component: AnyComponent( + ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: applicationTitle, + font: Font.regular(17.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + accessory: .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: "info", + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: applicationString, + font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0), + isInteractive: true + )), + action: nil + ) + )) + ) + + if let ipString { + clientSectionItems.append( + AnyComponentWithIdentity(id: "ip", component: AnyComponent( + ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.AuthSessions_View_IP, + font: Font.regular(17.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + accessory: .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: "info", + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: ipString, + font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )) + ), + 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: strings.AuthSessions_View_Location, + font: Font.regular(17.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + accessory: .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: "info", + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: locationString, + font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )) + ), + 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: strings.AuthSessions_View_LocationInfo, + 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 state.allowSecretChats != nil || state.allowIncomingCalls != nil { + contentHeight += 38.0 + + var optionsSectionItems: [AnyComponentWithIdentity] = [] + + if let allowSecretChats = state.allowSecretChats { + optionsSectionItems.append(AnyComponentWithIdentity(id: "allowSecretChats", component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.AuthSessions_View_AcceptSecretChats, + font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: allowSecretChats, action: { [weak state] _ in + guard let state else { + return + } + state.toggleAllowSecretChats() + })), + action: nil + )))) + } + if let allowIncomingCalls = state.allowIncomingCalls { + optionsSectionItems.append(AnyComponentWithIdentity(id: "allowIncomingCalls", component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.AuthSessions_View_AcceptIncomingCalls, + font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: allowIncomingCalls, action: { [weak state] _ in + guard let state else { + return + } + state.toggleAllowIncomingCalls() + })), + action: nil + )))) + } + let optionsSection = optionsSection.update( + component: ListSectionComponent( + theme: theme, + style: .glass, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.AuthSessions_View_AcceptTitle.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + 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 + + if let buttonString { + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let button = button.update( + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemDestructiveColor, + foreground: .white, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + ), + action: { [weak state] in + state?.terminate() + } + ), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), + transition: .immediate + ) + context.add(button + .position(CGPoint(x: context.availableSize.width / 2.0 , y: contentHeight + button.size.height / 2.0)) + ) + contentHeight += button.size.height + contentHeight += buttonInsets.bottom + } + + return CGSize(width: context.availableSize.width, height: contentHeight) + } + } +} + +private final class RecentSessionSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: RecentSessionScreen.Subject + + init( + context: AccountContext, + subject: RecentSessionScreen.Subject + ) { + self.context = context + self.subject = subject + } + + static func ==(lhs: RecentSessionSheetComponent, rhs: RecentSessionSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(RecentSessionSheetContent( + context: context.component.context, + subject: context.component.subject, + 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 RecentSessionScreen: ViewControllerComponentContainer { + public enum Subject { + case session(RecentAccountSession) + case website(WebAuthorization, EnginePeer?) + } + + private let context: AccountContext + fileprivate let updateAcceptSecretChats: (Bool) -> Void + fileprivate let updateAcceptIncomingCalls: (Bool) -> Void + fileprivate let remove: (@escaping () -> Void) -> Void + + public init( + context: AccountContext, + subject: RecentSessionScreen.Subject, + updateAcceptSecretChats: @escaping (Bool) -> Void, + updateAcceptIncomingCalls: @escaping (Bool) -> Void, + remove: @escaping (@escaping () -> Void) -> Void + ) { + self.context = context + self.updateAcceptSecretChats = updateAcceptSecretChats + self.updateAcceptIncomingCalls = updateAcceptIncomingCalls + self.remove = remove + + super.init( + context: context, + component: RecentSessionSheetComponent( + context: context, + subject: subject + ), + 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.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() } - - if let _ = self.acceptBackgroundNode.supernode { - contentHeight += secretFrame.maxY - locationInfoTextFrame.maxY - } - contentHeight += 40.0 - - let isCurrent: Bool - if case let .session(session) = self.subject, session.isCurrent { - isCurrent = true - } else { - isCurrent = false - } - - if isCurrent { - contentHeight -= 68.0 - self.terminateButton.isHidden = true - self.terminateButton.isAccessibilityElement = false - } else { - self.terminateButton.isHidden = false - self.terminateButton.isAccessibilityElement = true - } - - let sideInset = floor((layout.size.width - width) / 2.0) - let scrollContentHeight = max(layout.size.height, contentHeight) - let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: max(layout.statusBarHeight ?? 20.0, layout.size.height - contentHeight)), size: CGSize(width: width, height: contentHeight)) - let contentFrame = contentContainerFrame - - self.wrappingScrollNode.view.contentSize = CGSize(width: layout.size.width, height: scrollContentHeight) - - var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: width, height: contentFrame.height + 2000.0)) - if backgroundFrame.minY < contentFrame.minY { - backgroundFrame.origin.y = contentFrame.minY - } - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - - let doneButtonHeight = self.terminateButton.updateLayout(width: width - inset * 2.0, transition: transition) - transition.updateFrame(node: self.terminateButton, frame: CGRect(x: inset, y: contentHeight - doneButtonHeight - 40.0 - insets.bottom - 6.0, width: width, height: doneButtonHeight)) - - transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) - transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame) } } diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index 6635834096..67ef6d1348 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -30,6 +30,7 @@ import ContextUI import QuickReactionSetupController import AvatarEditorScreen import PeerSelectionScreen +import DeviceModel enum SettingsSearchableItemIcon { case profile @@ -3895,7 +3896,7 @@ private func appearanceSearchableItems(context: AccountContext) -> [SettingsSear present(.push, themeSettingsController(context: context, focusOnItemTag: itemTag)) } - return [ + var items: [SettingsSearchableItem] = [ SettingsSearchableItem( id: "appearance", title: strings.Settings_Appearance, @@ -4094,14 +4095,31 @@ private func appearanceSearchableItems(context: AccountContext) -> [SettingsSear ), SettingsSearchableItem( id: "appearance/tap-for-next-media", + title: strings.Appearance_ShowNextMediaOnTap, icon: icon, breadcrumbs: [strings.Settings_Appearance], isVisible: false, present: { context, _, present in presentAppearanceSettings(context, present, .tapForNextMedia) } - ), + ) ] + + if DeviceModel.current.isIpad { + items.append( + SettingsSearchableItem( + id: "appearance/send-with-cmd-enter", + title: strings.Appearance_SendWithCmdEnter, + icon: icon, + breadcrumbs: [strings.Settings_Appearance], + isVisible: false, + present: { context, _, present in + presentAppearanceSettings(context, present, .sendWithCmdEnter) + } + ) + ) + } + return items } private func languageSearchableItems(context: AccountContext, localizations: [LocalizationInfo]) -> [SettingsSearchableItem] { diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift index 4ba2046088..8140064aff 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift @@ -12,12 +12,7 @@ import AppBundle private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selected: Bool) -> UIImage? { return generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) - context.setFillColor(theme.list.itemBlocksBackgroundColor.cgColor) - context.fill(bounds) - - context.setBlendMode(.clear) - context.fillEllipse(in: bounds) - context.setBlendMode(.normal) + context.clear(bounds) let lineWidth: CGFloat if selected { @@ -113,8 +108,10 @@ private final class ThemeSettingsAppIconNode : ASDisplayNode { override init() { self.iconNode = ASImageNode() + self.iconNode.clipsToBounds = true self.iconNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 63.0, height: 63.0)) self.iconNode.isLayerBacked = true + self.iconNode.cornerRadius = 15.0 self.overlayNode = ASImageNode() self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 63.0, height: 63.0)) @@ -199,6 +196,7 @@ private let selectedTextFont = Font.medium(12.0) class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode + private let highlightNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode @@ -219,6 +217,9 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true + self.highlightNode = ASDisplayNode() + self.highlightNode.isLayerBacked = true + self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true @@ -234,6 +235,20 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { self.addSubnode(self.containerNode) } + 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: ThemeSettingsAppIconItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { return { item, params, neighbors in let contentSize: CGSize @@ -280,6 +295,7 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.highlightNode.backgroundColor = item.theme.list.itemSearchHighlightColor if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) @@ -321,6 +337,7 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.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: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index 37d174b993..726a00f856 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -38,13 +38,34 @@ private final class ThemeSettingsControllerArguments { let openBubbleSettings: () -> Void let openPowerSavingSettings: () -> Void let openStickersAndEmoji: () -> Void + let toggleSendWithCmdEnter: (Bool) -> Void let toggleShowNextMediaOnTap: (Bool) -> Void let selectAppIcon: (PresentationAppIcon) -> Void let editTheme: (PresentationCloudTheme) -> Void let themeContextAction: (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void let colorContextAction: (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void - init(context: AccountContext, selectTheme: @escaping (PresentationThemeReference) -> Void, openThemeSettings: @escaping () -> Void, openWallpaperSettings: @escaping () -> Void, openNameColorSettings: @escaping () -> Void, selectAccentColor: @escaping (PresentationThemeAccentColor?) -> Void, openAccentColorPicker: @escaping (PresentationThemeReference, Bool) -> Void, toggleNightTheme: @escaping (Bool) -> Void, openAutoNightTheme: @escaping () -> Void, openTextSize: @escaping () -> Void, openBubbleSettings: @escaping () -> Void, openPowerSavingSettings: @escaping () -> Void, openStickersAndEmoji: @escaping () -> Void, toggleShowNextMediaOnTap: @escaping (Bool) -> Void, selectAppIcon: @escaping (PresentationAppIcon) -> Void, editTheme: @escaping (PresentationCloudTheme) -> Void, themeContextAction: @escaping (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void, colorContextAction: @escaping (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void) { + init( + context: AccountContext, + selectTheme: @escaping (PresentationThemeReference) -> Void, + openThemeSettings: @escaping () -> Void, + openWallpaperSettings: @escaping () -> Void, + openNameColorSettings: @escaping () -> Void, + selectAccentColor: @escaping (PresentationThemeAccentColor?) -> Void, + openAccentColorPicker: @escaping (PresentationThemeReference, Bool) -> Void, + toggleNightTheme: @escaping (Bool) -> Void, + openAutoNightTheme: @escaping () -> Void, + openTextSize: @escaping () -> Void, + openBubbleSettings: @escaping () -> Void, + openPowerSavingSettings: @escaping () -> Void, + openStickersAndEmoji: @escaping () -> Void, + toggleSendWithCmdEnter: @escaping (Bool) -> Void, + toggleShowNextMediaOnTap: @escaping (Bool) -> Void, + selectAppIcon: @escaping (PresentationAppIcon) -> Void, + editTheme: @escaping (PresentationCloudTheme) -> Void, + themeContextAction: @escaping (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void, + colorContextAction: @escaping (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void + ) { self.context = context self.selectTheme = selectTheme self.openThemeSettings = openThemeSettings @@ -58,6 +79,7 @@ private final class ThemeSettingsControllerArguments { self.openBubbleSettings = openBubbleSettings self.openPowerSavingSettings = openPowerSavingSettings self.openStickersAndEmoji = openStickersAndEmoji + self.toggleSendWithCmdEnter = toggleSendWithCmdEnter self.toggleShowNextMediaOnTap = toggleShowNextMediaOnTap self.selectAppIcon = selectAppIcon self.editTheme = editTheme @@ -84,6 +106,7 @@ public enum ThemeSettingsEntryTag: ItemListItemTag { case powerSaving case stickersAndEmoji case animations + case sendWithCmdEnter case tapForNextMedia case nightMode @@ -112,6 +135,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { case powerSaving case stickersAndEmoji case otherHeader(PresentationTheme, String) + case sendWithCmdEnter(PresentationTheme, String, Bool) case showNextMediaOnTap(PresentationTheme, String, Bool) case showNextMediaOnTapInfo(PresentationTheme, String) @@ -127,7 +151,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { return ThemeSettingsControllerSection.icon.rawValue case .powerSaving, .stickersAndEmoji: return ThemeSettingsControllerSection.message.rawValue - case .otherHeader, .showNextMediaOnTap, .showNextMediaOnTapInfo: + case .otherHeader, .sendWithCmdEnter, .showNextMediaOnTap, .showNextMediaOnTapInfo: return ThemeSettingsControllerSection.other.rawValue } } @@ -164,10 +188,12 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { return 13 case .otherHeader: return 14 - case .showNextMediaOnTap: + case .sendWithCmdEnter: return 15 - case .showNextMediaOnTapInfo: + case .showNextMediaOnTap: return 16 + case .showNextMediaOnTapInfo: + return 17 } } @@ -263,6 +289,12 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { } else { return false } + case let .sendWithCmdEnter(lhsTheme, lhsTitle, lhsValue): + if case let .sendWithCmdEnter(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue { + return true + } else { + return false + } case let .showNextMediaOnTap(lhsTheme, lhsTitle, lhsValue): if case let .showNextMediaOnTap(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue { return true @@ -351,6 +383,10 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { }) case let .otherHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .sendWithCmdEnter(_, title, value): + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleSendWithCmdEnter(value) + }, tag: ThemeSettingsEntryTag.sendWithCmdEnter) 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) @@ -361,7 +397,21 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { } } -private func themeSettingsControllerEntries(presentationData: PresentationData, presentationThemeSettings: PresentationThemeSettings, mediaSettings: MediaDisplaySettings, themeReference: PresentationThemeReference, availableThemes: [PresentationThemeReference], availableAppIcons: [PresentationAppIcon], currentAppIconName: String?, isPremium: Bool, chatThemes: [PresentationThemeReference], animatedEmojiStickers: [String: [StickerPackItem]], accountPeer: EnginePeer?, nameColors: PeerNameColors) -> [ThemeSettingsControllerEntry] { +private func themeSettingsControllerEntries( + presentationData: PresentationData, + presentationThemeSettings: PresentationThemeSettings, + chatSettings: ChatSettings, + mediaSettings: MediaDisplaySettings, + themeReference: PresentationThemeReference, + availableThemes: [PresentationThemeReference], + availableAppIcons: [PresentationAppIcon], + currentAppIconName: String?, + isPremium: Bool, + chatThemes: [PresentationThemeReference], + animatedEmojiStickers: [String: [StickerPackItem]], + accountPeer: EnginePeer?, + nameColors: PeerNameColors +) -> [ThemeSettingsControllerEntry] { var entries: [ThemeSettingsControllerEntry] = [] let strings = presentationData.strings @@ -437,6 +487,7 @@ private func themeSettingsControllerEntries(presentationData: PresentationData, } entries.append(.otherHeader(presentationData.theme, strings.Appearance_Other.uppercased())) + entries.append(.sendWithCmdEnter(presentationData.theme, strings.Appearance_SendWithCmdEnter, chatSettings.sendWithCmdEnter)) entries.append(.showNextMediaOnTap(presentationData.theme, strings.Appearance_ShowNextMediaOnTap, mediaSettings.showNextMediaOnTap)) entries.append(.showNextMediaOnTapInfo(presentationData.theme, strings.Appearance_ShowNextMediaOnTapInfo)) @@ -557,6 +608,10 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The pushControllerImpl?(installedStickerPacksController(context: context, mode: .general, archivedPacks: archivedStickerPacks, updatedPacks: { _ in })) }) + }, toggleSendWithCmdEnter: { value in + let _ = updateChatSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + return current.withUpdatedSendWithCmdEnter(value) + }).start() }, toggleShowNextMediaOnTap: { value in let _ = updateMediaDisplaySettingsInteractively(accountManager: context.sharedContext.accountManager, { current in return current.withUpdatedShowNextMediaOnTap(value) @@ -1035,9 +1090,26 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The }) }) - let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings, SharedDataKeys.chatThemes, ApplicationSpecificSharedDataKeys.mediaDisplaySettings]), cloudThemes.get(), availableAppIcons, currentAppIconName.get(), removedThemeIndexesPromise.get(), animatedEmojiStickers, context.account.postbox.peerView(id: context.account.peerId), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))) + let signal = combineLatest( + queue: .mainQueue(), + context.sharedContext.presentationData, + context.sharedContext.accountManager.sharedData(keys: [ + ApplicationSpecificSharedDataKeys.presentationThemeSettings, + ApplicationSpecificSharedDataKeys.chatSettings, + ApplicationSpecificSharedDataKeys.mediaDisplaySettings, + SharedDataKeys.chatThemes + ]), + cloudThemes.get(), + availableAppIcons, + currentAppIconName.get(), + removedThemeIndexesPromise.get(), + animatedEmojiStickers, + context.account.postbox.peerView(id: context.account.peerId), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + ) |> map { presentationData, sharedData, cloudThemes, availableAppIcons, currentAppIconName, removedThemeIndexes, animatedEmojiStickers, peerView, accountPeer -> (ItemListControllerState, (ItemListNodeState, Any)) in let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings + let chatSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.chatSettings]?.get(ChatSettings.self) ?? ChatSettings.defaultSettings let mediaSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaDisplaySettings]?.get(MediaDisplaySettings.self) ?? MediaDisplaySettings.defaultSettings let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false @@ -1077,7 +1149,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The chatThemes.insert(.builtin(.dayClassic), at: 0) let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Appearance_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(presentationData: presentationData, presentationThemeSettings: settings, mediaSettings: mediaSettings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName, isPremium: isPremium, chatThemes: chatThemes, animatedEmojiStickers: animatedEmojiStickers, accountPeer: accountPeer, nameColors: context.peerNameColors), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(presentationData: presentationData, presentationThemeSettings: settings, chatSettings: chatSettings, mediaSettings: mediaSettings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName, isPremium: isPremium, chatThemes: chatThemes, animatedEmojiStickers: animatedEmojiStickers, accountPeer: accountPeer, nameColors: context.peerNameColors), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index bfdd214154..41b8ce6d22 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -289,9 +289,9 @@ private final class StickerPackContainer: ASDisplayNode { self.addSubnode(self.gridNode) self.titleContainer.addSubnode(self.titleNode) + self.addSubnode(self.topContainerNode) self.addSubnode(self.titleContainer) - self.addSubnode(self.topContainerNode) self.addSubnode(self.bottomContainerNode) self.gridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in @@ -654,6 +654,13 @@ private final class StickerPackContainer: ASDisplayNode { self.reorderingGestureRecognizer = reorderingGestureRecognizer self.gridNode.view.addGestureRecognizer(reorderingGestureRecognizer) + self.gridNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.gridNode.clipsToBounds = true + self.gridNode.layer.cornerRadius = 40.0 + + self.topEdgeEffectView.clipsToBounds = true + self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.topEdgeEffectView.layer.cornerRadius = 40.0 self.topContainerNode.view.addSubview(self.topEdgeEffectView) self.bottomContainerNode.view.addSubview(self.bottomEdgeEffectView) } @@ -1891,7 +1898,7 @@ private final class StickerPackContainer: ASDisplayNode { 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)) + let gridFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top)) let itemsPerRow = 5 let fillingWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) @@ -1923,12 +1930,13 @@ private final class StickerPackContainer: ASDisplayNode { let initialRevealedRowCount: CGFloat = 4.5 - let topInset: CGFloat + var topInset: CGFloat if case .regular = layout.metrics.widthClass { - topInset = 0.0 + topInset = titleAreaInset } else { topInset = insets.top + max(0.0, layout.size.height - floor(initialRevealedRowCount * itemWidth) - insets.top - actionAreaHeight - titleAreaInset) } + let additionalGridBottomInset = max(0.0, gridFrame.size.height - actionAreaHeight - contentHeight) let gridInsets = UIEdgeInsets(top: topInset, left: gridLeftInset, bottom: actionAreaHeight + additionalGridBottomInset, right: layout.size.width - fillingWidth - gridLeftInset) @@ -2112,6 +2120,10 @@ private final class StickerPackContainer: ASDisplayNode { buttonView.frame = buttonFrame } + let topEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: 90.0)) + transition.updateFrame(view: self.topEdgeEffectView, frame: topEdgeEffectFrame) + self.topEdgeEffectView.update(content: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 0.65, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition)) + 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)) @@ -2133,7 +2145,7 @@ private final class StickerPackContainer: ASDisplayNode { return } - let minBackgroundY = gridFrame.minY - titleAreaInset + let minBackgroundY = gridFrame.minY let unclippedBackgroundY = gridFrame.minY - presentationLayout.contentOffset.y - titleAreaInset let offsetFromInitialPosition = presentationLayout.contentOffset.y + gridInsets.top @@ -2173,7 +2185,7 @@ private final class StickerPackContainer: ASDisplayNode { var titleContainerFrame: CGRect if case .regular = layout.metrics.widthClass { backgroundFrame.origin.y = min(0.0, backgroundFrame.origin.y) - titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: floor((56.0) / 2.0)), size: CGSize()) + titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: floor((56.0) / 2.0) + 10.0), size: CGSize()) } else { titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: backgroundFrame.minY + floor((56.0) / 2.0) + 10.0), size: CGSize()) } @@ -3103,12 +3115,12 @@ private final class StickerPackContextReferenceContentSource: ContextReferenceCo private func generateShadowImage() -> UIImage? { - return generateImage(CGSize(width: 140.0, height: 140.0), rotatedContext: { size, context in + return generateImage(CGSize(width: 220.0, height: 220.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.saveGState() context.setShadow(offset: CGSize(), blur: 60.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor) - let path = UIBezierPath(roundedRect: CGRect(x: 60.0, y: 60.0, width: 20.0, height: 20.0), cornerRadius: 10.0).cgPath + let path = UIBezierPath(roundedRect: CGRect(x: 60.0, y: 60.0, width: 100.0, height: 100.0), cornerRadius: 40.0).cgPath context.addPath(path) context.fillPath() @@ -3117,7 +3129,7 @@ private func generateShadowImage() -> UIImage? { context.setBlendMode(.clear) context.addPath(path) context.fillPath() - })?.stretchableImage(withLeftCapWidth: 70, topCapHeight: 70) + })?.stretchableImage(withLeftCapWidth: 110, topCapHeight: 110) } private func generateArrowImage(color: UIColor) -> UIImage? { @@ -3290,38 +3302,6 @@ private class ReorderingGestureRecognizer: UIGestureRecognizer { } } -private func generateShadowImage(corners: CACornerMask, radius: CGFloat) -> UIImage? { - return generateImage(CGSize(width: 120.0, height: 120), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - -// context.saveGState() - context.setShadow(offset: CGSize(), blur: 28.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor) - - var rectCorners: UIRectCorner = [] - if corners.contains(.layerMinXMinYCorner) { - rectCorners.insert(.topLeft) - } - if corners.contains(.layerMaxXMinYCorner) { - rectCorners.insert(.topRight) - } - if corners.contains(.layerMinXMaxYCorner) { - rectCorners.insert(.bottomLeft) - } - if corners.contains(.layerMaxXMaxYCorner) { - rectCorners.insert(.bottomRight) - } - - let path = UIBezierPath(roundedRect: CGRect(x: 30.0, y: 30.0, width: 60.0, height: 60.0), byRoundingCorners: rectCorners, cornerRadii: CGSize(width: radius, height: radius)).cgPath - context.addPath(path) - context.fillPath() -// context.restoreGState() - -// context.setBlendMode(.clear) -// context.addPath(path) -// context.fillPath() - })?.stretchableImage(withLeftCapWidth: 60, topCapHeight: 60) -} - private final class CopyView: UIView { let shadow: UIImageView var snapshotView: UIView? diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index fea49560a1..07ef17b741 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -248,7 +248,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe guard let gift = StarGift(apiStarGift: apiGift) else { return nil } - return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, isPrepaidUpgrade: (flags & (1 << 11)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleAmount: resaleAmount.flatMap { CurrencyAmount(apiAmount: $0) }, canTransferDate: canTransferDate, canResaleDate: canResaleDate, dropOriginalDetailsStars: dropOriginalDetailsStars, assigned: (flags & (1 << 13)) != 0, fromOffer: (flags & (1 << 14)) != 0, canCraftAt: canCraftAt)) + return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, isPrepaidUpgrade: (flags & (1 << 11)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleAmount: resaleAmount.flatMap { CurrencyAmount(apiAmount: $0) }, canTransferDate: canTransferDate, canResaleDate: canResaleDate, dropOriginalDetailsStars: dropOriginalDetailsStars, assigned: (flags & (1 << 13)) != 0, fromOffer: (flags & (1 << 14)) != 0, canCraftAt: canCraftAt, isCrafted: (flags & (1 << 16)) != 0)) case let .messageActionPaidMessagesRefunded(messageActionPaidMessagesRefundedData): let (count, stars) = (messageActionPaidMessagesRefundedData.count, messageActionPaidMessagesRefundedData.stars) return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars)) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 850a25fb5a..4bb46b066e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -256,7 +256,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?) case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, prepaidUpgradeHash: String?, giftMessageId: Int32?, upgradeSeparate: Bool, isAuctionAcquired: Bool, toPeerId: EnginePeer.Id?, number: Int32?) - case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleAmount: CurrencyAmount?, canTransferDate: Int32?, canResaleDate: Int32?, dropOriginalDetailsStars: Int64?, assigned: Bool, fromOffer: Bool, canCraftAt: Int32?) + case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleAmount: CurrencyAmount?, canTransferDate: Int32?, canResaleDate: Int32?, dropOriginalDetailsStars: Int64?, assigned: Bool, fromOffer: Bool, canCraftAt: Int32?, isCrafted: Bool) case paidMessagesRefunded(count: Int32, stars: Int64) case paidMessagesPriceEdited(stars: Int64, broadcastMessagesAllowed: Bool) case conferenceCall(ConferenceCall) @@ -404,7 +404,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else if let stars = decoder.decodeOptionalInt64ForKey("resaleStars") { resaleAmount = CurrencyAmount(amount: StarsAmount(value: stars, nanos: 0), currency: .stars) } - self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleAmount: resaleAmount, canTransferDate: decoder.decodeOptionalInt32ForKey("canTransferDate"), canResaleDate: decoder.decodeOptionalInt32ForKey("canResaleDate"), dropOriginalDetailsStars: decoder.decodeOptionalInt64ForKey("dropOriginalDetailsStars"), assigned: decoder.decodeBoolForKey("assigned", orElse: false), fromOffer: decoder.decodeBoolForKey("fromOffer", orElse: false), canCraftAt: decoder.decodeOptionalInt32ForKey("canCraftAt")) + self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleAmount: resaleAmount, canTransferDate: decoder.decodeOptionalInt32ForKey("canTransferDate"), canResaleDate: decoder.decodeOptionalInt32ForKey("canResaleDate"), dropOriginalDetailsStars: decoder.decodeOptionalInt64ForKey("dropOriginalDetailsStars"), assigned: decoder.decodeBoolForKey("assigned", orElse: false), fromOffer: decoder.decodeBoolForKey("fromOffer", orElse: false), canCraftAt: decoder.decodeOptionalInt32ForKey("canCraftAt"), isCrafted: decoder.decodeBoolForKey("isCrafted", orElse: false)) case 46: self = .paidMessagesRefunded(count: decoder.decodeInt32ForKey("count", orElse: 0), stars: decoder.decodeInt64ForKey("stars", orElse: 0)) case 47: @@ -803,7 +803,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "number") } - case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars, assigned, fromOffer, canCraftAt): + case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars, assigned, fromOffer, canCraftAt, isCrafted): encoder.encodeInt32(45, forKey: "_rawValue") encoder.encodeObject(gift, forKey: "gift") encoder.encodeBool(isUpgrade, forKey: "isUpgrade") @@ -864,6 +864,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "canCraftAt") } + + encoder.encodeBool(isCrafted, forKey: "isCrafted") case let .paidMessagesRefunded(count, stars): encoder.encodeInt32(46, forKey: "_rawValue") encoder.encodeInt32(count, forKey: "count") @@ -971,7 +973,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { peerIds.append(toPeerId) } return peerIds - case let .starGiftUnique(gift, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _, _, _, _): + case let .starGiftUnique(gift, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _, _, _, _, _): var peerIds: [PeerId] = [] if let peerId { peerIds.append(peerId) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 2d67d2f562..af2ae892f5 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -1524,7 +1524,7 @@ func _internal_dropStarGiftOriginalDetails(account: Account, reference: StarGift storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) } var media = currentMessage.media - if let action = media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer, canCraftAt) = action.action, case let .unique(uniqueGift) = gift { + if let action = media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer, canCraftAt, isCrafted) = action.action, case let .unique(uniqueGift) = gift { let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo } media = [ TelegramMediaAction( @@ -1546,7 +1546,8 @@ func _internal_dropStarGiftOriginalDetails(account: Account, reference: StarGift dropOriginalDetailsStars: nil, assigned: assigned, fromOffer: fromOffer, - canCraftAt: canCraftAt + canCraftAt: canCraftAt, + isCrafted: isCrafted ) ) ] @@ -1653,7 +1654,7 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star let message = updateNewMessageData.message if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: false) { for media in message.media { - if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt) = action.action, case let .Id(messageId) = message.id { + if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt, _) = action.action, case let .Id(messageId) = message.id { let reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) @@ -3186,12 +3187,6 @@ 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 @@ -3325,12 +3320,6 @@ public final class CraftGiftsContext { } } - 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 impl.removeGifts(references: references) @@ -3395,7 +3384,7 @@ func _internal_craftStarGift(account: Account, references: [StarGiftReference]) let message = updateNewMessageData.message if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: false) { for media in message.media { - if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt) = action.action, case let .Id(messageId) = message.id { + if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt, _) = action.action, case let .Id(messageId) = message.id { let reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index caa4c59dcc..fa6c51a454 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -1658,7 +1658,7 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer, .premiumGift, .starGiftResale, .starGiftPrepaidUpgrade, .starGiftDropOriginalDetails, .starGiftAuctionBid: receiptMessageId = nil } - } else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, isRefunded, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt) = action.action, case let .Id(messageId) = message.id { + } else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, isRefunded, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt, _) = action.action, case let .Id(messageId) = message.id { let reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) diff --git a/submodules/TelegramPresentationData/Sources/PresentationData.swift b/submodules/TelegramPresentationData/Sources/PresentationData.swift index 17dc241a3d..ca3e549ff3 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationData.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationData.swift @@ -217,9 +217,10 @@ public final class InitialPresentationDataAndSettings { public let mediaInputSettings: MediaInputSettings public let mediaDisplaySettings: MediaDisplaySettings public let stickerSettings: StickerSettings + public let chatSettings: ChatSettings public let experimentalUISettings: ExperimentalUISettings - public init(presentationData: PresentationData, automaticMediaDownloadSettings: MediaAutoDownloadSettings, autodownloadSettings: AutodownloadSettings, callListSettings: CallListSettings, inAppNotificationSettings: InAppNotificationSettings, mediaInputSettings: MediaInputSettings, mediaDisplaySettings: MediaDisplaySettings, stickerSettings: StickerSettings, experimentalUISettings: ExperimentalUISettings) { + public init(presentationData: PresentationData, automaticMediaDownloadSettings: MediaAutoDownloadSettings, autodownloadSettings: AutodownloadSettings, callListSettings: CallListSettings, inAppNotificationSettings: InAppNotificationSettings, mediaInputSettings: MediaInputSettings, mediaDisplaySettings: MediaDisplaySettings, stickerSettings: StickerSettings, chatSettings: ChatSettings, experimentalUISettings: ExperimentalUISettings) { self.presentationData = presentationData self.automaticMediaDownloadSettings = automaticMediaDownloadSettings self.autodownloadSettings = autodownloadSettings @@ -228,6 +229,7 @@ public final class InitialPresentationDataAndSettings { self.mediaInputSettings = mediaInputSettings self.mediaDisplaySettings = mediaDisplaySettings self.stickerSettings = stickerSettings + self.chatSettings = chatSettings self.experimentalUISettings = experimentalUISettings } } @@ -245,6 +247,7 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager deliverOn(Queue(name: "PresentationData-Load", qos: .userInteractive)) @@ -365,6 +372,13 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + self.peer = peer + self.updated() + }) } func displayPhoneNumberConfirmation(commit: @escaping (Bool) -> Void) { @@ -99,13 +110,12 @@ private final class AuthConfirmationSheetContent: CombinedComponent { static var body: Body { let closeButton = Child(GlassBarButtonComponent.self) + let peerButton = Child(AvatarComponent.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) @@ -147,6 +157,24 @@ private final class AuthConfirmationSheetContent: CombinedComponent { availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) + context.add(closeButton + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) + ) + + if let peer = state.peer { + let peerButton = peerButton.update( + component: AvatarComponent( + context: component.context, + theme: environment.theme, + peer: peer + ), + availableSize: CGSize(width: 44.0, height: 44.0), + transition: .immediate + ) + context.add(peerButton + .position(CGPoint(x: context.availableSize.width - 16.0 - peerButton.size.width / 2.0, y: 16.0 + peerButton.size.height / 2.0)) + ) + } var contentHeight: CGFloat = 32.0 let avatar = avatar.update( @@ -320,7 +348,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent { contentHeight += clientSection.size.height if flags.contains(.requestWriteAccess) { - contentHeight += 22.0 + contentHeight += 38.0 var optionsSectionItems: [AnyComponentWithIdentity] = [] optionsSectionItems.append(AnyComponentWithIdentity(id: "allowWrite", component: AnyComponent(ListActionItemComponent( @@ -441,10 +469,6 @@ private final class AuthConfirmationSheetContent: CombinedComponent { 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) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h index f4a0508002..08c09cff99 100755 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h @@ -18,7 +18,7 @@ @property (nonatomic, copy) bool (^ _Nullable shouldPaste)(); @property (nonatomic, copy) bool (^ _Nullable shouldRespondToAction)(SEL _Nullable); @property (nonatomic, copy) ChatInputTextViewImplTargetForAction * _Nullable (^ _Nullable targetForAction)(SEL _Nullable); -@property (nonatomic, copy) bool (^ _Nullable shouldReturn)(); +@property (nonatomic, copy) bool (^ _Nullable shouldReturn)(UIKeyModifierFlags); @property (nonatomic, copy) void (^ _Nullable backspaceWhileEmpty)(); @property (nonatomic, copy) void (^ _Nullable dropAutocorrectioniOS16)(); diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m index a3356a56de..cb582b8985 100755 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m @@ -136,15 +136,22 @@ } - (NSArray *)keyCommands { - UIKeyCommand *plainReturn = [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:kNilOptions action:@selector(handlePlainReturn:)]; + UIKeyCommand *plainReturn = [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:kNilOptions action:@selector(handleReturn:)]; + UIKeyCommand *cmdReturn = [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:UIKeyModifierCommand action:@selector(handleReturn:)]; return @[ - plainReturn + plainReturn, + cmdReturn ]; } -- (void)handlePlainReturn:(id)__unused sender { +- (void)handleReturn:(UIKeyCommand *)__unused sender { + UIKeyModifierFlags modifierFlags = 0; + if ([sender isKindOfClass:[UIKeyCommand class]]) { + modifierFlags = sender.modifierFlags; + } + if (_shouldReturn) { - _shouldReturn(); + _shouldReturn(modifierFlags); } } diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift index ecb4ecd4dc..3512bd952b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift @@ -11,7 +11,7 @@ import TextNodeWithEntities public protocol ChatInputTextNodeDelegate: AnyObject { func chatInputTextNodeDidUpdateText() - func chatInputTextNodeShouldReturn() -> Bool + func chatInputTextNodeShouldReturn(modifierFlags: UIKeyModifierFlags) -> Bool func chatInputTextNodeDidChangeSelection(dueToEditing: Bool) func chatInputTextNodeDidBeginEditing() func chatInputTextNodeDidFinishEditing() @@ -1180,11 +1180,11 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate, } return self.customDelegate?.chatInputTextNodeShouldPaste() ?? true } - self.shouldReturn = { [weak self] in + self.shouldReturn = { [weak self] modifierFlags in guard let self else { return true } - return self.customDelegate?.chatInputTextNodeShouldReturn() ?? true + return self.customDelegate?.chatInputTextNodeShouldReturn(modifierFlags: modifierFlags) ?? true } self.backspaceWhileEmpty = { [weak self] in guard let self else { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 96ff976f1d..12e1c48300 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -282,7 +282,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { switch action.action { case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): releasedBy = gift.releasedBy - case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): releasedBy = gift.releasedBy default: break @@ -690,7 +690,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } } - case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _, _, fromOffer, _): + case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _, _, fromOffer, _, _): if case let .unique(uniqueGift) = gift { isStarGift = true diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index ff38c7c2d6..4e3fe61ad6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -4528,15 +4528,29 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } } - public func chatInputTextNodeShouldReturn() -> Bool { + public func chatInputTextNodeShouldReturn(modifierFlags: UIKeyModifierFlags) -> Bool { + var shouldSendMessage = false if self.sendActionButtons.sendButton.supernode != nil && !self.sendActionButtons.sendButton.isHidden && !self.sendActionButtons.sendContainerNode.alpha.isZero { - self.sendButtonPressed() + if let context = self.context, context.sharedContext.currentChatSettings.with({ $0 }).sendWithCmdEnter { + if modifierFlags.contains(.command) { + shouldSendMessage = true + } + } else { + if modifierFlags.isEmpty { + shouldSendMessage = true + } + } } - return false + if shouldSendMessage { + self.sendButtonPressed() + return false + } + + return true } @objc public func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { - return self.chatInputTextNodeShouldReturn() + return self.chatInputTextNodeShouldReturn(modifierFlags: []) } private func applyUpdateSendButtonIcon() { diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift index 4fe96e94df..35cd956873 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift @@ -18,6 +18,7 @@ import UIKitRuntimeUtils public final class GiftCompositionComponent: Component { public class ExternalState { public fileprivate(set) var previewPatternColor: UIColor? + public fileprivate(set) var backgroundColor: UIColor? public fileprivate(set) var previewModel: StarGift.UniqueGift.Attribute? public fileprivate(set) var previewBackdrop: StarGift.UniqueGift.Attribute? public fileprivate(set) var previewSymbol: StarGift.UniqueGift.Attribute? @@ -766,6 +767,7 @@ public final class GiftCompositionComponent: Component { } } + component.externalState?.backgroundColor = backgroundColor component.externalState?.previewPatternColor = secondBackgroundColor var animateBackdropSwipe = false diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift index 251e5097e7..51b83e7042 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift @@ -108,7 +108,7 @@ final class CraftTableComponent: Component { fatalError("init(coder:) has not been implemented") } - func setupFailAnimation() { + func setupFailureAnimation() { guard !self.didSetupFinishAnimation else { return } @@ -127,41 +127,29 @@ final class CraftTableComponent: Component { } for i in 0 ..< min(2, availableStickers.count) { if let sticker = availableStickers[i].view { - self.animationView.setSticker(sticker, face: 3 - i, mirror: isUpsideDown) + let face: Int + if isUpsideDown { + face = i + 1 + } else { + face = 3 - i + } + self.animationView.setSticker(sticker, face: face, mirror: isUpsideDown) } } - self.state?.updated() + self.flipFaces = isUpsideDown + + Queue.mainQueue().after(0.3, { + self.failWillFinish = true + self.component?.willFinish(false) + }) - if let failOverlayView = self.failOverlay.view as? LottieComponent.View { - failOverlayView.isHidden = false - failOverlayView.onFrameUpdate = { [weak self] frameIndex in - guard let self else { - return - } - if frameIndex >= 5 && !self.failDidStartCrossAnimation { - self.failDidStartCrossAnimation = true - self.craftFailPlayOnce.invoke(Void()) - } - if frameIndex >= 65 && !self.failDidBringToFront { - self.failDidBringToFront = true - failOverlayView.superview?.bringSubviewToFront(failOverlayView) - - self.animationView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - } - if frameIndex >= 75 && !self.failWillFinish { - self.failWillFinish = true - self.component?.willFinish(false) - } - if frameIndex >= 82 && !self.failDidFinish { - self.failDidFinish = true - - failOverlayView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.component?.finished(nil) - } - } - } - self.craftFailOverlayPlayOnce.invoke(Void()) + Queue.mainQueue().after(0.5, { + self.failDidFinish = true + self.component?.finished(nil) + }) + + self.state?.updated(transition: .easeInOut(duration: 0.4)) } } @@ -171,6 +159,8 @@ final class CraftTableComponent: Component { } self.didSetupFinishAnimation = true + self.animationView.isSuccess = true + self.animationView.onFinishApproach = { [weak self] isUpsideDown in guard let self else { return @@ -270,10 +260,10 @@ final class CraftTableComponent: Component { if index == 0 { faceItems.append( AnyComponentWithIdentity(id: "background", component: AnyComponent( - RoundedRectangle(color: component.buttonColor, cornerRadius: 28.0) + FilledRoundedRectangleComponent(color: component.buttonColor, cornerRadius: .value(28.0), smoothCorners: true) )) ) - if !component.isCrafting { + if !component.isCrafting || self.isFailed { faceItems.append( AnyComponentWithIdentity(id: "glass", component: AnyComponent( GlassBackgroundComponent(size: CGSize(width: cubeSide, height: cubeSide), cornerRadius: 28.0, isDark: true, tintColor: .init(kind: .custom(style: .default, color: component.buttonColor))) @@ -282,11 +272,20 @@ final class CraftTableComponent: Component { } if self.isFailed { faceItems.append( - AnyComponentWithIdentity(id: "fail", component: AnyComponent( - LottieComponent( - content: LottieComponent.AppBundleContent(name: "CraftFail"), - size: CGSize(width: 96.0, height: 96.0), - playOnce: self.craftFailPlayOnce + AnyComponentWithIdentity(id: "faildial", component: AnyComponent( + DialIndicatorComponent( + content: AnyComponentWithIdentity(id: "gift", component: AnyComponent(BundleIconComponent(name: "Premium/GiftCrash", tintColor: .white))), + backgroundColor: .white.withAlphaComponent(0.1), + foregroundColor: .white, + diameter: 84.0, + contentSize: CGSize(width: 44.0, height: 44.0), + lineWidth: 5.0, + fontSize: 18.0, + progress: 0.0, + value: component.gifts.count, + suffix: "", + isVisible: true, + isFlipped: self.flipFaces ) )) ) @@ -300,7 +299,9 @@ final class CraftTableComponent: Component { diameter: 84.0, lineWidth: 5.0, fontSize: 18.0, - percentage: permilleValue / 10, + progress: CGFloat(permilleValue / 10 / 100), + value: permilleValue / 10, + suffix: "%", isVisible: !component.isCrafting ) )) @@ -318,7 +319,7 @@ final class CraftTableComponent: Component { } else { faceItems.append( AnyComponentWithIdentity(id: "background", component: AnyComponent( - RoundedRectangle(color: component.buttonColor, cornerRadius: 28.0) + FilledRoundedRectangleComponent(color: component.buttonColor, cornerRadius: .value(28.0), smoothCorners: true) )) ) faceItems.append( @@ -412,35 +413,36 @@ final class CraftTableComponent: Component { self.setupSuccessAnimation(uniqueGift) } case .fail: - self.setupFailAnimation() + self.setupFailureAnimation() default: break } }) } + - if self.isFailed { - let failOverlaySize = self.failOverlay.update( - transition: .immediate, - component: AnyComponent( - LottieComponent( - content: LottieComponent.AppBundleContent(name: "CraftFailOverlay"), - size: CGSize(width: availableSize.width, height: availableSize.width), - playOnce: self.craftFailOverlayPlayOnce - ) - ), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: availableSize.width) - ) - let failOverlayFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - failOverlaySize.width) / 2.0), y: floor((availableSize.height - failOverlaySize.height) / 2.0)), size: failOverlaySize) - if let failOverlayView = self.failOverlay.view { - if failOverlayView.superview == nil { - failOverlayView.isHidden = true - self.insertSubview(failOverlayView, belowSubview: self.animationView) - } - failOverlayView.frame = failOverlayFrame - } - } +// if self.isFailed { +// let failOverlaySize = self.failOverlay.update( +// transition: .immediate, +// component: AnyComponent( +// LottieComponent( +// content: LottieComponent.AppBundleContent(name: "CraftFailOverlay"), +// size: CGSize(width: availableSize.width, height: availableSize.width), +// playOnce: self.craftFailOverlayPlayOnce +// ) +// ), +// environment: {}, +// containerSize: CGSize(width: availableSize.width, height: availableSize.width) +// ) +// let failOverlayFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - failOverlaySize.width) / 2.0), y: floor((availableSize.height - failOverlaySize.height) / 2.0)), size: failOverlaySize) +// if let failOverlayView = self.failOverlay.view { +// if failOverlayView.superview == nil { +// failOverlayView.isHidden = true +// self.insertSubview(failOverlayView, belowSubview: self.animationView) +// } +// failOverlayView.frame = failOverlayFrame +// } +// } return availableSize } @@ -546,7 +548,7 @@ final class GiftSlotComponent: Component { let backgroundFrame = CGRect(origin: .zero, size: availableSize).insetBy(dx: 1.0, dy: 1.0) self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 28.0, isDark: true, tintColor: .init(kind: .custom(style: .default, color: component.buttonColor)), transition: .immediate) transition.setFrame(view: self.backgroundView, frame: backgroundFrame) - if component.gift == nil && component.isCrafting { + if component.gift == nil && component.isCrafting && previousComponent?.isCrafting == false { transition.setBlur(layer: self.backgroundView.layer, radius: 10.0) self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false) transition.setBlur(layer: self.addIcon.layer, radius: 10.0) diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CubeAnimationView.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CubeAnimationView.swift index d65ce5986c..d8ef7f77b2 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CubeAnimationView.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CubeAnimationView.swift @@ -79,6 +79,8 @@ final class CubeAnimationView: UIView { private var finishDelayTimerY: Timer? private var cubeScale: Float = 1.0 private var hasFiredFinishApproach = false + + var isSuccess = false var onFinishApproach: ((Bool) -> Void)? @@ -485,7 +487,7 @@ final class CubeAnimationView: UIView { let duration: TimeInterval = 0.2 let startQuad = Quad(rect: sticker.frame) let animationView: UIView - if let snapshot = sticker.snapshotView(afterScreenUpdates: true) { + if let snapshot = sticker.snapshotView(afterScreenUpdates: false) { self.warpSnapshot?.removeFromSuperview() self.warpSnapshot = snapshot @@ -606,7 +608,7 @@ final class CubeAnimationView: UIView { } let stickerIndex = indices[index] if self.stickers.indices.contains(stickerIndex) { - let isLast = stickerIndex == indices.count - 1 + let isLast = index == indices.count - 1 self.launchStickerView(self.stickers[stickerIndex], emphasized: isLast, willFinish: isLast) } self.scheduleStickerSequence(from: index + 1, indices: indices) @@ -697,10 +699,12 @@ final class CubeAnimationView: UIView { let upsideDown = abs(shortestAngleDelta(from: self.rotation.x, to: Float.pi)) < (Float.pi / 2) self.onFinishApproach?(upsideDown) } - if absRemaining <= self.finishSuccessScaleTriggerAngle { + if self.isSuccess, absRemaining <= self.finishSuccessScaleTriggerAngle { let raw = (self.finishSuccessScaleTriggerAngle - absRemaining) / self.finishSuccessScaleTriggerAngle let eased = raw * raw * (3 - 2 * raw) self.cubeScale = 1.0 + (self.finishSuccessScale - 1.0) * eased + } else if !self.isSuccess { + self.cubeScale = 1.0 } if abs(remaining) < 0.0008 && abs(self.angularVelocity.y) < 0.0015 { self.finishRotationY = self.finishTargetYUnwrapped diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/DialIndicatorComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/DialIndicatorComponent.swift index accfe3fb76..a595eeb6db 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/DialIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/DialIndicatorComponent.swift @@ -13,29 +13,41 @@ final class DialIndicatorComponent: Component { let backgroundColor: UIColor let foregroundColor: UIColor let diameter: CGFloat + let contentSize: CGSize? let lineWidth: CGFloat let fontSize: CGFloat - let percentage: Int + let progress: CGFloat + let value: Int + let suffix: String let isVisible: Bool + let isFlipped: Bool public init( content: AnyComponentWithIdentity, backgroundColor: UIColor, foregroundColor: UIColor, diameter: CGFloat, + contentSize: CGSize? = nil, lineWidth: CGFloat, fontSize: CGFloat, - percentage: Int, - isVisible: Bool = true + progress: CGFloat, + value: Int, + suffix: String, + isVisible: Bool = true, + isFlipped: Bool = false, ) { self.content = content self.backgroundColor = backgroundColor self.foregroundColor = foregroundColor self.diameter = diameter + self.contentSize = contentSize self.lineWidth = lineWidth self.fontSize = fontSize - self.percentage = percentage + self.progress = progress + self.value = value + self.suffix = suffix self.isVisible = isVisible + self.isFlipped = isFlipped } public static func ==(lhs: DialIndicatorComponent, rhs: DialIndicatorComponent) -> Bool { @@ -51,18 +63,30 @@ final class DialIndicatorComponent: Component { if lhs.diameter != rhs.diameter { return false } + if lhs.contentSize != rhs.contentSize { + return false + } if lhs.lineWidth != rhs.lineWidth { return false } if lhs.fontSize != rhs.fontSize { return false } - if lhs.percentage != rhs.percentage { + if lhs.progress != rhs.progress { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.suffix != rhs.suffix { return false } if lhs.isVisible != rhs.isVisible { return false } + if lhs.isFlipped != rhs.isFlipped { + return false + } return true } @@ -119,7 +143,7 @@ final class DialIndicatorComponent: Component { self.foregroundLayer.path = CGPath(ellipseIn: pathFrame, transform: nil) self.foregroundLayer.transform = CATransform3DMakeRotation(.pi / 2.0, 0.0, 0.0, 1.0) self.foregroundLayer.strokeStart = strokeStart - transition.setShapeLayerStrokeEnd(layer: self.foregroundLayer, strokeEnd: strokeStart + (strokeEnd - strokeStart) * (CGFloat(component.percentage) / 100.0)) + transition.setShapeLayerStrokeEnd(layer: self.foregroundLayer, strokeEnd: strokeStart + (strokeEnd - strokeStart) * component.progress) self.foregroundLayer.frame = CGRect(origin: .zero, size: pathSize) if previousComponent?.content.id != component.content.id { @@ -136,7 +160,8 @@ final class DialIndicatorComponent: Component { self.content = ComponentView() } - let contentFrame = CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: CGSize(width: component.diameter - 16.0, height: component.diameter - 16.0)) + let contentSize = component.contentSize ?? CGSize(width: component.diameter - 16.0, height: component.diameter - 16.0) + let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((pathSize.width - contentSize.width) / 2.0), y: floorToScreenPixels((pathSize.height - contentSize.height) / 2.0)), size: contentSize) let _ = self.content.update( transition: .immediate, component: component.content.component, @@ -154,10 +179,12 @@ final class DialIndicatorComponent: Component { contentView.frame = contentFrame } - let labelItems: [AnimatedTextComponent.Item] = [ - AnimatedTextComponent.Item(id: "percent", content: .number(component.percentage, minDigits: 1)), - AnimatedTextComponent.Item(id: "suffix", content: .text("%")) + var labelItems: [AnimatedTextComponent.Item] = [ + AnimatedTextComponent.Item(id: "percent", content: .number(component.value, minDigits: 1)) ] + if !component.suffix.isEmpty { + labelItems.append(AnimatedTextComponent.Item(id: "suffix", content: .text(component.suffix))) + } let labelSize = self.label.update( transition: transition, @@ -181,6 +208,8 @@ final class DialIndicatorComponent: Component { transition.setAlpha(view: self.containerView, alpha: component.isVisible ? 1.0 : 0.0) transition.setBlur(layer: self.containerView.layer, radius: component.isVisible ? 0.0 : 10.0) + self.containerView.transform = CGAffineTransform(rotationAngle: component.isFlipped ? .pi : 0.0) + self.containerView.frame = CGRect(origin: .zero, size: pathSize) return pathSize diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift index 1a34ed0c24..efaaf2a3dc 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift @@ -42,27 +42,35 @@ private final class CraftGiftPageContent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment class ExternalState { - fileprivate(set) var giftMap: [Int64: GiftItem] + fileprivate(set) var giftsMap: [Int64: GiftItem] + fileprivate(set) var starGiftsMap: [Int64: StarGift.Gift] = [:] + fileprivate(set) var displayFailure = false fileprivate(set) var testFailOrSuccess: Bool? public init() { - self.giftMap = [:] + self.giftsMap = [:] } } + enum DisplayState { + case `default` + case crafting + case failure + } + let context: AccountContext let craftContext: CraftGiftsContext let resaleContext: () -> ResaleGiftsContext? let colors: (UIColor, UIColor, UIColor, UIColor, UIColor) let gift: StarGift.UniqueGift let selectedGiftIds: [Int32: Int64] - let displayCraftInfo: Bool - let isCrafting: Bool - let inProgress: Bool + let displayState: DisplayState + let displayInfo: Bool let result: CraftTableComponent.Result? let screenSize: CGSize let externalState: ExternalState + let starsTopUpOptionsPromise: Promise<[StarsTopUpOption]?> let selectGift: (Int32, GiftItem) -> Void let removeGift: (Int32) -> Void let dismiss: () -> Void @@ -74,12 +82,12 @@ private final class CraftGiftPageContent: Component { colors: (UIColor, UIColor, UIColor, UIColor, UIColor), gift: StarGift.UniqueGift, selectedGiftIds: [Int32: Int64], - displayCraftInfo: Bool, - isCrafting: Bool, - inProgress: Bool, + displayState: DisplayState, + displayInfo: Bool, result: CraftTableComponent.Result?, screenSize: CGSize, externalState: ExternalState, + starsTopUpOptionsPromise: Promise<[StarsTopUpOption]?>, selectGift: @escaping (Int32, GiftItem) -> Void, removeGift: @escaping (Int32) -> Void, dismiss: @escaping () -> Void @@ -90,12 +98,12 @@ private final class CraftGiftPageContent: Component { self.colors = colors self.gift = gift self.selectedGiftIds = selectedGiftIds - self.displayCraftInfo = displayCraftInfo - self.isCrafting = isCrafting - self.inProgress = inProgress + self.displayState = displayState + self.displayInfo = displayInfo self.result = result self.screenSize = screenSize self.externalState = externalState + self.starsTopUpOptionsPromise = starsTopUpOptionsPromise self.selectGift = selectGift self.removeGift = removeGift self.dismiss = dismiss @@ -114,13 +122,10 @@ private final class CraftGiftPageContent: Component { if lhs.selectedGiftIds != rhs.selectedGiftIds { return false } - if lhs.displayCraftInfo != rhs.displayCraftInfo { + if lhs.displayState != rhs.displayState { return false } - if lhs.isCrafting != rhs.isCrafting { - return false - } - if lhs.inProgress != rhs.inProgress { + if lhs.displayInfo != rhs.displayInfo { return false } if lhs.screenSize != rhs.screenSize { @@ -138,19 +143,22 @@ private final class CraftGiftPageContent: Component { private let title = ComponentView() private let descriptionText = ComponentView() - private let craftingTitle = ComponentView() - private let craftingSubtitle = ComponentView() - private let craftingDescription = ComponentView() - private let craftingProbability = ComponentView() - private var craftingProbabilityMeasure = ComponentView() + private var craftTable = ComponentView() private var backdropDial = ComponentView() private var symbolDial = ComponentView() private var variantsButton = ComponentView() private var variantsButtonMeasure = ComponentView() - private var craftTable = ComponentView() - private var selectedGifts: [AnyHashable: ComponentView] = [:] + private let craftingTitle = ComponentView() + private let craftingSubtitle = ComponentView() + private let craftingDescription = ComponentView() + private let craftingProbability = ComponentView() + private var craftingProbabilityMeasure = ComponentView() + + private let failureTitle = ComponentView() + private let failureDescription = ComponentView() + private var failedGifts: [AnyHashable: ComponentView] = [:] private let infoContainer = UIView() private var infoBackground = SimpleLayer() @@ -167,13 +175,10 @@ private final class CraftGiftPageContent: Component { private let upgradePreviewDisposable = DisposableSet() private var upgradePreview: [StarGift.UniqueGift.Attribute]? private var starGiftsMap: [Int64: StarGift.Gift] = [:] - - private let starsTopUpOptionsPromise = Promise<[StarsTopUpOption]?>(nil) - + private var availableGifts: [GiftItem] = [] private var giftMap: [Int64: GiftItem] = [:] private var isCrafting = false - private var isFailing = false private var component: CraftGiftPageContent? private weak var state: EmptyComponentState? @@ -260,7 +265,7 @@ private final class CraftGiftPageContent: Component { initialGiftItem ] self.giftMap = [initialGiftItem.gift.id: initialGiftItem] - component.externalState.giftMap = self.giftMap + component.externalState.giftsMap = self.giftMap self.craftStateDisposable = (component.craftContext.state |> deliverOnMainQueue).start(next: { [weak self] state in @@ -296,7 +301,7 @@ private final class CraftGiftPageContent: Component { } self.availableGifts = items self.giftMap = map - self.component?.externalState.giftMap = self.giftMap + self.component?.externalState.giftsMap = self.giftMap self.state?.updated(transition: .spring(duration: 0.4)) }) @@ -322,7 +327,7 @@ private final class CraftGiftPageContent: Component { self.upgradePreviewDisposable.add((.single(nil) |> then(component.context.engine.payments.cachedStarGifts()) |> deliverOnMainQueue).start(next: { [weak self] starGifts in - guard let self, let starGifts else { + guard let self, let component = self.component, let starGifts else { return } var starGiftsMap: [Int64: StarGift.Gift] = [:] @@ -332,9 +337,8 @@ private final class CraftGiftPageContent: Component { } } self.starGiftsMap = starGiftsMap + component.externalState.starGiftsMap = starGiftsMap })) - - self.starsTopUpOptionsPromise.set(component.context.engine.payments.starsTopUpOptions() |> map(Optional.init)) } transition.setGradientColors(layer: self.background, colors: [component.colors.0, component.colors.1]) @@ -348,6 +352,8 @@ private final class CraftGiftPageContent: Component { self.state = state self.environment = environment + let isCrafting = [.crafting, .failure].contains(component.displayState) + var selectedGifts: [Int32: GiftItem] = [:] for (index, giftId) in component.selectedGiftIds { if let gift = self.giftMap[giftId] { @@ -388,14 +394,14 @@ private final class CraftGiftPageContent: Component { avatarScale: 1.0, defaultHeight: 300.0, gradientOnTop: true, - avatarTransitionFraction: self.isFailing ? 1.0 : 0.0, + avatarTransitionFraction: 0.0, patternTransitionFraction: 0.0, patternIconScale: 1.5 )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 169.0 * 2.0) ) - let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: component.isCrafting ? floor((component.screenSize.height - backgroundSize.height) / 2.0) : 0.0), size: backgroundSize) + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: isCrafting && !"".isEmpty ? floor((component.screenSize.height - backgroundSize.height) / 2.0) : 0.0), size: backgroundSize) if let backgroundView = self.pattern.view { if backgroundView.layer.superlayer == nil { backgroundTransition = .immediate @@ -420,8 +426,8 @@ private final class CraftGiftPageContent: Component { self.addSubview(titleView) } transition.setFrame(view: titleView, frame: titleFrame) - transition.setAlpha(view: titleView, alpha: component.isCrafting ? 0.0 : 1.0) - transition.setBlur(layer: titleView.layer, radius: component.isCrafting ? 10.0 : 0.0) + transition.setAlpha(view: titleView, alpha: 1.0) + //transition.setBlur(layer: titleView.layer, radius: component.isCrafting ? 10.0 : 0.0) } let giftTitle = "\(component.gift.title) #\(formatCollectibleNumber(component.gift.number, dateTimeFormat: environment.dateTimeFormat))" @@ -431,7 +437,7 @@ private final class CraftGiftPageContent: Component { let descriptionFont = Font.regular(13.0) let descriptionBoldFont = Font.semibold(13.0) let descriptionColor = UIColor.white - let rawDescriptionString = "Add up to **4 gifts** to craft new\n**$ \(giftTitle)**.\n\nIf crafting fails, all selected gifts\nwill be consumed." + let rawDescriptionString = "Add up to **4 gifts** to craft new\n**$ \(giftTitle)**.\n\nIf crafting fails, all used gifts\nwill be lost." let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString if let gift = self.starGiftsMap[component.gift.giftId] { @@ -565,7 +571,9 @@ private final class CraftGiftPageContent: Component { diameter: 48.0, lineWidth: 4.0, fontSize: 10.0, - percentage: backdropPermille / 10 + progress: CGFloat(backdropPermille / 10 / 100), + value: backdropPermille / 10, + suffix: "%" ) ), action: { [weak self] in @@ -609,9 +617,12 @@ private final class CraftGiftPageContent: Component { backgroundColor: .white.withAlphaComponent(0.1), foregroundColor: .white, diameter: 48.0, + contentSize: CGSize(width: 28.0, height: 28.0), lineWidth: 4.0, fontSize: 10.0, - percentage: symbolPermille / 10 + progress: CGFloat(symbolPermille / 10 / 100), + value: symbolPermille / 10, + suffix: "%" ) ), action: { [weak self] in @@ -726,7 +737,7 @@ private final class CraftGiftPageContent: Component { craftContentHeight += 160.0 let originalCraftContentHeight = craftContentHeight - if component.isCrafting { + if !"".isEmpty, isCrafting { craftContentHeight = component.screenSize.height } @@ -736,8 +747,8 @@ private final class CraftGiftPageContent: Component { self.addSubview(descriptionTextView) } transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame) - transition.setAlpha(view: descriptionTextView, alpha: component.isCrafting ? 0.0 : 1.0) - transition.setBlur(layer: descriptionTextView.layer, radius: component.isCrafting ? 10.0 : 0.0) + transition.setAlpha(view: descriptionTextView, alpha: isCrafting ? 0.0 : 1.0) + transition.setBlur(layer: descriptionTextView.layer, radius: isCrafting ? 10.0 : 0.0) } let backdropDialFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5 - 9.0 - backdropDialSize.width, y: craftContentHeight - 145.0 - 78.0), size: backdropDialSize) @@ -746,8 +757,8 @@ private final class CraftGiftPageContent: Component { self.addSubview(backdropDialView) } transition.setFrame(view: backdropDialView, frame: backdropDialFrame) - transition.setAlpha(view: backdropDialView, alpha: component.isCrafting ? 0.0 : 1.0) - transition.setBlur(layer: backdropDialView.layer, radius: component.isCrafting ? 10.0 : 0.0) + transition.setAlpha(view: backdropDialView, alpha: isCrafting ? 0.0 : 1.0) + transition.setBlur(layer: backdropDialView.layer, radius: isCrafting ? 10.0 : 0.0) } let symbolDialFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5 + 9.0, y: craftContentHeight - 145.0 - 78.0), size: symbolDialSize) @@ -756,14 +767,14 @@ private final class CraftGiftPageContent: Component { self.addSubview(symbolDialView) } transition.setFrame(view: symbolDialView, frame: symbolDialFrame) - transition.setAlpha(view: symbolDialView, alpha: component.isCrafting ? 0.0 : 1.0) - transition.setBlur(layer: symbolDialView.layer, radius: component.isCrafting ? 10.0 : 0.0) + transition.setAlpha(view: symbolDialView, alpha: isCrafting ? 0.0 : 1.0) + transition.setBlur(layer: symbolDialView.layer, radius: isCrafting ? 10.0 : 0.0) } let variantsButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - variantsButtonSize.width) / 2.0), y: craftContentHeight - 145.0), size: variantsButtonSize) var varitantsButtonTransition = transition if let variantsButtonView = self.variantsButton.view { - if variantsButtonView.superview == nil && !component.isCrafting { + if variantsButtonView.superview == nil && component.displayState == .default { varitantsButtonTransition = .immediate if let symbolDialView = self.symbolDial.view { self.insertSubview(variantsButtonView, aboveSubview: symbolDialView) @@ -772,9 +783,9 @@ private final class CraftGiftPageContent: Component { } } varitantsButtonTransition.setFrame(view: variantsButtonView, frame: variantsButtonFrame) - varitantsButtonTransition.setBlur(layer: variantsButtonView.layer, radius: component.isCrafting ? 10.0 : 0.0) - if component.isCrafting { - variantsButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.41, removeOnCompletion: false, completion: { _ in + varitantsButtonTransition.setBlur(layer: variantsButtonView.layer, radius: isCrafting ? 10.0 : 0.0) + if component.displayState == .crafting { + variantsButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in variantsButtonView.removeFromSuperview() }) } @@ -782,11 +793,12 @@ private final class CraftGiftPageContent: Component { let permilleValue = selectedGifts.reduce(0, { $0 + Int($1.value.gift.craftChancePermille ?? 0) }) - if component.isCrafting { - var craftingOriginY = craftContentHeight * 0.5 + 160.0 + if component.displayState == .crafting { + //var craftingOriginY = craftContentHeight * 0.5 + 160.0 + var craftingOriginY = craftContentHeight * 0.5 - 16.0 let offset = -(craftContentHeight - originalCraftContentHeight) - let titleSize = self.craftingTitle.update( + let craftingTitleSize = self.craftingTitle.update( transition: transition, component: AnyComponent( MultilineTextComponent(text: .plain(NSAttributedString(string: "Crafting", font: Font.bold(20.0), textColor: .white))) @@ -794,21 +806,21 @@ private final class CraftGiftPageContent: Component { environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) - let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) * 0.5), y: craftingOriginY), size: titleSize) - if let titleView = self.craftingTitle.view { - if titleView.superview == nil { - transition.animateAlpha(view: titleView, from: 0.0, to: 1.0) - transition.animateBlur(layer: titleView.layer, fromRadius: 10.0, toRadius: 0.0) - transition.animatePosition(view: titleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true) + let craftingTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - craftingTitleSize.width) * 0.5), y: craftingOriginY), size: craftingTitleSize) + if let craftingTitleView = self.craftingTitle.view { + if craftingTitleView.superview == nil { + transition.animateAlpha(view: craftingTitleView, from: 0.0, to: 1.0) + transition.animateBlur(layer: craftingTitleView.layer, fromRadius: 10.0, toRadius: 0.0) + transition.animatePosition(view: craftingTitleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true) - self.addSubview(titleView) + self.addSubview(craftingTitleView) } - titleView.frame = titleFrame + craftingTitleView.frame = craftingTitleFrame } - craftingOriginY += titleSize.height + craftingOriginY += craftingTitleSize.height craftingOriginY += 7.0 - let subtitleSize = self.craftingSubtitle.update( + let craftingSubtitleSize = self.craftingSubtitle.update( transition: transition, component: AnyComponent( MultilineTextComponent(text: .plain(NSAttributedString(string: giftTitle, font: Font.semibold(13.0), textColor: .white.withAlphaComponent(0.5)))) @@ -816,34 +828,30 @@ private final class CraftGiftPageContent: Component { environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) - let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - subtitleSize.width) * 0.5), y: craftingOriginY), size: subtitleSize) - if let subtitleView = self.craftingSubtitle.view { - if subtitleView.superview == nil { - transition.animateAlpha(view: subtitleView, from: 0.0, to: 1.0) - transition.animateBlur(layer: subtitleView.layer, fromRadius: 10.0, toRadius: 0.0) - transition.animatePosition(view: subtitleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true) + let craftingSubtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - craftingSubtitleSize.width) * 0.5), y: craftingOriginY), size: craftingSubtitleSize) + if let craftingSubtitleView = self.craftingSubtitle.view { + if craftingSubtitleView.superview == nil { + transition.animateAlpha(view: craftingSubtitleView, from: 0.0, to: 1.0) + transition.animateBlur(layer: craftingSubtitleView.layer, fromRadius: 10.0, toRadius: 0.0) + transition.animatePosition(view: craftingSubtitleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true) - self.addSubview(subtitleView) + self.addSubview(craftingSubtitleView) } - subtitleView.frame = subtitleFrame + craftingSubtitleView.frame = craftingSubtitleFrame } - craftingOriginY += subtitleSize.height + craftingOriginY += craftingSubtitleSize.height craftingOriginY += 21.0 let descriptionFont = Font.regular(13.0) let descriptionBoldFont = Font.semibold(13.0) let descriptionColor = UIColor.white.withAlphaComponent(0.5) - let rawDescriptionString = "If crafting fails, all selected gifts\nwill be consumed." + let rawDescriptionString = "If crafting fails, all used gifts\nwill be lost." let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString let craftingDescriptionSize = self.craftingDescription.update( transition: transition, component: AnyComponent( - MultilineTextWithEntitiesComponent( - context: component.context, - animationCache: component.context.animationCache, - animationRenderer: component.context.animationRenderer, - placeholderColor: .white.withAlphaComponent(0.3), + MultilineTextComponent( text: .plain(descriptionString), horizontalAlignment: .center, maximumNumberOfLines: 0, @@ -904,6 +912,165 @@ private final class CraftGiftPageContent: Component { } craftingProbabilityView.frame = craftingProbabilityFrame } + } else { + if let craftingTitleView = self.craftingTitle.view { + transition.setAlpha(view: craftingTitleView, alpha: 0.0, completion: { _ in + craftingTitleView.removeFromSuperview() + }) + transition.animateBlur(layer: craftingTitleView.layer, fromRadius: 0.0, toRadius: 10.0) + } + if let craftingSubtitleView = self.craftingSubtitle.view { + transition.setAlpha(view: craftingSubtitleView, alpha: 0.0, completion: { _ in + craftingSubtitleView.removeFromSuperview() + }) + transition.animateBlur(layer: craftingSubtitleView.layer, fromRadius: 0.0, toRadius: 10.0) + } + if let craftingDescriptionView = self.craftingDescription.view { + transition.setAlpha(view: craftingDescriptionView, alpha: 0.0, completion: { _ in + craftingDescriptionView.removeFromSuperview() + }) + transition.animateBlur(layer: craftingDescriptionView.layer, fromRadius: 0.0, toRadius: 10.0) + } + if let craftingProbabilityView = self.craftingProbability.view { + craftingProbabilityView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + craftingProbabilityView.removeFromSuperview() + }) + transition.animateBlur(layer: craftingProbabilityView.layer, fromRadius: 0.0, toRadius: 10.0) + } + } + + if component.displayState == .failure { + var failureOriginY = craftContentHeight * 0.5 - 16.0 + let offset = -(craftContentHeight - originalCraftContentHeight) + + let failureTitleSize = self.failureTitle.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "Crafting Failed", font: Font.bold(20.0), textColor: UIColor(rgb: 0xff746d)))) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let failureTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - failureTitleSize.width) * 0.5), y: failureOriginY), size: failureTitleSize) + if let failureTitleView = self.failureTitle.view { + if failureTitleView.superview == nil { + transition.animateAlpha(view: failureTitleView, from: 0.0, to: 1.0) + transition.animateBlur(layer: failureTitleView.layer, fromRadius: 10.0, toRadius: 0.0) + transition.animatePosition(view: failureTitleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true) + + self.addSubview(failureTitleView) + } + failureTitleView.frame = failureTitleFrame + } + failureOriginY += failureTitleSize.height + failureOriginY += 17.0 + + let descriptionFont = Font.regular(13.0) + let descriptionBoldFont = Font.semibold(13.0) + let descriptionColor = UIColor(rgb: 0xf7af8c) + let rawDescriptionString = "This crafting attempt was unsuccessful.\n**\(component.selectedGiftIds.count) gifts** were lost." + let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString + + let failureDescriptionSize = self.failureDescription.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(descriptionString), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let failureDescriptionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - failureDescriptionSize.width) * 0.5), y: failureOriginY), size: failureDescriptionSize) + if let failureDescriptionView = self.failureDescription.view { + if failureDescriptionView.superview == nil { + transition.animateAlpha(view: failureDescriptionView, from: 0.0, to: 1.0) + transition.animateBlur(layer: failureDescriptionView.layer, fromRadius: 10.0, toRadius: 0.0) + transition.animatePosition(view: failureDescriptionView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true) + + self.addSubview(failureDescriptionView) + } + failureDescriptionView.frame = failureDescriptionFrame + } + failureOriginY += failureDescriptionSize.height + failureOriginY += 34.0 + + var indices: [Int] = [] + for index in component.selectedGiftIds.keys.sorted() { + indices.append(Int(index)) + } + var lostGifts: [GiftItem] = [] + for index in indices { + if let giftId = component.selectedGiftIds[Int32(index)], let gift = self.giftMap[giftId] { + lostGifts.append(gift) + } + } + + let itemSize = CGSize(width: 80.0, height: 80.0) + let itemSpacing: CGFloat = 16.0 + var itemDelay: Double = 0.2 + + let totalItemsWidth: CGFloat = itemSize.width * CGFloat(lostGifts.count) + itemSpacing * CGFloat(lostGifts.count - 1) + var itemOriginX: CGFloat = floor((availableSize.width - totalItemsWidth) / 2.0) + + for gift in lostGifts { + let itemId = AnyHashable(gift.gift.id) + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.failedGifts[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + self.failedGifts[itemId] = visibleItem + itemTransition = .immediate + } + + let ribbonText = "#\(gift.gift.number)" + let ribbonColor: GiftItemComponent.Ribbon.Color = .custom(0xff645b, 0xff645b) + + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent( + GiftItemComponent( + context: component.context, + style: .glass, + theme: environment.theme, + strings: environment.strings, + peer: nil, + subject: .uniqueGift(gift: gift.gift, price: nil), + ribbon: GiftItemComponent.Ribbon(text: ribbonText, font: .monospaced, color: ribbonColor, outline: nil), + badge: nil, + resellPrice: nil, + isHidden: false, + isSelected: false, + isPinned: false, + isEditing: false, + mode: .grid, + action: nil, + contextAction: nil + ) + ), + environment: {}, + containerSize: itemSize + ) + let itemFrame = CGRect(origin: CGPoint(x: itemOriginX, y: failureOriginY), size: itemSize) + if let itemView = visibleItem.view { + if itemView.superview == nil { + self.addSubview(itemView) + + if !transition.animation.isImmediate { + itemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25, delay: itemDelay) + itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: itemDelay) + } + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + itemOriginX += itemSize.width + itemSpacing + itemDelay += 0.07 + } } let tableSize = CGSize(width: availableSize.width, height: 320.0) @@ -914,7 +1081,7 @@ private final class CraftGiftPageContent: Component { context: component.context, gifts: selectedGifts, buttonColor: component.colors.3, - isCrafting: component.isCrafting, + isCrafting: isCrafting, result: component.result, select: { [weak self] index in guard let self, let component = self.component, let environment = self.environment, let genericGift = self.starGiftsMap[component.gift.giftId], let resaleContext = component.resaleContext() else { @@ -927,7 +1094,7 @@ private final class CraftGiftPageContent: Component { gift: component.gift, genericGift: genericGift, selectedGiftIds: Set(component.selectedGiftIds.values), - starsTopUpOptions: self.starsTopUpOptionsPromise.get(), + starsTopUpOptions: component.starsTopUpOptionsPromise.get(), selectGift: { [weak self] item in guard let self, let component = self.component else { return @@ -947,11 +1114,11 @@ private final class CraftGiftPageContent: Component { self.component?.removeGift(index) }, willFinish: { [weak self] success in - guard let self else { + guard let self, let component = self.component else { return } if !success { - self.isFailing = true + component.externalState.displayFailure = true } self.state?.updated(transition: .easeInOut(duration: 0.5)) }, @@ -972,13 +1139,7 @@ private final class CraftGiftPageContent: Component { controller.dismiss() }) } else { - if let navigationController = controller.navigationController { - controller.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { _ in - controller.dismiss() - }) - let previousController = navigationController.viewControllers[max(0, navigationController.viewControllers.count - 2)] - animateRipple(parentView: previousController.view, screenCornerRadius: environment.deviceMetrics.screenCornerRadius, location: CGPoint(x: previousController.view.bounds.midX, y: previousController.view.bounds.midY)) - } + } } ) @@ -986,7 +1147,7 @@ private final class CraftGiftPageContent: Component { environment: {}, containerSize: CGSize(width: availableSize.width, height: tableSize.height) ) - let craftTableFrame = CGRect(origin: CGPoint(x: 0.0, y: component.isCrafting ? floor((component.screenSize.height - craftTableSize.height) / 2.0) : 10.0), size: craftTableSize) + let craftTableFrame = CGRect(origin: CGPoint(x: 0.0, y: isCrafting && !"".isEmpty ? floor((component.screenSize.height - craftTableSize.height) / 2.0) : 10.0), size: craftTableSize) if let craftTableView = self.craftTable.view { if craftTableView.superview == nil { craftTableView.layer.cornerRadius = 40.0 @@ -997,7 +1158,7 @@ private final class CraftGiftPageContent: Component { transition.setFrame(view: craftTableView, frame: craftTableFrame) } - transition.setAlpha(view: self.infoContainer, alpha: component.displayCraftInfo ? 1.0 : 0.0) + transition.setAlpha(view: self.infoContainer, alpha: component.displayInfo ? 1.0 : 0.0) let infoHeaderSize = self.infoHeader.update( transition: transition, @@ -1033,6 +1194,10 @@ private final class CraftGiftPageContent: Component { self.infoContainer.addSubview(infoHeaderView) } transition.setFrame(view: infoHeaderView, frame: infoHeaderFrame) + + if self.subviews.last !== self.infoContainer { + self.bringSubviewToFront(self.infoContainer) + } } infoContentHeight += infoHeaderSize.height infoContentHeight += 16.0 @@ -1149,17 +1314,17 @@ private final class CraftGiftPageContent: Component { transition.setFrame(view: infoListView, frame: infoListFrame) } - if component.displayCraftInfo { + if component.displayInfo { infoContentHeight += infoListSize.height infoContentHeight += 95.0 } transition.setFrame(view: self.infoContainer, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: infoContentHeight))) transition.setFrame(layer: self.background, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: craftContentHeight))) - transition.setFrame(layer: self.overlay, frame: CGRect(origin: CGPoint(x: 0.0, y: component.isCrafting ? floor((component.screenSize.height - availableSize.width) / 2.0) : 169.0 - availableSize.width * 0.5), size: CGSize(width: availableSize.width, height: availableSize.width))) + transition.setFrame(layer: self.overlay, frame: CGRect(origin: CGPoint(x: 0.0, y: isCrafting && !"".isEmpty ? floor((component.screenSize.height - availableSize.width) / 2.0) : 169.0 - availableSize.width * 0.5), size: CGSize(width: availableSize.width, height: availableSize.width))) let effectiveContentHeight: CGFloat - if component.displayCraftInfo { + if component.displayInfo { effectiveContentHeight = infoContentHeight } else { effectiveContentHeight = craftContentHeight @@ -1209,11 +1374,14 @@ private final class SheetContainerComponent: CombinedComponent { private let context: AccountContext private let giftId: Int64 - var displayCraftInfo = false + var displayInfo = false var isCrafting = false var inProgress = false + var displayFailure = false var result: CraftTableComponent.Result? var selectedGiftIds: [Int32: Int64] = [:] + + let starsTopUpOptionsPromise = Promise<[StarsTopUpOption]?>(nil) private var _resaleContext: ResaleGiftsContext? var resaleContext: ResaleGiftsContext { @@ -1241,12 +1409,14 @@ private final class SheetContainerComponent: CombinedComponent { return } if count < 1 { - self.displayCraftInfo = true + self.displayInfo = true self.updated() let _ = ApplicationSpecificNotice.incrementGiftCraftingTips(accountManager: context.sharedContext.accountManager).start() } }) + + self.starsTopUpOptionsPromise.set(context.engine.payments.starsTopUpOptions() |> map(Optional.init)) } deinit { @@ -1269,6 +1439,13 @@ private final class SheetContainerComponent: CombinedComponent { let environment = context.environment[EnvironmentType.self] let state = context.state + let strings = environment.strings + + if externalState.displayFailure { + state.displayFailure = true + state.inProgress = false + } + let controller = environment.controller let craftContext = context.component.craftContext @@ -1290,11 +1467,15 @@ private final class SheetContainerComponent: CombinedComponent { let theme = environment.theme var colors: (UIColor, UIColor, UIColor, UIColor, UIColor) = ( - UIColor(rgb: 0x263245), UIColor(rgb: 0x232e3f), UIColor(rgb: 0x304059), UIColor(rgb: 0x425168), theme.list.itemCheckColors.fillColor + UIColor(rgb: 0x263245), + UIColor(rgb: 0x232e3f), + UIColor(rgb: 0x304059), + UIColor(rgb: 0x425168), + theme.list.itemCheckColors.fillColor ) var permilleValue: Int32 = 0 for id in state.selectedGiftIds.values { - if let gift = externalState.giftMap[id] { + if let gift = externalState.giftsMap[id] { permilleValue += gift.gift.craftChancePermille ?? 0 } } @@ -1303,34 +1484,47 @@ private final class SheetContainerComponent: CombinedComponent { colors.1 = UIColor(rgb: 0x1a2f38) colors.2 = UIColor(rgb: 0x22464a) colors.3 = UIColor(rgb: 0x2d4e50) - if !state.displayCraftInfo { + if !state.displayInfo { colors.4 = UIColor(rgb: 0x33bf54) } } + if state.displayFailure { + colors.0 = UIColor(rgb: 0x46231a) + colors.1 = UIColor(rgb: 0x381b1a) + colors.2 = UIColor(rgb: 0x51291f) + colors.3 = UIColor(rgb: 0x683e34) + if !state.displayInfo { + colors.4 = UIColor(rgb: 0x683e34) + } + } var buttonColor = colors.3 - if state.displayCraftInfo, let backdropAttribute = component.gift.attributes.first(where: { attribute in + if state.displayInfo, let backdropAttribute = component.gift.attributes.first(where: { attribute in if case .backdrop = attribute { return true } else { return false } - }), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute { - buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05) + }), case let .backdrop(_, _, _, outerColor, _, _, _) = backdropAttribute { + buttonColor = UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2) } var backgroundColor = colors.1 - if state.displayCraftInfo { + if state.displayInfo { backgroundColor = environment.theme.list.plainBackgroundColor } let giftTitle = "\(component.gift.title) #\(formatCollectibleNumber(component.gift.number, dateTimeFormat: environment.dateTimeFormat))" let buttonContent: AnyComponentWithIdentity - if state.displayCraftInfo { + if state.displayInfo { buttonContent = AnyComponentWithIdentity(id: "info", component: AnyComponent( MultilineTextComponent(text: .plain(NSAttributedString(string: "Select Gifts", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor))) )) + } else if state.displayFailure { + buttonContent = AnyComponentWithIdentity(id: "fail", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "Craft Another Gift", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor))) + )) } else { var buttonAnimatedItems: [AnimatedTextComponent.Item] = [] buttonAnimatedItems.append(AnimatedTextComponent.Item(id: "percent", content: .number(Int(permilleValue / 10), minDigits: 1))) @@ -1364,6 +1558,15 @@ private final class SheetContainerComponent: CombinedComponent { )) } + var displayState: CraftGiftPageContent.DisplayState = .default + if state.displayFailure { + displayState = .failure + } else if state.isCrafting { + displayState = .crafting + } + + let hideButtons = displayState == .crafting + let sheet = sheet.update( component: ResizableSheetComponent( content: AnyComponent( @@ -1376,12 +1579,12 @@ private final class SheetContainerComponent: CombinedComponent { colors: colors, gift: component.gift, selectedGiftIds: state.selectedGiftIds, - displayCraftInfo: state.displayCraftInfo, - isCrafting: state.isCrafting, - inProgress: state.inProgress, + displayState: displayState, + displayInfo: state.displayInfo, result: state.result, screenSize: context.availableSize, externalState: externalState, + starsTopUpOptionsPromise: state.starsTopUpOptionsPromise, selectGift: { [weak state] index, gift in guard let state else { return @@ -1401,7 +1604,7 @@ private final class SheetContainerComponent: CombinedComponent { } ) ), - leftItem: state.isCrafting ? nil : AnyComponent( + leftItem: hideButtons ? nil : AnyComponent( GlassBarButtonComponent( size: CGSize(width: 44.0, height: 44.0), backgroundColor: buttonColor, @@ -1418,7 +1621,7 @@ private final class SheetContainerComponent: CombinedComponent { } ) ), - rightItem: state.isCrafting || state.displayCraftInfo ? nil : AnyComponent( + rightItem: hideButtons || state.displayInfo ? nil : AnyComponent( GlassBarButtonComponent( size: CGSize(width: 44.0, height: 44.0), backgroundColor: buttonColor, @@ -1431,16 +1634,16 @@ private final class SheetContainerComponent: CombinedComponent { ) )), action: { [weak state] _ in - guard let state, !state.isCrafting else { + guard let state, !state.inProgress else { return } - state.displayCraftInfo = !state.displayCraftInfo + state.displayInfo = !state.displayInfo state.updated(transition: .spring(duration: 0.3)) } ) ), hasTopEdgeEffect: false, - bottomItem: state.isCrafting ? nil : AnyComponent( + bottomItem: hideButtons ? nil : AnyComponent( ButtonComponent( background: ButtonComponent.Background( style: .glass, @@ -1449,15 +1652,33 @@ private final class SheetContainerComponent: CombinedComponent { pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ), content: buttonContent, - isEnabled: state.displayCraftInfo ? true : state.selectedGiftIds.count > 0, + isEnabled: state.displayInfo ? true : state.selectedGiftIds.count > 0, displaysProgress: state.inProgress, action: { [weak state] in guard let state else { return } - if state.displayCraftInfo { - state.displayCraftInfo = false + if state.displayInfo { + state.displayInfo = false state.updated(transition: .spring(duration: 0.3)) + } else if state.displayFailure, let genericGift = externalState.starGiftsMap[component.gift.giftId] { + let selectController = SelectCraftGiftScreen( + context: component.context, + craftContext: component.craftContext, + resaleContext: state.resaleContext, + gift: component.gift, + genericGift: genericGift, + selectedGiftIds: Set(), + starsTopUpOptions: state.starsTopUpOptionsPromise.get(), + selectGift: { item in + let craftController = GiftCraftScreen(context: component.context, gift: item.gift) + if let controller = controller() as? GiftCraftScreen, let navigationController = controller.navigationController as? NavigationController { + controller.dismissAnimated() + navigationController.pushViewController(craftController) + } + } + ) + environment.controller()?.push(selectController) } else { state.inProgress = true state.updated(transition: .spring(duration: 0.3)) @@ -1481,7 +1702,7 @@ private final class SheetContainerComponent: CombinedComponent { } var references: [StarGiftReference] = [] for index in indices { - if let giftId = state.selectedGiftIds[Int32(index)], let gift = externalState.giftMap[giftId] { + if let giftId = state.selectedGiftIds[Int32(index)], let gift = externalState.giftsMap[giftId] { references.append(gift.reference) } } @@ -1512,10 +1733,14 @@ private final class SheetContainerComponent: CombinedComponent { state.isCrafting = true state.result = .fail state.updated(transition: .spring(duration: 0.8)) + + Queue.mainQueue().after(1.0) { + craftContext.reload() + } default: if let navigationController = controller()?.navigationController { dismiss(true) - let alertController = textAlertController(context: component.context, title: nil, text: "Unknown Error", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]) + let alertController = textAlertController(context: component.context, title: nil, text: strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]) (navigationController.topViewController as? ViewController)?.present(alertController, in: .window(.root)) } } @@ -1525,7 +1750,7 @@ private final class SheetContainerComponent: CombinedComponent { ) ), backgroundColor: .color(backgroundColor), - isFullscreen: state.isCrafting, + isFullscreen: false, //state.isCrafting, animateOut: animateOut ), environment: { diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift index 435b1905f3..ce7bf9fa8a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift @@ -379,6 +379,7 @@ final class SelectGiftPageContent: Component { starsContext: component.context.starsContext!, peerId: component.context.account.peerId, gift: component.genericGift, + isPlain: true, confirmPurchaseImmediately: true, starsTopUpOptions: component.starsTopUpOptions, scrollToTop: {}, @@ -530,9 +531,9 @@ private final class SheetContainerComponent: CombinedComponent { backgroundColor: nil, isDark: theme.overallDarkAppearance, state: .glass, - component: AnyComponentWithIdentity(id: "back", component: AnyComponent( + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( - name: "Navigation/Back", + name: "Navigation/Close", tintColor: theme.chat.inputPanel.panelControlColor ) )), diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD index 4b2ada38af..700ed3aaa2 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD @@ -24,6 +24,7 @@ swift_library( "//submodules/AccountContext", "//submodules/AppBundle", "//submodules/ItemListUI", + "//submodules/ContextUI", "//submodules/TelegramStringFormatting", "//submodules/PresentationDataUtils", "//submodules/Components/SheetComponent", diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/BalanceComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/BalanceComponent.swift new file mode 100644 index 0000000000..c8ac352788 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/BalanceComponent.swift @@ -0,0 +1,174 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import AccountContext +import MultilineTextComponent +import MultilineTextWithEntitiesComponent +import TextFormat +import TelegramStringFormatting +import Markdown + +final class BalanceComponent: Component { + private let context: AccountContext + private let theme: PresentationTheme + private let action: () -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + action: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.action = action + } + + static func ==(lhs: BalanceComponent, rhs: BalanceComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + return true + } + + public final class View: HighlightTrackingButton { + private var text = ComponentView() + + private var component: BalanceComponent? + private var componentState: EmptyComponentState? + + private var starsBalance: Int64 = 0 + private var tonBalance: Int64 = 0 + private var balanceDisposable: Disposable? + + override public init(frame: CGRect) { + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.balanceDisposable?.dispose() + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action() + } + + private var isUpdating = false + func update(component: BalanceComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + self.component = component + self.componentState = state + + if self.balanceDisposable == nil { + if let starsContext = component.context.starsContext, let tonContext = component.context.tonContext { + self.balanceDisposable = combineLatest(queue: Queue.mainQueue(), + starsContext.state, + tonContext.state + ).start(next: { [weak self] starsState, tonState in + guard let self else { + return + } + self.starsBalance = starsState?.balance.value ?? 0 + self.tonBalance = tonState?.balance.value ?? 0 + if !self.isUpdating { + self.componentState?.updated() + } + }) + } + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + var rawString: String = "" + let starsBalanceString = "**⭐️\(presentationStringsFormattedNumber(Int32(clamping: self.starsBalance), presentationData.dateTimeFormat.groupingSeparator))**" + if self.tonBalance > 0 { + let tonBalanceString = "**💎\(formatTonAmountText(self.tonBalance, dateTimeFormat: presentationData.dateTimeFormat))**" + rawString = starsBalanceString + "\n" + tonBalanceString + } else { + rawString = presentationData.strings.Stars_Purchase_Balance + "\n" + starsBalanceString + } + + let attributedText = parseMarkdownIntoAttributedString( + rawString, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(12.0), textColor: component.theme.rootController.navigationBar.primaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(12.0), textColor: component.theme.rootController.navigationBar.primaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(12.0), textColor: component.theme.rootController.navigationBar.primaryTextColor), + linkAttribute: { _ in + return nil + } + ), + textAlignment: .right + ).mutableCopy() as! NSMutableAttributedString + let starRange = (attributedText.string as NSString).range(of: "⭐️") + if starRange.location != NSNotFound { + attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: starRange) + attributedText.addAttribute(.baselineOffset, value: 1.0, range: starRange) + } + let tonRange = (attributedText.string as NSString).range(of: "💎") + if tonRange.location != NSNotFound { + attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton(tinted: false)), range: tonRange) + attributedText.addAttribute(.baselineOffset, value: 1.0, range: tonRange) + } + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .white, + text: .plain(attributedText), + horizontalAlignment: .right, + maximumNumberOfLines: 2, + lineSpacing: 0.1, + displaysAsynchronously: false + ) + ), + environment: {}, + containerSize: availableSize + ) + + let inset: CGFloat = 12.0 + let size = CGSize(width: textSize.width + inset * 2.0, height: 44.0) + + if let textView = self.text.view { + if textView.superview == nil { + textView.isUserInteractionEnabled = false + self.addSubview(textView) + + if !transition.animation.isImmediate { + transition.animateAlpha(view: textView, from: 0.0, to: 1.0) + } + } + textView.frame = CGRect(origin: CGPoint(x: inset, y: 8.0 - UIScreenPixel), size: textSize) + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift index 3639d65c48..73be35b2dd 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -29,6 +29,7 @@ import LottieComponent import GiftLoadingShimmerView import EdgeEffect import GlassBackgroundComponent +import ContextUI private let minimumCountToDisplayFilters = 18 @@ -45,6 +46,7 @@ public final class GiftStoreContentComponent: Component { let starsContext: StarsContext let peerId: EnginePeer.Id let gift: StarGift.Gift + let isPlain: Bool let confirmPurchaseImmediately: Bool let starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>? let scrollToTop: () -> Void @@ -64,6 +66,7 @@ public final class GiftStoreContentComponent: Component { starsContext: StarsContext, peerId: EnginePeer.Id, gift: StarGift.Gift, + isPlain: Bool, confirmPurchaseImmediately: Bool, starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>?, scrollToTop: @escaping () -> Void, @@ -82,6 +85,7 @@ public final class GiftStoreContentComponent: Component { self.starsContext = starsContext self.peerId = peerId self.gift = gift + self.isPlain = isPlain self.confirmPurchaseImmediately = confirmPurchaseImmediately self.starsTopUpOptions = starsTopUpOptions self.scrollToTop = scrollToTop @@ -1075,7 +1079,7 @@ public final class GiftStoreContentComponent: Component { let loadingSize = CGSize(width: availableSize.width, height: min(1000.0, availableSize.height)) if isLoading && self.showLoading { - self.loadingView.update(size: loadingSize, theme: component.theme, showFilters: !showingFilters, isPlain: true, transition: .immediate) + self.loadingView.update(size: loadingSize, theme: component.theme, showFilters: !showingFilters, isPlain: component.isPlain, transition: .immediate) loadingTransition.setAlpha(view: self.loadingView, alpha: 1.0) } else { loadingTransition.setAlpha(view: self.loadingView, alpha: 0.0) @@ -1145,10 +1149,8 @@ final class GiftStoreScreenComponent: Component { private let edgeEffectView: EdgeEffectView - private let balanceBackgroundView: GlassBackgroundView - private let balanceTitle = ComponentView() - private let balanceValue = ComponentView() - private let balanceIcon = ComponentView() + private let balance = ComponentView() + private let balanceBackgroundView: GlassContextExtractableContainer private let title = ComponentView() private let subtitle = ComponentView() @@ -1165,7 +1167,7 @@ final class GiftStoreScreenComponent: Component { private var isUpdating: Bool = false override init(frame: CGRect) { - self.balanceBackgroundView = GlassBackgroundView() + self.balanceBackgroundView = GlassContextExtractableContainer() self.scrollView = ScrollView() self.scrollView.showsVerticalScrollIndicator = true @@ -1211,6 +1213,68 @@ final class GiftStoreScreenComponent: Component { contentView.updateScrolling(bounds: bounds, interactive: interactive, transition: transition) } } + + func presentBalanceMenu() { + guard let component = self.component, let starsContext = component.context.starsContext, let tonContext = component.context.tonContext, let controller = self.environment?.controller() else { + return + } + let tonBalance = tonContext.currentState?.balance.value ?? 0 + if tonBalance == 0 { + let controller = component.context.sharedContext.makeStarsTransactionsScreen(context: component.context, starsContext: tonContext) + controller.push(controller) + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let sourceView = self.balanceBackgroundView + + let items: Signal<[ContextMenuItem], NoError> = combineLatest( + queue: Queue.mainQueue(), + starsContext.state, + tonContext.state + ) + |> take(1) + |> map { starsState, tonState -> [ContextMenuItem] in + let starsBalance = starsState?.balance ?? .zero + let tonBalance = tonState?.balance.value ?? 0 + + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem( + text: "My Stars", + textLayout: .secondLineWithValue(formatStarsAmountText(starsBalance, dateTimeFormat: presentationData.dateTimeFormat)), + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Stars"), color: theme.contextMenu.primaryColor) }, + action: { [weak self] _, f in + f(.dismissWithoutContent) + guard let self, let component = self.component, let environment = self.environment else { + return + } + let controller = component.context.sharedContext.makeStarsTransactionsScreen(context: component.context, starsContext: starsContext) + environment.controller()?.push(controller) + } + ))) + + items.append(.action(ContextMenuActionItem( + text: "My TON", + textLayout: .secondLineWithValue(formatTonAmountText(tonBalance, dateTimeFormat: presentationData.dateTimeFormat)), + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Ton"), color: theme.contextMenu.primaryColor) }, + action: { [weak self] _, f in + f(.dismissWithoutContent) + guard let self, let component = self.component, let environment = self.environment else { + return + } + let controller = component.context.sharedContext.makeStarsTransactionsScreen(context: component.context, starsContext: tonContext) + environment.controller()?.push(controller) + } + ))) + + return items + } + + let contextController = makeContextController(presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: nil) + controller.presentInGlobalOverlay(contextController) + } func update(component: GiftStoreScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true @@ -1255,42 +1319,37 @@ final class GiftStoreScreenComponent: Component { let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: edgeEffectHeight)) transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) self.edgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, blur: true, rect: edgeEffectFrame, edge: .top, edgeSize: min(30, edgeEffectFrame.height), transition: transition) - - let balanceTitleSize = self.balanceTitle.update( + + + let balanceSize = self.balance.update( transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: strings.Stars_Purchase_Balance, - font: Font.regular(14.0), - textColor: environment.theme.actionSheet.primaryTextColor - )), - maximumNumberOfLines: 1 - )), + component: AnyComponent( + BalanceComponent( + context: component.context, + theme: environment.theme, + action: { [weak self] in + guard let self else { + return + } + self.presentBalanceMenu() + } + ) + ), environment: {}, containerSize: availableSize ) - - let formattedBalance = formatStarsAmountText(self.starsState?.balance ?? StarsAmount.zero, dateTimeFormat: environment.dateTimeFormat) - let smallLabelFont = Font.regular(11.0) - let labelFont = Font.semibold(14.0) - let balanceText = tonAmountAttributedString(formattedBalance, integralFont: labelFont, fractionalFont: smallLabelFont, color: environment.theme.actionSheet.primaryTextColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator) - - let balanceValueSize = self.balanceValue.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(balanceText), - maximumNumberOfLines: 1 - )), - environment: {}, - containerSize: availableSize - ) - let balanceIconSize = self.balanceIcon.update( - transition: .immediate, - component: AnyComponent(BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil)), - environment: {}, - containerSize: availableSize - ) - + let balanceFrame = CGRect(origin: .zero, size: balanceSize) + if let balanceView = self.balance.view { + if balanceView.superview == nil { + self.balanceBackgroundView.contentView.addSubview(balanceView) + } + balanceView.frame = balanceFrame + + let balanceBackgroundFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - 16.0 - balanceSize.width, y: environment.navigationHeight - 60.0 + 2.0 + floor((60.0 - 44.0) * 0.5)), size: balanceSize) + + transition.setFrame(view: self.balanceBackgroundView, frame: balanceBackgroundFrame) + self.balanceBackgroundView.update(size: balanceBackgroundFrame.size, cornerRadius: balanceBackgroundFrame.height * 0.5, isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition) + } if self.balanceBackgroundView.superview == nil { component.overNavigationContainer.addSubview(self.balanceBackgroundView) } @@ -1300,32 +1359,6 @@ final class GiftStoreScreenComponent: Component { topInset = environment.statusBarHeight - 6.0 } - if let balanceTitleView = self.balanceTitle.view, let balanceValueView = self.balanceValue.view, let balanceIconView = self.balanceIcon.view { - if balanceTitleView.superview == nil { - self.balanceBackgroundView.contentView.addSubview(balanceTitleView) - self.balanceBackgroundView.contentView.addSubview(balanceValueView) - self.balanceBackgroundView.contentView.addSubview(balanceIconView) - } - - let topBalanceOriginY = (44.0 - balanceTitleSize.height - balanceValueSize.height) / 2.0 - - let balanceSideInset: CGFloat = 12.0 - var balanceBackgroundSize = CGSize(width: balanceTitleSize.width + balanceSideInset * 2.0, height: 44.0) - balanceBackgroundSize.width = max(balanceBackgroundSize.width, balanceValueSize.width + balanceIconSize.width + 2.0 + balanceSideInset * 2.0) - - let balanceBackgroundFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - 16.0 - balanceBackgroundSize.width, y: environment.navigationHeight - 60.0 + 2.0 + floor((60.0 - 44.0) * 0.5)), size: balanceBackgroundSize) - - transition.setFrame(view: self.balanceBackgroundView, frame: balanceBackgroundFrame) - self.balanceBackgroundView.update(size: balanceBackgroundFrame.size, cornerRadius: balanceBackgroundFrame.height * 0.5, isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: transition) - - balanceTitleView.center = CGPoint(x: balanceBackgroundFrame.width - balanceSideInset - balanceTitleSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height / 2.0) - balanceTitleView.bounds = CGRect(origin: .zero, size: balanceTitleSize) - balanceValueView.center = CGPoint(x: balanceBackgroundFrame.width - balanceSideInset - balanceValueSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0) - balanceValueView.bounds = CGRect(origin: .zero, size: balanceValueSize) - balanceIconView.center = CGPoint(x: balanceBackgroundFrame.width - balanceSideInset - balanceValueSize.width - balanceIconSize.width / 2.0 - 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0 - UIScreenPixel) - balanceIconView.bounds = CGRect(origin: .zero, size: balanceIconSize) - } - let titleSize = self.title.update( transition: transition, component: AnyComponent(MultilineTextComponent( @@ -1360,6 +1393,7 @@ final class GiftStoreScreenComponent: Component { starsContext: component.starsContext, peerId: component.peerId, gift: component.gift, + isPlain: false, confirmPurchaseImmediately: false, starsTopUpOptions: nil, scrollToTop: { [weak self] in diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index 777b3f5aa4..03093d1027 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -746,8 +746,8 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { } else { return false } - }), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute { - buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05) + }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute { + buttonColor = UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2) secondaryTextColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift index ce2128889d..6383c04808 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift @@ -294,8 +294,8 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { } else { return false } - }), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute { - buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05) + }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute { + buttonColor = UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2) secondaryTextColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift index 8c1535be94..5e9fd677f8 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift @@ -739,7 +739,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { return false } }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute { - buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05) + buttonColor = UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2) badgeColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05) let outer = UIColor(rgb: UInt32(bitPattern: outerColor)) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index ecbc6c58ef..8e33ce050c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -748,7 +748,7 @@ private final class GiftViewSheetContent: CombinedComponent { let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo } self.subject = .profileGift(peerId, gift.withGift(.unique(uniqueGift.withAttributes(updatedAttributes)))) case let .message(message): - if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer, canCraftAt) = action.action, case let .unique(uniqueGift) = gift { + if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer, canCraftAt, isCrafted) = action.action, case let .unique(uniqueGift) = gift { let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo } let updatedMedia: [Media] = [ TelegramMediaAction( @@ -770,7 +770,8 @@ private final class GiftViewSheetContent: CombinedComponent { dropOriginalDetailsStars: nil, assigned: assigned, fromOffer: fromOffer, - canCraftAt: canCraftAt + canCraftAt: canCraftAt, + isCrafted: isCrafted ) ) ] @@ -2909,8 +2910,8 @@ private final class GiftViewSheetContent: CombinedComponent { } var buttonColor: UIColor = UIColor.white.withAlphaComponent(0.16) - if let previewPatternColor = giftCompositionExternalState.previewPatternColor { - buttonColor = previewPatternColor + if let backgroundColor = giftCompositionExternalState.backgroundColor { + buttonColor = backgroundColor.mixedWith(.white, alpha: 0.2) } let variantsMeasureDescription = variantsMeasureDescription.update( @@ -5129,10 +5130,10 @@ private final class GiftViewSheetContent: CombinedComponent { } else { return false } - }), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute { - buttonsBackground = .color(UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05)) - } else if showUpgradePreview, let previewPatternColor = giftCompositionExternalState.previewPatternColor { - buttonsBackground = .color(previewPatternColor.withMultipliedBrightnessBy(1.05)) + }), case let .backdrop(_, _, _, outerColor, _, _, _) = backdropAttribute { + buttonsBackground = .color(UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2)) + } else if showUpgradePreview, let backgroundColor = giftCompositionExternalState.backgroundColor { + buttonsBackground = .color(backgroundColor.mixedWith(.white, alpha: 0.2)) } var isBackButton = false @@ -5449,7 +5450,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { let fromPeerId = senderId ?? message.author?.id return (message.id.peerId, fromPeerId, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash, upgradeSeparate, nil, toPeerId, number, nil) - case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftDate): + case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftDate, _): var reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 83f5f107da..91567bcc58 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -3734,7 +3734,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } else if case let .gift(gift) = subject { isGift = true - let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false, fromOffer: false, canCraftAt: nil))] + let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false, fromOffer: false, canCraftAt: nil, isCrafted: false))] let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) messages = .single([message]) } else { diff --git a/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift b/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift index 131a83ec67..7d92402b8d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift @@ -197,6 +197,8 @@ private class MessagePriceItemNode: ListViewItemNode, ItemListItemNode { } private let backgroundNode: ASDisplayNode + private let highlightNode: ASDisplayNode + private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode @@ -225,6 +227,9 @@ private class MessagePriceItemNode: ListViewItemNode, ItemListItemNode { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true + self.highlightNode = ASDisplayNode() + self.highlightNode.isLayerBacked = true + self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true @@ -297,6 +302,20 @@ private class MessagePriceItemNode: ListViewItemNode, ItemListItemNode { self.item?.openSetCustom?() } + 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: MessagePriceItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let currentItem = self.item @@ -329,6 +348,7 @@ private class MessagePriceItemNode: ListViewItemNode, ItemListItemNode { strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.highlightNode.backgroundColor = item.theme.list.itemSearchHighlightColor if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) @@ -370,6 +390,7 @@ private class MessagePriceItemNode: ListViewItemNode, ItemListItemNode { strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.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: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift index 1aacc73113..6f22305c27 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift @@ -587,8 +587,7 @@ public final class PeerInfoCoverComponent: Component { self.avatarPatternContentLayers.append(itemLayer) } - //itemLayer.frame = itemFrame - transition.setFrame(layer: itemLayer, frame: itemFrame) + itemLayer.frame = itemFrame itemLayer.layerTintColor = UIColor(white: 0.0, alpha: 0.8).cgColor transition.setAlpha(layer: itemLayer, alpha: 1.0 - itemScaleFraction) diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index ac25591b55..3cc56e06f8 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -589,7 +589,7 @@ public final class TextFieldComponent: Component { } } - public func chatInputTextNodeShouldReturn() -> Bool { + public func chatInputTextNodeShouldReturn(modifierFlags: UIKeyModifierFlags) -> Bool { guard let component = self.component else { return true } diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Stars.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Stars.imageset/Contents.json new file mode 100644 index 0000000000..bdcc010937 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Stars.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "star (3).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Stars.imageset/star (3).pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Stars.imageset/star (3).pdf new file mode 100644 index 0000000000000000000000000000000000000000..67f351d03a34d090588e6c46a62d4bf3dfe158d5 GIT binary patch literal 8084 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

RflcEG4o+@zLr4 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 C@)zs? literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/Contents.json new file mode 100644 index 0000000000..27e29557d3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "giftcrash.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/giftcrash.pdf b/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/giftcrash.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2bdf30a259a60c03903fd9330ae9ab963d1ee1ba GIT binary patch literal 4778 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/ChatControllerKeyShortcuts.swift b/submodules/TelegramUI/Sources/ChatControllerKeyShortcuts.swift index 65ad49f397..9a93f12694 100644 --- a/submodules/TelegramUI/Sources/ChatControllerKeyShortcuts.swift +++ b/submodules/TelegramUI/Sources/ChatControllerKeyShortcuts.swift @@ -15,16 +15,15 @@ import ChatControllerInteraction extension ChatControllerImpl { var keyShortcutsInternal: [KeyShortcut] { - if !self.traceVisibility() || !isTopmostChatController(self) { + if !isTopmostChatController(self) { return [] } let strings = self.presentationData.strings - var inputShortcuts: [KeyShortcut] + var inputShortcuts: [KeyShortcut] = [] if self.chatDisplayNode.isInputViewFocused { inputShortcuts = [ - KeyShortcut(title: strings.KeyCommand_SendMessage, input: "\r", action: {}), KeyShortcut(input: "B", modifiers: [.command], action: { [weak self] in if let strongSelf = self { strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in @@ -80,6 +79,16 @@ extension ChatControllerImpl { } }) ] + + if self.context.sharedContext.currentChatSettings.with({ $0 }).sendWithCmdEnter { + inputShortcuts.append( + KeyShortcut(title: strings.KeyCommand_SendMessage, input: "\r", modifiers: [.command], action: {}) + ) + } else { + inputShortcuts.append( + KeyShortcut(title: strings.KeyCommand_SendMessage, input: "\r", action: {}) + ) + } } else if UIResponder.currentFirst() == nil { inputShortcuts = [ KeyShortcut(title: strings.KeyCommand_FocusOnInputField, input: "\r", action: { [weak self] in diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 1bcadb3599..91e95e7521 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1136,7 +1136,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let sendGiftTitle: String var isIncoming = message.effectivelyIncoming(context.account.peerId) for media in message.media { - if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = action.action { + if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = action.action { if isUpgrade && message.author?.id == context.account.peerId { isIncoming = true } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 55403ba2bb..0ffb96cc38 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -255,6 +255,9 @@ public final class SharedAccountContextImpl: SharedAccountContext { public let currentMediaDisplaySettings: Atomic private var mediaDisplaySettingsDisposable: Disposable? + public let currentChatSettings: Atomic + private var chatSettingsDisposable: Disposable? + public let currentStickerSettings: Atomic private var stickerSettingsDisposable: Disposable? @@ -339,6 +342,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.currentMediaDisplaySettings = Atomic(value: initialPresentationDataAndSettings.mediaDisplaySettings) self.currentStickerSettings = Atomic(value: initialPresentationDataAndSettings.stickerSettings) self.currentInAppNotificationSettings = Atomic(value: initialPresentationDataAndSettings.inAppNotificationSettings) + self.currentChatSettings = Atomic(value: initialPresentationDataAndSettings.chatSettings) if automaticEnergyUsageShouldBeOnNow(settings: self.currentAutomaticMediaDownloadSettings) { self.energyUsageSettings = EnergyUsageSettings.powerSavingDefault @@ -485,6 +489,15 @@ public final class SharedAccountContextImpl: SharedAccountContext { } }) + self.chatSettingsDisposable = (self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.chatSettings]) + |> deliverOnMainQueue).start(next: { [weak self] sharedData in + if let strongSelf = self { + if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.chatSettings]?.get(ChatSettings.self) { + let _ = strongSelf.currentChatSettings.swap(settings) + } + } + }) + let immediateExperimentalUISettingsValue = self.immediateExperimentalUISettingsValue let _ = immediateExperimentalUISettingsValue.swap(initialPresentationDataAndSettings.experimentalUISettings) @@ -1078,6 +1091,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.inAppNotificationSettingsDisposable?.dispose() self.mediaInputSettingsDisposable?.dispose() self.mediaDisplaySettingsDisposable?.dispose() + self.chatSettingsDisposable?.dispose() + self.stickerSettingsDisposable?.dispose() self.callDisposable?.dispose() self.groupCallDisposable?.dispose() self.callStateDisposable?.dispose() diff --git a/submodules/TelegramUIPreferences/Sources/ChatSettings.swift b/submodules/TelegramUIPreferences/Sources/ChatSettings.swift new file mode 100644 index 0000000000..a6cfee2e81 --- /dev/null +++ b/submodules/TelegramUIPreferences/Sources/ChatSettings.swift @@ -0,0 +1,49 @@ +import Foundation +import TelegramCore +import SwiftSignalKit + +public struct ChatSettings: Codable, Equatable { + public let sendWithCmdEnter: Bool + + public static var defaultSettings: ChatSettings { + return ChatSettings(sendWithCmdEnter: false) + } + + public init(sendWithCmdEnter: Bool) { + self.sendWithCmdEnter = sendWithCmdEnter + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.sendWithCmdEnter = (try container.decode(Int32.self, forKey: "sendWithCmdEnter")) != 0 + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode((self.sendWithCmdEnter ? 1 : 0) as Int32, forKey: "sendWithCmdEnter") + } + + public static func ==(lhs: ChatSettings, rhs: ChatSettings) -> Bool { + return lhs.sendWithCmdEnter == rhs.sendWithCmdEnter + } + + public func withUpdatedSendWithCmdEnter(_ sendWithCmdEnter: Bool) -> ChatSettings { + return ChatSettings(sendWithCmdEnter: sendWithCmdEnter) + } +} + +public func updateChatSettingsInteractively(accountManager: AccountManager, _ f: @escaping (ChatSettings) -> ChatSettings) -> Signal { + return accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.chatSettings, { entry in + let currentSettings: ChatSettings + if let entry = entry?.get(ChatSettings.self) { + currentSettings = entry + } else { + currentSettings = ChatSettings.defaultSettings + } + return SharedPreferencesEntry(f(currentSettings)) + }) + } +} diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index 755c04caef..879c0c608c 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -43,6 +43,7 @@ private enum ApplicationSpecificSharedDataKeyValues: Int32 { case drawingSettings = 19 case mediaDisplaySettings = 20 case updateSettings = 21 + case chatSettings = 22 } public struct ApplicationSpecificSharedDataKeys { @@ -68,6 +69,7 @@ public struct ApplicationSpecificSharedDataKeys { public static let drawingSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.drawingSettings.rawValue) public static let mediaDisplaySettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.mediaDisplaySettings.rawValue) public static let updateSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.updateSettings.rawValue) + public static let chatSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.chatSettings.rawValue) } private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 { From 52a22d8cef60f7a14dba39d4475aa831d50b77e0 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 28 Jan 2026 00:23:23 +0400 Subject: [PATCH 02/12] Fix faq --- .../PeerInfoScreen/Sources/PeerInfoScreen.swift | 7 ++----- .../Sources/PeerInfoScreenSettingsActions.swift | 11 +++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 638ab2ec2a..89dc5d3362 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -313,7 +313,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let tipsPeerDisposable = MetaDisposable() let cachedFaq = Promise(nil) - private var didSetCachedFaq = false + var didSetCachedFaq = false weak var copyProtectionTooltipController: TooltipController? weak var emojiStatusSelectionController: ViewController? @@ -4804,10 +4804,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.headerNode.navigationButtonContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) if self.isSettings { - if !self.didSetCachedFaq { - self.cachedFaq.set(.single(nil) |> then(cachedFaqInstantPage(context: self.context) |> map(Optional.init))) - self.didSetCachedFaq = true - } + self.setupFaqIfNeeded() if let settings = self.data?.globalSettings { self.searchDisplayController = SearchDisplayController( diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenSettingsActions.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenSettingsActions.swift index 6a324daddc..0e64cec1d7 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenSettingsActions.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenSettingsActions.swift @@ -15,6 +15,7 @@ import PremiumUI import TelegramPresentationData import PresentationDataUtils import PasswordSetupUI +import InstantPageCache extension PeerInfoScreenNode { func openSettings(section: PeerInfoSettingsSection) { @@ -298,7 +299,16 @@ extension PeerInfoScreenNode { } } + func setupFaqIfNeeded() { + if !self.didSetCachedFaq { + self.cachedFaq.set(.single(nil) |> then(cachedFaqInstantPage(context: self.context) |> map(Optional.init))) + self.didSetCachedFaq = true + } + } + func openFaq(anchor: String? = nil) { + self.setupFaqIfNeeded() + let presentationData = self.presentationData let progressSignal = Signal { [weak self] subscriber in let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) @@ -314,6 +324,7 @@ extension PeerInfoScreenNode { let progressDisposable = progressSignal.start() let _ = (self.cachedFaq.get() + |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] resolvedUrl in progressDisposable.dispose() From a1aa4d9094c818c2af4d75673f65060078d98afc Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 28 Jan 2026 19:05:47 +0400 Subject: [PATCH 03/12] Various fixes --- .../Sources/ButtonComponent.swift | 19 ++++++++- .../Components/Gifts/GiftCraftScreen/BUILD | 1 + .../Sources/CraftTableComponent.swift | 2 +- .../Sources/DialIndicatorComponent.swift | 2 +- .../Sources/GiftCraftScreen.swift | 17 +++++--- .../Sources/SelectCraftGiftScreen.swift | 39 +++++++++++++------ .../Sources/GiftLoadingShimmerView.swift | 4 +- .../Sources/GiftUpgradeVariantsScreen.swift | 14 ++++++- .../Sources/GiftViewBuyGift.swift | 4 ++ .../Sources/PlainButtonComponent.swift | 18 +++++++-- 10 files changed, 94 insertions(+), 26 deletions(-) diff --git a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift index 0887af088a..6f983b088a 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift +++ b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift @@ -466,6 +466,9 @@ public final class ButtonComponent: Component { super.init(frame: frame) + self.button.isExclusiveTouch = true + self.layer.rasterizationScale = UIScreenScale + self.addSubview(self.containerView) self.addSubview(self.button) @@ -477,12 +480,24 @@ public final class ButtonComponent: Component { case .glass: let transition = ComponentTransition(animation: .curve(duration: highlighted ? 0.25 : 0.35, curve: .spring)) if highlighted { + self.layer.shouldRasterize = true + let highlightedColor = component.background.color.withMultiplied(hue: 1.0, saturation: 0.77, brightness: 1.01) transition.setBackgroundColor(view: self.containerView, color: highlightedColor) - transition.setScale(view: self.containerView, scale: 1.05) + transition.setScale(view: self.containerView, scale: 1.05, completion: { finished in + if finished { + self.layer.shouldRasterize = false + } + }) } else { + self.layer.shouldRasterize = true + transition.setBackgroundColor(view: self.containerView, color: component.background.color) - transition.setScale(view: self.containerView, scale: 1.0) + transition.setScale(view: self.containerView, scale: 1.0, completion: { finished in + if finished { + self.layer.shouldRasterize = false + } + }) } case .legacy: if highlighted { diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/BUILD index e5a3568ad1..7262f33a94 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/BUILD @@ -49,6 +49,7 @@ swift_library( "//submodules/TelegramUI/Components/SpaceWarpView", "//submodules/ConfettiEffect", "//submodules/TelegramNotices", + "//submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift index 51b83e7042..a6a3bff6eb 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift @@ -546,7 +546,7 @@ final class GiftSlotComponent: Component { self.state = state let backgroundFrame = CGRect(origin: .zero, size: availableSize).insetBy(dx: 1.0, dy: 1.0) - self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 28.0, isDark: true, tintColor: .init(kind: .custom(style: .default, color: component.buttonColor)), transition: .immediate) + self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 28.0, isDark: true, tintColor: .init(kind: .custom(style: .default, color: component.buttonColor)), isInteractive: true, transition: .immediate) transition.setFrame(view: self.backgroundView, frame: backgroundFrame) if component.gift == nil && component.isCrafting && previousComponent?.isCrafting == false { transition.setBlur(layer: self.backgroundView.layer, radius: 10.0) diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/DialIndicatorComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/DialIndicatorComponent.swift index a595eeb6db..468c44773f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/DialIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/DialIndicatorComponent.swift @@ -106,7 +106,7 @@ final class DialIndicatorComponent: Component { self.backgroundLayer.lineCap = .round self.foregroundLayer.lineCap = .round - + self.addSubview(self.containerView) self.containerView.layer.addSublayer(self.backgroundLayer) diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift index efaaf2a3dc..cef1be4731 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift @@ -166,9 +166,7 @@ private final class CraftGiftPageContent: Component { private let infoTitle = ComponentView() private let infoDescription = ComponentView() private var infoList = ComponentView() - - private var actionButton = ComponentView() - + private var craftState: CraftGiftsContext.State? private var craftStateDisposable: Disposable? @@ -338,6 +336,7 @@ private final class CraftGiftPageContent: Component { } self.starGiftsMap = starGiftsMap component.externalState.starGiftsMap = starGiftsMap + self.state?.updated() })) } @@ -1616,8 +1615,16 @@ private final class SheetContainerComponent: CombinedComponent { tintColor: .white ) )), - action: { _ in - dismiss(true) + action: { [weak state] _ in + guard let state else { + return + } + if state.displayInfo { + state.displayInfo = false + state.updated(transition: .spring(duration: 0.3)) + } else { + dismiss(true) + } } ) ), diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift index ce7bf9fa8a..ed0b4dc0af 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift @@ -21,6 +21,7 @@ import ResizableSheetComponent import TooltipUI import GlassBarButtonComponent import ConfettiEffect +import GiftLoadingShimmerView final class SelectGiftPageContent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -74,6 +75,7 @@ final class SelectGiftPageContent: Component { private let myGiftsTitle = ComponentView() private var gifts: [AnyHashable: ComponentView] = [:] private let myGiftsPlaceholder = ComponentView() + private let loadingView = GiftLoadingShimmerView() private let storeGiftsTitle = ComponentView() private let storeGifts = ComponentView() @@ -94,6 +96,8 @@ final class SelectGiftPageContent: Component { self.layer.cornerRadius = 40.0 self.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + self.addSubview(self.loadingView) } required init?(coder: NSCoder) { @@ -204,10 +208,25 @@ final class SelectGiftPageContent: Component { } let itemWidth = (availableSize.width - itemSideInset * 2.0 - itemSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow) let itemSize = CGSize(width: itemWidth, height: itemWidth) - var itemFrame = CGRect(origin: CGPoint(x: itemSideInset, y: contentHeight), size: itemSize) + + var isLoading = false + if self.availableGifts.isEmpty, case .loading = (self.craftState?.dataState ?? .loading) { + isLoading = true + } + let loadingTransition: ComponentTransition = .easeInOut(duration: 0.25) + let loadingSize = CGSize(width: availableSize.width, height: 180.0) + if isLoading { + contentHeight += 120.0 + self.loadingView.update(size: loadingSize, theme: environment.theme, itemSize: itemSize, showFilters: false, isPlain: true, transition: .immediate) + loadingTransition.setAlpha(view: self.loadingView, alpha: 1.0) + } else { + loadingTransition.setAlpha(view: self.loadingView, alpha: 0.0) + } + transition.setFrame(view: self.loadingView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight - 170.0), size: loadingSize)) + + var itemFrame = CGRect(origin: CGPoint(x: itemSideInset, y: contentHeight), size: itemSize) var itemsHeight: CGFloat = 0.0 - var validIds: [AnyHashable] = [] for gift in self.availableGifts { let isVisible = "".isEmpty @@ -272,17 +291,15 @@ final class SelectGiftPageContent: Component { ) if let itemView = visibleItem.view { if itemView.superview == nil { - self.addSubview(itemView) - + if let _ = self.loadingView.superview { + self.insertSubview(itemView, belowSubview: self.loadingView) + } else { + self.addSubview(itemView) + } if !transition.animation.isImmediate { - let delay = ((itemFrame.minY - contentHeight) / itemSize.height) * 0.07 - itemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25, delay: delay) - itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: delay) + itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } } - itemView.isUserInteractionEnabled = !component.selectedGiftIds.contains(gift.gift.id) - itemView.alpha = component.selectedGiftIds.contains(gift.gift.id) ? 0.4 : 1.0 - itemView.layer.allowsGroupOpacity = itemView.alpha < 1.0 itemTransition.setFrame(view: itemView, frame: itemFrame) } } @@ -315,7 +332,7 @@ final class SelectGiftPageContent: Component { for id in removeIds { self.gifts.removeValue(forKey: id) } - + if let state = self.craftState, case .ready = state.dataState, self.availableGifts.isEmpty { contentHeight += 10.0 let myGiftsPlaceholderSize = self.myGiftsPlaceholder.update( diff --git a/submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView/Sources/GiftLoadingShimmerView.swift b/submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView/Sources/GiftLoadingShimmerView.swift index a51f903f6e..2ba1af60ce 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView/Sources/GiftLoadingShimmerView.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView/Sources/GiftLoadingShimmerView.swift @@ -139,7 +139,7 @@ public final class GiftLoadingShimmerView: UIView { required init?(coder: NSCoder) { fatalError() } - public func update(size: CGSize, theme: PresentationTheme, showFilters: Bool = false, isPlain: Bool = false, transition: ContainedViewLayoutTransition) { + public func update(size: CGSize, theme: PresentationTheme, itemSize: CGSize? = nil, showFilters: Bool = false, isPlain: Bool = false, transition: ContainedViewLayoutTransition) { let backgroundColor = isPlain ? theme.list.itemBlocksBackgroundColor : theme.list.blocksBackgroundColor let color = theme.list.itemSecondaryTextColor.mixedWith(theme.list.blocksBackgroundColor, alpha: 0.85) @@ -169,7 +169,7 @@ public final class GiftLoadingShimmerView: UIView { let optionSpacing: CGFloat = 10.0 let optionWidth = (size.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0 - let itemSize = CGSize(width: optionWidth, height: 154.0) + let itemSize = itemSize ?? CGSize(width: optionWidth, height: 154.0) context.setBlendMode(.copy) context.setFillColor(UIColor.clear.cgColor) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift index 5e9fd677f8..833e8c3088 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift @@ -202,6 +202,8 @@ private final class GiftUpgradeVariantsScreenComponent: Component { self.containerView.addSubview(self.navigationBarContainer) self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + + self.alpha = 0.0 } required init?(coder: NSCoder) { @@ -271,6 +273,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { } func animateIn() { + self.alpha = 1.0 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) @@ -1061,6 +1064,13 @@ private final class GiftUpgradeVariantsScreenComponent: Component { self.previewTimerTick() } self.state?.updated(transition: .easeInOut(duration: 0.25)) + + if let buttonView = self.playbackButton.view { + buttonView.isUserInteractionEnabled = false + Queue.mainQueue().after(0.3, { + buttonView.isUserInteractionEnabled = true + }) + } } )), environment: {}, @@ -1194,7 +1204,9 @@ public class GiftUpgradeVariantsScreen: ViewControllerComponentContainer { self.didPlayAppearAnimation = true if let componentView = self.node.hostView.componentView as? GiftUpgradeVariantsScreenComponent.View { - componentView.animateIn() + Queue.mainQueue().justDispatch { + componentView.animateIn() + } } } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewBuyGift.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewBuyGift.swift index 48cbb81e84..ff996afa9c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewBuyGift.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewBuyGift.swift @@ -37,6 +37,8 @@ public func buyStarGiftImpl( parseMarkdown: true ) controller.present(alertController, in: .window(.root)) + + beforeCompletion() return } @@ -207,6 +209,8 @@ public func buyStarGiftImpl( context.sharedContext.applicationBindings.openUrl(fragmentUrl) } )) + + beforeCompletion() } else { proceed() } diff --git a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift index d36b09e40f..fadcc5c0d6 100644 --- a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift +++ b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift @@ -108,6 +108,7 @@ public final class PlainButtonComponent: Component { super.init(frame: frame) self.isExclusiveTouch = true + self.layer.rasterizationScale = UIScreenScale self.contentContainer.isUserInteractionEnabled = false self.addSubview(self.contentContainer) @@ -130,8 +131,13 @@ public final class PlainButtonComponent: Component { self.contentContainer.alpha = 0.7 } if animateScale { + self.layer.shouldRasterize = true let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) - transition.setScale(layer: self.contentContainer.layer, scale: topScale) + transition.setScale(layer: self.contentContainer.layer, scale: topScale, completion: { finished in + if finished { + self.layer.shouldRasterize = false + } + }) } } else { if animateAlpha { @@ -140,15 +146,21 @@ public final class PlainButtonComponent: Component { } if animateScale { + self.layer.shouldRasterize = true + let transition = ComponentTransition(animation: .none) transition.setScale(layer: self.contentContainer.layer, scale: 1.0) - + self.contentContainer.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in guard let self else { return } - self.contentContainer.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + self.contentContainer.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, completion: { finished in + if finished { + self.layer.shouldRasterize = false + } + }) }) } } From 809958df3b403f95a0fef17a0f9ff7264bd64227 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 28 Jan 2026 22:11:45 +0400 Subject: [PATCH 04/12] Update localization --- .../Telegram-iOS/en.lproj/Localizable.strings | 35 ++++++++++++ .../Sources/AuthConfirmationScreen.swift | 2 +- .../Sources/ButtonComponent.swift | 7 ++- .../Sources/GiftCraftScreen.swift | 54 +++++++++---------- .../Sources/GiftItemComponent.swift | 10 ++-- .../Sources/GiftUpgradeVariantsScreen.swift | 7 ++- .../Sources/GiftViewScreen.swift | 5 +- .../Sources/ProxyServerPreviewScreen.swift | 18 +++---- 8 files changed, 84 insertions(+), 54 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 38e8db2492..169f4ab264 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15673,3 +15673,38 @@ Error: %8$@"; "Appearance.SendWithCmdEnter" = "Send Messages with ⌘+Enter"; "Notification.StarsGift.Crafted" = "You crafted a new unique collectible"; + +"Gift.Craft.Title" = "Craft Gift"; +"Gift.Craft.Description" = "Add up to **4 gifts** to craft new\n**$ %@**.\n\nIf crafting fails, all used gifts\nwill be lost."; +"Gift.Craft.ViewVariants" = "View all new variants"; +"Gift.Craft.BackdropTooltip" = "There's **%1$@%** chance the crafted gift will have **%2$@** backdrop."; +"Gift.Craft.SymbolTooltip" = "There's **%1$@%** chance the crafted gift will have **%2$@** symbol."; +"Gift.Craft.SuccessChanceSuffix" = "% Success Chance"; +"Gift.Craft.Craft" = "Craft %@"; +"Gift.Craft.Crafting.Title" = "Crafting"; +"Gift.Craft.Crafting.Description" = "If crafting fails, all used gifts\nwill be lost."; +"Gift.Craft.Crafting.SuccessChance" = "%@% Success Chance"; +"Gift.Craft.CraftingFailed.Title" = "Crafting Failed"; +"Gift.Craft.CraftingFailed.Text_1" = "This crafting attempt was unsuccessful.\n**%@ gift** was lost."; +"Gift.Craft.CraftingFailed.Text_any" = "This crafting attempt was unsuccessful.\n**%@ gifts** were lost."; +"Gift.Craft.CraftingFailed.CraftAnotherGift" = "Craft Another Gift"; +"Gift.Craft.Info.Title" = "Craft Gift"; +"Gift.Craft.Info.Description" = "Use your existing gifts to craft new ones."; +"Gift.Craft.Info.Paragraph1.Title" = "Combine Gifts"; +"Gift.Craft.Info.Paragraph1.Text" = "Add up to 4 Gifts to attempt crafting a new upgraded model."; +"Gift.Craft.Info.Paragraph2.Title" = "Input Matters"; +"Gift.Craft.Info.Paragraph2.Text" = "Each craft has a success chance. Better combinations improve the outcome."; +"Gift.Craft.Info.Paragraph3.Title" = "Exclusive Look"; +"Gift.Craft.Info.Paragraph3.Text" = "Reforge gifts into a rarer collectibles with a new look."; +"Gift.Craft.Info.SelectGifts" = "Select Gifts"; +"Gift.Craft.Unavailable.Title" = "Try Later"; +"Gift.Craft.Unavailable.Text" = "You will be able to craft this gift on %@."; + +"Gift.Attribute.Rare" = "rare"; +"Gift.Attribute.Legendary" = "legendary"; +"Gift.Attribute.Epic" = "epic"; + +"SocksProxySetup.CheckStatus" = "Check Status"; +"SocksProxySetup.Warning.Title" = "Warning"; +"SocksProxySetup.Warning.Text" = "This action will expose your IP address to the admin of the proxy server."; +"SocksProxySetup.Warning.Proceed" = "Proceed"; diff --git a/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift b/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift index 766e55c16e..c796a2e4de 100644 --- a/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift +++ b/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift @@ -89,7 +89,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent { let alertController = textAlertController( context: self.context, title: "Phone Number", - text: "**\(domain)** wants to access your phone number **\(phoneNumber)**. Allow access?", + text: "**\(domain)** wants to access your phone number **\(phoneNumber)**.\n\nAllow access?", actions: [ TextAlertAction(type: .genericAction, title: "Deny", action: { commit(false) diff --git a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift index 6f983b088a..ce1af17211 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift +++ b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift @@ -613,6 +613,7 @@ public final class ButtonComponent: Component { if contentView.superview == nil { contentTransition = .immediate animateIn = true + contentView.layer.rasterizationScale = UIScreenScale contentView.isUserInteractionEnabled = false contentContainerView.addSubview(contentView) @@ -624,7 +625,10 @@ public final class ButtonComponent: Component { contentTransition.setAlpha(view: contentView, alpha: contentAlpha) if animateIn && previousContentItem != nil && !transition.animation.isImmediate { - contentView.layer.animateScale(from: 0.4, to: 1.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring) + contentView.layer.shouldRasterize = true + contentView.layer.animateScale(from: 0.4, to: 1.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in + contentView.layer.shouldRasterize = false + }) contentView.layer.animateAlpha(from: 0.0, to: contentAlpha, duration: 0.1) contentView.layer.animatePosition(from: CGPoint(x: 0.0, y: -size.height * 0.15), to: CGPoint(), duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } @@ -632,6 +636,7 @@ public final class ButtonComponent: Component { if let previousContentItem, let previousContentView = previousContentItem.view.view { if !transition.animation.isImmediate { + previousContentView.layer.shouldRasterize = true previousContentView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) previousContentView.layer.animateAlpha(from: contentAlpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousContentView] _ in previousContentView?.removeFromSuperview() diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift index cef1be4731..2c6d326920 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift @@ -414,7 +414,7 @@ private final class CraftGiftPageContent: Component { let titleSize = self.title.update( transition: transition, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Craft Gift", font: Font.semibold(17.0), textColor: .white))) + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Title, font: Font.semibold(17.0), textColor: .white))) ), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -426,17 +426,14 @@ private final class CraftGiftPageContent: Component { } transition.setFrame(view: titleView, frame: titleFrame) transition.setAlpha(view: titleView, alpha: 1.0) - //transition.setBlur(layer: titleView.layer, radius: component.isCrafting ? 10.0 : 0.0) } let giftTitle = "\(component.gift.title) #\(formatCollectibleNumber(component.gift.number, dateTimeFormat: environment.dateTimeFormat))" - //TODO:localize - let descriptionFont = Font.regular(13.0) let descriptionBoldFont = Font.semibold(13.0) let descriptionColor = UIColor.white - let rawDescriptionString = "Add up to **4 gifts** to craft new\n**$ \(giftTitle)**.\n\nIf crafting fails, all used gifts\nwill be lost." + let rawDescriptionString = environment.strings.Gift_Craft_Description(giftTitle).string let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString if let gift = self.starGiftsMap[component.gift.giftId] { @@ -582,7 +579,7 @@ private final class CraftGiftPageContent: Component { #if DEBUG self.component?.externalState.testFailOrSuccess = true #endif - self.showAttributeInfo(tag: backdropButtonTag, text: "There's **\(backdropPermille / 10)%** chance the crafted gift will have **\(backdropName)** backdrop.") + self.showAttributeInfo(tag: backdropButtonTag, text: environment.strings.Gift_Craft_BackdropTooltip("\(backdropPermille / 10)", backdropName).string) }, tag: backdropButtonTag ) @@ -631,7 +628,7 @@ private final class CraftGiftPageContent: Component { #if DEBUG self.component?.externalState.testFailOrSuccess = false #endif - self.showAttributeInfo(tag: symbolButtonTag, text: "There's **\(symbolPermille / 10)%** chance the crafted gift will have **\(symbolName)** backdrop.") + self.showAttributeInfo(tag: symbolButtonTag, text: environment.strings.Gift_Craft_SymbolTooltip("\(symbolPermille / 10)", symbolName).string) }, tag: symbolButtonTag ) @@ -642,7 +639,7 @@ private final class CraftGiftPageContent: Component { craftContentHeight += backdropDialSize.height craftContentHeight += 15.0 - let variantsString = "View all new variants" + let variantsString = environment.strings.Gift_Craft_ViewVariants let variantsButtonMeasure = self.variantsButtonMeasure.update( transition: .immediate, component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: variantsString, font: Font.semibold(13.0), textColor: .clear)))), @@ -800,7 +797,7 @@ private final class CraftGiftPageContent: Component { let craftingTitleSize = self.craftingTitle.update( transition: transition, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Crafting", font: Font.bold(20.0), textColor: .white))) + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Crafting_Title, font: Font.bold(20.0), textColor: .white))) ), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -844,7 +841,7 @@ private final class CraftGiftPageContent: Component { let descriptionFont = Font.regular(13.0) let descriptionBoldFont = Font.semibold(13.0) let descriptionColor = UIColor.white.withAlphaComponent(0.5) - let rawDescriptionString = "If crafting fails, all used gifts\nwill be lost." + let rawDescriptionString = environment.strings.Gift_Craft_Crafting_Description let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString let craftingDescriptionSize = self.craftingDescription.update( @@ -874,7 +871,7 @@ private final class CraftGiftPageContent: Component { craftingOriginY += craftingDescriptionSize.height craftingOriginY += 24.0 - let craftingProbabilityString = "\(permilleValue / 10)% Success Chance" + let craftingProbabilityString = environment.strings.Gift_Craft_Crafting_SuccessChance("\(permilleValue / 10)").string let craftingProbabilityMeasure = self.craftingProbabilityMeasure.update( transition: .immediate, component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: craftingProbabilityString, font: Font.semibold(13.0), textColor: .clear)))), @@ -945,7 +942,7 @@ private final class CraftGiftPageContent: Component { let failureTitleSize = self.failureTitle.update( transition: transition, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Crafting Failed", font: Font.bold(20.0), textColor: UIColor(rgb: 0xff746d)))) + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_CraftingFailed_Title, font: Font.bold(20.0), textColor: UIColor(rgb: 0xff746d)))) ), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -967,7 +964,7 @@ private final class CraftGiftPageContent: Component { let descriptionFont = Font.regular(13.0) let descriptionBoldFont = Font.semibold(13.0) let descriptionColor = UIColor(rgb: 0xf7af8c) - let rawDescriptionString = "This crafting attempt was unsuccessful.\n**\(component.selectedGiftIds.count) gifts** were lost." + let rawDescriptionString = environment.strings.Gift_Craft_CraftingFailed_Text(Int32(component.selectedGiftIds.count)) let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString let failureDescriptionSize = self.failureDescription.update( @@ -1204,7 +1201,7 @@ private final class CraftGiftPageContent: Component { let infoTitleSize = self.infoTitle.update( transition: transition, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Craft Gift", font: Font.bold(20.0), textColor: .white))) + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Info_Title, font: Font.bold(20.0), textColor: .white))) ), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -1221,7 +1218,7 @@ private final class CraftGiftPageContent: Component { transition: transition, component: AnyComponent(MultilineTextComponent( text: .markdown( - text: "Use your existing gifts to craft new ones.", + text: environment.strings.Gift_Craft_Info_Description, attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.6)), bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.6)), @@ -1256,11 +1253,11 @@ private final class CraftGiftPageContent: Component { var items: [AnyComponentWithIdentity] = [] items.append( AnyComponentWithIdentity( - id: "combine", + id: "paragraph1", component: AnyComponent(InfoParagraphComponent( - title: "Combine Gifts", + title: environment.strings.Gift_Craft_Info_Paragraph1_Title, titleColor: titleColor, - text: "Add up to 3 Gifts to attempt crafting a new upgraded model.", + text: environment.strings.Gift_Craft_Info_Paragraph1_Text, textColor: textColor, accentColor: accentColor, iconName: "Premium/Collectible/Badge", @@ -1270,11 +1267,11 @@ private final class CraftGiftPageContent: Component { ) items.append( AnyComponentWithIdentity( - id: "input", + id: "paragraph2", component: AnyComponent(InfoParagraphComponent( - title: "Input Matters", + title: environment.strings.Gift_Craft_Info_Paragraph2_Title, titleColor: titleColor, - text: "Each craft has a success chance. Better combinations improve the outcome.", + text: environment.strings.Gift_Craft_Info_Paragraph2_Text, textColor: textColor, accentColor: accentColor, iconName: "Premium/Collectible/Transferable", @@ -1284,11 +1281,11 @@ private final class CraftGiftPageContent: Component { ) items.append( AnyComponentWithIdentity( - id: "exclusive", + id: "paragraph3", component: AnyComponent(InfoParagraphComponent( - title: "Exclusive Look", + title: environment.strings.Gift_Craft_Info_Paragraph3_Title, titleColor: titleColor, - text: "Reforge gifts into a rarer collectibles with a new look", + text: environment.strings.Gift_Craft_Info_Paragraph3_Text, textColor: textColor, accentColor: accentColor, iconName: "Premium/Collectible/Unique", @@ -1517,17 +1514,18 @@ private final class SheetContainerComponent: CombinedComponent { let buttonContent: AnyComponentWithIdentity if state.displayInfo { + buttonContent = AnyComponentWithIdentity(id: "info", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Select Gifts", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Info_SelectGifts, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor))) )) } else if state.displayFailure { buttonContent = AnyComponentWithIdentity(id: "fail", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Craft Another Gift", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_CraftingFailed_CraftAnotherGift, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor))) )) } else { var buttonAnimatedItems: [AnimatedTextComponent.Item] = [] buttonAnimatedItems.append(AnimatedTextComponent.Item(id: "percent", content: .number(Int(permilleValue / 10), minDigits: 1))) - buttonAnimatedItems.append(AnimatedTextComponent.Item(id: "suffix", content: .text("% Success Chance"))) + buttonAnimatedItems.append(AnimatedTextComponent.Item(id: "suffix", content: .text(environment.strings.Gift_Craft_SuccessChanceSuffix))) buttonContent = AnyComponentWithIdentity(id: "craft", component: AnyComponent( VStack([ @@ -1537,7 +1535,7 @@ private final class SheetContainerComponent: CombinedComponent { HStack([ AnyComponentWithIdentity( id: AnyHashable("label"), - component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Craft \(giftTitle)", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor)))) + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Craft(giftTitle).string, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor)))) ) ], spacing: 2.0) ) diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 2a7d190d3f..6e2292adc3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -792,9 +792,7 @@ public final class GiftItemComponent: Component { func formatPercentage(_ value: Float) -> String { return String(format: "%0.1f", value).replacingOccurrences(of: ".0", with: "").replacingOccurrences(of: ",0", with: "") + "%" } - - //TODO:localize - + let badgeString: String var badgeColor: UIColor? switch rarity { @@ -805,13 +803,13 @@ public final class GiftItemComponent: Component { badgeString = formatPercentage(Float(value) * 0.1) } case .epic: - badgeString = "epic" + badgeString = component.strings.Gift_Attribute_Epic badgeColor = UIColor(rgb: 0xaf52de) case .legendary: - badgeString = "legendary" + badgeString = component.strings.Gift_Attribute_Legendary badgeColor = UIColor(rgb: 0xd57e32) case .rare: - badgeString = "rare" + badgeString = component.strings.Gift_Attribute_Rare badgeColor = UIColor(rgb: 0x79993d) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift index 833e8c3088..d8751ee850 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift @@ -1365,7 +1365,6 @@ private final class AttributeInfoComponent: Component { var badgeString = "" if let rarity { - //TODO:localize switch rarity { case let .permille(value): if value == 0 { @@ -1374,11 +1373,11 @@ private final class AttributeInfoComponent: Component { badgeString = formatPercentage(Float(value) * 0.1) } case .epic: - badgeString = "epic" + badgeString = component.strings.Gift_Attribute_Epic case .legendary: - badgeString = "legendary" + badgeString = component.strings.Gift_Attribute_Legendary case .rare: - badgeString = "rare" + badgeString = component.strings.Gift_Attribute_Rare } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index e6bb46e33a..169115aac9 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -1115,11 +1115,10 @@ private final class GiftViewSheetContent: CombinedComponent { let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) if let canCraftDate = arguments.canCraftDate, currentTime < canCraftDate { let dateString = stringForFullDate(timestamp: canCraftDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) - //TODO:localize let alertController = textAlertController( context: self.context, - title: "Try Later", - text: "You will be able to craft this gift on \(dateString).", + title: presentationData.strings.Gift_Craft_Unavailable_Title, + text: presentationData.strings.Gift_Craft_Unavailable_Text(dateString).string, actions: [ TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) ], diff --git a/submodules/TelegramUI/Components/ProxyServerPreviewScreen/Sources/ProxyServerPreviewScreen.swift b/submodules/TelegramUI/Components/ProxyServerPreviewScreen/Sources/ProxyServerPreviewScreen.swift index d7953bc77b..15b38f83d7 100644 --- a/submodules/TelegramUI/Components/ProxyServerPreviewScreen/Sources/ProxyServerPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ProxyServerPreviewScreen/Sources/ProxyServerPreviewScreen.swift @@ -195,15 +195,14 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { commit() return } - //TODO:localize let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let alertController = textAlertController( context: context, - title: "Warning", - text: "This action will expose your IP address to the admin of the proxy server.", + title: presentationData.strings.SocksProxySetup_Warning_Title, + text: presentationData.strings.SocksProxySetup_Warning_Text, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), - TextAlertAction(type: .defaultAction, title: "Proceed", action: { + TextAlertAction(type: .defaultAction, title: presentationData.strings.SocksProxySetup_Warning_Proceed, action: { commit() }) ] @@ -253,14 +252,11 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) - - let titleText: String = "Proxy" - let buttonText: String = "Connect Proxy" - + let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( - string: titleText, + string: strings.SocksProxySetup_Title, font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center @@ -323,7 +319,7 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { )) } - var statusText = "Check Status" + var statusText = strings.SocksProxySetup_CheckStatus var statusColor = tableLinkColor var statusIsActive = true if let status = state.status { @@ -378,7 +374,7 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { ), content: AnyComponentWithIdentity( id: AnyHashable(0), - component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: buttonText, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: strings.SocksProxySetup_ConnectAndSave, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) ), displaysProgress: state.inProgress, action: { From 1d59f24bd83ad4e7025901b8207fe6a1e90f5280 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 28 Jan 2026 23:50:31 +0400 Subject: [PATCH 05/12] Update API --- .../Telegram-iOS/en.lproj/Localizable.strings | 19 +- submodules/TelegramApi/Sources/Api0.swift | 1 + submodules/TelegramApi/Sources/Api25.swift | 84 +----- submodules/TelegramApi/Sources/Api26.swift | 256 +++++------------- submodules/TelegramApi/Sources/Api27.swift | 244 ++++++++++++----- submodules/TelegramApi/Sources/Api28.swift | 61 +++++ .../TelegramEngine/Payments/StarGifts.swift | 13 +- .../Sources/GiftCraftScreen.swift | 31 ++- .../Sources/GiftItemComponent.swift | 11 +- .../Sources/GiftUpgradeVariantsScreen.swift | 10 +- .../Sources/GiftViewScreen.swift | 46 ++-- 11 files changed, 418 insertions(+), 358 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 169f4ab264..38d981ea08 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15688,21 +15688,22 @@ Error: %8$@"; "Gift.Craft.CraftingFailed.Text_1" = "This crafting attempt was unsuccessful.\n**%@ gift** was lost."; "Gift.Craft.CraftingFailed.Text_any" = "This crafting attempt was unsuccessful.\n**%@ gifts** were lost."; "Gift.Craft.CraftingFailed.CraftAnotherGift" = "Craft Another Gift"; -"Gift.Craft.Info.Title" = "Craft Gift"; -"Gift.Craft.Info.Description" = "Use your existing gifts to craft new ones."; -"Gift.Craft.Info.Paragraph1.Title" = "Combine Gifts"; -"Gift.Craft.Info.Paragraph1.Text" = "Add up to 4 Gifts to attempt crafting a new upgraded model."; -"Gift.Craft.Info.Paragraph2.Title" = "Input Matters"; -"Gift.Craft.Info.Paragraph2.Text" = "Each craft has a success chance. Better combinations improve the outcome."; -"Gift.Craft.Info.Paragraph3.Title" = "Exclusive Look"; -"Gift.Craft.Info.Paragraph3.Text" = "Reforge gifts into a rarer collectibles with a new look."; -"Gift.Craft.Info.SelectGifts" = "Select Gifts"; +"Gift.Craft.Info.Title" = "Gift Crafting"; +"Gift.Craft.Info.Description" = "Turn your gifts intro rare, epic,\nuncommon and legendary versions."; +"Gift.Craft.Info.Paragraph1.Title" = "Get Rare Models"; +"Gift.Craft.Info.Paragraph1.Text" = "Select up to 4 gifts to craft a new exclusive model."; +"Gift.Craft.Info.Paragraph2.Title" = "Maximize Chances"; +"Gift.Craft.Info.Paragraph2.Text" = "Combine more gifts to increase your odds of success."; +"Gift.Craft.Info.Paragraph3.Title" = "Affect the Result"; +"Gift.Craft.Info.Paragraph3.Text" = "Use gifts with the same attribute to boost it's chance."; +"Gift.Craft.Info.Understood" = "Understood"; "Gift.Craft.Unavailable.Title" = "Try Later"; "Gift.Craft.Unavailable.Text" = "You will be able to craft this gift on %@."; "Gift.Attribute.Rare" = "rare"; "Gift.Attribute.Legendary" = "legendary"; "Gift.Attribute.Epic" = "epic"; +"Gift.Attribute.Uncommon" = "uncommon"; "SocksProxySetup.CheckStatus" = "Check Status"; "SocksProxySetup.Warning.Title" = "Warning"; diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 0c0d688c2d..2c64ac93c4 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -997,6 +997,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[2029777832] = { return Api.StarGiftAttributeRarity.parse_starGiftAttributeRarityEpic($0) } dict[-822614104] = { return Api.StarGiftAttributeRarity.parse_starGiftAttributeRarityLegendary($0) } dict[-259174037] = { return Api.StarGiftAttributeRarity.parse_starGiftAttributeRarityRare($0) } + dict[-607231095] = { return Api.StarGiftAttributeRarity.parse_starGiftAttributeRarityUncommon($0) } dict[1118831432] = { return Api.StarGiftAuctionAcquiredGift.parse_starGiftAuctionAcquiredGift($0) } dict[984483112] = { return Api.StarGiftAuctionRound.parse_starGiftAuctionRound($0) } dict[178266597] = { return Api.StarGiftAuctionRound.parse_starGiftAuctionRoundExtendable($0) } diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index db083b22fe..6193057ad5 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -1318,6 +1318,7 @@ public extension Api { case starGiftAttributeRarityEpic case starGiftAttributeRarityLegendary case starGiftAttributeRarityRare + case starGiftAttributeRarityUncommon public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -1342,6 +1343,11 @@ public extension Api { buffer.appendInt32(-259174037) } break + case .starGiftAttributeRarityUncommon: + if boxed { + buffer.appendInt32(-607231095) + } + break } } @@ -1355,6 +1361,8 @@ public extension Api { return ("starGiftAttributeRarityLegendary", []) case .starGiftAttributeRarityRare: return ("starGiftAttributeRarityRare", []) + case .starGiftAttributeRarityUncommon: + return ("starGiftAttributeRarityUncommon", []) } } @@ -1378,6 +1386,9 @@ public extension Api { public static func parse_starGiftAttributeRarityRare(_ reader: BufferReader) -> StarGiftAttributeRarity? { return Api.StarGiftAttributeRarity.starGiftAttributeRarityRare } + public static func parse_starGiftAttributeRarityUncommon(_ reader: BufferReader) -> StarGiftAttributeRarity? { + return Api.StarGiftAttributeRarity.starGiftAttributeRarityUncommon + } } } public extension Api { @@ -2127,76 +2138,3 @@ public extension Api { } } } -public extension Api { - enum StarsAmount: TypeConstructorDescription { - public class Cons_starsAmount { - public var amount: Int64 - public var nanos: Int32 - public init(amount: Int64, nanos: Int32) { - self.amount = amount - self.nanos = nanos - } - } - public class Cons_starsTonAmount { - public var amount: Int64 - public init(amount: Int64) { - self.amount = amount - } - } - case starsAmount(Cons_starsAmount) - case starsTonAmount(Cons_starsTonAmount) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .starsAmount(let _data): - if boxed { - buffer.appendInt32(-1145654109) - } - serializeInt64(_data.amount, buffer: buffer, boxed: false) - serializeInt32(_data.nanos, buffer: buffer, boxed: false) - break - case .starsTonAmount(let _data): - if boxed { - buffer.appendInt32(1957618656) - } - serializeInt64(_data.amount, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .starsAmount(let _data): - return ("starsAmount", [("amount", _data.amount as Any), ("nanos", _data.nanos as Any)]) - case .starsTonAmount(let _data): - return ("starsTonAmount", [("amount", _data.amount as Any)]) - } - } - - public static func parse_starsAmount(_ reader: BufferReader) -> StarsAmount? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.StarsAmount.starsAmount(Cons_starsAmount(amount: _1!, nanos: _2!)) - } - else { - return nil - } - } - public static func parse_starsTonAmount(_ reader: BufferReader) -> StarsAmount? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return Api.StarsAmount.starsTonAmount(Cons_starsTonAmount(amount: _1!)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index 3c8ff86c4b..a5dbafa1b7 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -1,3 +1,76 @@ +public extension Api { + enum StarsAmount: TypeConstructorDescription { + public class Cons_starsAmount { + public var amount: Int64 + public var nanos: Int32 + public init(amount: Int64, nanos: Int32) { + self.amount = amount + self.nanos = nanos + } + } + public class Cons_starsTonAmount { + public var amount: Int64 + public init(amount: Int64) { + self.amount = amount + } + } + case starsAmount(Cons_starsAmount) + case starsTonAmount(Cons_starsTonAmount) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsAmount(let _data): + if boxed { + buffer.appendInt32(-1145654109) + } + serializeInt64(_data.amount, buffer: buffer, boxed: false) + serializeInt32(_data.nanos, buffer: buffer, boxed: false) + break + case .starsTonAmount(let _data): + if boxed { + buffer.appendInt32(1957618656) + } + serializeInt64(_data.amount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsAmount(let _data): + return ("starsAmount", [("amount", _data.amount as Any), ("nanos", _data.nanos as Any)]) + case .starsTonAmount(let _data): + return ("starsTonAmount", [("amount", _data.amount as Any)]) + } + } + + public static func parse_starsAmount(_ reader: BufferReader) -> StarsAmount? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.StarsAmount.starsAmount(Cons_starsAmount(amount: _1!, nanos: _2!)) + } + else { + return nil + } + } + public static func parse_starsTonAmount(_ reader: BufferReader) -> StarsAmount? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.StarsAmount.starsTonAmount(Cons_starsTonAmount(amount: _1!)) + } + else { + return nil + } + } + } +} public extension Api { enum StarsGiftOption: TypeConstructorDescription { public class Cons_starsGiftOption { @@ -1662,186 +1735,3 @@ public extension Api { } } } -public extension Api { - enum StickerSetCovered: TypeConstructorDescription { - public class Cons_stickerSetCovered { - public var set: Api.StickerSet - public var cover: Api.Document - public init(set: Api.StickerSet, cover: Api.Document) { - self.set = set - self.cover = cover - } - } - public class Cons_stickerSetFullCovered { - public var set: Api.StickerSet - public var packs: [Api.StickerPack] - public var keywords: [Api.StickerKeyword] - public var documents: [Api.Document] - public init(set: Api.StickerSet, packs: [Api.StickerPack], keywords: [Api.StickerKeyword], documents: [Api.Document]) { - self.set = set - self.packs = packs - self.keywords = keywords - self.documents = documents - } - } - public class Cons_stickerSetMultiCovered { - public var set: Api.StickerSet - public var covers: [Api.Document] - public init(set: Api.StickerSet, covers: [Api.Document]) { - self.set = set - self.covers = covers - } - } - public class Cons_stickerSetNoCovered { - public var set: Api.StickerSet - public init(set: Api.StickerSet) { - self.set = set - } - } - case stickerSetCovered(Cons_stickerSetCovered) - case stickerSetFullCovered(Cons_stickerSetFullCovered) - case stickerSetMultiCovered(Cons_stickerSetMultiCovered) - case stickerSetNoCovered(Cons_stickerSetNoCovered) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .stickerSetCovered(let _data): - if boxed { - buffer.appendInt32(1678812626) - } - _data.set.serialize(buffer, true) - _data.cover.serialize(buffer, true) - break - case .stickerSetFullCovered(let _data): - if boxed { - buffer.appendInt32(1087454222) - } - _data.set.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.packs.count)) - for item in _data.packs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.keywords.count)) - for item in _data.keywords { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.documents.count)) - for item in _data.documents { - item.serialize(buffer, true) - } - break - case .stickerSetMultiCovered(let _data): - if boxed { - buffer.appendInt32(872932635) - } - _data.set.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.covers.count)) - for item in _data.covers { - item.serialize(buffer, true) - } - break - case .stickerSetNoCovered(let _data): - if boxed { - buffer.appendInt32(2008112412) - } - _data.set.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .stickerSetCovered(let _data): - return ("stickerSetCovered", [("set", _data.set as Any), ("cover", _data.cover as Any)]) - case .stickerSetFullCovered(let _data): - return ("stickerSetFullCovered", [("set", _data.set as Any), ("packs", _data.packs as Any), ("keywords", _data.keywords as Any), ("documents", _data.documents as Any)]) - case .stickerSetMultiCovered(let _data): - return ("stickerSetMultiCovered", [("set", _data.set as Any), ("covers", _data.covers as Any)]) - case .stickerSetNoCovered(let _data): - return ("stickerSetNoCovered", [("set", _data.set as Any)]) - } - } - - public static func parse_stickerSetCovered(_ reader: BufferReader) -> StickerSetCovered? { - var _1: Api.StickerSet? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StickerSet - } - var _2: Api.Document? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.Document - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.StickerSetCovered.stickerSetCovered(Cons_stickerSetCovered(set: _1!, cover: _2!)) - } - else { - return nil - } - } - public static func parse_stickerSetFullCovered(_ reader: BufferReader) -> StickerSetCovered? { - var _1: Api.StickerSet? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StickerSet - } - var _2: [Api.StickerPack]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) - } - var _3: [Api.StickerKeyword]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerKeyword.self) - } - var _4: [Api.Document]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.StickerSetCovered.stickerSetFullCovered(Cons_stickerSetFullCovered(set: _1!, packs: _2!, keywords: _3!, documents: _4!)) - } - else { - return nil - } - } - public static func parse_stickerSetMultiCovered(_ reader: BufferReader) -> StickerSetCovered? { - var _1: Api.StickerSet? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StickerSet - } - var _2: [Api.Document]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.StickerSetCovered.stickerSetMultiCovered(Cons_stickerSetMultiCovered(set: _1!, covers: _2!)) - } - else { - return nil - } - } - public static func parse_stickerSetNoCovered(_ reader: BufferReader) -> StickerSetCovered? { - var _1: Api.StickerSet? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StickerSet - } - let _c1 = _1 != nil - if _c1 { - return Api.StickerSetCovered.stickerSetNoCovered(Cons_stickerSetNoCovered(set: _1!)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index 350681bf02..395ba65b19 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -1,3 +1,186 @@ +public extension Api { + enum StickerSetCovered: TypeConstructorDescription { + public class Cons_stickerSetCovered { + public var set: Api.StickerSet + public var cover: Api.Document + public init(set: Api.StickerSet, cover: Api.Document) { + self.set = set + self.cover = cover + } + } + public class Cons_stickerSetFullCovered { + public var set: Api.StickerSet + public var packs: [Api.StickerPack] + public var keywords: [Api.StickerKeyword] + public var documents: [Api.Document] + public init(set: Api.StickerSet, packs: [Api.StickerPack], keywords: [Api.StickerKeyword], documents: [Api.Document]) { + self.set = set + self.packs = packs + self.keywords = keywords + self.documents = documents + } + } + public class Cons_stickerSetMultiCovered { + public var set: Api.StickerSet + public var covers: [Api.Document] + public init(set: Api.StickerSet, covers: [Api.Document]) { + self.set = set + self.covers = covers + } + } + public class Cons_stickerSetNoCovered { + public var set: Api.StickerSet + public init(set: Api.StickerSet) { + self.set = set + } + } + case stickerSetCovered(Cons_stickerSetCovered) + case stickerSetFullCovered(Cons_stickerSetFullCovered) + case stickerSetMultiCovered(Cons_stickerSetMultiCovered) + case stickerSetNoCovered(Cons_stickerSetNoCovered) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .stickerSetCovered(let _data): + if boxed { + buffer.appendInt32(1678812626) + } + _data.set.serialize(buffer, true) + _data.cover.serialize(buffer, true) + break + case .stickerSetFullCovered(let _data): + if boxed { + buffer.appendInt32(1087454222) + } + _data.set.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.packs.count)) + for item in _data.packs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.keywords.count)) + for item in _data.keywords { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.documents.count)) + for item in _data.documents { + item.serialize(buffer, true) + } + break + case .stickerSetMultiCovered(let _data): + if boxed { + buffer.appendInt32(872932635) + } + _data.set.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.covers.count)) + for item in _data.covers { + item.serialize(buffer, true) + } + break + case .stickerSetNoCovered(let _data): + if boxed { + buffer.appendInt32(2008112412) + } + _data.set.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .stickerSetCovered(let _data): + return ("stickerSetCovered", [("set", _data.set as Any), ("cover", _data.cover as Any)]) + case .stickerSetFullCovered(let _data): + return ("stickerSetFullCovered", [("set", _data.set as Any), ("packs", _data.packs as Any), ("keywords", _data.keywords as Any), ("documents", _data.documents as Any)]) + case .stickerSetMultiCovered(let _data): + return ("stickerSetMultiCovered", [("set", _data.set as Any), ("covers", _data.covers as Any)]) + case .stickerSetNoCovered(let _data): + return ("stickerSetNoCovered", [("set", _data.set as Any)]) + } + } + + public static func parse_stickerSetCovered(_ reader: BufferReader) -> StickerSetCovered? { + var _1: Api.StickerSet? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StickerSet + } + var _2: Api.Document? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Document + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.StickerSetCovered.stickerSetCovered(Cons_stickerSetCovered(set: _1!, cover: _2!)) + } + else { + return nil + } + } + public static func parse_stickerSetFullCovered(_ reader: BufferReader) -> StickerSetCovered? { + var _1: Api.StickerSet? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StickerSet + } + var _2: [Api.StickerPack]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) + } + var _3: [Api.StickerKeyword]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerKeyword.self) + } + var _4: [Api.Document]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.StickerSetCovered.stickerSetFullCovered(Cons_stickerSetFullCovered(set: _1!, packs: _2!, keywords: _3!, documents: _4!)) + } + else { + return nil + } + } + public static func parse_stickerSetMultiCovered(_ reader: BufferReader) -> StickerSetCovered? { + var _1: Api.StickerSet? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StickerSet + } + var _2: [Api.Document]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.StickerSetCovered.stickerSetMultiCovered(Cons_stickerSetMultiCovered(set: _1!, covers: _2!)) + } + else { + return nil + } + } + public static func parse_stickerSetNoCovered(_ reader: BufferReader) -> StickerSetCovered? { + var _1: Api.StickerSet? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StickerSet + } + let _c1 = _1 != nil + if _c1 { + return Api.StickerSetCovered.stickerSetNoCovered(Cons_stickerSetNoCovered(set: _1!)) + } + else { + return nil + } + } + } +} public extension Api { enum StoriesStealthMode: TypeConstructorDescription { public class Cons_storiesStealthMode { @@ -1532,64 +1715,3 @@ public extension Api { } } } -public extension Api { - enum TopPeerCategoryPeers: TypeConstructorDescription { - public class Cons_topPeerCategoryPeers { - public var category: Api.TopPeerCategory - public var count: Int32 - public var peers: [Api.TopPeer] - public init(category: Api.TopPeerCategory, count: Int32, peers: [Api.TopPeer]) { - self.category = category - self.count = count - self.peers = peers - } - } - case topPeerCategoryPeers(Cons_topPeerCategoryPeers) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .topPeerCategoryPeers(let _data): - if boxed { - buffer.appendInt32(-75283823) - } - _data.category.serialize(buffer, true) - serializeInt32(_data.count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.peers.count)) - for item in _data.peers { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .topPeerCategoryPeers(let _data): - return ("topPeerCategoryPeers", [("category", _data.category as Any), ("count", _data.count as Any), ("peers", _data.peers as Any)]) - } - } - - public static func parse_topPeerCategoryPeers(_ reader: BufferReader) -> TopPeerCategoryPeers? { - var _1: Api.TopPeerCategory? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.TopPeerCategory - } - var _2: Int32? - _2 = reader.readInt32() - var _3: [Api.TopPeer]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.TopPeer.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.TopPeerCategoryPeers.topPeerCategoryPeers(Cons_topPeerCategoryPeers(category: _1!, count: _2!, peers: _3!)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index 2fcefd2998..9cd5aa8cdf 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -1,3 +1,64 @@ +public extension Api { + enum TopPeerCategoryPeers: TypeConstructorDescription { + public class Cons_topPeerCategoryPeers { + public var category: Api.TopPeerCategory + public var count: Int32 + public var peers: [Api.TopPeer] + public init(category: Api.TopPeerCategory, count: Int32, peers: [Api.TopPeer]) { + self.category = category + self.count = count + self.peers = peers + } + } + case topPeerCategoryPeers(Cons_topPeerCategoryPeers) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .topPeerCategoryPeers(let _data): + if boxed { + buffer.appendInt32(-75283823) + } + _data.category.serialize(buffer, true) + serializeInt32(_data.count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.peers.count)) + for item in _data.peers { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .topPeerCategoryPeers(let _data): + return ("topPeerCategoryPeers", [("category", _data.category as Any), ("count", _data.count as Any), ("peers", _data.peers as Any)]) + } + } + + public static func parse_topPeerCategoryPeers(_ reader: BufferReader) -> TopPeerCategoryPeers? { + var _1: Api.TopPeerCategory? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.TopPeerCategory + } + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.TopPeer]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.TopPeer.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.TopPeerCategoryPeers.topPeerCategoryPeers(Cons_topPeerCategoryPeers(category: _1!, count: _2!, peers: _3!)) + } + else { + return nil + } + } + } +} public extension Api { indirect enum Update: TypeConstructorDescription { public class Cons_updateBotBusinessConnect { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index af2ae892f5..d8bda4bf32 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -460,12 +460,13 @@ public enum StarGift: Equatable, Codable, PostboxCoding { case rare case epic case legendary + case uncommon public var permilleValue: Int32 { switch self { case let .permille(value): return value - case .rare, .epic, .legendary: + case .rare, .epic, .legendary, .uncommon: return 0 } } @@ -501,6 +502,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { return .epic case 3: return .legendary + case 4: + return .uncommon default: let permille = try container.decodeIfPresent(Int32.self, forKey: .rarity) ?? 0 return .permille(permille) @@ -555,6 +558,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { return .epic case 3: return .legendary + case 4: + return .uncommon default: let permille = decoder.decodeInt32ForKey(CodingKeys.rarity.rawValue, orElse: 0) return .permille(permille) @@ -614,6 +619,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { try container.encode(Int32(2), forKey: .rarityType) case .legendary: try container.encode(Int32(3), forKey: .rarityType) + case .uncommon: + try container.encode(Int32(4), forKey: .rarityType) } } @@ -660,6 +667,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { encoder.encodeInt32(2, forKey: CodingKeys.rarityType.rawValue) case .legendary: encoder.encodeInt32(3, forKey: CodingKeys.rarityType.rawValue) + case .uncommon: + encoder.encodeInt32(4, forKey: CodingKeys.rarityType.rawValue) } } @@ -3503,6 +3512,8 @@ extension StarGift.UniqueGift.Attribute { return .epic case .starGiftAttributeRarityLegendary: return .legendary + case .starGiftAttributeRarityUncommon: + return .uncommon } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift index 2c6d326920..9af63b22e7 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift @@ -787,7 +787,6 @@ private final class CraftGiftPageContent: Component { } } - let permilleValue = selectedGifts.reduce(0, { $0 + Int($1.value.gift.craftChancePermille ?? 0) }) if component.displayState == .crafting { //var craftingOriginY = craftContentHeight * 0.5 + 160.0 @@ -814,7 +813,7 @@ private final class CraftGiftPageContent: Component { craftingTitleView.frame = craftingTitleFrame } craftingOriginY += craftingTitleSize.height - craftingOriginY += 7.0 + craftingOriginY += 6.0 let craftingSubtitleSize = self.craftingSubtitle.update( transition: transition, @@ -1173,7 +1172,7 @@ private final class CraftGiftPageContent: Component { ) ), environment: {}, - containerSize: CGSize(width: availableSize.width, height: 245.0) + containerSize: CGSize(width: availableSize.width, height: 260.0) ) let infoHeaderFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - infoHeaderSize.width) * 0.5), y: 0.0), size: infoHeaderSize) if let infoHeaderView = self.infoHeader.view { @@ -1206,7 +1205,7 @@ private final class CraftGiftPageContent: Component { environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) - let infoTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - infoTitleSize.width) * 0.5), y: infoHeaderSize.height - 73.0), size: infoTitleSize) + let infoTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - infoTitleSize.width) * 0.5), y: infoHeaderSize.height - 87.0), size: infoTitleSize) if let infoTitleView = self.infoTitle.view { if infoTitleView.superview == nil { self.infoContainer.addSubview(infoTitleView) @@ -1233,7 +1232,7 @@ private final class CraftGiftPageContent: Component { environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) - let infoDescriptionTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - infoDescriptionTextSize.width) * 0.5), y: infoHeaderSize.height - 40.0), size: infoDescriptionTextSize) + let infoDescriptionTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - infoDescriptionTextSize.width) * 0.5), y: infoHeaderSize.height - 56.0), size: infoDescriptionTextSize) if let infoDescriptionTextView = self.infoDescription.view { if infoDescriptionTextView.superview == nil { self.infoContainer.addSubview(infoDescriptionTextView) @@ -1429,6 +1428,7 @@ private final class SheetContainerComponent: CombinedComponent { let animateOut = StoredActionSlot(Action.self) let externalState = CraftGiftPageContent.ExternalState() + let playButtonAnimation = ActionSlot() return { context in let component = context.component @@ -1514,9 +1514,23 @@ private final class SheetContainerComponent: CombinedComponent { let buttonContent: AnyComponentWithIdentity if state.displayInfo { - + var buttonTitle: [AnyComponentWithIdentity] = [] + buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "anim_ok"), + color: environment.theme.list.itemCheckColors.foregroundColor, + startingPosition: .begin, + size: CGSize(width: 28.0, height: 28.0), + playOnce: playButtonAnimation + )))) + buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent( + text: strings.Gift_Craft_Info_Understood, + badge: 0, + textColor: environment.theme.list.itemCheckColors.foregroundColor, + badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, + badgeForeground: environment.theme.list.itemCheckColors.fillColor + )))) buttonContent = AnyComponentWithIdentity(id: "info", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Info_SelectGifts, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor))) + HStack(buttonTitle, spacing: 2.0) )) } else if state.displayFailure { buttonContent = AnyComponentWithIdentity(id: "fail", component: AnyComponent( @@ -1642,8 +1656,9 @@ private final class SheetContainerComponent: CombinedComponent { guard let state, !state.inProgress else { return } - state.displayInfo = !state.displayInfo + state.displayInfo = true state.updated(transition: .spring(duration: 0.3)) + playButtonAnimation.invoke(Void()) } ) ), diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 6e2292adc3..07ede21c03 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -810,14 +810,21 @@ public final class GiftItemComponent: Component { badgeColor = UIColor(rgb: 0xd57e32) case .rare: badgeString = component.strings.Gift_Attribute_Rare - badgeColor = UIColor(rgb: 0x79993d) + badgeColor = UIColor(rgb: 0x25a3b9) + case .uncommon: + badgeString = component.strings.Gift_Attribute_Uncommon + badgeColor = UIColor(rgb: 0x22b447) } var badgeTextColor = isColored ? .white : component.theme.list.itemSecondaryTextColor var badgeBackgroundColor = isColored ? UIColor(white: 0.0, alpha: 0.2) : component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.06) if let badgeColor { badgeTextColor = badgeColor - badgeBackgroundColor = badgeColor.withMultipliedAlpha(0.1) + if let backgroundColor { + badgeBackgroundColor = badgeColor.mixedWith(backgroundColor, alpha: 0.9) + } else { + badgeBackgroundColor = badgeColor.withMultipliedAlpha(0.1) + } } let badgeTextSize = self.badgeText.update( transition: .spring(duration: 0.2), diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift index d8751ee850..c4f51dbc38 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift @@ -1364,6 +1364,7 @@ private final class AttributeInfoComponent: Component { } var badgeString = "" + var badgeColor = component.badgeColor if let rarity { switch rarity { case let .permille(value): @@ -1374,10 +1375,16 @@ private final class AttributeInfoComponent: Component { } case .epic: badgeString = component.strings.Gift_Attribute_Epic + badgeColor = UIColor(rgb: 0xaf52de) case .legendary: badgeString = component.strings.Gift_Attribute_Legendary + badgeColor = UIColor(rgb: 0xd57e32) case .rare: badgeString = component.strings.Gift_Attribute_Rare + badgeColor = UIColor(rgb: 0x25a3b9) + case .uncommon: + badgeString = component.strings.Gift_Attribute_Uncommon + badgeColor = UIColor(rgb: 0x22b447) } } @@ -1417,8 +1424,7 @@ private final class AttributeInfoComponent: Component { let badgeBackgroundFrame = badgeFrame.insetBy(dx: -5.5, dy: -2.0) transition.setFrame(layer: self.badgeBackground, frame: badgeBackgroundFrame) - transition.setBackgroundColor(layer: self.badgeBackground, color: component.badgeColor) - + transition.setBackgroundColor(layer: self.badgeBackground, color: badgeColor) return availableSize } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 169115aac9..445f322d0f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -3063,7 +3063,6 @@ private final class GiftViewSheetContent: CombinedComponent { originY += 16.0 if case .upgradePreview = component.subject { - } else if !incoming { } else { let checkTheme = CheckComponent.Theme( @@ -3120,17 +3119,13 @@ private final class GiftViewSheetContent: CombinedComponent { var descriptionText: String var hasDescriptionButton = false if let uniqueGift { - titleString = uniqueGift.title - var isCrafted = false + titleString = uniqueGift.title + " **#\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))**" + descriptionText = "\(strings.Gift_Unique_Collectible) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))" for attribute in uniqueGift.attributes { - if case let .model(_, _, _, crafted) = attribute { - isCrafted = crafted + if case let .model(name, _, _, _) = attribute { + descriptionText = name } } - //TODO:localize - let prefix: String = isCrafted ? "Crafted Collectible" : strings.Gift_Unique_Collectible - descriptionText = "\(prefix) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))" - if let releasedBy = uniqueGift.releasedBy, let peer = state.peerMap[releasedBy], let addressName = peer.addressName { descriptionText = strings.Gift_Unique_CollectibleBy("#\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))", "[@\(addressName)]()").string hasDescriptionButton = true @@ -3204,14 +3199,24 @@ private final class GiftViewSheetContent: CombinedComponent { descriptionText = modifiedString } + let titleFont = Font.bold(20.0) + let smallTitleFont = Font.bold(15.0) + let titleAttributedString: NSAttributedString + if let uniqueGift { + let numberFont = uniqueGift.number < 1000 ? titleFont : smallTitleFont + titleAttributedString = parseMarkdownIntoAttributedString(titleString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: .white), bold: MarkdownAttributeSet(font: numberFont, textColor: vibrantColor), link: MarkdownAttributeSet(font: titleFont, textColor: .white), linkAttribute: { _ in return nil }), textAlignment: .center) + } else { + titleAttributedString = NSAttributedString( + string: titleString, + font: titleFont, + textColor: theme.actionSheet.primaryTextColor, + paragraphAlignment: .center + ) + } + let title = title.update( component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: titleString, - font: Font.bold(20.0), - textColor: uniqueGift != nil ? .white : theme.actionSheet.primaryTextColor, - paragraphAlignment: .center - )), + text: .plain(titleAttributedString), horizontalAlignment: .center, maximumNumberOfLines: 1 ), @@ -4103,14 +4108,17 @@ private final class GiftViewSheetContent: CombinedComponent { badgeString = formatPercentage(Float(value) * 0.1) } case .epic: - badgeString = "epic" + badgeString = strings.Gift_Attribute_Epic badgeColor = UIColor(rgb: 0xaf52de) case .legendary: - badgeString = "legendary" + badgeString = strings.Gift_Attribute_Legendary badgeColor = UIColor(rgb: 0xd57e32) case .rare: - badgeString = "rare" - badgeColor = UIColor(rgb: 0x79993d) + badgeString = strings.Gift_Attribute_Rare + badgeColor = UIColor(rgb: 0x25a3b9) + case .uncommon: + badgeString = strings.Gift_Attribute_Uncommon + badgeColor = UIColor(rgb: 0x22b447) } items.append(AnyComponentWithIdentity( id: AnyHashable(1), From 4d64ba28a5fe63ed14694535a1327de9d738cccd Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 29 Jan 2026 02:48:56 +0400 Subject: [PATCH 06/12] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 5 + .../Sources/AccountContext.swift | 4 +- .../Sources/GiftCraftScreen.swift | 2 +- .../Sources/GiftSetupScreen.swift | 2 +- .../Sources/GiftAuctionViewScreen.swift | 2 +- .../Sources/GiftUpgradeVariantsScreen.swift | 139 +++++++++++++----- .../Sources/GiftViewScreen.swift | 86 +++++++---- .../Sources/SharedAccountContext.swift | 8 +- 8 files changed, 174 insertions(+), 74 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 38d981ea08..73405bd474 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15709,3 +15709,8 @@ Error: %8$@"; "SocksProxySetup.Warning.Title" = "Warning"; "SocksProxySetup.Warning.Text" = "This action will expose your IP address to the admin of the proxy server."; "SocksProxySetup.Warning.Proceed" = "Proceed"; + +"Gift.Variants.CollectionInfo.CraftableModel_1" = "**%@** craftable model"; +"Gift.Variants.CollectionInfo.CraftableModel_any" = "**%@** craftable models"; +"Gift.Variants.ViewCraftableModels" = "View Craftable Models"; +"Gift.Variants.ViewPrimaryModels" = "View Primary Models"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index c621d5458e..1b40090279 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1454,13 +1454,13 @@ public protocol SharedAccountContext: AnyObject { func makeGiftViewScreen(context: AccountContext, message: EngineMessage, shareStory: ((StarGift.UniqueGift) -> Void)?) -> ViewController func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, openChatTheme: (() -> Void)?, dismissed: (() -> Void)?) -> ViewController func makeGiftWearPreviewScreen(context: AccountContext, gift: StarGift, attributes: [StarGift.UniqueGift.Attribute]?) -> ViewController - func makeGiftUpgradePreviewScreen(context: AccountContext, attributes: [StarGift.UniqueGift.Attribute], peerName: String) -> ViewController + func makeGiftUpgradePreviewScreen(context: AccountContext, gift: StarGift.Gift, attributes: [StarGift.UniqueGift.Attribute], peerName: String) -> ViewController func makeGiftAuctionInfoScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: (() -> Void)?) -> ViewController func makeGiftAuctionBidScreen(context: AccountContext, toPeerId: EnginePeer.Id, text: String?, entities: [MessageTextEntity]?, hideName: Bool, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?) -> ViewController func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext, peerId: EnginePeer.Id?, completion: @escaping (Signal<[GiftAuctionAcquiredGift], NoError>, [StarGift.UniqueGift.Attribute]?) -> Void) -> ViewController func makeGiftAuctionActiveBidsScreen(context: AccountContext) -> ViewController func makeGiftOfferScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController - func makeGiftUpgradeVariantsScreen(context: AccountContext, gift: StarGift, onlyCrafted: Bool, attributes: [StarGift.UniqueGift.Attribute], selectedAttributes: [StarGift.UniqueGift.Attribute]?, focusedAttribute: StarGift.UniqueGift.Attribute?) -> ViewController + func makeGiftUpgradeVariantsScreen(context: AccountContext, gift: StarGift, crafted: 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 diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift index 9af63b22e7..ada2db7a98 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift @@ -239,7 +239,7 @@ private final class CraftGiftPageContent: Component { let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsScreen( context: component.context, gift: .generic(gift), - onlyCrafted: true, + crafted: true, attributes: attributes, selectedAttributes: nil, focusedAttribute: nil diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 3cc6bfae11..a095f5449b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -1605,7 +1605,7 @@ private final class GiftSetupScreenComponent: Component { guard let self, let component = self.component, let controller = self.environment?.controller(), let upgradePreview else { return } - let previewController = component.context.sharedContext.makeGiftUpgradePreviewScreen(context: component.context, attributes: upgradePreview.attributes, peerName: peerName) + let previewController = component.context.sharedContext.makeGiftUpgradePreviewScreen(context: component.context, gift: gift, attributes: upgradePreview.attributes, peerName: peerName) controller.push(previewController) }) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index 03093d1027..e4b70c9faf 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -1083,7 +1083,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { guard let state, let attributes = state.giftUpgradeAttributes else { return } - let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsScreen(context: component.context, gift: .generic(gift), onlyCrafted: false, attributes: attributes, selectedAttributes: nil, focusedAttribute: nil) + let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsScreen(context: component.context, gift: .generic(gift), crafted: false, attributes: attributes, selectedAttributes: nil, focusedAttribute: nil) environment.controller()?.push(variantsController) }, animateScale: false), availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height), diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift index c4f51dbc38..dfb46929db 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift @@ -29,7 +29,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { let context: AccountContext let gift: StarGift - let onlyCrafted: Bool + let crafted: Bool let attributes: [StarGift.UniqueGift.Attribute] let selectedAttributes: [StarGift.UniqueGift.Attribute]? let focusedAttribute: StarGift.UniqueGift.Attribute? @@ -37,14 +37,14 @@ private final class GiftUpgradeVariantsScreenComponent: Component { init( context: AccountContext, gift: StarGift, - onlyCrafted: Bool, + crafted: Bool, attributes: [StarGift.UniqueGift.Attribute], selectedAttributes: [StarGift.UniqueGift.Attribute]?, focusedAttribute: StarGift.UniqueGift.Attribute? ) { self.context = context self.gift = gift - self.onlyCrafted = onlyCrafted + self.crafted = crafted self.attributes = attributes self.selectedAttributes = selectedAttributes self.focusedAttribute = focusedAttribute @@ -112,6 +112,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { private var giftItems: [AnyHashable: ComponentView] = [:] private var selectedSection: SelectedSection = .models + private var displayCraftableModels = false private let giftCompositionExternalState = GiftCompositionComponent.ExternalState() @@ -122,7 +123,8 @@ private final class GiftUpgradeVariantsScreenComponent: Component { private var previewBackdropIndex: Int = 0 private var previewSymbolIndex: Int = 0 - private var previewModels: [StarGift.UniqueGift.Attribute] = [] + private var previewPrimaryModels: [StarGift.UniqueGift.Attribute] = [] + private var previewCraftableModels: [StarGift.UniqueGift.Attribute] = [] private var previewBackdrops: [StarGift.UniqueGift.Attribute] = [] private var previewSymbols: [StarGift.UniqueGift.Attribute] = [] @@ -130,10 +132,15 @@ private final class GiftUpgradeVariantsScreenComponent: Component { private var selectedBackdrop: StarGift.UniqueGift.Attribute? private var selectedSymbol: StarGift.UniqueGift.Attribute? - private var modelCount: Int32 = 0 + private var craftableModelCount: Int32 = 0 + private var primaryModelCount: Int32 = 0 private var backdropCount: Int32 = 0 private var symbolCount: Int32 = 0 + private var currentDescriptionHeight: CGFloat = 0.0 + + private var cachedSmallChevronImage: (UIImage, PresentationTheme)? + private var ignoreScrolling: Bool = false private var component: GiftUpgradeVariantsScreenComponent? @@ -293,8 +300,9 @@ private final class GiftUpgradeVariantsScreenComponent: Component { } private func previewTimerTick() { - guard !self.previewModels.isEmpty else { return } - self.previewModelIndex = (self.previewModelIndex + 1) % self.previewModels.count + let previewModels = self.displayCraftableModels ? self.previewCraftableModels : self.previewPrimaryModels + guard !previewModels.isEmpty else { return } + self.previewModelIndex = (self.previewModelIndex + 1) % previewModels.count let previousSymbolIndex = self.previewSymbolIndex var randomSymbolIndex = previousSymbolIndex @@ -329,13 +337,15 @@ private final class GiftUpgradeVariantsScreenComponent: Component { } private var effectiveGifts: [[StarGift.UniqueGift.Attribute]] = [] - private func updateEffectiveGifts(attributes: [StarGift.UniqueGift.Attribute], onlyCrafted: Bool) { + private func updateEffectiveGifts(attributes: [StarGift.UniqueGift.Attribute]) { var effectiveGifts: [[StarGift.UniqueGift.Attribute]] = [] switch self.selectedSection { case .models: let models = Array(attributes.filter({ attribute in if case let .model(_, _, _, crafted) = attribute { - if onlyCrafted && !crafted { + if self.displayCraftableModels && !crafted { + return false + } else if !self.displayCraftableModels && crafted { return false } return true @@ -347,7 +357,8 @@ private final class GiftUpgradeVariantsScreenComponent: Component { effectiveGifts.append([model]) } case .backdrops: - let selectedModel = self.selectedModel ?? self.previewModels[self.previewModelIndex] + let previewModels = self.displayCraftableModels ? self.previewCraftableModels : previewPrimaryModels + let selectedModel = self.selectedModel ?? previewModels[self.previewModelIndex] let selectedSymbol = self.selectedSymbol ?? self.previewSymbols[self.previewSymbolIndex] let backdrops = Array(attributes.filter({ attribute in if case .backdrop = attribute { @@ -403,7 +414,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { let optionWidth = (fillingSize - 16.0 * 2.0 - optionSpacing * 2.0) / 3.0 let optionSize = CGSize(width: optionWidth, height: 126.0) - let topInset: CGFloat = 393.0 + let topInset: CGFloat = 375.0 + self.currentDescriptionHeight var validIds: [AnyHashable] = [] var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset + 9.0), size: optionSize) @@ -607,15 +618,19 @@ private final class GiftUpgradeVariantsScreenComponent: Component { let sideInset: CGFloat = rawSideInset + 16.0 if self.component == nil { - var modelCount: Int32 = 0 + self.displayCraftableModels = component.crafted + + var primaryModelCount: Int32 = 0 + var craftableModelCount: Int32 = 0 var backdropCount: Int32 = 0 var symbolCount: Int32 = 0 for attribute in component.attributes { switch attribute { case let .model(_, _, _, crafted): - if component.onlyCrafted && !crafted { + if crafted { + craftableModelCount += 1 } else { - modelCount += 1 + primaryModelCount += 1 } case .backdrop: backdropCount += 1 @@ -625,13 +640,14 @@ private final class GiftUpgradeVariantsScreenComponent: Component { break } } - self.modelCount = modelCount + self.primaryModelCount = primaryModelCount + self.craftableModelCount = craftableModelCount self.backdropCount = backdropCount self.symbolCount = symbolCount - let randomModels = Array(component.attributes.filter({ attribute in + let randomPrimaryModels = Array(component.attributes.filter({ attribute in if case let .model(_, _, _, crafted) = attribute { - if component.onlyCrafted && !crafted { + if crafted { return false } return true @@ -639,7 +655,19 @@ private final class GiftUpgradeVariantsScreenComponent: Component { return false } }).shuffled().prefix(15)) - self.previewModels = randomModels + self.previewPrimaryModels = randomPrimaryModels + + let randomCraftableModels = Array(component.attributes.filter({ attribute in + if case let .model(_, _, _, crafted) = attribute { + if !crafted { + return false + } + return true + } else { + return false + } + }).shuffled().prefix(15)) + self.previewCraftableModels = randomCraftableModels let randomBackdrops = Array(component.attributes.filter({ attribute in if case .backdrop = attribute { @@ -687,7 +715,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { } } - self.updateEffectiveGifts(attributes: component.attributes, onlyCrafted: component.onlyCrafted) + self.updateEffectiveGifts(attributes: component.attributes) } self.component = component @@ -708,14 +736,15 @@ private final class GiftUpgradeVariantsScreenComponent: Component { var badgeColor: UIColor = .white.withAlphaComponent(0.4) var attributes: [StarGift.UniqueGift.Attribute] = [] - if !self.previewModels.isEmpty { + let previewModels = self.displayCraftableModels ? self.previewCraftableModels : self.previewPrimaryModels + if !previewModels.isEmpty { if self.isPlaying { - attributes.append(self.previewModels[self.previewModelIndex]) + attributes.append(previewModels[self.previewModelIndex]) attributes.append(self.previewBackdrops[self.previewBackdropIndex]) attributes.append(self.previewSymbols[self.previewSymbolIndex]) } else { if self.selectedModel == nil { - self.selectedModel = self.previewModels[self.previewModelIndex] + self.selectedModel = previewModels[self.previewModelIndex] } if self.selectedBackdrop == nil { self.selectedBackdrop = self.previewBackdrops[self.previewBackdropIndex] @@ -789,8 +818,11 @@ private final class GiftUpgradeVariantsScreenComponent: Component { contentHeight += headerSize.height var titleText: String = "" - if case let .generic(gift) = component.gift { + switch component.gift { + case let .generic(gift): titleText = gift.title ?? "" + case let .unique(gift): + titleText = gift.title } let titleSize = self.title.update( @@ -841,7 +873,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { } let attributeSpacing: CGFloat = 10.0 - let attributeWidth: CGFloat = floor((fillingSize - attributeSpacing * CGFloat(attributes.count - 1)) / CGFloat(attributes.count)) + let attributeWidth: CGFloat = floor((fillingSize - 32.0 - attributeSpacing * CGFloat(attributes.count - 1)) / CGFloat(attributes.count)) let attributeHeight: CGFloat = 45.0 for i in 0 ..< attributes.count { @@ -914,7 +946,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { self.selectedSection = id self.isPlaying = false - self.updateEffectiveGifts(attributes: component.attributes, onlyCrafted: component.onlyCrafted) + self.updateEffectiveGifts(attributes: component.attributes) self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) })), environment: {}, @@ -935,17 +967,22 @@ private final class GiftUpgradeVariantsScreenComponent: Component { let itemHeight: CGFloat = 126.0 let itemSpacing: CGFloat = 10.0 - let descriptionText: String + var descriptionText: String let itemCount: Int32 switch self.selectedSection { case .models: - //TODO:localize - if component.onlyCrafted { - descriptionText = environment.strings.Gift_Variants_CollectionInfo("**\(self.modelCount)** craftable models").string + if self.displayCraftableModels { + descriptionText = environment.strings.Gift_Variants_CollectionInfo(environment.strings.Gift_Variants_CollectionInfo_CraftableModel(self.craftableModelCount)).string + itemCount = self.craftableModelCount + descriptionText += "\n[\(environment.strings.Gift_Variants_ViewPrimaryModels) >]()" } else { - descriptionText = environment.strings.Gift_Variants_CollectionInfo(environment.strings.Gift_Variants_CollectionInfo_Model(self.modelCount)).string + descriptionText = environment.strings.Gift_Variants_CollectionInfo(environment.strings.Gift_Variants_CollectionInfo_Model(self.primaryModelCount)).string + itemCount = self.primaryModelCount + + if self.craftableModelCount > 0 { + descriptionText += "\n[\(environment.strings.Gift_Variants_ViewCraftableModels) >]()" + } } - itemCount = self.modelCount case .backdrops: descriptionText = environment.strings.Gift_Variants_CollectionInfo(environment.strings.Gift_Variants_CollectionInfo_Backdrop(self.backdropCount)).string itemCount = self.backdropCount @@ -957,14 +994,45 @@ private final class GiftUpgradeVariantsScreenComponent: Component { let descriptionFont = Font.regular(13.0) let descriptionBoldFont = Font.semibold(13.0) let descriptionTextColor = theme.list.itemSecondaryTextColor - let descriptionMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionTextColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionTextColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionTextColor), linkAttribute: { contents in + let descriptionLinkColor = theme.list.itemAccentColor + let descriptionMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionTextColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionTextColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionLinkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) + if self.cachedSmallChevronImage == nil || self.cachedSmallChevronImage?.1 !== environment.theme { + self.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: descriptionLinkColor)!, theme) + } + + let descriptionAttributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: descriptionMarkdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = descriptionAttributedString.string.range(of: ">"), let chevronImage = self.cachedSmallChevronImage?.0 { + descriptionAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: descriptionAttributedString.string)) + } + let descriptionSize = self.descriptionText.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .markdown(text: descriptionText, attributes: descriptionMarkdownAttributes) + text: .plain(descriptionAttributedString), + horizontalAlignment: .center, + maximumNumberOfLines: 3, + lineSpacing: 0.2, + highlightColor: descriptionLinkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { [weak self] attributes, _ in + if let self, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + self.displayCraftableModels = !self.displayCraftableModels + self.isPlaying = false + + self.updateEffectiveGifts(attributes: component.attributes) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) + } + } )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -976,6 +1044,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { } descriptionView.frame = descriptionFrame } + self.currentDescriptionHeight = descriptionSize.height contentHeight += descriptionSize.height contentHeight += 26.0 @@ -1166,7 +1235,7 @@ public class GiftUpgradeVariantsScreen: ViewControllerComponentContainer { public init( context: AccountContext, gift: StarGift, - onlyCrafted: Bool = false, + crafted: Bool = false, attributes: [StarGift.UniqueGift.Attribute], selectedAttributes: [StarGift.UniqueGift.Attribute]?, focusedAttribute: StarGift.UniqueGift.Attribute? @@ -1176,7 +1245,7 @@ public class GiftUpgradeVariantsScreen: ViewControllerComponentContainer { super.init(context: context, component: GiftUpgradeVariantsScreenComponent( context: context, gift: gift, - onlyCrafted: onlyCrafted, + crafted: crafted, attributes: attributes, selectedAttributes: selectedAttributes, focusedAttribute: focusedAttribute diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 445f322d0f..9cbeafdeac 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -1405,15 +1405,26 @@ private final class GiftViewSheetContent: CombinedComponent { func openUpgradeVariants(attribute: StarGift.UniqueGift.Attribute? = nil) { - guard let controller = self.getController() as? GiftViewScreen, let arguments = self.subject.arguments else { + guard let controller = self.getController() as? GiftViewScreen else { return } + + var gift: StarGift? var selectedAttributes: [StarGift.UniqueGift.Attribute]? - if case let .unique(uniqueGift) = arguments.gift { - selectedAttributes = uniqueGift.attributes + if let arguments = self.subject.arguments { + gift = arguments.gift + if case let .unique(uniqueGift) = arguments.gift { + selectedAttributes = uniqueGift.attributes + } + } else if case let .upgradePreview(genericGift, _, _) = self.subject { + gift = .generic(genericGift) } - self.giftVariantsDisposable.set((self.context.engine.payments.getStarGiftUpgradeAttributes(giftId: arguments.gift.giftId) + guard let gift else { + return + } + + self.giftVariantsDisposable.set((self.context.engine.payments.getStarGiftUpgradeAttributes(giftId: gift.giftId) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] attributes in guard let self, let attributes else { @@ -1421,8 +1432,8 @@ private final class GiftViewSheetContent: CombinedComponent { } let variantsController = self.context.sharedContext.makeGiftUpgradeVariantsScreen( context: self.context, - gift: arguments.gift, - onlyCrafted: false, + gift: gift, + crafted: false, attributes: attributes, selectedAttributes: selectedAttributes, focusedAttribute: attribute @@ -2592,7 +2603,7 @@ private final class GiftViewSheetContent: CombinedComponent { } else if state.inUpgradePreview, let attributes = state.upgradePreview?.attributes { headerHeight = 246.0 headerSubject = .preview(attributes) - } else if case let .upgradePreview(attributes, _) = component.subject { + } else if case let .upgradePreview(_, attributes, _) = component.subject { headerHeight = 246.0 headerSubject = .preview(attributes) } else if case let .wearPreview(_, attributes) = component.subject, let attributes { @@ -2857,7 +2868,7 @@ private final class GiftViewSheetContent: CombinedComponent { title = environment.strings.Gift_Upgrade_GiftTitle uniqueText = strings.Gift_Upgrade_Unique_GiftDescription(peerName).string tradableText = strings.Gift_Upgrade_Tradable_GiftDescription(peerName).string - } else if case let .upgradePreview(_, peerName) = component.subject { + } else if case let .upgradePreview(_, _, peerName) = component.subject { var peerName = peerName if peerName.count > 22 { peerName = "\(peerName.prefix(22))…" @@ -2871,28 +2882,27 @@ private final class GiftViewSheetContent: CombinedComponent { tradableText = strings.Gift_Upgrade_Tradable_Description } - let upgradeTitle = upgradeTitle.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: title, - font: Font.bold(20.0), - textColor: .white, - paragraphAlignment: .center - )), - horizontalAlignment: .center, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate - ) + var variant1: GiftItemComponent.Subject? + var variant2: GiftItemComponent.Subject? + var variant3: GiftItemComponent.Subject? - if case let .generic(gift) = component.subject.arguments?.gift, let upgradePreview = state.upgradePreview { - var variant1: GiftItemComponent.Subject = .starGift(gift: gift, price: "") - var variant2: GiftItemComponent.Subject = .starGift(gift: gift, price: "") - var variant3: GiftItemComponent.Subject = .starGift(gift: gift, price: "") + var upgradeAttributes: [StarGift.UniqueGift.Attribute]? + + if case let .generic(gift) = component.subject.arguments?.gift { + variant1 = .starGift(gift: gift, price: "") + variant2 = .starGift(gift: gift, price: "") + variant3 = .starGift(gift: gift, price: "") + } + + if let upgradePreview = state.upgradePreview { + upgradeAttributes = upgradePreview.attributes + } else if case let .upgradePreview(_, attributes, _) = component.subject { + upgradeAttributes = attributes + } + if let upgradeAttributes { var i = 0 - for attribute in upgradePreview.attributes { + for attribute in upgradeAttributes { if case .model = attribute { switch i { case 0: @@ -2907,7 +2917,9 @@ private final class GiftViewSheetContent: CombinedComponent { i += 1 } } - + } + + if let variant1, let variant2, let variant3 { var buttonColor: UIColor = UIColor.white.withAlphaComponent(0.16) if let backgroundColor = giftCompositionExternalState.backgroundColor { buttonColor = backgroundColor.mixedWith(.white, alpha: 0.2) @@ -2923,7 +2935,21 @@ private final class GiftViewSheetContent: CombinedComponent { ) let variantsButtonSize = CGSize(width: variantsMeasureDescription.size.width + 87.0, height: 24.0) - + + let upgradeTitle = upgradeTitle.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: title, + font: Font.bold(20.0), + textColor: .white, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) let upgradeDescription = upgradeDescription.update( component: GlassBarButtonComponent( size: variantsButtonSize, @@ -5415,7 +5441,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { case uniqueGift(StarGift.UniqueGift, EnginePeer.Id?) case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) case soldOutGift(StarGift.Gift) - case upgradePreview([StarGift.UniqueGift.Attribute], String) + case upgradePreview(StarGift.Gift, [StarGift.UniqueGift.Attribute], String) case wearPreview(StarGift, [StarGift.UniqueGift.Attribute]?) var arguments: ( diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 0ffb96cc38..7af47de9ab 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3867,8 +3867,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return GiftViewScreen(context: context, subject: .wearPreview(gift, attributes)) } - public func makeGiftUpgradePreviewScreen(context: AccountContext, attributes: [StarGift.UniqueGift.Attribute], peerName: String) -> ViewController { - return GiftViewScreen(context: context, subject: .upgradePreview(attributes, peerName)) + public func makeGiftUpgradePreviewScreen(context: AccountContext, gift: StarGift.Gift, attributes: [StarGift.UniqueGift.Attribute], peerName: String) -> ViewController { + return GiftViewScreen(context: context, subject: .upgradePreview(gift, attributes, peerName)) } public func makeGiftAuctionInfoScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: (() -> Void)?) -> ViewController { @@ -3891,8 +3891,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return giftOfferAlertController(context: context, updatedPresentationData: updatedPresentationData, gift: gift, peer: peer, amount: amount, commit: commit) } - public func makeGiftUpgradeVariantsScreen(context: AccountContext, gift: StarGift, onlyCrafted: Bool, attributes: [StarGift.UniqueGift.Attribute], selectedAttributes: [StarGift.UniqueGift.Attribute]?, focusedAttribute: StarGift.UniqueGift.Attribute?) -> ViewController { - return GiftUpgradeVariantsScreen(context: context, gift: gift, onlyCrafted: onlyCrafted, attributes: attributes, selectedAttributes: selectedAttributes, focusedAttribute: focusedAttribute) + public func makeGiftUpgradeVariantsScreen(context: AccountContext, gift: StarGift, crafted: Bool, attributes: [StarGift.UniqueGift.Attribute], selectedAttributes: [StarGift.UniqueGift.Attribute]?, focusedAttribute: StarGift.UniqueGift.Attribute?) -> ViewController { + return GiftUpgradeVariantsScreen(context: context, gift: gift, crafted: crafted, attributes: attributes, selectedAttributes: selectedAttributes, focusedAttribute: focusedAttribute) } public func makeGiftAuctionWearPreviewScreen(context: AccountContext, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?, attributes: [StarGift.UniqueGift.Attribute], completion: @escaping () -> Void) -> ViewController { From b459de93b65813705b7111884870a36ef58d2628 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 29 Jan 2026 13:27:18 +0400 Subject: [PATCH 07/12] Various fixes --- .../Sources/GiftCraftScreen.swift | 6 +++--- .../Premium/Craft/Chance.imageset/Contents.json | 12 ++++++++++++ .../Premium/Craft/Chance.imageset/puzzle.pdf | Bin 0 -> 5906 bytes .../Images.xcassets/Premium/Craft/Contents.json | 9 +++++++++ .../Premium/Craft/Rare.imageset/Contents.json | 12 ++++++++++++ .../Premium/Craft/Rare.imageset/diamond (2).pdf | Bin 0 -> 6671 bytes .../Premium/Craft/Result.imageset/Contents.json | 12 ++++++++++++ .../Premium/Craft/Result.imageset/chance.pdf | Bin 0 -> 4959 bytes .../ChatInterfaceStateAccessoryPanels.swift | 2 +- 9 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Craft/Chance.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Craft/Chance.imageset/puzzle.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Craft/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Craft/Rare.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Craft/Rare.imageset/diamond (2).pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Craft/Result.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Craft/Result.imageset/chance.pdf diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift index ada2db7a98..8867406dd3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift @@ -1259,7 +1259,7 @@ private final class CraftGiftPageContent: Component { text: environment.strings.Gift_Craft_Info_Paragraph1_Text, textColor: textColor, accentColor: accentColor, - iconName: "Premium/Collectible/Badge", + iconName: "Premium/Craft/Rare", iconColor: environment.theme.list.itemAccentColor )) ) @@ -1273,7 +1273,7 @@ private final class CraftGiftPageContent: Component { text: environment.strings.Gift_Craft_Info_Paragraph2_Text, textColor: textColor, accentColor: accentColor, - iconName: "Premium/Collectible/Transferable", + iconName: "Premium/Craft/Chance", iconColor: accentColor )) ) @@ -1287,7 +1287,7 @@ private final class CraftGiftPageContent: Component { text: environment.strings.Gift_Craft_Info_Paragraph3_Text, textColor: textColor, accentColor: accentColor, - iconName: "Premium/Collectible/Unique", + iconName: "Premium/Craft/Result", iconColor: accentColor )) ) diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Craft/Chance.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Chance.imageset/Contents.json new file mode 100644 index 0000000000..e731ca1da7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Chance.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "puzzle.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Craft/Chance.imageset/puzzle.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Chance.imageset/puzzle.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f72afb819356d1732be3d00b935a7762dac38bfb GIT binary patch literal 5906 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 GRQNwaTKoP0 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Craft/Result.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Result.imageset/Contents.json new file mode 100644 index 0000000000..62994ab790 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Result.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "chance.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Craft/Result.imageset/chance.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Result.imageset/chance.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a6ca2bbfb2f5434adf900dc62916e600767c3b82 GIT binary patch literal 4959 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index 9358698e53..1278c6bb61 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -182,7 +182,7 @@ func textInputAccessoryPanel( let alertController = AlertScreen( context: context, - configuration: AlertScreen.Configuration(actionAlignment: .vertical), + configuration: AlertScreen.Configuration(actionAlignment: .vertical, allowInputInset: true), content: content, actions: [ .init(title: strings.Conversation_ForwardOptions_ShowOptions, action: { From 4e43b0cbb226daa385d53214124f2ed90c99b342 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 29 Jan 2026 17:02:59 +0400 Subject: [PATCH 08/12] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../Sources/SheetComponent.swift | 7 +- .../Themes/ThemeSettingsController.swift | 5 +- .../Sources/ButtonsComponent.swift | 267 ---------- .../Sources/GiftViewScreen.swift | 492 ++++++++++++------ .../Sources/GlassControlPanel.swift | 8 +- .../Sources/PeerInfoRatingComponent.swift | 2 +- 7 files changed, 359 insertions(+), 424 deletions(-) delete mode 100644 submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 73405bd474..db3598e081 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15714,3 +15714,5 @@ Error: %8$@"; "Gift.Variants.CollectionInfo.CraftableModel_any" = "**%@** craftable models"; "Gift.Variants.ViewCraftableModels" = "View Craftable Models"; "Gift.Variants.ViewPrimaryModels" = "View Primary Models"; + +"Gift.View.OnSale" = "On sale for %@"; diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index 69ed8881fa..0122ac4a17 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -191,9 +191,7 @@ public final class SheetComponent: C self.scrollView = ScrollView() self.scrollView.delaysContentTouches = false - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.scrollView.contentInsetAdjustmentBehavior = .never - } + self.scrollView.contentInsetAdjustmentBehavior = .never self.scrollView.showsVerticalScrollIndicator = false self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.alwaysBounceVertical = true @@ -351,6 +349,9 @@ public final class SheetComponent: C }) } else { var targetOffset: CGFloat = self.scrollView.contentSize.height + abs(contentView.frame.minY) + 6.0 + if self.currentHasInputHeight { + targetOffset += 330.0 + } if self.isCentered { targetOffset = self.frame.height + self.scrollView.frame.height * 0.5 } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index 726a00f856..9b85255901 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -23,6 +23,7 @@ import ThemeCarouselItem import ThemeAccentColorScreen import WallpaperGridScreen import PeerNameColorItem +import DeviceModel private final class ThemeSettingsControllerArguments { let context: AccountContext @@ -487,7 +488,9 @@ private func themeSettingsControllerEntries( } entries.append(.otherHeader(presentationData.theme, strings.Appearance_Other.uppercased())) - entries.append(.sendWithCmdEnter(presentationData.theme, strings.Appearance_SendWithCmdEnter, chatSettings.sendWithCmdEnter)) + if DeviceModel.current.isIpad { + entries.append(.sendWithCmdEnter(presentationData.theme, strings.Appearance_SendWithCmdEnter, chatSettings.sendWithCmdEnter)) + } entries.append(.showNextMediaOnTap(presentationData.theme, strings.Appearance_ShowNextMediaOnTap, mediaSettings.showNextMediaOnTap)) entries.append(.showNextMediaOnTapInfo(presentationData.theme, strings.Appearance_ShowNextMediaOnTapInfo)) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift deleted file mode 100644 index 5d05bf9d1c..0000000000 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift +++ /dev/null @@ -1,267 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import ComponentFlow -import TelegramCore -import BundleIconComponent -import MultilineTextComponent -import MoreButtonNode -import AccountContext -import TelegramPresentationData -import TelegramStringFormatting - -final class PriceButtonComponent: Component { - let price: CurrencyAmount - let dateTimeFormat: PresentationDateTimeFormat - - init( - price: CurrencyAmount, - dateTimeFormat: PresentationDateTimeFormat - ) { - self.price = price - self.dateTimeFormat = dateTimeFormat - } - - static func ==(lhs: PriceButtonComponent, rhs: PriceButtonComponent) -> Bool { - return lhs.price == rhs.price && lhs.dateTimeFormat == rhs.dateTimeFormat - } - - final class View: UIView { - private let backgroundView = UIView() - - private let icon = UIImageView() - private let text = ComponentView() - - private var component: PriceButtonComponent? - private weak var state: EmptyComponentState? - - override init(frame: CGRect) { - super.init(frame: frame) - - self.backgroundView.clipsToBounds = true - self.addSubview(self.backgroundView) - - self.backgroundView.addSubview(self.icon) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func update(component: PriceButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - let previousComponent = self.component - self.component = component - self.state = state - - if previousComponent?.price.currency != component.price.currency { - switch component.price.currency { - case .stars: - self.icon.image = UIImage(bundleImageName: "Premium/Stars/ButtonStar")?.withRenderingMode(.alwaysTemplate) - case .ton: - self.icon.image = UIImage(bundleImageName: "Ads/TonAbout")?.withRenderingMode(.alwaysTemplate) - } - } - - var backgroundSize = CGSize(width: 42.0, height: 30.0) - let textSize = self.text.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: formatCurrencyAmountText(component.price, dateTimeFormat: component.dateTimeFormat), - font: Font.semibold(11.0), - textColor: UIColor(rgb: 0xffffff) - )) - )), - environment: {}, - containerSize: availableSize - ) - let textFrame = CGRect(origin: CGPoint(x: 32.0, y: floorToScreenPixels((backgroundSize.height - textSize.height) / 2.0)), size: textSize) - if let textView = self.text.view { - if textView.superview == nil { - self.backgroundView.addSubview(textView) - } - transition.setFrame(view: textView, frame: textFrame) - } - backgroundSize.width += textSize.width - - self.backgroundView.layer.cornerRadius = backgroundSize.height / 2.0 - - let backgroundColor: UIColor = UIColor(rgb: 0xffffff, alpha: 0.1) - transition.setBackgroundColor(view: self.backgroundView, color: backgroundColor) - - let backgroundFrame = CGRect(origin: .zero, size: backgroundSize) - transition.setFrame(view: self.backgroundView, frame: backgroundFrame) - - if let iconSize = self.icon.image?.size { - let iconFrame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((backgroundSize.height - iconSize.height) / 2.0)), size: iconSize) - transition.setFrame(view: self.icon, frame: iconFrame) - } - self.icon.tintColor = UIColor(rgb: 0xffffff) - - return backgroundSize - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - -final class ButtonsComponent: Component { - let theme: PresentationTheme - let isOverlay: Bool - let showMoreButton: Bool - let closePressed: () -> Void - let morePressed: (ASDisplayNode, ContextGesture?) -> Void - - init( - theme: PresentationTheme, - isOverlay: Bool, - showMoreButton: Bool, - closePressed: @escaping () -> Void, - morePressed: @escaping (ASDisplayNode, ContextGesture?) -> Void - ) { - self.theme = theme - self.isOverlay = isOverlay - self.showMoreButton = showMoreButton - self.closePressed = closePressed - self.morePressed = morePressed - } - - static func ==(lhs: ButtonsComponent, rhs: ButtonsComponent) -> Bool { - return lhs.theme === rhs.theme && lhs.isOverlay == rhs.isOverlay && lhs.showMoreButton == rhs.showMoreButton - } - - final class View: UIView { - private let backgroundView = UIView() - - private let closeButton = HighlightTrackingButton() - private let closeIcon = UIImageView() - private let moreNode = MoreButtonNode(theme: defaultPresentationTheme, size: CGSize(width: 36.0, height: 36.0), encircled: false) - - private var component: ButtonsComponent? - private weak var state: EmptyComponentState? - - override init(frame: CGRect) { - super.init(frame: frame) - - self.backgroundView.clipsToBounds = true - self.addSubview(self.backgroundView) - - self.closeIcon.image = generateCloseButtonImage() - self.moreNode.updateColor(.white, transition: .immediate) - - self.backgroundView.addSubview(self.moreNode.view) - self.backgroundView.addSubview(self.closeButton) - self.backgroundView.addSubview(self.closeIcon) - - self.closeButton.highligthedChanged = { [weak self] highlighted in - guard let self else { - return - } - if highlighted { - self.closeIcon.layer.removeAnimation(forKey: "opacity") - self.closeIcon.alpha = 0.6 - } else { - self.closeIcon.alpha = 1.0 - self.closeIcon.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) - } - } - self.closeButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func closePressed() { - guard let component = self.component else { - return - } - component.closePressed() - } - - func update(component: ButtonsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.component = component - self.state = state - - let backgroundSize = CGSize(width: component.showMoreButton ? 70.0 : 30.0, height: 30.0) - self.backgroundView.layer.cornerRadius = backgroundSize.height / 2.0 - - let backgroundColor: UIColor = component.isOverlay ? UIColor(rgb: 0xffffff, alpha: 0.1) : UIColor(rgb: 0x808084, alpha: 0.1) - let foregroundColor: UIColor = component.isOverlay ? .white : component.theme.actionSheet.inputClearButtonColor - transition.setBackgroundColor(view: self.backgroundView, color: backgroundColor) - transition.setTintColor(view: self.closeIcon, color: foregroundColor) - - let backgroundFrame = CGRect(origin: .zero, size: backgroundSize) - transition.setFrame(view: self.backgroundView, frame: backgroundFrame) - - transition.setFrame(view: self.moreNode.view, frame: CGRect(origin: CGPoint(x: -7.0, y: -4.0), size: CGSize(width: 36.0, height: 36.0))) - transition.setAlpha(view: self.moreNode.view, alpha: component.showMoreButton ? 1.0 : 0.0) - self.moreNode.action = { [weak self] node, gesture in - guard let self, let component = self.component else { - return - } - component.morePressed(node, gesture) - } - - let closeFrame = CGRect(origin: CGPoint(x: backgroundSize.width - 30.0 - (component.showMoreButton ? 3.0 : 0.0), y: 0.0), size: CGSize(width: 30.0, height: 30.0)) - transition.setFrame(view: self.closeIcon, frame: closeFrame) - transition.setFrame(view: self.closeButton, frame: closeFrame) - - return backgroundSize - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - -private func generateCloseButtonImage() -> UIImage? { - return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setLineWidth(2.0) - context.setLineCap(.round) - context.setStrokeColor(UIColor.white.cgColor) - - context.move(to: CGPoint(x: 10.0, y: 10.0)) - context.addLine(to: CGPoint(x: 20.0, y: 20.0)) - context.strokePath() - - context.move(to: CGPoint(x: 20.0, y: 10.0)) - context.addLine(to: CGPoint(x: 10.0, y: 20.0)) - context.strokePath() - })?.withRenderingMode(.alwaysTemplate) -} - -func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { - return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setFillColor(backgroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - - context.setLineWidth(2.0) - context.setLineCap(.round) - context.setStrokeColor(foregroundColor.cgColor) - - context.move(to: CGPoint(x: 10.0, y: 10.0)) - context.addLine(to: CGPoint(x: 20.0, y: 20.0)) - context.strokePath() - - context.move(to: CGPoint(x: 20.0, y: 10.0)) - context.addLine(to: CGPoint(x: 10.0, y: 20.0)) - context.strokePath() - }) -} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 9cbeafdeac..1f921373e4 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -46,6 +46,7 @@ import PeerTableCellComponent import AvatarComponent import GlassControls import GlassBarButtonComponent +import GlassBackgroundComponent private final class GiftViewSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -2398,8 +2399,6 @@ private final class GiftViewSheetContent: CombinedComponent { } static var body: Body { - let priceButton = Child(PlainButtonComponent.self) - let buttons = Child(GlassControlPanelComponent.self) let animation = Child(GiftCompositionComponent.self) let title = Child(MultilineTextComponent.self) @@ -2409,9 +2408,9 @@ private final class GiftViewSheetContent: CombinedComponent { let description = Child(MultilineTextComponent.self) let animatedDescription = Child(HStack.self) - let transferButton = Child(PlainButtonComponent.self) - let wearButton = Child(PlainButtonComponent.self) - let resellButton = Child(PlainButtonComponent.self) + let transferButton = Child(HeaderButtonComponent.self) + let wearButton = Child(HeaderButtonComponent.self) + let resellButton = Child(HeaderButtonComponent.self) let wearAvatar = Child(AvatarComponent.self) let wearPeerName = Child(MultilineTextComponent.self) @@ -2431,7 +2430,10 @@ private final class GiftViewSheetContent: CombinedComponent { let upgradePerks = Child(List.self) let upgradeKeepName = Child(PlainButtonComponent.self) let upgradePriceButton = Child(PlainButtonComponent.self) - let variantsMeasureDescription = Child(MultilineTextComponent.self) + let upgradeDescriptionMeasure = Child(MultilineTextComponent.self) + + let priceButtonMeasure = Child(MultilineTextWithEntitiesComponent.self) + let priceButton = Child(GlassBarButtonComponent.self) let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: []) @@ -2617,6 +2619,19 @@ private final class GiftViewSheetContent: CombinedComponent { headerSubject = nil } + var buttonsBackground: GlassControlGroupComponent.Background = .panel + if let uniqueGift, let backdropAttribute = uniqueGift.attributes.first(where: { attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + }), case let .backdrop(_, _, _, outerColor, _, _, _) = backdropAttribute { + buttonsBackground = .color(UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2)) + } else if showUpgradePreview, let backgroundColor = giftCompositionExternalState.backgroundColor { + buttonsBackground = .color(backgroundColor.mixedWith(.white, alpha: 0.2)) + } + var ownerPeerId: EnginePeer.Id? if let uniqueGift { if case let .peerId(peerId) = uniqueGift.owner { @@ -2730,6 +2745,16 @@ private final class GiftViewSheetContent: CombinedComponent { let tableTextColor = theme.list.itemPrimaryTextColor let tableLinkColor = theme.list.itemAccentColor + var resellAmount: CurrencyAmount? + var selling = false + if let uniqueGift { + if uniqueGift.resellForTonOnly { + resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == .ton }) + } else { + resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == .stars }) + } + } + if let headerSubject { let animation = animation.update( component: GiftCompositionComponent( @@ -2925,16 +2950,16 @@ private final class GiftViewSheetContent: CombinedComponent { buttonColor = backgroundColor.mixedWith(.white, alpha: 0.2) } - let variantsMeasureDescription = variantsMeasureDescription.update( + let upgradeDescriptionMeasure = upgradeDescriptionMeasure.update( component: MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Upgrade_ViewAllVariants, font: Font.semibold(13.0), textColor: .clear))), availableSize: context.availableSize, transition: .immediate ) - context.add(variantsMeasureDescription + context.add(upgradeDescriptionMeasure .position(CGPoint(x: -10000.0, y: -10000.0)) ) - let variantsButtonSize = CGSize(width: variantsMeasureDescription.size.width + 87.0, height: 24.0) + let variantsButtonSize = CGSize(width: upgradeDescriptionMeasure.size.width + 87.0, height: 24.0) let upgradeTitle = upgradeTitle.update( component: MultilineTextComponent( @@ -3153,7 +3178,7 @@ private final class GiftViewSheetContent: CombinedComponent { } } if let releasedBy = uniqueGift.releasedBy, let peer = state.peerMap[releasedBy], let addressName = peer.addressName { - descriptionText = strings.Gift_Unique_CollectibleBy("#\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))", "[@\(addressName)]()").string + descriptionText = strings.Gift_View_ReleasedBy("[@\(addressName)]()").string hasDescriptionButton = true releasedByPeer = peer } @@ -3300,6 +3325,88 @@ private final class GiftViewSheetContent: CombinedComponent { }) descriptionOffset += subtitle.size.height } + + if let resellAmount { + if incoming || ownerPeerId == component.context.account.peerId { + var valueString = formatCurrencyAmountText(resellAmount, dateTimeFormat: environment.dateTimeFormat) + switch resellAmount.currency { + case .stars: + valueString = "⭐️\(valueString)" + case .ton: + valueString = "💎\(valueString)" + } + let priceButtonAttributedString = NSMutableAttributedString(string: strings.Gift_View_OnSale(valueString).string, font: Font.regular(13.0), textColor: .white) + let starRange = (priceButtonAttributedString.string as NSString).range(of: "⭐️") + if starRange.location != NSNotFound { + priceButtonAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: starRange) + priceButtonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: starRange) + } + let tonRange = (priceButtonAttributedString.string as NSString).range(of: "💎") + if tonRange.location != NSNotFound { + priceButtonAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 1, file: nil, custom: .ton(tinted: true)), range: tonRange) + priceButtonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: tonRange) + } + + let priceButtonMeasure = priceButtonMeasure.update( + component: MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .white, + text: .plain(priceButtonAttributedString) + ), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(priceButtonMeasure + .position(CGPoint(x: -10000.0, y: -10000.0)) + ) + + var buttonColor: UIColor = UIColor(rgb: 0xffffff, alpha: 0.1) + if case let .color(color) = buttonsBackground { + buttonColor = color + } + + let priceButtonSize = CGSize(width: priceButtonMeasure.size.width + 18.0, height: 19.0) + let priceButton = priceButton.update( + component: GlassBarButtonComponent( + size: priceButtonSize, + backgroundColor: buttonColor, + isDark: true, + state: .tintedGlass, + component: AnyComponentWithIdentity(id: "label", component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .white, + text: .plain(priceButtonAttributedString) + ) + )), + action: { [weak state] _ in + state?.resellGift(update: true) + } + ), + availableSize: priceButtonSize, + transition: context.transition + ) + headerComponents.append({ + context.add(priceButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: 207.0 + descriptionOffset + priceButton.size.height / 2.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + }) + + descriptionText = "" + originY += 7.0 + } + if case let .uniqueGift(_, recipientPeerId) = component.subject, recipientPeerId != nil { + } else if ownerPeerId != component.context.account.peerId { + selling = true + } + } + var useDescriptionTint = false if !descriptionText.isEmpty { var linkColor = theme.actionSheet.controlAccentColor @@ -3787,18 +3894,19 @@ private final class GiftViewSheetContent: CombinedComponent { let buttonWidth = floor(context.availableSize.width - sideInset * 2.0 - buttonSpacing * CGFloat(buttonsCount - 1)) / CGFloat(buttonsCount) let buttonHeight: CGFloat = 58.0 + var buttonColor: UIColor = UIColor(rgb: 0xffffff, alpha: 0.1) + if case let .color(color) = buttonsBackground { + buttonColor = color + } + var buttonOriginX = sideInset if canTransfer { let transferButton = transferButton.update( - component: PlainButtonComponent( - content: AnyComponent( - HeaderButtonComponent( - title: strings.Gift_View_Header_Transfer, - iconName: "Premium/Collectible/Transfer", - isLocked: isMyHostedUniqueGift - ) - ), - effectAlignment: .center, + component: HeaderButtonComponent( + title: strings.Gift_View_Header_Transfer, + buttonColor: buttonColor, + iconName: "Premium/Collectible/Transfer", + isLocked: isMyHostedUniqueGift, action: { [weak state] in state?.transferGift() } @@ -3819,19 +3927,15 @@ private final class GiftViewSheetContent: CombinedComponent { } let wearButton = wearButton.update( - component: PlainButtonComponent( - content: AnyComponent( - HeaderButtonComponent( - title: isWearing ? strings.Gift_View_Header_TakeOff : strings.Gift_View_Header_Wear, - iconName: isWearing ? "Premium/Collectible/Unwear" : "Premium/Collectible/Wear" - ) - ), - effectAlignment: .center, + component: HeaderButtonComponent( + title: isWearing ? strings.Gift_View_Header_TakeOff : strings.Gift_View_Header_Wear, + buttonColor: buttonColor, + iconName: isWearing ? "Premium/Collectible/Unwear" : "Premium/Collectible/Wear", action: { [weak state] in if let state { if isWearing { state.commitTakeOff() - + state.showAttributeInfo(tag: state.statusTag, text: strings.Gift_View_TookOff("\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))").string) } else { if let controller = controller() as? GiftViewScreen { @@ -3851,7 +3955,7 @@ private final class GiftViewSheetContent: CombinedComponent { canWear = component.context.isPremium } let _ = (ApplicationSpecificNotice.getStarGiftWearTips(accountManager: component.context.sharedContext.accountManager) - |> deliverOnMainQueue).start(next: { [weak state] count in + |> deliverOnMainQueue).start(next: { [weak state] count in guard let state else { return } @@ -3882,15 +3986,11 @@ private final class GiftViewSheetContent: CombinedComponent { if canResell { let resellButton = resellButton.update( - component: PlainButtonComponent( - content: AnyComponent( - HeaderButtonComponent( - title: (uniqueGift.resellAmounts ?? []).isEmpty ? strings.Gift_View_Sell : strings.Gift_View_Unlist, - iconName: (uniqueGift.resellAmounts ?? []).isEmpty ? "Premium/Collectible/Sell" : "Premium/Collectible/Unlist", - isLocked: isMyHostedUniqueGift - ) - ), - effectAlignment: .center, + component: HeaderButtonComponent( + title: (uniqueGift.resellAmounts ?? []).isEmpty ? strings.Gift_View_Sell : strings.Gift_View_Unlist, + buttonColor: buttonColor, + iconName: (uniqueGift.resellAmounts ?? []).isEmpty ? "Premium/Collectible/Sell" : "Premium/Collectible/Unlist", + isLocked: isMyHostedUniqueGift, action: { [weak state] in state?.resellGift() } @@ -4440,44 +4540,6 @@ private final class GiftViewSheetContent: CombinedComponent { component() } - var resellAmount: CurrencyAmount? - var selling = false - if let uniqueGift { - if uniqueGift.resellForTonOnly { - resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == .ton }) - } else { - resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == .stars }) - } - if let resellAmount, wearPeerNameChild == nil { - if incoming || ownerPeerId == component.context.account.peerId { - let priceButton = priceButton.update( - component: PlainButtonComponent( - content: AnyComponent( - PriceButtonComponent(price: resellAmount, dateTimeFormat: environment.dateTimeFormat) - ), - effectAlignment: .center, - action: { [weak state] in - state?.resellGift(update: true) - }, - animateScale: false - ), - availableSize: CGSize(width: 150.0, height: 30.0), - transition: context.transition - ) - context.add(priceButton - .position(CGPoint(x: environment.safeInsets.left + 16.0 + priceButton.size.width / 2.0, y: 76.0 + priceButton.size.height / 2.0)) - .appear(.default(scale: true, alpha: true)) - .disappear(.default(scale: true, alpha: true)) - ) - } - if case let .uniqueGift(_, recipientPeerId) = component.subject, recipientPeerId != nil { - } else if ownerPeerId != component.context.account.peerId { - selling = true - } - } - } - - var isChatTheme = false if let controller = controller() as? GiftViewScreen, controller.openChatTheme != nil { isChatTheme = true @@ -5155,20 +5217,7 @@ private final class GiftViewSheetContent: CombinedComponent { ) originY += upgradePriceButton.size.height } - - var buttonsBackground: GlassControlGroupComponent.Background = .panel - if let uniqueGift, let backdropAttribute = uniqueGift.attributes.first(where: { attribute in - if case .backdrop = attribute { - return true - } else { - return false - } - }), case let .backdrop(_, _, _, outerColor, _, _, _) = backdropAttribute { - buttonsBackground = .color(UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2)) - } else if showUpgradePreview, let backgroundColor = giftCompositionExternalState.backgroundColor { - buttonsBackground = .color(backgroundColor.mixedWith(.white, alpha: 0.2)) - } - + var isBackButton = false if state.inWearPreview || state.inUpgradePreview { isBackButton = true @@ -5233,6 +5282,11 @@ private final class GiftViewSheetContent: CombinedComponent { )) } + var buttonsIsDark = theme.overallDarkAppearance + if case .color = buttonsBackground { + buttonsIsDark = true + } + let buttons = buttons.update( component: GlassControlPanelComponent( theme: theme, @@ -5246,6 +5300,7 @@ private final class GiftViewSheetContent: CombinedComponent { background: buttonsBackground ), centerAlignmentIfPossible: true, + isDark: buttonsIsDark, tag: state.controlButtonsTag ), availableSize: CGSize(width: context.availableSize.width - 16.0 * 2.0, height: 44.0), @@ -6095,25 +6150,34 @@ private final class GiftViewContextReferenceContentSource: ContextReferenceConte } } -private final class HeaderButtonComponent: CombinedComponent { +private final class HeaderButtonComponent: Component { let title: String + let buttonColor: UIColor let iconName: String let isLocked: Bool + let action: () -> Void public init( title: String, + buttonColor: UIColor, iconName: String, - isLocked: Bool = false + isLocked: Bool = false, + action: @escaping () -> Void ) { self.title = title + self.buttonColor = buttonColor self.iconName = iconName self.isLocked = isLocked + self.action = action } static func ==(lhs: HeaderButtonComponent, rhs: HeaderButtonComponent) -> Bool { if lhs.title != rhs.title { return false } + if lhs.buttonColor != rhs.buttonColor { + return false + } if lhs.iconName != rhs.iconName { return false } @@ -6123,79 +6187,205 @@ private final class HeaderButtonComponent: CombinedComponent { return true } - static var body: Body { - let background = Child(RoundedRectangle.self) - let title = Child(MultilineTextComponent.self) - let icon = Child(BundleIconComponent.self) - let lockIcon = Child(BundleIconComponent.self) + final class View: UIView { + private var component: HeaderButtonComponent? + private weak var componentState: EmptyComponentState? - return { context in - let component = context.component + private let backgroundView = GlassBackgroundView() + private let title = ComponentView() + private let icon = ComponentView() + private let lockIcon = ComponentView() + private let button = HighlightTrackingButton() + + override init(frame: CGRect) { + super.init(frame: frame) - let background = background.update( - component: RoundedRectangle( - color: UIColor.white.withAlphaComponent(0.16), - cornerRadius: 16.0 + self.addSubview(self.backgroundView) + self.backgroundView.contentView.addSubview(self.button) + + self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func buttonPressed() { + if let component = self.component { + component.action() + } + } + + func update(component: HeaderButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.componentState = state + + let bounds = CGRect(origin: .zero, size: availableSize) + + self.backgroundView.update(size: bounds.size, cornerRadius: 16.0, isDark: true, tintColor: .init(kind: .custom(style: .default, color: component.buttonColor)), isInteractive: true, transition: transition) + transition.setFrame(view: self.backgroundView, frame: bounds) + + let iconSize = self.icon.update( + transition: transition, + component: AnyComponent( + BundleIconComponent( + name: component.iconName, + tintColor: UIColor.white + ) ), - availableSize: context.availableSize, - transition: .immediate + environment: {}, + containerSize: availableSize ) - context.add(background - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + if let iconView = self.icon.view { + if iconView.superview == nil { + iconView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(iconView) + } + iconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: floorToScreenPixels(22.0 - iconSize.height * 0.5)), size: iconSize) + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.title, + font: Font.regular(11.0), + textColor: UIColor.white, + paragraphAlignment: .natural + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0, height: availableSize.height) ) - let icon = icon.update( - component: BundleIconComponent( - name: component.iconName, - tintColor: UIColor.white - ), - availableSize: context.availableSize, - transition: .immediate - ) - context.add(icon - .position(CGPoint(x: context.availableSize.width / 2.0, y: 22.0)) - ) - - let title = title.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: component.title, - font: Font.regular(11.0), - textColor: UIColor.white, - paragraphAlignment: .natural - )), - horizontalAlignment: .center, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: context.availableSize.width - 16.0, height: context.availableSize.height), - transition: .immediate - ) - var totalTitleWidth = title.size.width - var titleOriginX = context.availableSize.width / 2.0 - totalTitleWidth / 2.0 + var totalTitleWidth = titleSize.width + var titleOriginX = availableSize.width / 2.0 - totalTitleWidth / 2.0 if component.isLocked { let titleSpacing: CGFloat = 3.0 - let lockIcon = lockIcon.update( - component: BundleIconComponent( - name: "Chat List/StatusLockIcon", - tintColor: UIColor.white + + let lockIconSize = self.lockIcon.update( + transition: transition, + component: AnyComponent( + BundleIconComponent( + name: "Chat List/StatusLockIcon", + tintColor: .white + ) ), - availableSize: context.availableSize, - transition: .immediate + environment: {}, + containerSize: availableSize ) - totalTitleWidth += lockIcon.size.width + titleSpacing - titleOriginX = context.availableSize.width / 2.0 - totalTitleWidth / 2.0 - context.add(lockIcon - .position(CGPoint(x: titleOriginX + lockIcon.size.width / 2.0, y: 42.0)) - ) - titleOriginX += lockIcon.size.width + titleSpacing + totalTitleWidth += lockIconSize.width + titleSpacing + titleOriginX = availableSize.width / 2.0 - totalTitleWidth / 2.0 + + if let lockIconView = self.lockIcon.view { + if lockIconView.superview == nil { + lockIconView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(lockIconView) + } + lockIconView.frame = CGRect(origin: CGPoint(x: titleOriginX, y: floorToScreenPixels(42.0 - lockIconSize.height * 0.5)), size: lockIconSize) + } + titleOriginX += lockIconSize.width + titleSpacing } - context.add(title - .position(CGPoint(x: titleOriginX + title.size.width / 2.0, y: 42.0)) - ) - return context.availableSize + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(titleView) + } + titleView.frame = CGRect(origin: CGPoint(x: titleOriginX, y: floorToScreenPixels(42.0 - titleSize.height * 0.5)), size: titleSize) + } + + self.button.frame = bounds + + return availableSize } } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } + +// static var body: Body { +// let background = Child(RoundedRectangle.self) +// let title = Child(MultilineTextComponent.self) +// let icon = Child(BundleIconComponent.self) +// let lockIcon = Child(BundleIconComponent.self) +// +// return { context in +// let component = context.component +// +// let background = background.update( +// component: RoundedRectangle( +// color: UIColor.white.withAlphaComponent(0.16), +// cornerRadius: 16.0 +// ), +// availableSize: context.availableSize, +// transition: .immediate +// ) +// context.add(background +// .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) +// ) +// +// let icon = icon.update( +// component: BundleIconComponent( +// name: component.iconName, +// tintColor: UIColor.white +// ), +// availableSize: context.availableSize, +// transition: .immediate +// ) +// context.add(icon +// .position(CGPoint(x: context.availableSize.width / 2.0, y: 22.0)) +// ) +// +// let title = title.update( +// component: MultilineTextComponent( +// text: .plain(NSAttributedString( +// string: component.title, +// font: Font.regular(11.0), +// textColor: UIColor.white, +// paragraphAlignment: .natural +// )), +// horizontalAlignment: .center, +// maximumNumberOfLines: 1 +// ), +// availableSize: CGSize(width: context.availableSize.width - 16.0, height: context.availableSize.height), +// transition: .immediate +// ) +// var totalTitleWidth = title.size.width +// var titleOriginX = context.availableSize.width / 2.0 - totalTitleWidth / 2.0 +// if component.isLocked { +// let titleSpacing: CGFloat = 3.0 +// let lockIcon = lockIcon.update( +// component: BundleIconComponent( +// name: "Chat List/StatusLockIcon", +// tintColor: UIColor.white +// ), +// availableSize: context.availableSize, +// transition: .immediate +// ) +// totalTitleWidth += lockIcon.size.width + titleSpacing +// titleOriginX = context.availableSize.width / 2.0 - totalTitleWidth / 2.0 +// context.add(lockIcon +// .position(CGPoint(x: titleOriginX + lockIcon.size.width / 2.0, y: 42.0)) +// ) +// titleOriginX += lockIcon.size.width + titleSpacing +// } +// context.add(title +// .position(CGPoint(x: titleOriginX + title.size.width / 2.0, y: 42.0)) +// ) +// +// return context.availableSize +// } +// } } private struct GiftViewConfiguration { diff --git a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift index 44e6dcb841..351a2144d4 100644 --- a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift +++ b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift @@ -36,6 +36,7 @@ public final class GlassControlPanelComponent: Component { public let rightItem: Item? public let centralItem: Item? public let centerAlignmentIfPossible: Bool + public let isDark: Bool? public let tag: AnyObject? public init( @@ -44,6 +45,7 @@ public final class GlassControlPanelComponent: Component { centralItem: Item?, rightItem: Item?, centerAlignmentIfPossible: Bool = false, + isDark: Bool? = nil, tag: AnyObject? = nil ) { self.theme = theme @@ -51,6 +53,7 @@ public final class GlassControlPanelComponent: Component { self.centralItem = centralItem self.rightItem = rightItem self.centerAlignmentIfPossible = centerAlignmentIfPossible + self.isDark = isDark self.tag = tag } @@ -70,6 +73,9 @@ public final class GlassControlPanelComponent: Component { if lhs.centerAlignmentIfPossible != rhs.centerAlignmentIfPossible { return false } + if lhs.isDark != rhs.isDark { + return false + } if lhs.tag !== rhs.tag { return false } @@ -298,7 +304,7 @@ public final class GlassControlPanelComponent: Component { } transition.setFrame(view: self.glassContainerView, frame: CGRect(origin: CGPoint(), size: availableSize)) - self.glassContainerView.update(size: availableSize, isDark: component.theme.overallDarkAppearance, transition: transition) + self.glassContainerView.update(size: availableSize, isDark: component.isDark ?? component.theme.overallDarkAppearance, transition: transition) return availableSize } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift index 27ddd942a1..ba7530fe28 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift @@ -89,7 +89,7 @@ private func generateNumberOffsets() -> [CGPoint] { CGPoint(x: 0.9900000000000001, y: 0.0), CGPoint(x: 0.66, y: 0.0), CGPoint(x: 0.5775, y: 0.0), - CGPoint(x: 1.2375, y: 0.0), + CGPoint(x: 0.7425, y: 0.0), CGPoint(x: 0.9900000000000001, y: 0.0), CGPoint(x: 1.32, y: 0.0), CGPoint(x: 1.155, y: 0.0), From 422e3e4e24eb0b5c16c7b255bed7770251369f61 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 29 Jan 2026 21:37:06 +0400 Subject: [PATCH 09/12] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 5 + .../Sources/AccountContext.swift | 2 +- .../Sources/ResizableSheetComponent.swift | 136 ++++++++++++++++-- .../TelegramEngine/Payments/StarGifts.swift | 28 ++-- .../Sources/GiftCraftScreen.swift | 24 ++-- .../Sources/SelectCraftGiftScreen.swift | 9 +- .../Sources/GiftViewScreen.swift | 24 ++-- .../Sources/PeerInfoScreen.swift | 1 + .../Sources/GiftsListView.swift | 1 + .../Sources/SharedAccountContext.swift | 4 +- 10 files changed, 188 insertions(+), 46 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index db3598e081..54b50858b9 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15700,6 +15700,11 @@ Error: %8$@"; "Gift.Craft.Unavailable.Title" = "Try Later"; "Gift.Craft.Unavailable.Text" = "You will be able to craft this gift on %@."; +"Gift.Craft.Select.Title" = "Select Gifts"; +"Gift.Craft.Select.YourGifts" = "Your Gifts"; +"Gift.Craft.Select.SaleGifts" = "Suitable Gifts On Sale"; +"Gift.Craft.Select.NoGiftsFromCollection" = "You don't have other gifts\nfrom this collection"; + "Gift.Attribute.Rare" = "rare"; "Gift.Attribute.Legendary" = "legendary"; "Gift.Attribute.Epic" = "epic"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 1b40090279..fd73c89d38 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1462,7 +1462,7 @@ public protocol SharedAccountContext: AnyObject { func makeGiftOfferScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController func makeGiftUpgradeVariantsScreen(context: AccountContext, gift: StarGift, crafted: 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 makeGiftCraftScreen(context: AccountContext, gift: StarGift.UniqueGift, profileGiftsContext: ProfileGiftsContext?) -> 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)?) diff --git a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift index dbb51b9746..f48f0541a5 100644 --- a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift +++ b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift @@ -179,7 +179,7 @@ public final class ResizableSheetComponent Bool { + if gestureRecognizer === self.dismissPanGesture { + let pan = gestureRecognizer as! UIPanGestureRecognizer + let velocity = pan.velocity(in: self) + if abs(velocity.y) <= abs(velocity.x) { + return false + } + } + return true + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer === self.dismissPanGesture { + if otherGestureRecognizer === self.scrollView.panGestureRecognizer { + return true + } + } + return false + } + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.dismissAnimated() @@ -328,6 +358,79 @@ public final class ResizableSheetComponent 0.0 + if shouldStartDismiss { + if !self.isDismissingInteractively { + self.isDismissingInteractively = true + self.dismissStartTranslation = translation.y + self.scrollView.isScrollEnabled = false + } + + let start = self.dismissStartTranslation ?? translation.y + let dismissOffset = max(0.0, translation.y - start) + self.scrollView.contentOffset = .zero + self.updateDismissTranslation(dismissOffset) + } else if self.isDismissingInteractively { + let start = self.dismissStartTranslation ?? translation.y + let dismissOffset = max(0.0, translation.y - start) + self.updateDismissTranslation(dismissOffset) + } + case .ended, .cancelled: + if self.isDismissingInteractively { + let velocityY = recognizer.velocity(in: self).y + let currentOffset = self.dismissTranslation + let threshold = min(180.0, self.bounds.height * 0.25) + let shouldDismiss = currentOffset > threshold || velocityY > 1000.0 + + self.isDismissingInteractively = false + self.scrollView.isScrollEnabled = !component.isFullscreen + + if shouldDismiss { + let animateOffset = self.bounds.height - self.backgroundLayer.frame.minY + let initialVelocity = animateOffset > 0.0 ? max(0.0, velocityY) / animateOffset : 0.0 + self.animateOut(initialVelocity: initialVelocity, completion: { [weak self] in + self?.environment?.dismiss(false) + }) + } else { + self.resetDismissTranslation(animated: true) + } + } + default: + break + } + } + private func updateScrolling(transition: ComponentTransition) { guard let itemLayout = self.itemLayout, let component = self.component else { return @@ -356,6 +459,7 @@ public final class ResizableSheetComponent Void) { + func animateOut(initialVelocity: CGFloat? = nil, 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) + self.dimView.layer.animateAlpha(from: self.dimView.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false) + if let initialVelocity = initialVelocity { + let transition = ContainedViewLayoutTransition.animated(duration: 0.35, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + + transition.updatePosition(layer: self.scrollContentClippingView.layer, position: CGPoint(x: self.scrollContentClippingView.layer.position.x, y: self.scrollContentClippingView.layer.position.y + animateOffset), completion: { _ in + completion() + }) + transition.updatePosition(layer: self.backgroundLayer, position: CGPoint(x: self.backgroundLayer.position.x, y: self.backgroundLayer.position.y + animateOffset)) + transition.updatePosition(layer: self.navigationBarContainer.layer, position: CGPoint(x: self.navigationBarContainer.layer.position.x, y: self.navigationBarContainer.layer.position.y + animateOffset)) + transition.updatePosition(layer: self.bottomContainer.layer, position: CGPoint(x: self.bottomContainer.layer.position.x, y: self.bottomContainer.layer.position.y + animateOffset)) + } else { + let duration: Double = 0.25 + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.bottomContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + } } func update(component: ResizableSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index d8bda4bf32..1579c044f0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -2380,12 +2380,16 @@ private final class ProfileGiftsContextImpl { } func removeStarGifts(references: [StarGiftReference]) { - self.gifts.removeAll(where: { - if let reference = $0.reference { - return references.contains(reference) - } else { - return false + self.gifts.removeAll(where: { gift in + if let reference = gift.reference, references.contains(reference) { + return true } + for reference in references { + if case let .slug(slug) = reference, case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug { + return true + } + } + return false }) self.pushState() @@ -2400,13 +2404,17 @@ private final class ProfileGiftsContextImpl { } else { updatedGifts = [] } - updatedGifts = updatedGifts.filter { gift in - if let reference = gift.reference { - return !references.contains(reference) - } else { + updatedGifts.removeAll(where: { gift in + if let reference = gift.reference, references.contains(reference) { return true } - } + for reference in references { + if case let .slug(slug) = reference, case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug { + return true + } + } + return false + }) updatedCount -= Int32(references.count) if let entry = CodableEntry(CachedProfileGifts(gifts: updatedGifts, count: updatedCount, notificationsEnabled: nil)) { transaction.putItemCacheEntry(id: giftsEntryId(peerId: peerId, collectionId: collectionId), entry: entry) diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift index 8867406dd3..0935742682 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift @@ -270,7 +270,6 @@ private final class CraftGiftPageContent: Component { guard let self else { return } - //let isFirstTime = self.craftState == nil self.craftState = state var items: [GiftItem] = [] @@ -1118,9 +1117,14 @@ private final class CraftGiftPageContent: Component { self.state?.updated(transition: .easeInOut(duration: 0.5)) }, finished: { [weak self] view in - guard let self, let component = self.component, let environment = self.environment, let controller = environment.controller() else { + guard let self, let component = self.component, let environment = self.environment, let controller = environment.controller() as? GiftCraftScreen else { return } + var references: [StarGiftReference] = [] + for gift in selectedGifts.values { + references.append(gift.reference) + } + controller.profileGiftsContext?.removeStarGifts(references: references) if let _ = view { if case let .gift(gift) = component.result { let giftController = GiftViewScreen(context: component.context, subject: .profileGift(component.context.account.peerId, gift)) @@ -1129,12 +1133,12 @@ private final class CraftGiftPageContent: Component { navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds)) } + + controller.profileGiftsContext?.insertStarGifts(gifts: [gift]) } controller.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { _ in controller.dismiss() }) - } else { - } } ) @@ -1691,8 +1695,8 @@ private final class SheetContainerComponent: CombinedComponent { selectedGiftIds: Set(), starsTopUpOptions: state.starsTopUpOptionsPromise.get(), selectGift: { item in - let craftController = GiftCraftScreen(context: component.context, gift: item.gift) if let controller = controller() as? GiftCraftScreen, let navigationController = controller.navigationController as? NavigationController { + let craftController = GiftCraftScreen(context: component.context, gift: item.gift, profileGiftsContext: controller.profileGiftsContext) controller.dismissAnimated() navigationController.pushViewController(craftController) } @@ -1770,7 +1774,7 @@ private final class SheetContainerComponent: CombinedComponent { ) ), backgroundColor: .color(backgroundColor), - isFullscreen: false, //state.isCrafting, + isFullscreen: false, animateOut: animateOut ), environment: { @@ -1805,12 +1809,16 @@ private final class SheetContainerComponent: CombinedComponent { public class GiftCraftScreen: ViewControllerComponentContainer { + fileprivate weak var profileGiftsContext: ProfileGiftsContext? + public init( context: AccountContext, - gift: StarGift.UniqueGift + gift: StarGift.UniqueGift, + profileGiftsContext: ProfileGiftsContext? ) { - let craftContext = CraftGiftsContext(account: context.account, giftId: gift.giftId) + self.profileGiftsContext = profileGiftsContext + let craftContext = CraftGiftsContext(account: context.account, giftId: gift.giftId) super.init( context: context, component: SheetContainerComponent( diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift index ed0b4dc0af..250ec068cf 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift @@ -129,7 +129,6 @@ final class SelectGiftPageContent: Component { guard let self else { return } - //let isFirstTime = self.craftState == nil self.craftState = state var items: [GiftItem] = [] @@ -179,7 +178,7 @@ final class SelectGiftPageContent: Component { let myGiftsTitleSize = self.myGiftsTitle.update( transition: transition, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Your Gifts".uppercased(), font: Font.semibold(14.0), textColor: environment.theme.actionSheet.secondaryTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Select_YourGifts.uppercased(), font: Font.semibold(14.0), textColor: environment.theme.actionSheet.secondaryTextColor))) ), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -339,7 +338,7 @@ final class SelectGiftPageContent: Component { transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: "You don't have other gifts\nfrom this collection", font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor)), + text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Select_NoGiftsFromCollection, font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 3, lineSpacing: 0.1 @@ -365,7 +364,7 @@ final class SelectGiftPageContent: Component { let storeGiftsTitleSize = self.storeGiftsTitle.update( transition: transition, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "SUITABLE GIFTS ON SALE".uppercased(), font: Font.semibold(14.0), textColor: environment.theme.actionSheet.secondaryTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Select_SaleGifts.uppercased(), font: Font.semibold(14.0), textColor: environment.theme.actionSheet.secondaryTextColor))) ), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -540,7 +539,7 @@ private final class SheetContainerComponent: CombinedComponent { ) ), titleItem: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Select Gifts", font: Font.semibold(17.0), textColor: environment.theme.actionSheet.primaryTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Select_Title, font: Font.semibold(17.0), textColor: environment.theme.actionSheet.primaryTextColor))) ), leftItem: AnyComponent( GlassBarButtonComponent( diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 1f921373e4..f2efd7b646 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -1134,7 +1134,8 @@ private final class GiftViewSheetContent: CombinedComponent { let craftScreen = self.context.sharedContext.makeGiftCraftScreen( context: self.context, - gift: gift + gift: gift, + profileGiftsContext: controller.profileGiftsContext ) navigationController.pushViewController(craftScreen) } @@ -2554,7 +2555,7 @@ private final class GiftViewSheetContent: CombinedComponent { } if let number = arguments.giftNumber, let title = genericGift?.title { - titleString = "\(title) #\(formatCollectibleNumber(number, dateTimeFormat: environment.dateTimeFormat))" + titleString = "\(title) **#\(formatCollectibleNumber(number, dateTimeFormat: environment.dateTimeFormat))**" } else if isSelfGift { titleString = strings.Gift_View_Self_Title } else { @@ -3252,17 +3253,17 @@ private final class GiftViewSheetContent: CombinedComponent { let titleFont = Font.bold(20.0) let smallTitleFont = Font.bold(15.0) + let numberFont: UIFont + if let number = context.component.subject.arguments?.giftNumber, number < 1000 { + numberFont = titleFont + } else { + numberFont = smallTitleFont + } let titleAttributedString: NSAttributedString - if let uniqueGift { - let numberFont = uniqueGift.number < 1000 ? titleFont : smallTitleFont + if let _ = uniqueGift { titleAttributedString = parseMarkdownIntoAttributedString(titleString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: .white), bold: MarkdownAttributeSet(font: numberFont, textColor: vibrantColor), link: MarkdownAttributeSet(font: titleFont, textColor: .white), linkAttribute: { _ in return nil }), textAlignment: .center) } else { - titleAttributedString = NSAttributedString( - string: titleString, - font: titleFont, - textColor: theme.actionSheet.primaryTextColor, - paragraphAlignment: .center - ) + titleAttributedString = parseMarkdownIntoAttributedString(titleString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: numberFont, textColor: theme.actionSheet.secondaryTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil }), textAlignment: .center) } let title = title.update( @@ -5639,6 +5640,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { fileprivate let balanceOverlay = ComponentView() + fileprivate let profileGiftsContext: ProfileGiftsContext? fileprivate let updateSavedToProfile: ((StarGiftReference, Bool) -> Void)? fileprivate let convertToStars: ((StarGiftReference) -> Void)? fileprivate let dropOriginalDetails: ((StarGiftReference) -> Signal)? @@ -5658,6 +5660,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { allSubjects: [GiftViewScreen.Subject]? = nil, index: Int? = nil, forceDark: Bool = false, + profileGiftsContext: ProfileGiftsContext? = nil, updateSavedToProfile: ((StarGiftReference, Bool) -> Void)? = nil, convertToStars: ((StarGiftReference) -> Void)? = nil, dropOriginalDetails: ((StarGiftReference) -> Signal)? = nil, @@ -5672,6 +5675,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { self.context = context self.subject = subject + self.profileGiftsContext = profileGiftsContext self.updateSavedToProfile = updateSavedToProfile self.convertToStars = convertToStars self.dropOriginalDetails = dropOriginalDetails diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 89dc5d3362..7307b15bc3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -2368,6 +2368,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let controller = GiftViewScreen( context: self.context, subject: .profileGift(self.peerId, gift), + profileGiftsContext: profileGifts, updateSavedToProfile: { [weak profileGifts] reference, added in guard let profileGifts else { return diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift index 280848e31e..832f899e52 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift @@ -606,6 +606,7 @@ final class GiftsListView: UIView { subject: .profileGift(self.peerId, product), allSubjects: allSubjects, index: index, + profileGiftsContext: self.profileGifts, updateSavedToProfile: { [weak self] reference, added in guard let self else { return diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 7af47de9ab..df4c15bdc1 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3899,8 +3899,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return GiftAuctionWearPreviewScreen(context: context, auctionContext: auctionContext, attributes: attributes, completion: completion) } - public func makeGiftCraftScreen(context: AccountContext, gift: StarGift.UniqueGift) -> ViewController { - return GiftCraftScreen(context: context, gift: gift) + public func makeGiftCraftScreen(context: AccountContext, gift: StarGift.UniqueGift, profileGiftsContext: ProfileGiftsContext?) -> ViewController { + return GiftCraftScreen(context: context, gift: gift, profileGiftsContext: profileGiftsContext) } public func makeGiftDemoScreen(context: AccountContext) -> ViewController { From d07d38df251475a9ed96233e319c184a0701b838 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 30 Jan 2026 00:11:52 +0400 Subject: [PATCH 10/12] Various improvements --- .../Sources/ResizableSheetComponent.swift | 7 +- .../TelegramEngine/Payments/StarGifts.swift | 21 +- .../Sources/SelectCraftGiftScreen.swift | 224 ++++++++++-------- .../Sources/GiftViewScreen.swift | 18 +- 4 files changed, 161 insertions(+), 109 deletions(-) diff --git a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift index f48f0541a5..6a3f505327 100644 --- a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift +++ b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift @@ -19,6 +19,7 @@ public final class ResizableSheetComponentEnvironment: Equatable { public let screenSize: CGSize public let regularMetricsSize: CGSize? public let dismiss: (Bool) -> Void + public let boundsUpdated: ActionSlot public init( theme: PresentationTheme, @@ -30,7 +31,8 @@ public final class ResizableSheetComponentEnvironment: Equatable { isCentered: Bool, screenSize: CGSize, regularMetricsSize: CGSize?, - dismiss: @escaping (Bool) -> Void + dismiss: @escaping (Bool) -> Void, + boundsUpdated: ActionSlot = ActionSlot() ) { self.theme = theme self.statusBarHeight = statusBarHeight @@ -42,6 +44,7 @@ public final class ResizableSheetComponentEnvironment: Equatable { self.screenSize = screenSize self.regularMetricsSize = regularMetricsSize self.dismiss = dismiss + self.boundsUpdated = boundsUpdated } public static func ==(lhs: ResizableSheetComponentEnvironment, rhs: ResizableSheetComponentEnvironment) -> Bool { @@ -469,6 +472,8 @@ public final class ResizableSheetComponent let selectGift: (GiftItem) -> Void let dismiss: () -> Void + let boundsUpdated: ActionSlot init( context: AccountContext, @@ -45,7 +46,8 @@ final class SelectGiftPageContent: Component { selectedGiftIds: Set, starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>, selectGift: @escaping (GiftItem) -> Void, - dismiss: @escaping () -> Void + dismiss: @escaping () -> Void, + boundsUpdated: ActionSlot ) { self.context = context self.craftContext = craftContext @@ -56,6 +58,7 @@ final class SelectGiftPageContent: Component { self.starsTopUpOptions = starsTopUpOptions self.selectGift = selectGift self.dismiss = dismiss + self.boundsUpdated = boundsUpdated } static func ==(lhs: SelectGiftPageContent, rhs: SelectGiftPageContent) -> Bool { @@ -86,6 +89,9 @@ final class SelectGiftPageContent: Component { private var availableGifts: [GiftItem] = [] private var giftMap: [Int64: GiftItem] = [:] + private var availableSize: CGSize? + private var currentBounds: CGRect? + private var component: SelectGiftPageContent? private weak var state: EmptyComponentState? private var environment: ViewControllerComponentContainer.Environment? @@ -107,92 +113,16 @@ final class SelectGiftPageContent: Component { deinit { self.craftStateDisposable?.dispose() } - - func update(component: SelectGiftPageContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.isUpdating = true - defer { - self.isUpdating = false + + func updateScrolling(interactive: Bool, transition: ComponentTransition) -> CGFloat { + guard let bounds = self.currentBounds, let availableSize = self.availableSize, let component = self.component, let environment = self.environment else { + return 0.0 } - if self.component == nil { - let initialGiftItem = GiftItem( - gift: component.gift, - reference: .slug(slug: component.gift.slug) - ) - self.availableGifts = [ - initialGiftItem - ] - self.giftMap = [initialGiftItem.gift.id: initialGiftItem] - - self.craftStateDisposable = (component.craftContext.state - |> deliverOnMainQueue).start(next: { [weak self] state in - guard let self else { - return - } - self.craftState = state - - var items: [GiftItem] = [] - var map: [Int64: GiftItem] = [:] - var foundInitial = false - for gift in state.gifts { - guard let reference = gift.reference, case let .unique(uniqueGift) = gift.gift else { - continue - } - let giftItem = GiftItem( - gift: uniqueGift, - reference: reference - ) - map[uniqueGift.id] = giftItem - if uniqueGift.id == component.gift.id { - foundInitial = true - } else { - if component.selectedGiftIds.contains(uniqueGift.id) { - continue - } - items.append(giftItem) - } - } - - if !foundInitial { - map[initialGiftItem.gift.id] = initialGiftItem - } - self.availableGifts = items - self.giftMap = map - - self.state?.updated(transition: .spring(duration: 0.4)) - }) - } + let visibleBounds = bounds.insetBy(dx: 0.0, dy: -10.0) - let environment = environment[ViewControllerComponentContainer.Environment.self].value + var contentHeight: CGFloat = 88.0 + 32.0 - let sideInset: CGFloat = 16.0 + environment.safeInsets.left - - self.component = component - self.state = state - self.environment = environment - - self.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor - - var contentHeight: CGFloat = 88.0 - - let myGiftsTitleSize = self.myGiftsTitle.update( - transition: transition, - component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Select_YourGifts.uppercased(), font: Font.semibold(14.0), textColor: environment.theme.actionSheet.secondaryTextColor))) - ), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) - ) - let myGiftsTitleFrame = CGRect(origin: CGPoint(x: 26.0, y: contentHeight), size: myGiftsTitleSize) - if let myGiftsTitleView = self.myGiftsTitle.view { - if myGiftsTitleView.superview == nil { - self.addSubview(myGiftsTitleView) - } - transition.setFrame(view: myGiftsTitleView, frame: myGiftsTitleFrame) - } - - contentHeight += 32.0 - let itemSpacing: CGFloat = 10.0 let itemSideInset = 16.0 let itemsInRow: Int @@ -207,8 +137,7 @@ final class SelectGiftPageContent: Component { } let itemWidth = (availableSize.width - itemSideInset * 2.0 - itemSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow) let itemSize = CGSize(width: itemWidth, height: itemWidth) - - + var isLoading = false if self.availableGifts.isEmpty, case .loading = (self.craftState?.dataState ?? .loading) { isLoading = true @@ -228,10 +157,10 @@ final class SelectGiftPageContent: Component { var itemsHeight: CGFloat = 0.0 var validIds: [AnyHashable] = [] for gift in self.availableGifts { - let isVisible = "".isEmpty -// if visibleBounds.intersects(itemFrame) { -// isVisible = true -// } + var isVisible = false + if visibleBounds.intersects(itemFrame) { + isVisible = true + } if isVisible { let itemId = AnyHashable(gift.gift.id) validIds.append(itemId) @@ -255,8 +184,6 @@ final class SelectGiftPageContent: Component { } } - let badge: String? = gift.gift.craftChancePermille.flatMap { "+\($0 / 10)%" } - let _ = visibleItem.update( transition: itemTransition, component: AnyComponent( @@ -268,7 +195,7 @@ final class SelectGiftPageContent: Component { peer: nil, subject: .uniqueGift(gift: gift.gift, price: nil), ribbon: GiftItemComponent.Ribbon(text: ribbonText, font: .monospaced, color: ribbonColor, outline: nil), - badge: badge, + badge: gift.gift.craftChancePermille.flatMap { "+\($0 / 10)%" }, resellPrice: nil, isHidden: false, isSelected: false, @@ -361,6 +288,111 @@ final class SelectGiftPageContent: Component { contentHeight += 24.0 } + if let storeGiftsView = self.storeGifts.view as? GiftStoreContentComponent.View { + storeGiftsView.updateScrolling(bounds: bounds.offsetBy(dx: 0.0, dy: -contentHeight), interactive: interactive, transition: .immediate) + } + + return contentHeight + } + + func update(component: SelectGiftPageContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.availableSize = availableSize + if self.component == nil { + self.currentBounds = CGRect(origin: .zero, size: availableSize) + + component.boundsUpdated.connect { [weak self] bounds in + guard let self else { + return + } + self.currentBounds = bounds + let _ = self.updateScrolling(interactive: true, transition: .immediate) + } + + let initialGiftItem = GiftItem( + gift: component.gift, + reference: .slug(slug: component.gift.slug) + ) + self.availableGifts = [ + initialGiftItem + ] + self.giftMap = [initialGiftItem.gift.id: initialGiftItem] + + self.craftStateDisposable = (component.craftContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + self.craftState = state + + var items: [GiftItem] = [] + var map: [Int64: GiftItem] = [:] + var foundInitial = false + for gift in state.gifts { + guard let reference = gift.reference, case let .unique(uniqueGift) = gift.gift else { + continue + } + let giftItem = GiftItem( + gift: uniqueGift, + reference: reference + ) + map[uniqueGift.id] = giftItem + if uniqueGift.id == component.gift.id { + foundInitial = true + } else { + if component.selectedGiftIds.contains(uniqueGift.id) { + continue + } + items.append(giftItem) + } + } + + if !foundInitial { + map[initialGiftItem.gift.id] = initialGiftItem + } + self.availableGifts = items + self.giftMap = map + + self.state?.updated(transition: .spring(duration: 0.4)) + }) + } + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + self.component = component + self.state = state + self.environment = environment + + self.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor + + var contentHeight: CGFloat = 88.0 + + let myGiftsTitleSize = self.myGiftsTitle.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Select_YourGifts.uppercased(), font: Font.semibold(14.0), textColor: environment.theme.actionSheet.secondaryTextColor))) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let myGiftsTitleFrame = CGRect(origin: CGPoint(x: 26.0, y: contentHeight), size: myGiftsTitleSize) + if let myGiftsTitleView = self.myGiftsTitle.view { + if myGiftsTitleView.superview == nil { + self.addSubview(myGiftsTitleView) + } + transition.setFrame(view: myGiftsTitleView, frame: myGiftsTitleFrame) + } + + contentHeight += 32.0 + + contentHeight = self.updateScrolling(interactive: false, transition: transition) + let storeGiftsTitleSize = self.storeGiftsTitle.update( transition: transition, component: AnyComponent( @@ -496,6 +528,8 @@ private final class SheetContainerComponent: CombinedComponent { static var body: Body { let sheet = Child(ResizableSheetComponent.self) let animateOut = StoredActionSlot(Action.self) + + let boundsUpdated = ActionSlot() return { context in let component = context.component @@ -535,7 +569,8 @@ private final class SheetContainerComponent: CombinedComponent { selectGift: component.selectGift, dismiss: { dismiss(true) - } + }, + boundsUpdated: boundsUpdated ) ), titleItem: AnyComponent( @@ -578,7 +613,8 @@ private final class SheetContainerComponent: CombinedComponent { regularMetricsSize: CGSize(width: 430.0, height: 900.0), dismiss: { animated in dismiss(animated) - } + }, + boundsUpdated: boundsUpdated ) }, availableSize: context.availableSize, diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index f2efd7b646..262e1a9b51 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -1511,7 +1511,7 @@ private final class GiftViewSheetContent: CombinedComponent { } if case let .unique(gift) = arguments.gift, let resellAmount = gift.resellAmounts?.first, resellAmount.amount.value > 0 { - if arguments.reference != nil || gift.owner.peerId == context.account.peerId { + if arguments.reference != nil || gift.owner?.peerId == self.context.account.peerId { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_ChangePrice, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/PriceTag"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in @@ -2483,6 +2483,7 @@ private final class GiftViewSheetContent: CombinedComponent { var isMyHostedUniqueGift = false var releasedByPeer: EnginePeer? var canGiftUpgrade = false + var isDismantled = false if case let .soldOutGift(gift) = subject { animationFile = gift.file @@ -2571,6 +2572,10 @@ private final class GiftViewSheetContent: CombinedComponent { titleString = "" } + if let uniqueGift, uniqueGift.owner == nil { + isDismantled = true + } + if !canUpgrade, let gift = state.starGiftsMap[giftId], let _ = gift.upgradeStars { canUpgrade = true } @@ -3764,6 +3769,8 @@ private final class GiftViewSheetContent: CombinedComponent { ) ) )) + default: + break } if let peerId = uniqueGift.hostPeerId, let peer = state.peerMap[peerId] { @@ -4332,7 +4339,7 @@ private final class GiftViewSheetContent: CombinedComponent { ) ), at: hasOriginalInfo ? tableItems.count - 1 : tableItems.count) - if let valueAmount = uniqueGift.valueAmount, let valueCurrency = uniqueGift.valueCurrency { + if let valueAmount = uniqueGift.valueAmount, let valueCurrency = uniqueGift.valueCurrency, !isDismantled { tableItems.insert(.init( id: "fiatValue", title: strings.Gift_Unique_Value, @@ -4545,7 +4552,7 @@ private final class GiftViewSheetContent: CombinedComponent { if let controller = controller() as? GiftViewScreen, controller.openChatTheme != nil { isChatTheme = true } - if ((incoming && !converted && !upgraded) || exported || selling || isChatTheme) && (!showUpgradePreview && !showWearPreview) { + if ((incoming && !converted && !upgraded) || exported || selling || isChatTheme) && (!showUpgradePreview && !showWearPreview && !isDismantled) { let textFont = Font.regular(13.0) let textColor = theme.list.itemSecondaryTextColor let linkColor = theme.actionSheet.controlAccentColor @@ -4654,6 +4661,7 @@ private final class GiftViewSheetContent: CombinedComponent { foreground: theme.list.itemCheckColors.foregroundColor, pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ) + let buttonChild: _UpdatedChildComponent if state.canSkip { buttonChild = button.update( @@ -5027,7 +5035,7 @@ private final class GiftViewSheetContent: CombinedComponent { availableSize: buttonSize, transition: context.transition ) - } else if incoming && !converted && !savedToProfile { + } else if incoming && !converted && !savedToProfile && !isDismantled { let buttonTitle = isChannelGift ? strings.Gift_View_Display_Channel : strings.Gift_View_Display buttonChild = button.update( component: ButtonComponent( @@ -5246,7 +5254,7 @@ private final class GiftViewSheetContent: CombinedComponent { )) var rightControlItems: [GlassControlGroupComponent.Item] = [] - if uniqueGift != nil && !showWearPreview { + if uniqueGift != nil && !showWearPreview && !isDismantled { if let _ = component.subject.arguments?.canCraftDate { var canCraft = false if let data = component.context.currentAppConfiguration.with({ $0 }).data { From f0c0f806e66e9de7160695cececce6028b04a899 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 30 Jan 2026 01:45:59 +0400 Subject: [PATCH 11/12] Various improvements --- .../Sources/GiftCraftScreen.swift | 2 +- .../Sources/GiftViewScreen.swift | 9 +++- .../StoryQualityUpgradeSheetScreen/BUILD | 1 + ...yQualityUpgradeSheetContentComponent.swift | 1 - .../StoryQualityUpgradeSheetScreen.swift | 46 +++++++++++-------- 5 files changed, 36 insertions(+), 23 deletions(-) delete mode 100644 submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/Sources/StoryQualityUpgradeSheetContentComponent.swift diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift index 0935742682..bdcc2ad5dc 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift @@ -1245,7 +1245,7 @@ private final class CraftGiftPageContent: Component { } - self.infoBackground.backgroundColor = environment.theme.list.plainBackgroundColor.cgColor + self.infoBackground.backgroundColor = environment.theme.list.modalPlainBackgroundColor.cgColor let infoBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 80.0), size: CGSize(width: availableSize.width, height: 1000.0)) transition.setFrame(layer: self.infoBackground, frame: infoBackgroundFrame) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 262e1a9b51..118b4c5739 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -5606,7 +5606,14 @@ public class GiftViewScreen: ViewControllerComponentContainer { if case let .unique(uniqueGift) = gift.gift { resellAmounts = uniqueGift.resellAmounts } - return (peerId, gift.fromPeer?.id, gift.fromPeer?.debugDisplayTitle, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellAmounts, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate, gift.prepaidUpgradeHash, gift.upgradeSeparate, gift.dropOriginalDetailsStars, nil, gift.number, gift.canCraftAt) + + var number: Int32? + if case let .unique(uniqueGift) = gift.gift { + number = uniqueGift.number + } else if let numberValue = gift.number { + number = numberValue + } + return (peerId, gift.fromPeer?.id, gift.fromPeer?.debugDisplayTitle, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellAmounts, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate, gift.prepaidUpgradeHash, gift.upgradeSeparate, gift.dropOriginalDetailsStars, nil, number, gift.canCraftAt) case .soldOutGift: return nil case .upgradePreview: diff --git a/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/BUILD index 316cc15fa8..d0df650baa 100644 --- a/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/BUILD @@ -22,6 +22,7 @@ swift_library( "//submodules/Components/SheetComponent", "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/Sources/StoryQualityUpgradeSheetContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/Sources/StoryQualityUpgradeSheetContentComponent.swift deleted file mode 100644 index 8b13789179..0000000000 --- a/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/Sources/StoryQualityUpgradeSheetContentComponent.swift +++ /dev/null @@ -1 +0,0 @@ - diff --git a/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/Sources/StoryQualityUpgradeSheetScreen.swift b/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/Sources/StoryQualityUpgradeSheetScreen.swift index 024a40960a..bf0ea4525f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/Sources/StoryQualityUpgradeSheetScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen/Sources/StoryQualityUpgradeSheetScreen.swift @@ -12,6 +12,7 @@ import BalancedTextComponent import Markdown import TelegramStringFormatting import BundleIconComponent +import GlassBarButtonComponent public final class ButtonSubtitleComponent: CombinedComponent { public let title: String @@ -124,23 +125,29 @@ private final class StoryQualityUpgradeSheetContentComponent: Component { } let cancelButtonSize = cancelButton.update( transition: transition, - component: AnyComponent(Button( - content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.list.itemAccentColor)), - action: { [weak self] in - guard let self, let component = self.component 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: { _ in component.dismiss() } - ).minSize(CGSize(width: 8.0, height: 44.0))), + )), environment: {}, - containerSize: CGSize(width: 200.0, height: 100.0) + containerSize: CGSize(width: 44.0, height: 44.0) ) if let cancelButtonView = cancelButton.view { if cancelButtonView.superview == nil { self.addSubview(cancelButtonView) } - transition.setFrame(view: cancelButtonView, frame: CGRect(origin: CGPoint(x: 16.0, y: 6.0), size: cancelButtonSize)) + transition.setFrame(view: cancelButtonView, frame: CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelButtonSize)) } var contentHeight: CGFloat = 0.0 @@ -152,7 +159,8 @@ private final class StoryQualityUpgradeSheetContentComponent: Component { content: LottieComponent.AppBundleContent(name: "StoryUpgradeSheet"), color: nil, startingPosition: .begin, - size: CGSize(width: 100.0, height: 100.0) + size: CGSize(width: 100.0, height: 100.0), + loop: true )), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) @@ -217,16 +225,18 @@ private final class StoryQualityUpgradeSheetContentComponent: Component { color: environment.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7) )))) + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let buttonSize = self.button.update( transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: environment.theme.list.itemCheckColors.fillColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) ), content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( - VStack(buttonContents, spacing: 3.0) + VStack(buttonContents, spacing: 1.0) )), isEnabled: true, allowActionWhenDisabled: true, @@ -240,9 +250,9 @@ private final class StoryQualityUpgradeSheetContentComponent: Component { } )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) ) - let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize) + let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: buttonSize) if let buttonView = self.button.view { if buttonView.superview == nil { self.addSubview(buttonView) @@ -250,12 +260,7 @@ private final class StoryQualityUpgradeSheetContentComponent: Component { transition.setFrame(view: buttonView, frame: buttonFrame) } contentHeight += buttonSize.height - - if environment.safeInsets.bottom.isZero { - contentHeight += 16.0 - } else { - contentHeight += environment.safeInsets.bottom + 14.0 - } + contentHeight += buttonInsets.bottom return CGSize(width: availableSize.width, height: contentHeight) } @@ -355,7 +360,8 @@ private final class StoryQualityUpgradeSheetScreenComponent: Component { }) } )), - backgroundColor: .color(environment.theme.overallDarkAppearance ? environment.theme.list.itemBlocksBackgroundColor : environment.theme.list.blocksBackgroundColor), + style: .glass, + backgroundColor: .color(environment.theme.list.modalPlainBackgroundColor), animateOut: self.sheetAnimateOut )), environment: { From 90a4dbe855d4d2542a292713ebf919f5846f7705 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 30 Jan 2026 03:21:18 +0400 Subject: [PATCH 12/12] Fix stake dice result message --- .../Sources/ServiceMessageStrings.swift | 22 ++++++++++++++++++- .../StringForMessageTimestampStatus.swift | 3 +-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index d1e8365451..0952a9b132 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1780,7 +1780,27 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, rawString = strings.Conversation_EmojiStake_WonYou(value).string } } else { - let compactPeerName = message.peers[message.id.peerId].flatMap(EnginePeer.init)?.compactDisplayTitle ?? "" + var compactPeerName = "" + if let authorSignature = message.forwardInfo?.authorSignature { + compactPeerName = authorSignature + } else if let author = message.forwardInfo?.author { + compactPeerName = EnginePeer(author).compactDisplayTitle + } else if let source = message.forwardInfo?.source { + compactPeerName = EnginePeer(source).compactDisplayTitle + } else { + if let author = message.author, case .user = author { + compactPeerName = author.compactDisplayTitle + } else { + for attribute in message.attributes { + if let attribute = attribute as? AuthorSignatureMessageAttribute { + compactPeerName = attribute.signature + } + } + if compactPeerName.isEmpty { + compactPeerName = message.peers[message.id.peerId].flatMap(EnginePeer.init)?.compactDisplayTitle ?? "" + } + } + } if value == 1, let tonAmount = dice.tonAmount { let value = formatTonAmountText(tonAmount, dateTimeFormat: dateTimeFormat) rawString = strings.Conversation_EmojiStake_Lost(compactPeerName, value).string diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift index 926ccdee44..938c5ad110 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift @@ -149,8 +149,7 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess dayText = strings.Date_ChatDateHeaderYear(monthAtIndex(Int(timeinfo.tm_mon), strings: strings), "\(timeinfo.tm_mday)", "\(1900 + timeinfo.tm_year)").string } dateText = strings.Message_FullDateFormat(dayText, stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: dateTimeFormat)).string - } - else if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) { + } else if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) { dateText = strings.Message_ImportedDateFormat(dateStringForDay(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: forwardInfo.date), stringForMessageTimestamp(timestamp: forwardInfo.date, dateTimeFormat: dateTimeFormat), dateText).string }