From a25bedd36b5f109e2db336f270939159959c74e0 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 5 Aug 2025 17:43:59 +0200 Subject: [PATCH] Rating --- .../Sources/ChatListController.swift | 68 +-- .../Sources/ChatListControllerNode.swift | 7 +- .../Sources/ChatListSearchContainerNode.swift | 5 +- .../Sources/ChatListSearchListPaneNode.swift | 2 +- .../ChatListSearchPaneContainerNode.swift | 6 + .../Sources/AnimatedTextComponent.swift | 119 ++++- .../Sources/PeerInfoRatingComponent.swift | 11 + .../Sources/PeerInfoHeaderNode.swift | 62 ++- .../Sources/ProfileLevelInfoScreen.swift | 107 +++-- .../Sources/ProfileLevelRatingBarBadge.swift | 181 ++++---- .../ProfileLevelRatingBarComponent.swift | 406 +++++++++++++++--- .../Contents.json | 12 + .../badratingprofile.pdf | Bin 0 -> 4094 bytes .../Contents.json | 12 + .../badratingslider.pdf | Bin 0 -> 4088 bytes .../Resources/Animations/badge_with_tail.json | 2 +- 16 files changed, 730 insertions(+), 270 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/InlineRatingWarning.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/InlineRatingWarning.imageset/badratingprofile.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/ProfileLevelWarningIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/ProfileLevelWarningIcon.imageset/badratingslider.pdf diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 5b8e949137..36f25182b6 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -4613,44 +4613,46 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let _ = (combineLatest(self.chatListDisplayNode.mainContainerNode.currentItemNode.contentsReady |> take(1), self.context.account.postbox.tailChatListView(groupId: .root, count: 16, summaryComponents: ChatListEntrySummaryComponents(components: [:])) |> take(1)) |> deliverOnMainQueue).startStandalone(next: { [weak self] _, chatListView in - guard let strongSelf = self else { - return - } - - /*if let scrollToTop = strongSelf.scrollToTop { - scrollToTop() - }*/ - - let tabsIsEmpty: Bool - if let (resolvedItems, displayTabsAtBottom, _) = strongSelf.tabContainerData { - tabsIsEmpty = resolvedItems.count <= 1 || displayTabsAtBottom - } else { - tabsIsEmpty = true - } - let _ = tabsIsEmpty - //TODO:swap tabs - - let displaySearchFilters = true - - if let filterContainerNodeAndActivate = strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, displaySearchFilters: displaySearchFilters, hasDownloads: strongSelf.hasDownloads, initialFilter: filter, navigationController: strongSelf.navigationController as? NavigationController) { - let (filterContainerNode, activate) = filterContainerNodeAndActivate - if displaySearchFilters { - let searchTabsNode = SparseNode() - strongSelf.searchTabsNode = searchTabsNode - searchTabsNode.addSubnode(filterContainerNode) + Task { @MainActor in + guard let strongSelf = self else { + return } - activate(filter != .downloads) + /*if let scrollToTop = strongSelf.scrollToTop { + scrollToTop() + }*/ - if let searchContentNode = strongSelf.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { - searchContentNode.search(filter: filter, query: query) + let tabsIsEmpty: Bool + if let (resolvedItems, displayTabsAtBottom, _) = strongSelf.tabContainerData { + tabsIsEmpty = resolvedItems.count <= 1 || displayTabsAtBottom + } else { + tabsIsEmpty = true } + let _ = tabsIsEmpty + //TODO:swap tabs + + let displaySearchFilters = true + + if let filterContainerNodeAndActivate = await strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, displaySearchFilters: displaySearchFilters, hasDownloads: strongSelf.hasDownloads, initialFilter: filter, navigationController: strongSelf.navigationController as? NavigationController) { + let (filterContainerNode, activate) = filterContainerNodeAndActivate + if displaySearchFilters { + let searchTabsNode = SparseNode() + strongSelf.searchTabsNode = searchTabsNode + searchTabsNode.addSubnode(filterContainerNode) + } + + activate(filter != .downloads) + + if let searchContentNode = strongSelf.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { + searchContentNode.search(filter: filter, query: query) + } + } + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + strongSelf.setDisplayNavigationBar(false, transition: transition) + + (strongSelf.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.4, curve: .spring)) } - - let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) - strongSelf.setDisplayNavigationBar(false, transition: transition) - - (strongSelf.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.4, curve: .spring)) }) self.isSearchActive = true diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 357cf6e848..c413dc8a53 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -20,6 +20,7 @@ import ComponentFlow import ChatFolderLinkPreviewScreen import ChatListHeaderComponent import StoryPeerListComponent +import TelegramNotices public enum ChatListContainerNodeFilter: Equatable { case all @@ -1649,7 +1650,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { } } - func activateSearch(placeholderNode: SearchBarPlaceholderNode, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?) -> (ASDisplayNode, (Bool) -> Void)? { + @MainActor + func activateSearch(placeholderNode: SearchBarPlaceholderNode, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?) async -> (ASDisplayNode, (Bool) -> Void)? { guard let (containerLayout, _, _, cleanNavigationBarHeight, _) = self.containerLayout, self.searchDisplayController == nil else { return nil } @@ -1688,6 +1690,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { self?.controller?.openAdInfo(node: node, adPeer: adPeer) } + let searchTips = await ApplicationSpecificNotice.getGlobalPostsSearch(accountManager: self.context.sharedContext.accountManager).get() + contentNode.displayGlobalPostsNewBadge = searchTips < 3 + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, contentNode: contentNode, cancel: { [weak self] in if let requestDeactivateSearch = self?.requestDeactivateSearch { requestDeactivateSearch() diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index a1fc5e5fdc..289cfa96d8 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -131,6 +131,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private var forumPeer: EnginePeer? private var hasPublicPostsTab = false private var showPublicPostsTab = false + public var displayGlobalPostsNewBadge = false private var shareStatusDisposable: MetaDisposable? @@ -715,7 +716,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: !isForum && self.hasDownloads, hasPublicPosts: self.showPublicPostsTab).map(\.filter) } - self.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, displayGlobalPostsNewBadge: true, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: transition) + self.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, displayGlobalPostsNewBadge: self.displayGlobalPostsNewBadge, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: transition) } } @@ -786,7 +787,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } let overflowInset: CGFloat = 20.0 - self.filterContainerNode.update(size: CGSize(width: layout.size.width - overflowInset * 2.0, height: 38.0), sideInset: layout.safeInsets.left - overflowInset, filters: filters.map { .filter($0) }, displayGlobalPostsNewBadge: true, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) + self.filterContainerNode.update(size: CGSize(width: layout.size.width - overflowInset * 2.0, height: 38.0), sideInset: layout.safeInsets.left - overflowInset, filters: filters.map { .filter($0) }, displayGlobalPostsNewBadge: self.displayGlobalPostsNewBadge, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) if isFirstTime { self.filterContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 4611c2577b..a19bd6c521 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2061,7 +2061,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } var defaultFoundRemoteMessagesSignal: Signal<([FoundRemoteMessages], Bool), NoError> = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) - if key == .globalPosts { + if key == .globalPosts, let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_load_empty_global_posts"] as? Double, value != 0.0 { let searchSignal = context.engine.messages.searchMessages(location: .general(scope: .globalPosts(allowPaidStars: nil), tags: nil, minDate: nil, maxDate: nil), query: "", state: nil, limit: 50) |> map { resultData -> ChatListSearchMessagesResult in let (result, updatedState) = resultData diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index ac48fb5c95..7d52bddb6a 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -9,6 +9,7 @@ import AccountContext import ContextUI import AnimationCache import MultiAnimationRenderer +import TelegramNotices protocol ChatListSearchPaneNode: ASDisplayNode { var isReady: Signal { get } @@ -238,6 +239,11 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD } return } + + if key == .globalPosts { + let _ = ApplicationSpecificNotice.incrementGlobalPostsSearch(accountManager: self.context.sharedContext.accountManager).startStandalone() + } + #if DEBUG #else self.isAdjacentLoadingEnabled = true diff --git a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift index 0967a73fbe..c5d1ea1d80 100644 --- a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift +++ b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift @@ -4,6 +4,23 @@ import Display import ComponentFlow import TelegramPresentationData +extension ComponentTransition { + func animateBlur(layer: CALayer, from: CGFloat, to: CGFloat, delay: Double = 0.0, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + if let blurFilter = CALayer.blur() { + blurFilter.setValue(to as NSNumber, forKey: "inputRadius") + layer.filters = [blurFilter] + layer.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, delay: delay, removeOnCompletion: removeOnCompletion, completion: { [weak layer] _ in + guard let layer else { + return + } + if to == 0.0 && removeOnCompletion { + layer.filters = nil + } + }) + } + } +} + public final class AnimatedTextComponent: Component { public struct Item: Equatable { public enum Content: Equatable { @@ -27,19 +44,25 @@ public final class AnimatedTextComponent: Component { public let items: [Item] public let noDelay: Bool public let animateScale: Bool + public let preferredDirectionIsDown: Bool + public let blur: Bool public init( font: UIFont, color: UIColor, items: [Item], noDelay: Bool = false, - animateScale: Bool = true + animateScale: Bool = true, + preferredDirectionIsDown: Bool = false, + blur: Bool = false ) { self.font = font self.color = color self.items = items self.noDelay = noDelay self.animateScale = animateScale + self.preferredDirectionIsDown = preferredDirectionIsDown + self.blur = blur } public static func ==(lhs: AnimatedTextComponent, rhs: AnimatedTextComponent) -> Bool { @@ -58,6 +81,12 @@ public final class AnimatedTextComponent: Component { if lhs.animateScale != rhs.animateScale { return false } + if lhs.preferredDirectionIsDown != rhs.preferredDirectionIsDown { + return false + } + if lhs.blur != rhs.blur { + return false + } return true } @@ -88,8 +117,14 @@ public final class AnimatedTextComponent: Component { var size = CGSize() let delayNorm: CGFloat = 0.002 + var offsetNorm: CGFloat = 0.4 + let transitionBlurRadius: CGFloat = 6.0 var firstDelayWidth: CGFloat? + if component.preferredDirectionIsDown { + firstDelayWidth = 0.0 + offsetNorm = 0.8 + } var validKeys: [CharacterKey] = [] for item in component.items { @@ -119,6 +154,57 @@ public final class AnimatedTextComponent: Component { index += 1 validKeys.append(characterKey) + } + } + + var outLastDelayWidth: CGFloat? + var outFirstDelayWidth: CGFloat? + if component.preferredDirectionIsDown { + for (key, characterView) in self.characters { + if !validKeys.contains(key), let characterView = characterView.view { + if let outFirstDelayWidthValue = outFirstDelayWidth { + outFirstDelayWidth = max(outFirstDelayWidthValue, characterView.frame.center.x) + } else { + outFirstDelayWidth = characterView.frame.center.x + } + + if let outLastDelayWidthValue = outLastDelayWidth { + outLastDelayWidth = min(outLastDelayWidthValue, characterView.frame.center.x) + } else { + outLastDelayWidth = characterView.frame.center.x + } + } + } + } + if outLastDelayWidth != nil { + firstDelayWidth = outLastDelayWidth + } + + for item in component.items { + var itemText: [String] = [] + switch item.content { + case let .text(text): + if item.isUnbreakable { + itemText = [text] + } else { + itemText = text.map(String.init) + } + case let .number(value, minDigits): + var valueText: String = "\(value)" + while valueText.count < minDigits { + valueText.insert("0", at: valueText.startIndex) + } + + if item.isUnbreakable { + itemText = [valueText] + } else { + itemText = valueText.map(String.init) + } + } + var index = 0 + for character in itemText { + let characterKey = CharacterKey(itemId: item.id, index: index, value: character) + index += 1 var characterTransition = transition let characterView: ComponentView @@ -155,7 +241,11 @@ public final class AnimatedTextComponent: Component { } else { var delayWidth: Double = 0.0 if let firstDelayWidth { - delayWidth = size.width - firstDelayWidth + if characterFrame.midX > characterComponentView.frame.midX { + delayWidth = 0.0 + } else { + delayWidth = abs(size.width - firstDelayWidth) + } } else { firstDelayWidth = size.width } @@ -170,7 +260,7 @@ public final class AnimatedTextComponent: Component { if animateIn, !transition.animation.isImmediate { var delayWidth: Double = 0.0 - if !component.noDelay { + if !component.noDelay || component.preferredDirectionIsDown { if let firstDelayWidth { delayWidth = size.width - firstDelayWidth } else { @@ -181,7 +271,10 @@ public final class AnimatedTextComponent: Component { if component.animateScale { characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring) } - characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * 0.5), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + if component.blur { + ComponentTransition.easeInOut(duration: 0.2).animateBlur(layer: characterComponentView.layer, from: transitionBlurRadius, to: 0.0, delay: delayNorm * delayWidth) + } + characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * offsetNorm), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true) characterComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18, delay: delayNorm * delayWidth) } } @@ -194,8 +287,6 @@ public final class AnimatedTextComponent: Component { let outScaleTransition: ComponentTransition = .spring(duration: 0.4) let outAlphaTransition: ComponentTransition = .easeInOut(duration: 0.18) - var outFirstDelayWidth: CGFloat? - var removedKeys: [CharacterKey] = [] for (key, characterView) in self.characters { if !validKeys.contains(key) { @@ -205,18 +296,28 @@ public final class AnimatedTextComponent: Component { if !transition.animation.isImmediate { var delayWidth: Double = 0.0 if let outFirstDelayWidth { - delayWidth = characterComponentView.frame.minX - outFirstDelayWidth + delayWidth = abs(characterComponentView.frame.midX - outFirstDelayWidth) } else { - outFirstDelayWidth = characterComponentView.frame.minX + outFirstDelayWidth = characterComponentView.frame.midX } + delayWidth = max(0.0, delayWidth) if component.animateScale { outScaleTransition.setScale(view: characterComponentView, scale: 0.01, delay: delayNorm * delayWidth) } - outScaleTransition.setPosition(view: characterComponentView, position: CGPoint(x: characterComponentView.center.x, y: characterComponentView.center.y - characterComponentView.bounds.height * 0.4), delay: delayNorm * delayWidth) + let targetY: CGFloat + if component.preferredDirectionIsDown { + targetY = characterComponentView.center.y - characterComponentView.bounds.height * offsetNorm + } else { + targetY = characterComponentView.center.y - characterComponentView.bounds.height * offsetNorm + } + outScaleTransition.setPosition(view: characterComponentView, position: CGPoint(x: characterComponentView.center.x, y: targetY), delay: delayNorm * delayWidth) outAlphaTransition.setAlpha(view: characterComponentView, alpha: 0.0, delay: delayNorm * delayWidth, completion: { [weak characterComponentView] _ in characterComponentView?.removeFromSuperview() }) + if component.blur { + outAlphaTransition.animateBlur(layer: characterComponentView.layer, from: 0.0, to: transitionBlurRadius, delay: delayNorm * delayWidth, removeOnCompletion: false) + } } else { characterComponentView.removeFromSuperview() } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift index 07bbb69c5b..27ddd942a1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift @@ -338,6 +338,10 @@ public final class PeerInfoRatingComponent: Component { context.clear(CGRect(origin: CGPoint(), size: size)) + if level < 0 { + return + } + if let url = Bundle.main.url(forResource: "profile_level\(levelIndex)_outer", withExtension: "svg"), let data = try? Data(contentsOf: url) { if let image = generateTintedImage(image: drawSvgImage(data, size, nil, nil, 0.0, false), color: component.borderColor) { image.draw(in: CGRect(origin: CGPoint(x: 0.0, y: backgroundOffsetsY[levelIndex] ?? 0.0), size: size), blendMode: .normal, alpha: 1.0) @@ -360,6 +364,13 @@ public final class PeerInfoRatingComponent: Component { context.clear(CGRect(origin: CGPoint(), size: size)) + if level < 0 { + if let image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/InlineRatingWarning"), color: component.backgroundColor) { + image.draw(in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) * 0.5), y: floorToScreenPixels((size.height - image.size.height) * 0.5)), size: image.size)) + } + return + } + if let url = Bundle.main.url(forResource: "profile_level\(levelIndex)_inner", withExtension: "svg"), let data = try? Data(contentsOf: url) { if let image = generateTintedImage(image: drawSvgImage(data, size, nil, nil, 0.0, false), color: component.backgroundColor) { image.draw(in: CGRect(origin: CGPoint(x: 0.0, y: backgroundOffsetsY[levelIndex] ?? 0.0), size: size), blendMode: .normal, alpha: 1.0) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 6d8ad714ce..ce470d6b7b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -781,6 +781,13 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.avatarClippingNode.clipsToBounds = true } + let accentRatingBackgroundColor: UIColor + if let currentStarRating = self.currentStarRating, currentStarRating.level < 0 { + accentRatingBackgroundColor = UIColor(rgb: 0xFF3B30) + } else { + accentRatingBackgroundColor = presentationData.theme.list.itemCheckColors.fillColor + } + let ratingBackgroundColor: UIColor let ratingBorderColor: UIColor let ratingForegroundColor: UIColor @@ -796,7 +803,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { headerButtonBackgroundColor = collapsedHeaderButtonBackgroundColor - ratingBackgroundColor = presentationData.theme.list.itemCheckColors.fillColor + ratingBackgroundColor = accentRatingBackgroundColor ratingBorderColor = .clear ratingForegroundColor = presentationData.theme.list.itemCheckColors.foregroundColor } else if self.isAvatarExpanded { @@ -833,7 +840,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { if let status = peer?.emojiStatus, case let .starGift(_, _, _, _, _, innerColor, outerColor, patternColorValue, _) = status.content { let _ = innerColor - ratingBackgroundColor = UIColor(white: 1.0, alpha: 1.0).mixedWith(presentationData.theme.list.itemCheckColors.fillColor, alpha: effectiveTransitionFraction) + ratingBackgroundColor = UIColor(white: 1.0, alpha: 1.0).mixedWith(accentRatingBackgroundColor, alpha: effectiveTransitionFraction) let innerColor = UIColor(rgb: UInt32(bitPattern: innerColor)) let outerColor = UIColor(rgb: UInt32(bitPattern: outerColor)) @@ -855,29 +862,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { ratingBorderColor = patternColor.withAlphaComponent(0.1).blendOver(background: backgroundColor).mixedWith(.clear, alpha: effectiveTransitionFraction) ratingForegroundColor = ratingBorderColor.mixedWith(presentationData.theme.list.itemCheckColors.foregroundColor, alpha: effectiveTransitionFraction) } else { - ratingBackgroundColor = presentationData.theme.list.itemCheckColors.fillColor + ratingBackgroundColor = accentRatingBackgroundColor ratingBorderColor = UIColor.clear ratingForegroundColor = presentationData.theme.list.itemCheckColors.foregroundColor } - - /*if let status = peer?.emojiStatus, case let .starGift(_, _, _, _, _, innerColor, outerColor, _, _) = status.content { - let _ = outerColor - let mainColor = UIColor(rgb: UInt32(bitPattern: innerColor)) - - ratingBackgroundColor = UIColor(white: 1.0, alpha: 1.0).mixedWith(presentationData.theme.list.itemCheckColors.fillColor, alpha: effectiveTransitionFraction) - ratingForegroundColor = mainColor.withMultiplied(hue: 1.0, saturation: 1.1, brightness: 0.9).mixedWith(UIColor.clear, alpha: effectiveTransitionFraction) - ratingBorderColor = ratingForegroundColor.mixedWith(presentationData.theme.list.itemCheckColors.foregroundColor, alpha: effectiveTransitionFraction) - } else if let profileColor = peer?.profileColor { - let backgroundColors = self.context.peerNameColors.getProfile(profileColor, dark: presentationData.theme.overallDarkAppearance) - - ratingBackgroundColor = UIColor(white: 1.0, alpha: 1.0).mixedWith(presentationData.theme.list.itemCheckColors.fillColor, alpha: effectiveTransitionFraction) - ratingForegroundColor = backgroundColors.main.withMultiplied(hue: 1.0, saturation: 1.1, brightness: 0.9).mixedWith(UIColor.clear, alpha: effectiveTransitionFraction) - ratingBorderColor = ratingForegroundColor.mixedWith(presentationData.theme.list.itemCheckColors.foregroundColor, alpha: effectiveTransitionFraction) - } else { - ratingBackgroundColor = presentationData.theme.list.itemCheckColors.fillColor - ratingBorderColor = UIColor.clear - ratingForegroundColor = presentationData.theme.list.itemCheckColors.foregroundColor - }*/ } do { @@ -1986,20 +1974,30 @@ final class PeerInfoHeaderNode: ASDisplayNode { if let cachedData = cachedData as? CachedUserData, let starRating = cachedData.starRating { self.currentStarRating = starRating self.currentPendingStarRating = cachedData.pendingStarRating - - #if DEBUG - if let _ = starRating.nextLevelStars { - self.currentPendingStarRating = TelegramStarPendingRating(rating: TelegramStarRating(level: starRating.level, currentLevelStars: starRating.currentLevelStars, stars: starRating.stars + 234, nextLevelStars: starRating.nextLevelStars), timestamp: Int32(Date().timeIntervalSince1970) + 60 * 60 * 24 * 3) - self.currentPendingStarRating = TelegramStarPendingRating(rating: TelegramStarRating(level: starRating.level + 1, currentLevelStars: starRating.nextLevelStars!, stars: starRating.nextLevelStars! + starRating.nextLevelStars! / 2 + starRating.nextLevelStars! / 4, nextLevelStars: starRating.nextLevelStars! * 2), timestamp: Int32(Date().timeIntervalSince1970) + 60 * 60 * 24 * 3) - } - #endif } else { self.currentStarRating = nil self.currentPendingStarRating = nil } - if let cachedData = cachedData as? CachedUserData, let starRating = cachedData.starRating { - //if "".isEmpty { + #if DEBUG + if "".isEmpty { + let starRating: TelegramStarRating + + if self.context.account.peerId.id._internalGetInt64Value() == 654152421 { + starRating = TelegramStarRating(level: -1, currentLevelStars: -1, stars: -100, nextLevelStars: 0) + } else { + starRating = TelegramStarRating(level: 2, currentLevelStars: 1000, stars: 2000, nextLevelStars: 3000) + } + self.currentStarRating = starRating + + if let _ = starRating.nextLevelStars { + //self.currentPendingStarRating = TelegramStarPendingRating(rating: TelegramStarRating(level: starRating.level, currentLevelStars: starRating.currentLevelStars, stars: starRating.stars + 234, nextLevelStars: starRating.nextLevelStars), timestamp: Int32(Date().timeIntervalSince1970) + 60 * 60 * 24 * 3) + self.currentPendingStarRating = TelegramStarPendingRating(rating: TelegramStarRating(level: starRating.level + 2, currentLevelStars: starRating.nextLevelStars!, stars: max(500, starRating.nextLevelStars! + starRating.nextLevelStars! / 2 - starRating.nextLevelStars! / 4), nextLevelStars: max(1000, starRating.nextLevelStars! * 2)), timestamp: Int32(Date().timeIntervalSince1970) + 60 * 60 * 24 * 3) + } + } + #endif + + if let starRating = self.currentStarRating { let subtitleRating: ComponentView var subtitleRatingTransition = ComponentTransition(transition) if let current = self.subtitleRating { @@ -2018,7 +2016,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { foregroundColor: ratingForegroundColor, level: Int(starRating.level), action: { [weak self] in - guard let self, let peer, let currentStarRating = self.currentStarRating else { + guard let self, let peer = self.peer, let currentStarRating = self.currentStarRating else { return } self.controller?.push(ProfileLevelInfoScreen( diff --git a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift index b5feaba4b9..fd2a593c0f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift @@ -17,6 +17,7 @@ import PlainButtonComponent import Markdown import PremiumUI import LottieComponent +import AnimatedTextComponent private final class ProfileLevelInfoScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -376,10 +377,38 @@ private final class ProfileLevelInfoScreenComponent: Component { descriptionTextString = environment.strings.ProfileLevelInfo_OtherDescription(component.peer.compactDisplayTitle).string } + //TODO:localize + var titleItems: [AnimatedTextComponent.Item] = [] + if self.isPreviewingPendingRating { + titleItems.append(AnimatedTextComponent.Item( + id: AnyHashable(0), + isUnbreakable: false, + content: .text("Future ") + )) + titleItems.append(AnimatedTextComponent.Item( + id: AnyHashable(1), + isUnbreakable: true, + content: .text("Rating") + )) + } else { + titleItems.append(AnimatedTextComponent.Item( + id: AnyHashable(1), + isUnbreakable: true, + content: .text("Rating") + )) + } + + let _ = titleString let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + transition: transition, + component: AnyComponent(AnimatedTextComponent( + font: Font.semibold(17.0), + color: environment.theme.list.itemPrimaryTextColor, + items: titleItems, + noDelay: true, + animateScale: false, + preferredDirectionIsDown: true, + blur: true )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -389,7 +418,7 @@ private final class ProfileLevelInfoScreenComponent: Component { if titleView.superview == nil { self.navigationBarContainer.addSubview(titleView) } - titleView.frame = titleFrame + transition.setFrame(view: titleView, frame: titleFrame) } contentHeight += 56.0 @@ -433,47 +462,25 @@ private final class ProfileLevelInfoScreenComponent: Component { if let nextLevelStars = component.starRating.nextLevelStars { badgeTextSuffix = " / \(starCountString(Int64(nextLevelStars), decimalSeparator: "."))" } - if let nextLevelStars = component.starRating.nextLevelStars { + if component.starRating.stars < 0 { + levelFraction = 0.5 + } else if let nextLevelStars = component.starRating.nextLevelStars { levelFraction = Double(component.starRating.stars - component.starRating.currentLevelStars) / Double(nextLevelStars - component.starRating.currentLevelStars) - } else if component.starRating.stars > 0 { - levelFraction = 1.0 } else { - levelFraction = 0.0 + levelFraction = 1.0 } } levelFraction = max(0.0, levelFraction) - - /*let levelInfoSize = self.levelInfo.update( - transition: .immediate, - component: AnyComponent(PremiumLimitDisplayComponent( - inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5), - activeColors: gradientColors, - inactiveTitle: environment.strings.ProfileLevelInfo_LevelIndex(Int32(currentLevel)), - inactiveValue: "", - inactiveTitleColor: environment.theme.list.itemPrimaryTextColor, - activeTitle: "", - activeValue: nextLevel.flatMap { environment.strings.ProfileLevelInfo_LevelIndex(Int32($0)) } ?? "", - activeTitleColor: .white, - badgeIconName: "Peer Info/ProfileLevelProgressIcon", - badgeText: badgeText, - badgeTextSuffix: badgeTextSuffix, - badgePosition: levelFraction, - badgeGraphPosition: levelFraction, - invertProgress: true, - isPremiumDisabled: false - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 200.0) - )*/ - let _ = levelFraction + + //TODO:localize let levelInfoSize = self.levelInfo.update( transition: isChangingPreview ? ComponentTransition.immediate.withUserData(ProfileLevelRatingBarComponent.TransitionHint(animate: true)) : .immediate, component: AnyComponent(ProfileLevelRatingBarComponent( theme: environment.theme, value: levelFraction, - leftLabel: environment.strings.ProfileLevelInfo_LevelIndex(Int32(currentLevel)), - rightLabel: nextLevel.flatMap { environment.strings.ProfileLevelInfo_LevelIndex(Int32($0)) } ?? "", + leftLabel: currentLevel < 0 ? "" : environment.strings.ProfileLevelInfo_LevelIndex(Int32(currentLevel)), + rightLabel: currentLevel < 0 ? "Negative rating" : nextLevel.flatMap { environment.strings.ProfileLevelInfo_LevelIndex(Int32($0)) } ?? "", badgeValue: badgeText, badgeTotal: badgeTextSuffix, level: Int(currentLevel) @@ -491,12 +498,21 @@ private final class ProfileLevelInfoScreenComponent: Component { contentHeight += 129.0 if let secondaryDescriptionTextString { + let changingPreviewAnimationOffset: CGFloat = self.isPreviewingPendingRating ? -100.0 : 100.0 + let transitionBlurRadius: CGFloat = 10.0 + if isChangingPreview, let secondaryDescriptionTextView = self.secondaryDescriptionText?.view { self.secondaryDescriptionText = nil - transition.setTransform(view: secondaryDescriptionTextView, transform: CATransform3DMakeScale(0.9, 0.9, 1.0)) + transition.setTransform(view: secondaryDescriptionTextView, transform: CATransform3DMakeTranslation(changingPreviewAnimationOffset, 0.0, 0.0)) alphaTransition.setAlpha(view: secondaryDescriptionTextView, alpha: 0.0, completion: { [weak secondaryDescriptionTextView] _ in secondaryDescriptionTextView?.removeFromSuperview() }) + + if let blurFilter = CALayer.blur() { + blurFilter.setValue(transitionBlurRadius as NSNumber, forKey: "inputRadius") + secondaryDescriptionTextView.layer.filters = [blurFilter] + secondaryDescriptionTextView.layer.animate(from: 0.0 as NSNumber, to: transitionBlurRadius as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, removeOnCompletion: false) + } } contentHeight -= 8.0 @@ -510,9 +526,16 @@ private final class ProfileLevelInfoScreenComponent: Component { self.secondaryDescriptionText = secondaryDescriptionText } + let secondaryTextColor: UIColor + if currentLevel < 0 { + secondaryTextColor = UIColor(rgb: 0xFF3B30) + } else { + secondaryTextColor = environment.theme.list.itemSecondaryTextColor + } + let secondaryDescriptionAttributedString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(secondaryDescriptionTextString, attributes: MarkdownAttributes( - body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor), - bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemSecondaryTextColor), + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: secondaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: secondaryTextColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), linkAttribute: { url in return ("URL", url) @@ -563,8 +586,16 @@ private final class ProfileLevelInfoScreenComponent: Component { if secondaryDescriptionTextView.superview == nil { self.scrollContentView.addSubview(secondaryDescriptionTextView) if isChangingPreview { - transition.animateScale(view: secondaryDescriptionTextView, from: 0.9, to: 1.0) + transition.animatePosition(view: secondaryDescriptionTextView, from: CGPoint(x: -changingPreviewAnimationOffset, y: 0.0), to: CGPoint(), additive: true) alphaTransition.animateAlpha(view: secondaryDescriptionTextView, from: 0.0, to: 1.0) + + if let blurFilter = CALayer.blur() { + blurFilter.setValue(transitionBlurRadius as NSNumber, forKey: "inputRadius") + secondaryDescriptionTextView.layer.filters = [blurFilter] + secondaryDescriptionTextView.layer.animate(from: transitionBlurRadius as NSNumber, to: 0.0 as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, removeOnCompletion: false, completion: { [weak secondaryDescriptionTextView] _ in + secondaryDescriptionTextView?.layer.filters = nil + }) + } } } secondaryDescriptionTextTransition.setPosition(view: secondaryDescriptionTextView, position: secondaryDescriptionTextFrame.center) diff --git a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarBadge.swift b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarBadge.swift index 80fb328174..c79a7af616 100644 --- a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarBadge.swift +++ b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarBadge.swift @@ -47,11 +47,12 @@ final class ProfileLevelRatingBarBadge: Component { final class View: UIView { private let badgeView: UIView private let badgeMaskView: UIView - private let badgeShapeLayer = SimpleShapeLayer() + private let badgeShapeView: UIImageView private let badgeShapeAnimation = ComponentView() private let badgeForeground: SimpleLayer - let badgeIcon: UIImageView + private let badgeIcon: UIImageView + private var disappearingBadgeIcon: UIImageView? private let badgeLabel = ComponentView() private let suffixLabel = ComponentView() @@ -68,11 +69,10 @@ final class ProfileLevelRatingBarBadge: Component { self.badgeView.alpha = 0.0 self.badgeView.layer.anchorPoint = CGPoint() - self.badgeShapeLayer.fillColor = UIColor.white.cgColor - self.badgeShapeLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + self.badgeShapeView = UIImageView() self.badgeMaskView = UIView() - self.badgeMaskView.layer.addSublayer(self.badgeShapeLayer) + self.badgeMaskView.addSubview(self.badgeShapeView) self.badgeView.mask = self.badgeMaskView self.badgeForeground = SimpleLayer() @@ -108,17 +108,49 @@ final class ProfileLevelRatingBarBadge: Component { self.isUpdating = false } - if self.component == nil { - self.badgeIcon.image = UIImage(bundleImageName: "Peer Info/ProfileLevelProgressIcon")?.withRenderingMode(.alwaysTemplate) + var labelsTransition = transition + if let hint = transition.userData(TransitionHint.self), hint.animateText { + labelsTransition = .spring(duration: 0.5) + } + + let previousComponent = self.component + if previousComponent == nil || (previousComponent?.title == "") != (component.title == "") { + if !labelsTransition.animation.isImmediate, self.badgeIcon.image != nil { + if let disappearingBadgeIcon = self.disappearingBadgeIcon { + self.disappearingBadgeIcon = nil + disappearingBadgeIcon.removeFromSuperview() + } + let disappearingBadgeIcon = UIImageView() + disappearingBadgeIcon.contentMode = self.badgeIcon.contentMode + disappearingBadgeIcon.frame = self.badgeIcon.frame + disappearingBadgeIcon.image = self.badgeIcon.image + disappearingBadgeIcon.tintColor = self.badgeIcon.tintColor + + self.badgeView.insertSubview(disappearingBadgeIcon, aboveSubview: self.badgeIcon) + labelsTransition.setScale(view: disappearingBadgeIcon, scale: 0.001) + labelsTransition.setAlpha(view: disappearingBadgeIcon, alpha: 0.0, completion: { [weak self, weak disappearingBadgeIcon] _ in + guard let self, let disappearingBadgeIcon else { + return + } + disappearingBadgeIcon.removeFromSuperview() + if self.disappearingBadgeIcon === disappearingBadgeIcon { + self.disappearingBadgeIcon = nil + } + }) + labelsTransition.animateScale(view: self.badgeIcon, from: 0.001, to: 1.0) + labelsTransition.animateAlpha(view: self.badgeIcon, from: 0.0, to: 1.0) + } + + if component.title.isEmpty { + self.badgeIcon.image = UIImage(bundleImageName: "Peer Info/ProfileLevelWarningIcon")?.withRenderingMode(.alwaysTemplate) + } else { + self.badgeIcon.image = UIImage(bundleImageName: "Peer Info/ProfileLevelProgressIcon")?.withRenderingMode(.alwaysTemplate) + } } self.component = component self.badgeIcon.tintColor = component.theme.list.itemCheckColors.foregroundColor - - var labelsTransition = transition - if let hint = transition.userData(TransitionHint.self), hint.animateText { - labelsTransition = .spring(duration: 0.4) - } + self.disappearingBadgeIcon?.tintColor = component.theme.list.itemCheckColors.foregroundColor let badgeLabelSize = self.badgeLabel.update( transition: labelsTransition, @@ -150,7 +182,12 @@ final class ProfileLevelRatingBarBadge: Component { containerSize: CGSize(width: 300.0, height: 100.0) ) - var badgeWidth: CGFloat = badgeLabelSize.width + 3.0 + 60.0 + var badgeWidth: CGFloat = 0.0 + if !component.title.isEmpty { + badgeWidth += badgeLabelSize.width + 3.0 + 60.0 + } else { + badgeWidth += 78.0 + } if component.suffix != nil { badgeWidth += badgeSuffixSize.width + badgeSuffixSpacing } @@ -158,13 +195,21 @@ final class ProfileLevelRatingBarBadge: Component { let badgeSize = CGSize(width: badgeWidth, height: 48.0) let badgeFullSize = CGSize(width: badgeWidth, height: 48.0 + 12.0) self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize) - self.badgeShapeLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: badgeFullSize) self.badgeView.bounds = CGRect(origin: .zero, size: badgeFullSize) self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 600.0, height: badgeFullSize.height + 10.0)) - self.badgeIcon.frame = CGRect(x: 13.0, y: 8.0, width: 30.0, height: 30.0) + let badgeIconFrame: CGRect + if !component.title.isEmpty { + badgeIconFrame = CGRect(x: 13.0, y: 8.0, width: 30.0, height: 30.0) + } else { + badgeIconFrame = CGRect(x: floor((badgeWidth - 30.0) * 0.5), y: 8.0, width: 30.0, height: 30.0) + } + labelsTransition.setFrame(view: self.badgeIcon, frame: badgeIconFrame) + if let disappearingBadgeIcon = self.disappearingBadgeIcon { + labelsTransition.setFrame(view: disappearingBadgeIcon, frame: badgeIconFrame) + } self.badgeView.alpha = 1.0 @@ -194,77 +239,63 @@ final class ProfileLevelRatingBarBadge: Component { if self.previousAvailableSize != availableSize { self.previousAvailableSize = availableSize - - let activeColors: [UIColor] = [ - component.theme.list.itemCheckColors.fillColor, - component.theme.list.itemCheckColors.fillColor - ] - - var locations: [CGFloat] = [] - let delta = 1.0 / CGFloat(activeColors.count - 1) - for i in 0 ..< activeColors.count { - locations.append(delta * CGFloat(i)) - } - - let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: activeColors, locations: locations, direction: .horizontal) - self.badgeForeground.contentsGravity = .resizeAspectFill - self.badgeForeground.contents = gradient?.cgImage - - self.setupGradientAnimations() } return size } - func adjustTail(size: CGSize, overflowWidth: CGFloat, transition: ComponentTransition) { - guard let component else { - return + func updateColors(background: UIColor) { + self.badgeForeground.backgroundColor = background.cgColor + } + + func adjustTail(size: CGSize, tailOffset: CGFloat, transition: ComponentTransition) { + if self.badgeShapeView.image == nil { + self.badgeShapeView.image = generateStretchableFilledCircleImage(diameter: 48.0, color: UIColor.white) } - let _ = component + self.badgeShapeView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: 48.0)) - var tailPosition = size.width * 0.5 - tailPosition += overflowWidth - tailPosition = max(36.0, min(size.width - 36.0, tailPosition)) - - let tailPositionFraction = tailPosition / size.width - transition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateRoundedRectWithTailPath(rectSize: size, tailPosition: tailPositionFraction, transformTail: false).cgPath) - - let badgeShapeSize = CGSize(width: 128, height: 128) + let badgeShapeSize = CGSize(width: 78, height: 60) let _ = self.badgeShapeAnimation.update( transition: .immediate, component: AnyComponent(LottieComponent( content: LottieComponent.AppBundleContent(name: "badge_with_tail"), - color: .red,//component.theme.list.itemCheckColors.fillColor, + color: .white, placeholderColor: nil, startingPosition: .begin, size: badgeShapeSize, - renderingScale: nil, + renderingScale: UIScreenScale, loop: false, playOnce: nil )), environment: {}, containerSize: badgeShapeSize ) - if let badgeShapeAnimationView = self.badgeShapeAnimation.view as? LottieComponent.View, !"".isEmpty { + if let badgeShapeAnimationView = self.badgeShapeAnimation.view as? LottieComponent.View { if badgeShapeAnimationView.superview == nil { badgeShapeAnimationView.layer.anchorPoint = CGPoint() - self.addSubview(badgeShapeAnimationView) + self.badgeMaskView.addSubview(badgeShapeAnimationView) } - let transition: ComponentTransition = .immediate + var shapeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: badgeShapeSize) + + let badgeShapeWidth = badgeShapeSize.width + + let midFrame = 359 / 2 + if tailOffset < badgeShapeWidth * 0.5 { + let frameIndex = Int(floor(CGFloat(midFrame) * tailOffset / (badgeShapeWidth * 0.5))) + badgeShapeAnimationView.setFrameIndex(index: frameIndex) + } else if tailOffset >= size.width - badgeShapeWidth * 0.5 { + let endOffset = tailOffset - (size.width - badgeShapeWidth * 0.5) + let frameIndex = midFrame + Int(floor(CGFloat(359 - midFrame) * endOffset / (badgeShapeWidth * 0.5))) + badgeShapeAnimationView.setFrameIndex(index: frameIndex) + shapeFrame.origin.x = size.width - badgeShapeWidth + } else { + badgeShapeAnimationView.setFrameIndex(index: midFrame) + shapeFrame.origin.x = tailOffset - badgeShapeWidth * 0.5 + } - let shapeFrame = CGRect(origin: CGPoint(x: 0.0, y: -10.0), size: badgeShapeSize) badgeShapeAnimationView.center = shapeFrame.origin badgeShapeAnimationView.bounds = CGRect(origin: CGPoint(), size: shapeFrame.size) - - let scaleFactor: CGFloat = 144.0 / (946.0 / 4.0) - transition.setScale(view: badgeShapeAnimationView, scale: scaleFactor) - - let badgeShapeWidth = floor(shapeFrame.width * scaleFactor) - let badgeShapeOffset = -overflowWidth / badgeShapeWidth - let _ = badgeShapeOffset - - //badgeShapeAnimationView.setFrameIndex(index: 0) } //let transition: ContainedViewLayoutTransition = .immediate @@ -275,38 +306,6 @@ final class ProfileLevelRatingBarBadge: Component { let transition: ContainedViewLayoutTransition = .immediate transition.updateTransformRotation(view: self.badgeView, angle: angle) } - - private func setupGradientAnimations() { - guard let _ = self.component else { - return - } - if let _ = self.badgeForeground.animation(forKey: "movement") { - } else { - CATransaction.begin() - - let badgePreviousValue = self.badgeForeground.position.x - let badgeNewValue: CGFloat - if self.badgeForeground.position.x == -300.0 { - badgeNewValue = 0.0 - } else { - badgeNewValue = -300.0 - } - self.badgeForeground.position = CGPoint(x: badgeNewValue, y: 0.0) - - let badgeAnimation = CABasicAnimation(keyPath: "position.x") - badgeAnimation.duration = 4.5 - badgeAnimation.fromValue = badgePreviousValue - badgeAnimation.toValue = badgeNewValue - badgeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) - - CATransaction.setCompletionBlock { [weak self] in - self?.setupGradientAnimations() - } - self.badgeForeground.add(badgeAnimation, forKey: "movement") - - CATransaction.commit() - } - } } func makeView() -> View { diff --git a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarComponent.swift b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarComponent.swift index 42c7b1931b..ce10d04682 100644 --- a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarComponent.swift @@ -6,7 +6,6 @@ import ComponentFlow import MultilineTextComponent import BundleIconComponent import HierarchyTrackingLayer -import AnimatedTextComponent final class ProfileLevelRatingBarComponent: Component { final class TransitionHint { @@ -69,20 +68,33 @@ final class ProfileLevelRatingBarComponent: Component { } private final class AnimationState { + enum Wraparound { + case left + case right + } + + let fromLevel: Int + let toLevel: Int + let fromLeftLabelText: String + let fromRightLabelText: String let fromValue: CGFloat let toValue: CGFloat let fromBadgeSize: CGSize let startTime: Double let duration: Double - let isWraparound: Bool + let wraparound: Wraparound? - init(fromValue: CGFloat, toValue: CGFloat, fromBadgeSize: CGSize, startTime: Double, duration: Double, isWraparound: Bool) { + init(fromLevel: Int, toLevel: Int, fromLeftLabelText: String, fromRightLabelText: String, fromValue: CGFloat, toValue: CGFloat, fromBadgeSize: CGSize, startTime: Double, duration: Double, wraparound: Wraparound?) { + self.fromLevel = fromLevel + self.toLevel = toLevel + self.fromLeftLabelText = fromLeftLabelText + self.fromRightLabelText = fromRightLabelText self.fromValue = fromValue self.toValue = toValue self.fromBadgeSize = fromBadgeSize self.startTime = startTime self.duration = duration - self.isWraparound = isWraparound + self.wraparound = wraparound } func timeFraction(at timestamp: Double) -> CGFloat { @@ -91,8 +103,30 @@ final class ProfileLevelRatingBarComponent: Component { return fraction } + func stepFraction(at timestamp: Double) -> (step: Int, fraction: CGFloat) { + if self.wraparound != nil { + var t = self.timeFraction(at: timestamp) + t = bezierPoint(0.6, 0.0, 0.4, 1.0, t) + if t < 0.5 { + let vt = t / 0.5 + return (0, vt) + } else { + let vt = (t - 0.5) / 0.5 + return (1, vt) + } + } else { + let t = self.timeFraction(at: timestamp) + return (0, listViewAnimationCurveSystem(t)) + } + } + func fraction(at timestamp: Double) -> CGFloat { - return listViewAnimationCurveSystem(self.timeFraction(at: timestamp)) + let t = self.timeFraction(at: timestamp) + if self.wraparound != nil { + return listViewAnimationCurveEaseInOut(t) + } else { + return listViewAnimationCurveSystem(t) + } } func value(at timestamp: Double) -> CGFloat { @@ -100,14 +134,12 @@ final class ProfileLevelRatingBarComponent: Component { return (1.0 - fraction) * self.fromValue + fraction * self.toValue } - func wrapAroundValue(at timestamp: Double, topValue: CGFloat) -> CGFloat { - let fraction = self.fraction(at: timestamp) - if fraction <= 0.5 { - let halfFraction = fraction / 0.5 - return (1.0 - halfFraction) * self.fromValue + halfFraction * topValue + func wrapAroundValue(at timestamp: Double, bottomValue: CGFloat, topValue: CGFloat) -> CGFloat { + let (step, fraction) = self.stepFraction(at: timestamp) + if step == 0 { + return (1.0 - fraction) * self.fromValue + fraction * topValue } else { - let halfFraction = (fraction - 0.5) / 0.5 - return halfFraction * self.toValue + return (1.0 - fraction) * bottomValue + fraction * self.toValue } } @@ -123,6 +155,7 @@ final class ProfileLevelRatingBarComponent: Component { final class View: UIView { private let barBackground: UIImageView private let backgroundClippingContainer: UIView + private let foregroundBarClippingContainer: UIView private let foregroundClippingContainer: UIView private let barForeground: UIImageView @@ -139,13 +172,29 @@ final class ProfileLevelRatingBarComponent: Component { private var hierarchyTracker: HierarchyTrackingLayer? private var animationLink: SharedDisplayLinkDriver.Link? + private var badgePhysicsLink: SharedDisplayLinkDriver.Link? private var animationState: AnimationState? + private var previousAnimationTimestamp: Double? + private var previousAnimationTimeFraction: CGFloat? + private var animationDeltaTime: Double? + private var animationIsMovingOverStep: Bool = false + + private var badgeAngularSpeed: CGFloat = 0.0 + private var badgeScale: CGFloat = 1.0 + private var badgeAngle: CGFloat = 0.0 + private var previousPhysicsTimestamp: Double? + + private var testFraction: CGFloat? + private var startTestFraction: CGFloat? + override init(frame: CGRect) { self.barBackground = UIImageView() self.backgroundClippingContainer = UIView() self.backgroundClippingContainer.clipsToBounds = true + self.foregroundBarClippingContainer = UIView() + self.foregroundBarClippingContainer.clipsToBounds = true self.foregroundClippingContainer = UIView() self.foregroundClippingContainer.clipsToBounds = true self.barForeground = UIImageView() @@ -161,7 +210,28 @@ final class ProfileLevelRatingBarComponent: Component { return } self.updateAnimations() + + if value { + if self.badgePhysicsLink == nil { + let badgePhysicsLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in + guard let self else { + return + } + self.updateBadgePhysics() + }) + self.badgePhysicsLink = badgePhysicsLink + } + } else { + if let badgePhysicsLink = self.badgePhysicsLink { + self.badgePhysicsLink = nil + badgePhysicsLink.invalidate() + } + } } + + #if DEBUG + self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.onPanGesture(_:)))) + #endif } required init?(coder: NSCoder) { @@ -171,7 +241,40 @@ final class ProfileLevelRatingBarComponent: Component { deinit { } + @objc private func onPanGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began, .changed: + if self.testFraction == nil { + self.testFraction = self.component?.value + } + if self.startTestFraction == nil { + if let testFraction = self.testFraction { + self.startTestFraction = testFraction + } + } + if let startTestFraction = self.startTestFraction { + let x = recognizer.translation(in: self).x + var value: CGFloat = startTestFraction + x / self.bounds.width + value = max(0.0, min(1.0, value)) + self.testFraction = value + self.state?.updated(transition: .immediate, isLocal: true) + } + case .ended, .cancelled: + self.startTestFraction = nil + default: + break + } + } + private func updateAnimations() { + let timestamp = CACurrentMediaTime() + let deltaTime: CGFloat + if let previousAnimationTimestamp = self.previousAnimationTimestamp { + deltaTime = min(0.2, timestamp - previousAnimationTimestamp) + } else { + deltaTime = 1.0 / 60.0 + } + if let hierarchyTracker = self.hierarchyTracker, hierarchyTracker.isInHierarchy { if self.animationState != nil { if self.animationLink == nil { @@ -194,15 +297,104 @@ final class ProfileLevelRatingBarComponent: Component { } if let animationState = self.animationState { - if animationState.timeFraction(at: CACurrentMediaTime()) >= 1.0 { + let timeFraction = animationState.timeFraction(at: timestamp) + if timeFraction >= 1.0 { self.animationState = nil self.updateAnimations() + return + } else { + if let previousAnimationTimeFraction = self.previousAnimationTimeFraction { + if previousAnimationTimeFraction < 0.5 && timeFraction >= 0.5 { + self.animationIsMovingOverStep = true + } + } + self.previousAnimationTimeFraction = timeFraction } + } else { + self.previousAnimationTimeFraction = nil } + self.animationDeltaTime = Double(deltaTime) + if self.animationState != nil && !self.isUpdating { self.state?.updated(transition: .immediate, isLocal: true) } + + self.animationDeltaTime = nil + self.animationIsMovingOverStep = false + } + + private func addBadgeDeltaX(value: CGFloat, deltaTime: CGFloat) { + var deltaTime = deltaTime + deltaTime /= UIView.animationDurationFactor() + let horizontalVelocity = value / deltaTime + var badgeAngle = self.badgeAngle + badgeAngle -= horizontalVelocity * 0.00005 + let maxAngle: CGFloat = 0.1 + if abs(badgeAngle) > maxAngle { + badgeAngle = badgeAngle < 0.0 ? -maxAngle : maxAngle + } + self.badgeAngle = badgeAngle + } + + private func updateBadgePhysics() { + let timestamp = CACurrentMediaTime() + + var deltaTime: CGFloat + if let previousPhysicsTimestamp = self.previousPhysicsTimestamp { + deltaTime = CGFloat(min(1.0 / 60.0, timestamp - previousPhysicsTimestamp)) + } else { + deltaTime = CGFloat(1.0 / 60.0) + } + self.previousPhysicsTimestamp = timestamp + deltaTime /= UIView.animationDurationFactor() + + let testSpringFriction: CGFloat = 18.5 + let testSpringConstant: CGFloat = 243.0 + + let frictionConstant: CGFloat = testSpringFriction + let springConstant: CGFloat = testSpringConstant + let time: CGFloat = deltaTime + + var badgeAngle = self.badgeAngle + + // friction force = velocity * friction constant + let frictionForce = self.badgeAngularSpeed * frictionConstant + // spring force = (target point - current position) * spring constant + let springForce = -badgeAngle * springConstant + // force = spring force - friction force + let force = springForce - frictionForce + + // velocity = current velocity + force * time / mass + self.badgeAngularSpeed = self.badgeAngularSpeed + force * time + // position = current position + velocity * time + badgeAngle = badgeAngle + self.badgeAngularSpeed * time + badgeAngle = badgeAngle.isNaN ? 0.0 : badgeAngle + + let epsilon: CGFloat = 0.01 + if abs(badgeAngle) < epsilon && abs(self.badgeAngularSpeed) < epsilon { + badgeAngle = 0.0 + self.badgeAngularSpeed = 0.0 + } + + if abs(badgeAngle) > 0.22 { + badgeAngle = badgeAngle < 0.0 ? -0.22 : 0.22 + } + + if self.badgeAngle != badgeAngle { + self.badgeAngle = badgeAngle + self.updateBadgeTransform() + } + } + + private func updateBadgeTransform() { + guard let badgeView = self.badge.view else { + return + } + var transform = CATransform3DIdentity + transform = CATransform3DScale(transform, self.badgeScale, self.badgeScale, 1.0) + transform = CATransform3DRotate(transform, self.badgeAngle, 0.0, 0.0, 1.0) + badgeView.layer.transform = transform } func update(component: ProfileLevelRatingBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -215,11 +407,31 @@ final class ProfileLevelRatingBarComponent: Component { var labelsTransition = transition if let previousComponent = self.component, let hint = transition.userData(TransitionHint.self), hint.animate { - labelsTransition = .spring(duration: 0.4) + labelsTransition = .spring(duration: 0.5) + var fromLevel = previousComponent.level + var fromLeftLabelText = previousComponent.leftLabel + var fromRightLabelText = previousComponent.rightLabel + let toLevel: Int = component.level let fromValue: CGFloat if let animationState = self.animationState { - fromValue = animationState.value(at: CACurrentMediaTime()) + if let wraparound = animationState.wraparound { + let wraparoundEnd: CGFloat + switch wraparound { + case .left: + wraparoundEnd = 0.0 + case .right: + wraparoundEnd = 1.0 + } + if animationState.stepFraction(at: CACurrentMediaTime()).step == 0 { + fromLevel = animationState.fromLevel + fromLeftLabelText = animationState.fromLeftLabelText + fromRightLabelText = animationState.fromRightLabelText + } + fromValue = animationState.wrapAroundValue(at: CACurrentMediaTime(), bottomValue: 1.0 - wraparoundEnd, topValue: wraparoundEnd) + } else { + fromValue = animationState.value(at: CACurrentMediaTime()) + } } else { fromValue = previousComponent.value } @@ -229,13 +441,23 @@ final class ProfileLevelRatingBarComponent: Component { } else { fromBadgeSize = CGSize() } + var wraparound: AnimationState.Wraparound? + var duration = 0.4 + if previousComponent.level != component.level { + wraparound = component.level > previousComponent.level ? .right : .left + duration = 0.8 + } self.animationState = AnimationState( + fromLevel: fromLevel, + toLevel: toLevel, + fromLeftLabelText: fromLeftLabelText, + fromRightLabelText: fromRightLabelText, fromValue: fromValue, toValue: component.value, fromBadgeSize: fromBadgeSize, startTime: CACurrentMediaTime(), - duration: 0.4 * UIView.animationDurationFactor(), - isWraparound: false//previousComponent.level < component.level + duration: duration * UIView.animationDurationFactor(), + wraparound: wraparound ) self.updateAnimations() } @@ -248,20 +470,19 @@ final class ProfileLevelRatingBarComponent: Component { self.barForeground.image = self.barBackground.image } - self.barBackground.tintColor = component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5) - self.barForeground.tintColor = component.theme.list.itemCheckColors.fillColor - if self.barBackground.superview == nil { self.addSubview(self.barBackground) self.addSubview(self.backgroundClippingContainer) + self.addSubview(self.foregroundBarClippingContainer) + self.foregroundBarClippingContainer.addSubview(self.barForeground) + self.addSubview(self.foregroundClippingContainer) - self.foregroundClippingContainer.addSubview(self.barForeground) } let progressValue: CGFloat - if let animationState = self.animationState { - progressValue = animationState.value(at: CACurrentMediaTime()) + if let testFraction = self.testFraction { + progressValue = testFraction } else { progressValue = component.value } @@ -269,18 +490,84 @@ final class ProfileLevelRatingBarComponent: Component { let barBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - barHeight), size: CGSize(width: availableSize.width, height: barHeight)) transition.setFrame(view: self.barBackground, frame: barBackgroundFrame) - let barForegroundFrame = CGRect(origin: barBackgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progressValue * barBackgroundFrame.width), height: barBackgroundFrame.height)) + var barForegroundFrame = CGRect(origin: barBackgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progressValue * barBackgroundFrame.width), height: barBackgroundFrame.height)) - var barApparentForegroundFrame = barForegroundFrame - if let animationState = self.animationState, animationState.isWraparound { - let progressValue = animationState.wrapAroundValue(at: CACurrentMediaTime(), topValue: 1.0) - barApparentForegroundFrame = CGRect(origin: barBackgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progressValue * barBackgroundFrame.width), height: barBackgroundFrame.height)) + var foregroundAlpha: CGFloat = 1.0 + var foregroundContentsAlpha: CGFloat = 1.0 + var badgeScale: CGFloat = 1.0 + var currentIsNegativeRating: Bool = component.level < 0 + var leftLabelText = component.leftLabel + var rightLabelText = component.rightLabel + + if let animationState = self.animationState { + if let wraparound = animationState.wraparound { + let (step, progress) = animationState.stepFraction(at: CACurrentMediaTime()) + if step == 0 { + currentIsNegativeRating = animationState.fromLevel < 0 + leftLabelText = animationState.fromLeftLabelText + rightLabelText = animationState.fromRightLabelText + } else { + currentIsNegativeRating = animationState.toLevel < 0 + } + let wraparoundEnd: CGFloat + switch wraparound { + case .left: + wraparoundEnd = 0.0 + if step == 0 { + foregroundContentsAlpha = 1.0 * (1.0 - progress) + badgeScale = 1.0 * (1.0 - progress) + 0.3 * progress + } else { + foregroundAlpha = 1.0 * progress + foregroundContentsAlpha = foregroundAlpha + badgeScale = 1.0 * progress + 0.3 * (1.0 - progress) + } + case .right: + wraparoundEnd = 1.0 + if step == 0 { + foregroundAlpha = 1.0 * (1.0 - progress) + foregroundContentsAlpha = foregroundAlpha + badgeScale = 1.0 * (1.0 - progress) + 0.3 * progress + } else { + foregroundContentsAlpha = 1.0 * progress + badgeScale = 1.0 * progress + 0.3 * (1.0 - progress) + } + } + + let progressValue = animationState.wrapAroundValue(at: CACurrentMediaTime(), bottomValue: 1.0 - wraparoundEnd, topValue: wraparoundEnd) + barForegroundFrame = CGRect(origin: barBackgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progressValue * barBackgroundFrame.width), height: barBackgroundFrame.height)) + } else { + let progressValue = animationState.value(at: CACurrentMediaTime()) + barForegroundFrame = CGRect(origin: barBackgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progressValue * barBackgroundFrame.width), height: barBackgroundFrame.height)) + } } - transition.setFrame(view: self.foregroundClippingContainer, frame: barApparentForegroundFrame) - let backgroundClippingFrame = CGRect(origin: CGPoint(x: barBackgroundFrame.minX + barApparentForegroundFrame.width, y: barBackgroundFrame.minY), size: CGSize(width: barBackgroundFrame.width - barApparentForegroundFrame.width, height: barBackgroundFrame.height)) + let badgeColor: UIColor + if currentIsNegativeRating { + badgeColor = UIColor(rgb: 0xFF3B30) + } else { + badgeColor = component.theme.list.itemCheckColors.fillColor + } + + self.barBackground.tintColor = component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5) + self.barForeground.tintColor = badgeColor + + var effectiveBarForegroundFrame = barForegroundFrame + if currentIsNegativeRating { + effectiveBarForegroundFrame.size.width = barBackgroundFrame.maxX - barForegroundFrame.maxX + effectiveBarForegroundFrame.origin.x = barBackgroundFrame.maxX - effectiveBarForegroundFrame.width + } + transition.setPosition(view: self.foregroundBarClippingContainer, position: effectiveBarForegroundFrame.center) + transition.setBounds(view: self.foregroundBarClippingContainer, bounds: CGRect(origin: CGPoint(x: effectiveBarForegroundFrame.minX - barForegroundFrame.minX, y: 0.0), size: effectiveBarForegroundFrame.size)) + transition.setPosition(view: self.foregroundClippingContainer, position: effectiveBarForegroundFrame.center) + transition.setBounds(view: self.foregroundClippingContainer, bounds: CGRect(origin: CGPoint(x: effectiveBarForegroundFrame.minX - barForegroundFrame.minX, y: 0.0), size: effectiveBarForegroundFrame.size)) + + transition.setAlpha(view: self.foregroundBarClippingContainer, alpha: foregroundAlpha) + transition.setAlpha(view: self.foregroundClippingContainer, alpha: foregroundContentsAlpha) + + let backgroundClippingFrame = CGRect(origin: CGPoint(x: barBackgroundFrame.minX + barForegroundFrame.width, y: barBackgroundFrame.minY), size: CGSize(width: barBackgroundFrame.width - barForegroundFrame.width, height: barBackgroundFrame.height)) transition.setPosition(view: self.backgroundClippingContainer, position: backgroundClippingFrame.center) transition.setBounds(view: self.backgroundClippingContainer, bounds: CGRect(origin: CGPoint(x: backgroundClippingFrame.minX - barBackgroundFrame.minX, y: 0.0), size: backgroundClippingFrame.size)) + transition.setAlpha(view: self.backgroundClippingContainer, alpha: foregroundContentsAlpha) transition.setFrame(view: self.barForeground, frame: CGRect(origin: CGPoint(), size: barBackgroundFrame.size)) @@ -288,52 +575,32 @@ final class ProfileLevelRatingBarComponent: Component { let leftLabelSize = self.backgroundLeftLabel.update( transition: labelsTransition, - component: AnyComponent(AnimatedTextComponent( - font: labelFont, - color: component.theme.list.itemPrimaryTextColor, - items: [AnimatedTextComponent.Item( - id: AnyHashable(0), - content: .text(component.leftLabel) - )] + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: leftLabelText, font: labelFont, textColor: component.theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0) ) let _ = self.foregroundLeftLabel.update( transition: labelsTransition, - component: AnyComponent(AnimatedTextComponent( - font: labelFont, - color: component.theme.list.itemCheckColors.foregroundColor, - items: [AnimatedTextComponent.Item( - id: AnyHashable(0), - content: .text(component.leftLabel) - )] + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: leftLabelText, font: labelFont, textColor: component.theme.list.itemCheckColors.foregroundColor)) )), environment: {}, containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0) ) let rightLabelSize = self.backgroundRightLabel.update( transition: labelsTransition, - component: AnyComponent(AnimatedTextComponent( - font: labelFont, - color: component.theme.list.itemPrimaryTextColor, - items: [AnimatedTextComponent.Item( - id: AnyHashable(0), - content: .text(component.rightLabel) - )] + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: rightLabelText, font: labelFont, textColor: component.theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0) ) let _ = self.foregroundRightLabel.update( transition: labelsTransition, - component: AnyComponent(AnimatedTextComponent( - font: labelFont, - color: component.theme.list.itemCheckColors.foregroundColor, - items: [AnimatedTextComponent.Item( - id: AnyHashable(0), - content: .text(component.rightLabel) - )] + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: rightLabelText, font: labelFont, textColor: component.theme.list.itemCheckColors.foregroundColor)) )), environment: {}, containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0) @@ -379,8 +646,8 @@ final class ProfileLevelRatingBarComponent: Component { transition: transition.withUserData(ProfileLevelRatingBarBadge.TransitionHint(animateText: !labelsTransition.animation.isImmediate)), component: AnyComponent(ProfileLevelRatingBarBadge( theme: component.theme, - title: "\(component.badgeValue)", - suffix: component.badgeTotal + title: component.level < 0 ? "" : "\(component.badgeValue)", + suffix: component.level < 0 ? nil : component.badgeTotal )), environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) @@ -412,9 +679,24 @@ final class ProfileLevelRatingBarComponent: Component { } badgeFrame.origin.x += badgeOverflowWidth - badgeView.frame = badgeFrame + let badgeTailOffset = (barBackgroundFrame.minX + barForegroundFrame.width) - badgeFrame.minX + let badgePosition = CGPoint(x: badgeFrame.minX + badgeTailOffset, y: badgeFrame.maxY) - badgeView.adjustTail(size: apparentBadgeSize, overflowWidth: -badgeOverflowWidth, transition: transition) + if let animationDeltaTime = self.animationDeltaTime, self.animationState != nil, !self.animationIsMovingOverStep { + let previousX = badgeView.center.x + self.addBadgeDeltaX(value: badgePosition.x - previousX, deltaTime: animationDeltaTime) + } + + badgeView.center = badgePosition + badgeView.bounds = CGRect(origin: CGPoint(), size: badgeFrame.size) + transition.setAnchorPoint(layer: badgeView.layer, anchorPoint: CGPoint(x: max(0.0, min(1.0, badgeTailOffset / badgeFrame.width)), y: 1.0)) + + badgeView.updateColors(background: badgeColor) + + badgeView.adjustTail(size: apparentBadgeSize, tailOffset: badgeTailOffset, transition: transition) + transition.setAlpha(view: badgeView, alpha: foregroundContentsAlpha) + self.badgeScale = badgeScale + self.updateBadgeTransform() } return availableSize diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/InlineRatingWarning.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/InlineRatingWarning.imageset/Contents.json new file mode 100644 index 0000000000..2bbef9aa48 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/InlineRatingWarning.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "badratingprofile.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/InlineRatingWarning.imageset/badratingprofile.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/InlineRatingWarning.imageset/badratingprofile.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f52ed6fbbe0af04d63ffd5a5c8b5b44260a00350 GIT binary patch literal 4094 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ProfileLevelWarningIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ProfileLevelWarningIcon.imageset/Contents.json new file mode 100644 index 0000000000..93248aa458 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ProfileLevelWarningIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "badratingslider.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ProfileLevelWarningIcon.imageset/badratingslider.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/ProfileLevelWarningIcon.imageset/badratingslider.pdf new file mode 100644 index 0000000000000000000000000000000000000000..dbd046137ac7037c6cc7336d0e48e0075dfcf856 GIT binary patch literal 4088 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/badge_with_tail.json b/submodules/TelegramUI/Resources/Animations/badge_with_tail.json index 56717e56b5..7129fda312 100644 --- a/submodules/TelegramUI/Resources/Animations/badge_with_tail.json +++ b/submodules/TelegramUI/Resources/Animations/badge_with_tail.json @@ -1 +1 @@ -{"v":"5.12.1","fr":60,"ip":0,"op":360,"w":780,"h":600,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"1 Outlines 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[390,300,0],"ix":2,"l":2},"a":{"a":0,"k":[390,300,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":181,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[15.637,15.858],[0,0],[22.824,0]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[-11.265,-11.424],[-132.5,0]],"v":[[-390,-60],[390,-60],[150,180],[102.53,197.841],[28.313,273.107],[-28.313,273.107],[-102.53,197.841],[-150,180]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":270,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[15.637,15.858],[0,0],[29.086,29.579]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[-11.264,-11.424],[-29.086,-29.579]],"v":[[-390.002,-60],[390,-60],[-40,180],[-87.47,197.841],[-161.687,273.107],[-218.313,273.107],[-292.53,197.841],[-350,140]],"c":true}]},{"t":359,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[0,20.894],[0,0],[0,32.083]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[0,-17.841],[0,-32.083]],"v":[[-390,-60],[390,-60],[-240,180],[-287.47,197.841],[-361.687,273.107],[-390,263.672],[-390,197.841],[-390,140]],"c":true}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":360,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Blob 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[390,300,0],"ix":2,"l":2},"a":{"a":0,"k":[390,300,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-132.548],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[132.548,0]],"v":[[390,-60],[0,-60],[0,-300],[150,-300]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"RT","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,132.493]],"o":[[0,0],[0,0],[0,0],[132.493,0],[0,0]],"v":[[390,-60],[0,-60],[0,180],[150.1,180],[390,-59.9]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"RB","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-132.548],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[-132.548,0]],"v":[[-390,-60],[0,-60],[0,-300],[-150,-300]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"LT","np":3,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":181,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,132.493]],"o":[[0,0],[0,0],[0,0],[-132.493,0],[0,0]],"v":[[-390,-60],[0,-60],[0,180],[-150.1,180],[-390,-59.9]],"c":true}]},{"t":359,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,66.274]],"o":[[0,0],[0,0],[0,0],[-66.274,0],[0,0]],"v":[[-390,-60],[0,-60],[0,180],[-270,180],[-390,60]],"c":true}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"LB","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":360,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"1 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[390,300,0],"ix":2,"l":2},"a":{"a":0,"k":[390,300,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[0,20.894],[0,0],[0,32.083]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[0,-17.841],[0,-32.083]],"v":[[-390,-60],[390,-60],[-240,180],[-287.47,197.841],[-361.687,273.107],[-390,263.672],[-390,197.841],[-390,140]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":90,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[15.637,15.858],[0,0],[29.086,29.579]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[-11.264,-11.424],[-29.086,-29.579]],"v":[[-390.002,-60],[390,-60],[-40,180],[-87.47,197.841],[-161.687,273.107],[-218.313,273.107],[-292.53,197.841],[-350,140]],"c":true}]},{"t":179,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[15.637,15.858],[0,0],[22.824,0]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[-11.265,-11.424],[-132.5,0]],"v":[[-390,-60],[390,-60],[150,180],[102.53,197.841],[28.313,273.107],[-28.313,273.107],[-102.53,197.841],[-150,180]],"c":true}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Blob","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[390,300,0],"ix":2,"l":2},"a":{"a":0,"k":[390,300,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-132.548],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[132.548,0]],"v":[[390,-60],[0,-60],[0,-300],[150,-300]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"RT","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,132.493]],"o":[[0,0],[0,0],[0,0],[132.493,0],[0,0]],"v":[[390,-60],[0,-60],[0,180],[150.1,180],[390,-59.9]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"RB","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-132.548],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[-132.548,0]],"v":[[-390,-60],[0,-60],[0,-300],[-150,-300]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"LT","np":3,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":1,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,66.274]],"o":[[0,0],[0,0],[0,0],[-66.274,0],[0,0]],"v":[[-390,-60],[0,-60],[0,180],[-270,180],[-390,60]],"c":true}]},{"t":179,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,132.493]],"o":[[0,0],[0,0],[0,0],[-132.493,0],[0,0]],"v":[[-390,-60],[0,-60],[0,180],[-150.1,180],[-390,-59.9]],"c":true}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"LB","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}} \ No newline at end of file +{"v":"5.12.1","fr":60,"ip":0,"op":360,"w":78,"h":60,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"1 Outlines 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[39,30,0],"ix":2,"l":2},"a":{"a":0,"k":[390,300,0],"ix":1,"l":2},"s":{"a":0,"k":[-10,10,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":181,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[15.637,15.858],[0,0],[22.824,0]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[-11.265,-11.424],[-132.5,0]],"v":[[-390,-60],[390,-60],[150,180],[102.53,197.841],[28.313,273.107],[-28.313,273.107],[-102.53,197.841],[-150,180]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":270,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[15.637,15.858],[0,0],[29.086,29.579]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[-11.264,-11.424],[-29.086,-29.579]],"v":[[-390.002,-60],[390,-60],[-40,180],[-87.47,197.841],[-161.687,273.107],[-218.313,273.107],[-292.53,197.841],[-350,140]],"c":true}]},{"t":359,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[0,20.894],[0,0],[0,32.083]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[0,-17.841],[0,-32.083]],"v":[[-390,-60],[390,-60],[-240,180],[-287.47,197.841],[-361.687,273.107],[-390,263.672],[-390,197.841],[-390,140]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":360,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Blob 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[39,30,0],"ix":2,"l":2},"a":{"a":0,"k":[390,300,0],"ix":1,"l":2},"s":{"a":0,"k":[-10,10,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-132.548],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[132.548,0]],"v":[[390,-60],[0,-60],[0,-300],[150,-300]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"RT","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,132.493]],"o":[[0,0],[0,0],[0,0],[132.493,0],[0,0]],"v":[[390,-60],[0,-60],[0,180],[150.1,180],[390,-59.9]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"RB","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-132.548],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[-132.548,0]],"v":[[-390,-60],[0,-60],[0,-300],[-150,-300]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"LT","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":181,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,132.493]],"o":[[0,0],[0,0],[0,0],[-132.493,0],[0,0]],"v":[[-390,-60],[0,-60],[0,180],[-150.1,180],[-390,-59.9]],"c":true}]},{"t":359,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,66.274]],"o":[[0,0],[0,0],[0,0],[-66.274,0],[0,0]],"v":[[-390,-60],[0,-60],[0,180],[-270,180],[-390,60]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"LB","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":360,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"1 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[39,30,0],"ix":2,"l":2},"a":{"a":0,"k":[390,300,0],"ix":1,"l":2},"s":{"a":0,"k":[10,10,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":1,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[0,20.894],[0,0],[0,32.083]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[0,-17.841],[0,-32.083]],"v":[[-390,-60],[390,-60],[-240,180],[-287.47,197.841],[-361.687,273.107],[-390,263.672],[-390,197.841],[-390,140]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":90,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[15.637,15.858],[0,0],[29.086,29.579]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[-11.264,-11.424],[-29.086,-29.579]],"v":[[-390.002,-60],[390,-60],[-40,180],[-87.47,197.841],[-161.687,273.107],[-218.313,273.107],[-292.53,197.841],[-350,140]],"c":true}]},{"t":179,"s":[{"i":[[0,132.5],[0,0],[132.5,0],[11.264,-11.424],[0,0],[15.637,15.858],[0,0],[22.824,0]],"o":[[0,0],[0,132.548],[-22.081,0],[0,0],[-15.637,15.858],[0,0],[-11.265,-11.424],[-132.5,0]],"v":[[-390,-60],[390,-60],[150,180],[102.53,197.841],[28.313,273.107],[-28.313,273.107],[-102.53,197.841],[-150,180]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Blob","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[39,30,0],"ix":2,"l":2},"a":{"a":0,"k":[390,300,0],"ix":1,"l":2},"s":{"a":0,"k":[10,10,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-132.548],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[132.548,0]],"v":[[390,-60],[0,-60],[0,-300],[150,-300]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"RT","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,132.493]],"o":[[0,0],[0,0],[0,0],[132.493,0],[0,0]],"v":[[390,-60],[0,-60],[0,180],[150.1,180],[390,-59.9]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"RB","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-132.548],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[-132.548,0]],"v":[[-390,-60],[0,-60],[0,-300],[-150,-300]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"LT","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":1,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,66.274]],"o":[[0,0],[0,0],[0,0],[-66.274,0],[0,0]],"v":[[-390,-60],[0,-60],[0,180],[-270,180],[-390,60]],"c":true}]},{"t":179,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,132.493]],"o":[[0,0],[0,0],[0,0],[-132.493,0],[0,0]],"v":[[-390,-60],[0,-60],[0,180],[-150.1,180],[-390,-59.9]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[390,300],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"LB","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}} \ No newline at end of file