diff --git a/Telegram/Telegram-iOS/Resources/CraftFail.tgs b/Telegram/Telegram-iOS/Resources/CraftFail.tgs deleted file mode 100644 index eaf5e1db3f..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/CraftFail.tgs and /dev/null differ diff --git a/Telegram/Telegram-iOS/Resources/CraftFailOverlay.tgs b/Telegram/Telegram-iOS/Resources/CraftFailOverlay.tgs deleted file mode 100644 index 983d067102..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/CraftFailOverlay.tgs and /dev/null differ 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 0000000000..67f351d03a Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Stars.imageset/star (3).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ton.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ton.imageset/Contents.json new file mode 100644 index 0000000000..24d7aec578 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ton.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ton (3).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ton.imageset/ton (3).pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ton.imageset/ton (3).pdf new file mode 100644 index 0000000000..494a8fa170 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ton.imageset/ton (3).pdf differ 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 0000000000..2bdf30a259 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/giftcrash.pdf differ 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 {