diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 4a7d0d41da..1ecd06f426 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9100,7 +9100,7 @@ Sorry for the inconvenience."; "TextFormat.EditLinkTitle" = "Edit Link"; -"PeerInfo.Username" = "Username"; +"PeerInfo.BotLinks" = "Public Links"; "Username.BotLinksOrderInfo" = "Drag and drop links to change the order in which they will be displayed on the bot info page."; "Wallpaper.ApplyForAll" = "Apply For All Chats"; @@ -9140,6 +9140,10 @@ Sorry for the inconvenience."; "Username.BotLinkHint" = "This username cannot be edited."; "Username.BotLinkHintExtended" = "This username cannot be edited. You can acquire additional usernames on [Fragment]()."; +"Username.BotActivateAlertText" = "Do you want to show this link on the bot's info page?"; +"Username.BotDeactivateAlertText" = "Do you want to hide this link from the bot's info page?"; +"Username.BotActiveLimitReachedError" = "Sorry, you have too many active public links already. Please hide one of the bot's active public links first."; + "PeerInfo.Bot.EditIntro" = "Edit Intro"; "PeerInfo.Bot.EditCommands" = "Edit Commands"; "PeerInfo.Bot.ChangeSettings" = "Change Bot Settings"; @@ -9327,3 +9331,5 @@ Sorry for the inconvenience."; "ChatList.EmptyListContactsHeader" = "YOUR CONTACTS ON TELEGRAM"; "ChatList.EmptyListContactsHeaderHide" = "hide"; "ChatList.EmptyListTooltip" = "Send a message or\nstart a group here."; + +"Username.BotTitle" = "Public Links"; diff --git a/submodules/AnimationUI/Sources/AnimationNode.swift b/submodules/AnimationUI/Sources/AnimationNode.swift index c7dfb88f43..4c42e29c82 100644 --- a/submodules/AnimationUI/Sources/AnimationNode.swift +++ b/submodules/AnimationUI/Sources/AnimationNode.swift @@ -117,6 +117,12 @@ public final class AnimationNode: ASDisplayNode { self.animationView()?.currentProgress = progress } + public func animate(from: CGFloat, to: CGFloat, completion: @escaping () -> Void) { + self.animationView()?.play(fromProgress: from, toProgress: to, completion: { _ in + completion() + }) + } + public func setAnimation(name: String, colors: [String: UIColor]? = nil) { self.currentParams = (name, colors) if let url = getAppBundle().url(forResource: name, withExtension: "json"), let animation = Animation.filepath(url.path) { diff --git a/submodules/Display/Source/CAAnimationUtils.swift b/submodules/Display/Source/CAAnimationUtils.swift index ac53fb66ca..585317db34 100644 --- a/submodules/Display/Source/CAAnimationUtils.swift +++ b/submodules/Display/Source/CAAnimationUtils.swift @@ -392,6 +392,16 @@ public extension CALayer { self.animate(from: NSValue(cgPoint: from), to: NSValue(cgPoint: to), keyPath: "position", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) } + func animateAnchorPoint(from: CGPoint, to: CGPoint, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if from == to && !force { + if let completion = completion { + completion(true) + } + return + } + self.animate(from: NSValue(cgPoint: from), to: NSValue(cgPoint: to), keyPath: "anchorPoint", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) + } + func animateBounds(from: CGRect, to: CGRect, duration: Double, delay: Double = 0.0, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if from == to && !force { if let completion = completion { diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 5686e645fb..6aaea8af80 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -423,6 +423,29 @@ public extension ContainedViewLayoutTransition { } } + func updateAnchorPoint(layer: CALayer, anchorPoint: CGPoint, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if layer.anchorPoint.equalTo(anchorPoint) && !force { + completion?(true) + } else { + switch self { + case .immediate: + layer.removeAnimation(forKey: "anchorPoint") + layer.anchorPoint = anchorPoint + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousAnchorPoint = layer.anchorPoint + layer.anchorPoint = anchorPoint + layer.animateAnchorPoint(from: previousAnchorPoint, to: anchorPoint, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + } + func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: diff --git a/submodules/SettingsUI/Sources/UsernameSetupController.swift b/submodules/SettingsUI/Sources/UsernameSetupController.swift index 4959dbfba0..c3eff1a4ba 100644 --- a/submodules/SettingsUI/Sources/UsernameSetupController.swift +++ b/submodules/SettingsUI/Sources/UsernameSetupController.swift @@ -488,13 +488,23 @@ public func usernameSetupController(context: AccountContext, mode: UsernameSetup }, activateLink: { name in dismissInputImpl?() let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Username_ActivateAlertTitle, text: presentationData.strings.Username_ActivateAlertText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Username_ActivateAlertShow, action: { + let alertText: String + if case .bot = mode { + alertText = presentationData.strings.Username_BotActivateAlertText + } else { + alertText = presentationData.strings.Username_ActivateAlertText + } + presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Username_ActivateAlertTitle, text: alertText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Username_ActivateAlertShow, action: { let _ = (context.engine.peers.toggleAddressNameActive(domain: domain, name: name, active: true) |> deliverOnMainQueue).start(error: { error in let errorText: String switch error { case .activeLimitReached: - errorText = presentationData.strings.Username_ActiveLimitReachedError + if case .bot = mode { + errorText = presentationData.strings.Username_BotActiveLimitReachedError + } else { + errorText = presentationData.strings.Username_ActiveLimitReachedError + } default: errorText = presentationData.strings.Login_UnknownError } @@ -504,7 +514,13 @@ public func usernameSetupController(context: AccountContext, mode: UsernameSetup }, deactivateLink: { name in dismissInputImpl?() let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Username_DeactivateAlertTitle, text: presentationData.strings.Username_DeactivateAlertText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Username_DeactivateAlertHide, action: { + let alertText: String + if case .bot = mode { + alertText = presentationData.strings.Username_BotDeactivateAlertText + } else { + alertText = presentationData.strings.Username_DeactivateAlertText + } + presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Username_DeactivateAlertTitle, text: alertText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Username_DeactivateAlertHide, action: { let _ = context.engine.peers.toggleAddressNameActive(domain: domain, name: name, active: false).start() })]), nil) }, openAuction: { username in @@ -577,7 +593,13 @@ public func usernameSetupController(context: AccountContext, mode: UsernameSetup dismissImpl?() }) - let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Username_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let title: String + if case .bot = mode { + title = presentationData.strings.Username_BotTitle + } else { + title = presentationData.strings.Username_Title + } + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: usernameSetupControllerEntries(presentationData: presentationData, view: view, state: state, temporaryOrder: temporaryOrder, mode: mode), style: .blocks, focusItemTag: mode == .account ? UsernameEntryTag.username : nil, animateChanges: true) return (controllerState, (listState, arguments)) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 1c6a9576ec..c399cfad06 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -860,12 +860,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let wallpaperPreviewController = WallpaperGalleryController(context: strongSelf.context, source: .wallpaper(wallpaper, nil, [], nil, nil, nil), mode: .peer(EnginePeer(peer), true)) wallpaperPreviewController.apply = { [weak wallpaperPreviewController] entry, options, _, _, brightness in var settings: WallpaperSettings? - if case let .wallpaper(wallpaper, _) = entry, case let .file(file) = wallpaper, !file.isPattern { - var intensity: Int32? - if let brightness { - intensity = max(0, min(100, Int32(brightness * 100.0))) + if case let .wallpaper(wallpaper, _) = entry { + let baseSettings = wallpaper.settings + var intensity: Int32? = baseSettings?.intensity + if case let .file(file) = wallpaper, !file.isPattern { + if let brightness { + intensity = max(0, min(100, Int32(brightness * 100.0))) + } } - settings = WallpaperSettings(blur: options.contains(.blur), motion: options.contains(.motion), intensity: intensity) + settings = WallpaperSettings(blur: options.contains(.blur), motion: options.contains(.motion), colors: baseSettings?.colors ?? [], intensity: intensity, rotation: baseSettings?.rotation) } let _ = (strongSelf.context.engine.themes.setExistingChatWallpaper(messageId: message.id, settings: settings) |> deliverOnMainQueue).start() diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index ed8fbd51da..1edb30a8f1 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -1022,6 +1022,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { final class PeerInfoAvatarListNode: ASDisplayNode { private let isSettings: Bool + let containerNode: ASDisplayNode let pinchSourceNode: PinchSourceContainerNode let bottomCoverNode: ASDisplayNode fileprivate let maskNode: DynamicIslandMaskNode @@ -1040,6 +1041,8 @@ final class PeerInfoAvatarListNode: ASDisplayNode { init(context: AccountContext, readyWhenGalleryLoads: Bool, isSettings: Bool) { self.isSettings = isSettings + + self.containerNode = ASDisplayNode() self.bottomCoverNode = ASDisplayNode() self.bottomCoverNode.backgroundColor = .black @@ -1057,12 +1060,13 @@ final class PeerInfoAvatarListNode: ASDisplayNode { super.init() - self.addSubnode(self.bottomCoverNode) - self.addSubnode(self.pinchSourceNode) + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.bottomCoverNode) + self.containerNode.addSubnode(self.pinchSourceNode) self.pinchSourceNode.contentNode.addSubnode(self.avatarContainerNode) self.listContainerTransformNode.addSubnode(self.listContainerNode) self.pinchSourceNode.contentNode.addSubnode(self.listContainerTransformNode) - self.addSubnode(self.topCoverNode) + self.containerNode.addSubnode(self.topCoverNode) let avatarReady = (self.avatarContainerNode.avatarNode.ready |> mapToSignal { _ -> Signal in @@ -1129,6 +1133,7 @@ final class PeerInfoAvatarListNode: ASDisplayNode { self.arguments = (peer, threadId, threadInfo, theme, avatarSize, isExpanded) self.maskNode.isForum = isForum self.pinchSourceNode.update(size: size, transition: transition) + self.containerNode.frame = CGRect(origin: CGPoint(), size: size) self.pinchSourceNode.frame = CGRect(origin: CGPoint(), size: size) self.avatarContainerNode.update(peer: peer, threadId: threadId, threadInfo: threadInfo, item: self.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: self.isSettings) } @@ -2244,6 +2249,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { var skipCollapseCompletion = false var ignoreCollapse = false + let avatarClippingNode: SparseNode let avatarListNode: PeerInfoAvatarListNode let buttonsContainerNode: SparseNode @@ -2305,6 +2311,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { var emojiStatusPackDisposable = MetaDisposable() var emojiStatusFileAndPackTitle = Promise<(TelegramMediaFile, LoadedStickerPack)?>() + private var validWidth: CGFloat? + init(context: AccountContext, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, isMediaOnly: Bool, isSettings: Bool, forumTopicThreadId: Int64?, chatLocation: ChatLocation) { self.context = context self.isAvatarExpanded = avatarInitiallyExpanded @@ -2314,6 +2322,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.forumTopicThreadId = forumTopicThreadId self.chatLocation = chatLocation + self.avatarClippingNode = SparseNode() + self.avatarClippingNode.clipsToBounds = true self.avatarListNode = PeerInfoAvatarListNode(context: context, readyWhenGalleryLoads: avatarInitiallyExpanded, isSettings: isSettings) self.titleNodeContainer = ASDisplayNode() @@ -2388,7 +2398,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { self?.requestUpdateLayout?(false) } - if !isMediaOnly { self.addSubnode(self.buttonsContainerNode) } @@ -2400,7 +2409,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { // self.subtitleNodeContainer.addSubnode(self.nextPanelSubtitleNode) self.usernameNodeContainer.addSubnode(self.usernameNode) - self.regularContentNode.addSubnode(self.avatarListNode) + self.regularContentNode.addSubnode(self.avatarClippingNode) + self.avatarClippingNode.addSubnode(self.avatarListNode) self.regularContentNode.addSubnode(self.avatarListNode.listContainerNode.controlsClippingOffsetNode) self.regularContentNode.addSubnode(self.titleNodeContainer) self.regularContentNode.addSubnode(self.subtitleNodeContainer) @@ -2567,6 +2577,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.peer = peer self.threadData = threadData self.avatarListNode.listContainerNode.peer = peer + self.validWidth = width let previousPanelStatusData = self.currentPanelStatusData self.currentPanelStatusData = panelStatusData.0 @@ -3277,6 +3288,14 @@ final class PeerInfoHeaderNode: ASDisplayNode { apparentAvatarFrame = CGRect(origin: CGPoint(x: avatarCenter.x - avatarFrame.width / 2.0, y: -contentOffset + avatarOffset + avatarCenter.y - avatarFrame.height / 2.0), size: avatarFrame.size) controlsClippingFrame = apparentAvatarFrame } + + let avatarClipOffset: CGFloat = !self.isAvatarExpanded && deviceMetrics.hasDynamicIsland ? 48.0 : 0.0 + let clippingNodeTransition = ContainedViewLayoutTransition.immediate + clippingNodeTransition.updateFrame(layer: self.avatarClippingNode.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: avatarClipOffset), size: CGSize(width: width, height: 1000.0))) + clippingNodeTransition.updateSublayerTransformOffset(layer: self.avatarClippingNode.layer, offset: CGPoint(x: 0.0, y: -avatarClipOffset)) + let clippingNodeRadiusTransition = ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut) + clippingNodeRadiusTransition.updateCornerRadius(node: self.avatarClippingNode, cornerRadius: avatarClipOffset > 0.0 ? width / 2.5 : 0.0) + transition.updateFrameAdditive(node: self.avatarListNode, frame: CGRect(origin: apparentAvatarFrame.center, size: CGSize())) transition.updateFrameAdditive(node: self.avatarOverlayNode, frame: CGRect(origin: apparentAvatarFrame.center, size: CGSize())) @@ -3318,26 +3337,34 @@ final class PeerInfoHeaderNode: ASDisplayNode { } if deviceMetrics.hasDynamicIsland && self.forumTopicThreadId == nil { - self.avatarListNode.maskNode.frame = CGRect(origin: CGPoint(x: -85.5, y: -self.avatarListNode.frame.minY + 48.0), size: CGSize(width: 171.0, height: 171.0)) - self.avatarListNode.bottomCoverNode.frame = self.avatarListNode.maskNode.frame - self.avatarListNode.topCoverNode.frame = self.avatarListNode.maskNode.frame - let maskValue = max(0.0, min(1.0, contentOffset / 120.0)) + self.avatarListNode.containerNode.view.mask = self.avatarListNode.maskNode.view if maskValue > 0.03 { self.avatarListNode.bottomCoverNode.isHidden = false self.avatarListNode.topCoverNode.isHidden = false - self.avatarListNode.view.mask = self.avatarListNode.maskNode.view + self.avatarListNode.maskNode.backgroundColor = .clear } else { self.avatarListNode.bottomCoverNode.isHidden = true self.avatarListNode.topCoverNode.isHidden = true - self.avatarListNode.view.mask = nil + self.avatarListNode.maskNode.backgroundColor = .white } - self.avatarListNode.maskNode.update(maskValue) self.avatarListNode.topCoverNode.update(maskValue) + self.avatarListNode.maskNode.update(maskValue) + + self.avatarListNode.listContainerNode.topShadowNode.isHidden = !self.isAvatarExpanded + + self.avatarListNode.maskNode.position = CGPoint(x: 0.0, y: -self.avatarListNode.frame.minY + 48.0 + 85.5) + self.avatarListNode.maskNode.bounds = CGRect(origin: .zero, size: CGSize(width: 171.0, height: 171.0)) + + self.avatarListNode.bottomCoverNode.position = self.avatarListNode.maskNode.position + self.avatarListNode.bottomCoverNode.bounds = self.avatarListNode.maskNode.bounds + + self.avatarListNode.topCoverNode.position = self.avatarListNode.maskNode.position + self.avatarListNode.topCoverNode.bounds = self.avatarListNode.maskNode.bounds } else { self.avatarListNode.bottomCoverNode.isHidden = true self.avatarListNode.topCoverNode.isHidden = true - self.avatarListNode.view.mask = nil + self.avatarListNode.containerNode.view.mask = nil } self.avatarListNode.listContainerNode.update(size: expandedAvatarListSize, peer: peer, isExpanded: self.isAvatarExpanded, transition: transition) @@ -3662,12 +3689,22 @@ final class PeerInfoHeaderNode: ASDisplayNode { if case .animated = transition, !isAvatarExpanded { self.avatarListNode.animateAvatarCollapse(transition: transition) } + + if let width = self.validWidth { + let maskScale: CGFloat = isAvatarExpanded ? width / 100.0 : 1.0 + transition.updateTransformScale(layer: self.avatarListNode.maskNode.layer, scale: maskScale) + transition.updateTransformScale(layer: self.avatarListNode.bottomCoverNode.layer, scale: maskScale) + transition.updateTransformScale(layer: self.avatarListNode.topCoverNode.layer, scale: maskScale) + + let maskAnchorPoint = CGPoint(x: 0.5, y: isAvatarExpanded ? 0.37 : 0.5) + transition.updateAnchorPoint(layer: self.avatarListNode.maskNode.layer, anchorPoint: maskAnchorPoint) + } } } } private class DynamicIslandMaskNode: ASDisplayNode { - private var animationNode: AnimationNode? + var animationNode: AnimationNode? var isForum = false { didSet { @@ -3693,6 +3730,8 @@ private class DynamicIslandMaskNode: ASDisplayNode { self.animationNode?.setProgress(value) } + var animating = false + override func layout() { self.animationNode?.frame = self.bounds } @@ -3701,7 +3740,7 @@ private class DynamicIslandMaskNode: ASDisplayNode { private class DynamicIslandBlurNode: ASDisplayNode { private var effectView: UIVisualEffectView? private let fadeNode = ASDisplayNode() - private let gradientNode = ASImageNode() + let gradientNode = ASImageNode() private var hierarchyTrackingNode: HierarchyTrackingNode? diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 27f6840b40..8c99cffcda 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -1401,7 +1401,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL let ItemBotInfo = 9 if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { - items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text("@\(user.addressName ?? "")"), text: presentationData.strings.PeerInfo_Username, icon: nil, action: { + items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text("@\(user.addressName ?? "")"), text: presentationData.strings.PeerInfo_BotLinks, icon: nil, action: { interaction.editingOpenPublicLinkSetup() }))