From 732a43011979812d66772e9771d9c24e0a906e64 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 8 Apr 2025 20:07:07 +0400 Subject: [PATCH 1/4] Fix negative number formatting --- .../Sources/NumericFormat.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/submodules/TelegramPresentationData/Sources/NumericFormat.swift b/submodules/TelegramPresentationData/Sources/NumericFormat.swift index cc556add44..368f464e75 100644 --- a/submodules/TelegramPresentationData/Sources/NumericFormat.swift +++ b/submodules/TelegramPresentationData/Sources/NumericFormat.swift @@ -27,15 +27,19 @@ public func presentationStringsFormattedNumber(_ count: Int32, _ groupingSeparat if groupingSeparator.isEmpty || abs(count) < 1000 { return string } else { - var groupedString: String = "" - for i in 0 ..< Int(ceil(Double(string.count) / 3.0)) { - let index = string.count - Int(i + 1) * 3 - if !groupedString.isEmpty { - groupedString = groupingSeparator + groupedString + if count < 0 { + return "-\(presentationStringsFormattedNumber(abs(count), groupingSeparator))" + } else { + var groupedString: String = "" + for i in 0 ..< Int(ceil(Double(string.count) / 3.0)) { + let index = string.count - Int(i + 1) * 3 + if !groupedString.isEmpty { + groupedString = groupingSeparator + groupedString + } + groupedString = String(string[string.index(string.startIndex, offsetBy: max(0, index)) ..< string.index(string.startIndex, offsetBy: index + 3)]) + groupedString } - groupedString = String(string[string.index(string.startIndex, offsetBy: max(0, index)) ..< string.index(string.startIndex, offsetBy: index + 3)]) + groupedString + return groupedString } - return groupedString } } From a2234e9271900192390598197084bebababea734 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 8 Apr 2025 20:17:13 +0400 Subject: [PATCH 2/4] Conference updates --- .../ContactMultiselectionController.swift | 1 + .../CallListUI/Sources/CallListCallItem.swift | 2 +- .../Sources/CallListController.swift | 15 +- .../ChatMessageCallBubbleContentNode.swift | 2 +- .../ContactMultiselectionController.swift | 7 + .../ContactMultiselectionControllerNode.swift | 130 ++++++++++++++++-- .../Sources/SharedAccountContext.swift | 5 +- 7 files changed, 144 insertions(+), 18 deletions(-) diff --git a/submodules/AccountContext/Sources/ContactMultiselectionController.swift b/submodules/AccountContext/Sources/ContactMultiselectionController.swift index 43f815e7d0..6126ff1075 100644 --- a/submodules/AccountContext/Sources/ContactMultiselectionController.swift +++ b/submodules/AccountContext/Sources/ContactMultiselectionController.swift @@ -157,4 +157,5 @@ public protocol ContactMultiselectionController: ViewController { var result: Signal { get } var displayProgress: Bool { get set } var dismissed: (() -> Void)? { get set } + var isCallVideoOptionSelected: Bool { get } } diff --git a/submodules/CallListUI/Sources/CallListCallItem.swift b/submodules/CallListUI/Sources/CallListCallItem.swift index a949037d2f..a408a63cbc 100644 --- a/submodules/CallListUI/Sources/CallListCallItem.swift +++ b/submodules/CallListUI/Sources/CallListCallItem.swift @@ -704,7 +704,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { strongSelf.conferenceAvatarListNode = conferenceAvatarListNode strongSelf.containerNode.addSubnode(conferenceAvatarListNode) } - let avatarListContents = conferenceAvatarListContext.update(peers: conferenceAvatars, animated: false) + let avatarListContents = conferenceAvatarListContext.update(peers: Array(conferenceAvatars.prefix(3)), animated: false) let avatarListSize = conferenceAvatarListNode.update(context: item.context, content: avatarListContents, itemSize: CGSize(width: CGFloat(multipleAvatarDiameter), height: CGFloat(multipleAvatarDiameter)), customSpacing: multipleAvatarDiameter - 8.0, font: multipleAvatarFont, animated: false, synchronousLoad: synchronousLoads) conferenceAvatarListNode.frame = CGRect(origin: CGPoint(x: avatarFrame.minX + floor((avatarFrame.width - avatarListSize.width) / 2.0), y: avatarFrame.minY + floor((avatarFrame.height - avatarListSize.height) / 2.0)), size: avatarListSize) } else { diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 0607e99d09..1acf78752c 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -208,7 +208,7 @@ public final class CallListController: TelegramBaseController { } } - private func createGroupCall(peerIds: [EnginePeer.Id], completion: (() -> Void)? = nil) { + private func createGroupCall(peerIds: [EnginePeer.Id], isVideo: Bool, completion: (() -> Void)? = nil) { self.view.endEditing(true) guard !self.presentAccountFrozenInfoIfNeeded() else { @@ -274,7 +274,7 @@ public final class CallListController: TelegramBaseController { isStream: false ), reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash), - beginWithVideo: false, + beginWithVideo: isVideo, invitePeerIds: peerIds ) completion?() @@ -401,7 +401,7 @@ public final class CallListController: TelegramBaseController { } }, createGroupCall: { [weak self] in if let strongSelf = self { - strongSelf.createGroupCall(peerIds: []) + strongSelf.createGroupCall(peerIds: [], isVideo: false) } }) @@ -520,7 +520,7 @@ public final class CallListController: TelegramBaseController { guard let self else { return } - self.createGroupCall(peerIds: []) + self.createGroupCall(peerIds: [], isVideo: false) }, clearHighlightAutomatically: true)] let controller = self.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams( @@ -565,11 +565,12 @@ public final class CallListController: TelegramBaseController { controller?.dismiss() return } + + let isVideo = controller?.isCallVideoOptionSelected ?? false if peerIds.count == 1 { - //TODO:release isVideo controller?.dismiss() - self.call(peerIds[0], isVideo: false, began: { [weak self] in + self.call(peerIds[0], isVideo: isVideo, began: { [weak self] in if let strongSelf = self { let _ = (strongSelf.context.sharedContext.hasOngoingCall.get() |> filter { $0 } @@ -586,7 +587,7 @@ public final class CallListController: TelegramBaseController { } }) } else { - self.createGroupCall(peerIds: peerIds, completion: { + self.createGroupCall(peerIds: peerIds, isVideo: isVideo, completion: { controller?.dismiss() }) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift index 8f414535fd..03136f8bde 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift @@ -323,7 +323,7 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.addSubnode(peopleAvatarsNode) } - let peopleAvatarsContent = peopleAvatarsContext.update(peers: peopleAvatars.map(EnginePeer.init), animated: false) + let peopleAvatarsContent = peopleAvatarsContext.update(peers: peopleAvatars.prefix(3).map(EnginePeer.init), animated: false) let peopleAvatarsSize = peopleAvatarsNode.update(context: item.context, content: peopleAvatarsContent, itemSize: CGSize(width: peopleAvatarSize, height: peopleAvatarSize), customSpacing: peopleAvatarSize - peopleAvatarSpacing, font: avatarFont, animated: false, synchronousLoad: false) peopleAvatarsNode.frame = CGRect(origin: CGPoint(x: labelFrame.maxX + avatarsLeftInset, y: labelFrame.minY - 1.0), size: peopleAvatarsSize) } else { diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index e97a1232a6..31ffa8fc5f 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -86,6 +86,13 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection private let onlyWriteable: Bool private let isGroupInvitation: Bool private let limit: Int32? + + public var isCallVideoOptionSelected: Bool { + guard self.displayNode.isNodeLoaded, let displayNode = self.displayNode as? ContactMultiselectionControllerNode else { + return false + } + return displayNode.isCallVideoOptionSelected + } init(_ params: ContactMultiselectionControllerParams) { self.params = params diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index e555404005..58fa17ca74 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -15,6 +15,8 @@ import EditableTokenListNode import SolidRoundedButtonNode import ContextUI import ComponentFlow +import MultilineTextComponent +import CheckComponent private struct SearchResultEntry: Identifiable { let index: Int @@ -84,7 +86,9 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { private let onlyWriteable: Bool private let isGroupInvitation: Bool - private var bottomPanel: ComponentView? + var isCallVideoOptionSelected: Bool { + return self.footerPanelNode?.isCheckOptionSelected ?? false + } init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?, options: Signal<[ContactListAdditionalOption], NoError>, filters: [ContactListFilter], onlyWriteable: Bool, isGroupInvitation: Bool, limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?, present: @escaping (ViewController, Any?) -> Void) { self.navigationBar = navigationBar @@ -119,19 +123,19 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { shortPlaceholder = self.presentationData.strings.Common_Search self.footerPanelNode = FooterPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: { proceedImpl?() - }) + }, checkOptionTitle: nil) case .requestedUsersSelection: placeholder = self.presentationData.strings.RequestPeer_SelectUsers_SearchPlaceholder self.footerPanelNode = FooterPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: { proceedImpl?() - }) + }, checkOptionTitle: nil) case let .groupCreation(isCall): if isCall { //TODO:localize placeholder = "Search for contacts or usernames" self.footerPanelNode = FooterPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: { proceedImpl?() - }) + }, checkOptionTitle: isCall ? "Call with video enabled" : nil) } else { placeholder = self.presentationData.strings.Compose_TokenListPlaceholder self.footerPanelNode = nil @@ -554,9 +558,16 @@ private final class FooterPanelNode: ASDisplayNode { private let theme: PresentationTheme private let strings: PresentationStrings + + private let checkOptionTitle: String? + private var checkOptionButton: HighlightTrackingButton? + private var checkOptionText: ComponentView? + private var checkOptionControl: ComponentView? private let separatorNode: ASDisplayNode private let button: SolidRoundedButtonView + + private(set) var isCheckOptionSelected: Bool = false private var validLayout: (CGFloat, CGFloat, CGFloat)? @@ -573,14 +584,15 @@ private final class FooterPanelNode: ASDisplayNode { } } - init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void, checkOptionTitle: String?) { self.theme = theme self.strings = strings + self.checkOptionTitle = checkOptionTitle self.separatorNode = ASDisplayNode() self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor - self.button = SolidRoundedButtonView(theme: SolidRoundedButtonTheme(theme: theme), height: 48.0, cornerRadius: 10.0) + self.button = SolidRoundedButtonView(theme: SolidRoundedButtonTheme(theme: theme), height: 50.0, cornerRadius: 10.0) self.content = Content(title: self.strings.Premium_Gift_ContactSelection_Proceed, badge: "") @@ -599,9 +611,17 @@ private final class FooterPanelNode: ASDisplayNode { super.didLoad() self.view.addSubview(self.button) } + + @objc private func checkOptionButtonPressed() { + self.isCheckOptionSelected = !self.isCheckOptionSelected + if let validLayout = self.validLayout { + let _ = self.updateLayout(width: validLayout.0, sideInset: validLayout.1, bottomInset: validLayout.2, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } func updateLayout(width: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { self.validLayout = (width, sideInset, bottomInset) + let topInset: CGFloat = 9.0 var bottomInset = bottomInset bottomInset += topInset - (bottomInset.isZero ? 0.0 : 4.0) @@ -609,10 +629,104 @@ private final class FooterPanelNode: ASDisplayNode { let buttonInset: CGFloat = 16.0 + sideInset let buttonWidth = width - buttonInset * 2.0 let buttonHeight = self.button.updateLayout(width: buttonWidth, transition: transition) - transition.updateFrame(view: self.button, frame: CGRect(x: buttonInset, y: topInset, width: buttonWidth, height: buttonHeight)) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) - return topInset + buttonHeight + bottomInset + var height = topInset + buttonHeight + bottomInset + + var buttonOffset: CGFloat = 0.0 + if let checkOptionTitle = self.checkOptionTitle { + let checkSpacing: CGFloat = 10.0 + + let checkOptionButton: HighlightTrackingButton + if let current = self.checkOptionButton { + checkOptionButton = current + } else { + checkOptionButton = HighlightTrackingButton() + self.checkOptionButton = checkOptionButton + self.view.addSubview(checkOptionButton) + checkOptionButton.addTarget(self, action: #selector(self.checkOptionButtonPressed), for: .touchUpInside) + } + + let checkOptionText: ComponentView + if let current = self.checkOptionText { + checkOptionText = current + } else { + checkOptionText = ComponentView() + self.checkOptionText = checkOptionText + } + + let checkOptionControl: ComponentView + if let current = self.checkOptionControl { + checkOptionControl = current + } else { + checkOptionControl = ComponentView() + self.checkOptionControl = checkOptionControl + } + + let checkOptionTextSize = checkOptionText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: checkOptionTitle, font: Font.regular(13.0), textColor: theme.rootController.navigationBar.primaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: width - sideInset * 2.0 - checkSpacing - 20.0, height: 100.0) + ) + + let checkTheme = CheckComponent.Theme( + backgroundColor: self.theme.list.itemCheckColors.fillColor, + strokeColor: self.theme.list.itemCheckColors.foregroundColor, + borderColor: self.theme.list.itemCheckColors.strokeColor, + overlayBorder: false, + hasInset: false, + hasShadow: false + ) + let checkOptionControlSize = checkOptionControl.update( + transition: transition.isAnimated ? .easeInOut(duration: 0.2) : .immediate, + component: AnyComponent(CheckComponent( + theme: checkTheme, + size: CGSize(width: 18.0, height: 18.0), + selected: self.isCheckOptionSelected + )), + environment: {}, + containerSize: CGSize(width: 18.0, height: 18.0) + ) + + let checkContentWidth = checkOptionControlSize.width + checkSpacing + checkOptionTextSize.width + let checkContentHeight = 49.0 + + let checkOptionControlFrame = CGRect(origin: CGPoint(x: floor((width - checkContentWidth) * 0.5), y: floor(checkContentHeight - checkOptionControlSize.height) * 0.5), size: checkOptionControlSize) + let checkOptionTextFrame = CGRect(origin: CGPoint(x: checkOptionControlFrame.maxX + checkSpacing, y: floor((checkContentHeight - checkOptionTextSize.height) * 0.5)), size: checkOptionTextSize) + + if let checkOptionControlView = checkOptionControl.view { + if checkOptionControlView.superview == nil { + checkOptionControlView.isUserInteractionEnabled = false + checkOptionButton.addSubview(checkOptionControlView) + } + checkOptionControlView.frame = checkOptionControlFrame + } + + if let checkOptionTextView = checkOptionText.view { + if checkOptionTextView.superview == nil { + checkOptionTextView.isUserInteractionEnabled = false + checkOptionButton.addSubview(checkOptionTextView) + } + checkOptionTextView.frame = checkOptionTextFrame + } + + checkOptionButton.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: checkContentHeight)) + + height += checkContentHeight + buttonOffset += checkContentHeight + } else { + if let checkOptionButton = self.checkOptionButton { + self.checkOptionButton = nil + checkOptionButton.removeFromSuperview() + } + } + + transition.updateFrame(view: self.button, frame: CGRect(x: buttonInset, y: topInset + buttonOffset, width: buttonWidth, height: buttonHeight)) + + return height } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index d3ec4e7a5a..d3bd0f15bf 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1943,7 +1943,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { let _ = (controller.result |> take(1) - |> deliverOnMainQueue).startStandalone(next: { [weak controller] result in + |> deliverOnMainQueue).startStandalone(next: { [weak controller, weak parentController] result in + guard let parentController else { + return + } guard case let .result(rawPeerIds, _) = result else { controller?.dismiss() return From 85fba8226686da125adb7a3ee9378c9fe0b11050 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 8 Apr 2025 20:17:45 +0400 Subject: [PATCH 3/4] Remove appcenter --- .gitlab-ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 36b7e04b97..19dc6c985f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,7 +23,6 @@ internal: - export PATH=`gem environment gemdir`/bin:$PATH - python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appcenter-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=adhoc --configuration=release_arm64 - python3 -u build-system/Make/DeployToFirebase.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/firebase-configurations/firebase-internal.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" - - python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appcenter-internal.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" environment: name: internal artifacts: @@ -56,7 +55,6 @@ appstore_development: - tags script: - python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64 - - python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appstore-development.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" environment: name: appstore-development artifacts: @@ -76,7 +74,6 @@ experimental_i: - export PATH=/opt/homebrew/opt/ruby/bin:$PATH - export PATH=`gem environment gemdir`/bin:$PATH - python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appstore-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=adhoc --configuration=release_arm64 - - python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appcenter-experimental.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" environment: name: experimental artifacts: @@ -94,7 +91,6 @@ experimental: - tags script: - python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64 - - python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appcenter-experimental2.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" environment: name: experimental-2 artifacts: From 3cc52311851a796edf8fdcaf5f6de9672f18f90d Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 8 Apr 2025 20:36:32 +0400 Subject: [PATCH 4/4] Various fixes --- .../TelegramUI/Components/CameraScreen/BUILD | 1 + .../Sources/ShutterBlobView.swift | 120 +---------------- .../Sources/GiftSetupScreen.swift | 1 - .../Sources/FlipButtonContentComponent.swift | 2 +- .../Sources/PeerInfoScreen.swift | 4 +- .../Stories/StoryPeerListComponent/BUILD | 1 + .../MetalResources/storyBlob.metal | 2 +- .../Sources/StoryComposeLayer.swift | 119 +---------------- .../Components/Utils/AnimatableProperty/BUILD | 19 +++ .../Sources/AnimatableProperty.swift | 123 ++++++++++++++++++ 10 files changed, 150 insertions(+), 242 deletions(-) create mode 100644 submodules/TelegramUI/Components/Utils/AnimatableProperty/BUILD create mode 100644 submodules/TelegramUI/Components/Utils/AnimatableProperty/Sources/AnimatableProperty.swift diff --git a/submodules/TelegramUI/Components/CameraScreen/BUILD b/submodules/TelegramUI/Components/CameraScreen/BUILD index 33964a446c..be81f37a3d 100644 --- a/submodules/TelegramUI/Components/CameraScreen/BUILD +++ b/submodules/TelegramUI/Components/CameraScreen/BUILD @@ -86,6 +86,7 @@ swift_library( "//submodules/UndoUI", "//submodules/ContextUI", "//submodules/AvatarNode", + "//submodules/TelegramUI/Components/Utils/AnimatableProperty", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift index f0f68e80b7..f04b2c3925 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift @@ -4,125 +4,7 @@ import MetalKit import ComponentFlow import Display import MetalImageView - -private final class PropertyAnimation { - let from: T - let to: T - let animation: ComponentTransition.Animation - let startTimestamp: Double - private let interpolator: (Interpolatable, Interpolatable, CGFloat) -> Interpolatable - - init(fromValue: T, toValue: T, animation: ComponentTransition.Animation, startTimestamp: Double) { - self.from = fromValue - self.to = toValue - self.animation = animation - self.startTimestamp = startTimestamp - self.interpolator = T.interpolator() - } - - func valueAt(_ t: CGFloat) -> Interpolatable { - if t <= 0.0 { - return self.from - } else if t >= 1.0 { - return self.to - } else { - return self.interpolator(self.from, self.to, t) - } - } -} - -private final class AnimatableProperty { - var presentationValue: T - var value: T - private var animation: PropertyAnimation? - - init(value: T) { - self.value = value - self.presentationValue = value - } - - func update(value: T, transition: ComponentTransition = .immediate) { - let currentTimestamp = CACurrentMediaTime() - if case .none = transition.animation { - if let animation = self.animation, case let .curve(duration, curve) = animation.animation { - self.value = value - let elapsed = duration - (currentTimestamp - animation.startTimestamp) - if let presentationValue = self.presentationValue as? CGFloat, let newValue = value as? CGFloat, abs(presentationValue - newValue) > 0.56 { - self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed * 0.8, curve: curve), startTimestamp: currentTimestamp) - } else { - self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed, curve: curve), startTimestamp: currentTimestamp) - } - } else { - self.value = value - self.presentationValue = value - self.animation = nil - } - } else { - self.value = value - self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: transition.animation, startTimestamp: currentTimestamp) - } - } - - func tick(timestamp: Double) -> Bool { - guard let animation = self.animation, case let .curve(duration, curve) = animation.animation else { - return false - } - - let timeFromStart = timestamp - animation.startTimestamp - var t = max(0.0, timeFromStart / duration) - switch curve { - case .linear: - break - case .easeInOut: - t = listViewAnimationCurveEaseInOut(t) - case .spring: - t = lookupSpringValue(t) - case let .custom(x1, y1, x2, y2): - t = bezierPoint(CGFloat(x1), CGFloat(y1), CGFloat(x2), CGFloat(y2), t) - } - self.presentationValue = animation.valueAt(t) as! T - - if timeFromStart <= duration { - return true - } - self.animation = nil - return false - } -} - -private func lookupSpringValue(_ t: CGFloat) -> CGFloat { - let table: [(CGFloat, CGFloat)] = [ - (0.0, 0.0), - (0.0625, 0.1123005598783493), - (0.125, 0.31598418951034546), - (0.1875, 0.5103585720062256), - (0.25, 0.6650152802467346), - (0.3125, 0.777747631072998), - (0.375, 0.8557760119438171), - (0.4375, 0.9079672694206238), - (0.5, 0.942038357257843), - (0.5625, 0.9638798832893372), - (0.625, 0.9776856303215027), - (0.6875, 0.9863143563270569), - (0.75, 0.991658091545105), - (0.8125, 0.9949421286582947), - (0.875, 0.9969474077224731), - (0.9375, 0.9981651306152344), - (1.0, 1.0) - ] - - for i in 0 ..< table.count - 2 { - let lhs = table[i] - let rhs = table[i + 1] - - if t >= lhs.0 && t <= rhs.0 { - let fraction = (t - lhs.0) / (rhs.0 - lhs.0) - let value = lhs.1 + fraction * (rhs.1 - lhs.1) - return value - } - } - return 1.0 -} +import AnimatableProperty private class ShutterBlobLayer: MetalImageLayer { override public init() { diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 04e03cab3a..76e2d6e3fa 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -504,7 +504,6 @@ final class GiftSetupScreenComponent: Component { self.state?.updated() starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) - let _ = (starsContext.onUpdate |> deliverOnMainQueue).start(next: { proceed() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/FlipButtonContentComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/FlipButtonContentComponent.swift index 3947ddd1b1..20286a1546 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/FlipButtonContentComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/FlipButtonContentComponent.swift @@ -32,7 +32,7 @@ final class FlipButtonContentComponent: Component { private let icon = SimpleLayer() init() { - self.backgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.5), enableBlur: true) + self.backgroundView = BlurredBackgroundView(color: UIColor(white: 0.2, alpha: 0.45), enableBlur: true) super.init(frame: CGRect()) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index c48f21a471..ebd3998789 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1001,9 +1001,9 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p })) } if let starsState = data.starsState { - if !isPremiumDisabled || starsState.balance > StarsAmount.zero { + if !isPremiumDisabled || abs(starsState.balance.value) > 0 { let balanceText: NSAttributedString - if starsState.balance > StarsAmount.zero { + if abs(starsState.balance.value) > 0 { let formattedLabel = formatStarsAmountText(starsState.balance, dateTimeFormat: presentationData.dateTimeFormat) let smallLabelFont = Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 13.0)) let labelFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize) diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD index faa2fbb5e0..07d4ac7084 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD @@ -69,6 +69,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/Components/HierarchyTrackingLayer", "//submodules/TelegramUI/Components/ChatListTitleView", + "//submodules/TelegramUI/Components/Utils/AnimatableProperty", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/MetalResources/storyBlob.metal b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/MetalResources/storyBlob.metal index 5f1848a3d7..2af505f4af 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/MetalResources/storyBlob.metal +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/MetalResources/storyBlob.metal @@ -87,7 +87,7 @@ fragment half4 cameraBlobFragment( } else if (primaryParameters.y < 0) { float cutRadius = abs(primaryParameters.y); float2 primaryFeatheredOffset = primaryOffset; - primaryFeatheredOffset.x *= 1.129; + primaryFeatheredOffset.x *= 1.131; float distFromCenter = length(uv - primaryFeatheredOffset); diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryComposeLayer.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryComposeLayer.swift index 1ebf14db78..5ff4735024 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryComposeLayer.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryComposeLayer.swift @@ -5,7 +5,7 @@ import MetalKit import MetalEngine import ComponentFlow import TelegramPresentationData - +import AnimatableProperty private var metalLibraryValue: MTLLibrary? func metalLibrary(device: MTLDevice) -> MTLLibrary? { @@ -28,124 +28,7 @@ func metalLibrary(device: MTLDevice) -> MTLLibrary? { return library } -private final class PropertyAnimation { - let from: T - let to: T - let animation: ComponentTransition.Animation - let startTimestamp: Double - private let interpolator: (Interpolatable, Interpolatable, CGFloat) -> Interpolatable - - init(fromValue: T, toValue: T, animation: ComponentTransition.Animation, startTimestamp: Double) { - self.from = fromValue - self.to = toValue - self.animation = animation - self.startTimestamp = startTimestamp - self.interpolator = T.interpolator() - } - - func valueAt(_ t: CGFloat) -> Interpolatable { - if t <= 0.0 { - return self.from - } else if t >= 1.0 { - return self.to - } else { - return self.interpolator(self.from, self.to, t) - } - } -} -private final class AnimatableProperty { - var presentationValue: T - var value: T - private var animation: PropertyAnimation? - - init(value: T) { - self.value = value - self.presentationValue = value - } - - func update(value: T, transition: ComponentTransition = .immediate) { - let currentTimestamp = CACurrentMediaTime() - if case .none = transition.animation { - if let animation = self.animation, case let .curve(duration, curve) = animation.animation { - self.value = value - let elapsed = duration - (currentTimestamp - animation.startTimestamp) - if let presentationValue = self.presentationValue as? CGFloat, let newValue = value as? CGFloat, abs(presentationValue - newValue) > 0.56 { - self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed * 0.8, curve: curve), startTimestamp: currentTimestamp) - } else { - self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed, curve: curve), startTimestamp: currentTimestamp) - } - } else { - self.value = value - self.presentationValue = value - self.animation = nil - } - } else { - self.value = value - self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: transition.animation, startTimestamp: currentTimestamp) - } - } - - func tick(timestamp: Double) -> Bool { - guard let animation = self.animation, case let .curve(duration, curve) = animation.animation else { - return false - } - - let timeFromStart = timestamp - animation.startTimestamp - var t = max(0.0, timeFromStart / duration) - switch curve { - case .linear: - break - case .easeInOut: - t = listViewAnimationCurveEaseInOut(t) - case .spring: - t = lookupSpringValue(t) - case let .custom(x1, y1, x2, y2): - t = bezierPoint(CGFloat(x1), CGFloat(y1), CGFloat(x2), CGFloat(y2), t) - } - self.presentationValue = animation.valueAt(t) as! T - - if timeFromStart <= duration { - return true - } - self.animation = nil - return false - } -} - -private func lookupSpringValue(_ t: CGFloat) -> CGFloat { - let table: [(CGFloat, CGFloat)] = [ - (0.0, 0.0), - (0.0625, 0.1123005598783493), - (0.125, 0.31598418951034546), - (0.1875, 0.5103585720062256), - (0.25, 0.6650152802467346), - (0.3125, 0.777747631072998), - (0.375, 0.8557760119438171), - (0.4375, 0.9079672694206238), - (0.5, 0.942038357257843), - (0.5625, 0.9638798832893372), - (0.625, 0.9776856303215027), - (0.6875, 0.9863143563270569), - (0.75, 0.991658091545105), - (0.8125, 0.9949421286582947), - (0.875, 0.9969474077224731), - (0.9375, 0.9981651306152344), - (1.0, 1.0) - ] - - for i in 0 ..< table.count - 2 { - let lhs = table[i] - let rhs = table[i + 1] - - if t >= lhs.0 && t <= rhs.0 { - let fraction = (t - lhs.0) / (rhs.0 - lhs.0) - let value = lhs.1 + fraction * (rhs.1 - lhs.1) - return value - } - } - return 1.0 -} final class StoryBlobLayer: MetalEngineSubjectLayer, MetalEngineSubject { var internalData: MetalEngineSubjectInternalData? diff --git a/submodules/TelegramUI/Components/Utils/AnimatableProperty/BUILD b/submodules/TelegramUI/Components/Utils/AnimatableProperty/BUILD new file mode 100644 index 0000000000..f71b179806 --- /dev/null +++ b/submodules/TelegramUI/Components/Utils/AnimatableProperty/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AnimatableProperty", + module_name = "AnimatableProperty", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Utils/AnimatableProperty/Sources/AnimatableProperty.swift b/submodules/TelegramUI/Components/Utils/AnimatableProperty/Sources/AnimatableProperty.swift new file mode 100644 index 0000000000..8e9885e9b4 --- /dev/null +++ b/submodules/TelegramUI/Components/Utils/AnimatableProperty/Sources/AnimatableProperty.swift @@ -0,0 +1,123 @@ +import Foundation +import UIKit +import ComponentFlow +import Display + +private final class PropertyAnimation { + let from: T + let to: T + let animation: ComponentTransition.Animation + let startTimestamp: Double + private let interpolator: (Interpolatable, Interpolatable, CGFloat) -> Interpolatable + + init(fromValue: T, toValue: T, animation: ComponentTransition.Animation, startTimestamp: Double) { + self.from = fromValue + self.to = toValue + self.animation = animation + self.startTimestamp = startTimestamp + self.interpolator = T.interpolator() + } + + func valueAt(_ t: CGFloat) -> Interpolatable { + if t <= 0.0 { + return self.from + } else if t >= 1.0 { + return self.to + } else { + return self.interpolator(self.from, self.to, t) + } + } +} + +public final class AnimatableProperty { + public private(set) var presentationValue: T + public private(set) var value: T + private var animation: PropertyAnimation? + + public init(value: T) { + self.value = value + self.presentationValue = value + } + + public func update(value: T, transition: ComponentTransition = .immediate) { + let currentTimestamp = CACurrentMediaTime() + if case .none = transition.animation { + if let animation = self.animation, case let .curve(duration, curve) = animation.animation { + self.value = value + let elapsed = duration - (currentTimestamp - animation.startTimestamp) + if let presentationValue = self.presentationValue as? CGFloat, let newValue = value as? CGFloat, abs(presentationValue - newValue) > 0.56 { + self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed * 0.8, curve: curve), startTimestamp: currentTimestamp) + } else { + self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed, curve: curve), startTimestamp: currentTimestamp) + } + } else { + self.value = value + self.presentationValue = value + self.animation = nil + } + } else { + self.value = value + self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: transition.animation, startTimestamp: currentTimestamp) + } + } + + public func tick(timestamp: Double) -> Bool { + guard let animation = self.animation, case let .curve(duration, curve) = animation.animation else { + return false + } + + let timeFromStart = timestamp - animation.startTimestamp + var t = max(0.0, timeFromStart / duration) + switch curve { + case .linear: + break + case .easeInOut: + t = listViewAnimationCurveEaseInOut(t) + case .spring: + t = lookupSpringValue(t) + case let .custom(x1, y1, x2, y2): + t = bezierPoint(CGFloat(x1), CGFloat(y1), CGFloat(x2), CGFloat(y2), t) + } + self.presentationValue = animation.valueAt(t) as! T + + if timeFromStart <= duration { + return true + } + self.animation = nil + return false + } +} + +private func lookupSpringValue(_ t: CGFloat) -> CGFloat { + let table: [(CGFloat, CGFloat)] = [ + (0.0, 0.0), + (0.0625, 0.1123005598783493), + (0.125, 0.31598418951034546), + (0.1875, 0.5103585720062256), + (0.25, 0.6650152802467346), + (0.3125, 0.777747631072998), + (0.375, 0.8557760119438171), + (0.4375, 0.9079672694206238), + (0.5, 0.942038357257843), + (0.5625, 0.9638798832893372), + (0.625, 0.9776856303215027), + (0.6875, 0.9863143563270569), + (0.75, 0.991658091545105), + (0.8125, 0.9949421286582947), + (0.875, 0.9969474077224731), + (0.9375, 0.9981651306152344), + (1.0, 1.0) + ] + + for i in 0 ..< table.count - 2 { + let lhs = table[i] + let rhs = table[i + 1] + + if t >= lhs.0 && t <= rhs.0 { + let fraction = (t - lhs.0) / (rhs.0 - lhs.0) + let value = lhs.1 + fraction * (rhs.1 - lhs.1) + return value + } + } + return 1.0 +}