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 0000000000..f52ed6fbbe Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/InlineRatingWarning.imageset/badratingprofile.pdf differ 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 0000000000..dbd046137a Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/ProfileLevelWarningIcon.imageset/badratingslider.pdf differ 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