diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index a55e3970c2..1cab68ba72 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14845,7 +14845,7 @@ Sorry for the inconvenience."; "ProfileLevelInfo.MyDescriptionDays_any" = "%@ days"; "ProfileLevelInfo.MyDescriptionPoints_1" = "%@ point is"; "ProfileLevelInfo.MyDescriptionPoints_any" = "%@ points are"; -"ProfileLevelInfo.MyDescription" = "The rating updates in %@ after purchases.\n\%@ pending."; +"ProfileLevelInfo.MyDescriptionPreview" = "The rating updates in %@ after purchases.\n\%@ pending. [Preview >]()"; "ProfileLevelInfo.OtherDescription" = "The rating reflects **%@'s** activity on Telegram. What affects it:"; "ProfileLevelInfo.LevelIndex_1" = "Level %@"; "ProfileLevelInfo.LevelIndex_any" = "Level %@"; diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 6cbc2f70c6..ebb233f45b 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2580,7 +2580,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { foundRemotePeers = .single(([], [], [], false)) } let searchLocations: [SearchMessagesLocation] - if let options = options { + if key == .globalPosts { + searchLocations = [SearchMessagesLocation.general(scope: .globalPosts(allowPaidStars: approvedGlobalPostQueryState?.price), tags: nil, minDate: nil, maxDate: nil)] + } else if let options = options { if case let .forum(peerId) = location { searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, reactions: nil, threadId: nil, minDate: options.date?.0, maxDate: options.date?.1), .general(scope: .everywhere, tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1)] } else if let (peerId, _, _) = options.peer { diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index a6da53980a..785965b4b4 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -783,6 +783,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { let spec: Spec let backgroundColor: UInt32 + let isDark: Bool let sideInsets: CGFloat let imageFrame: CGRect @@ -799,6 +800,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { init( spec: Spec, backgroundColor: UInt32, + isDark: Bool, sideInsets: CGFloat, imageFrame: CGRect, imageSize: CGSize, @@ -810,6 +812,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { ) { self.spec = spec self.backgroundColor = backgroundColor + self.isDark = isDark self.sideInsets = sideInsets self.imageFrame = imageFrame self.imageSize = imageSize @@ -853,6 +856,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { } let backgroundColor = spec.component.chosenOrder != nil ? spec.component.colors.selectedBackground : spec.component.colors.deselectedBackground + let isDark = spec.component.colors.isDark let imageFrame: CGRect if spec.component.isTag { @@ -892,7 +896,11 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { ) } counterLayout = counterValue - size.width += spacing + counterValue.size.width + if spec.component.count != 0 { + size.width += spacing + counterValue.size.width + } else { + size.width -= 1.0 + } if spec.component.isTag { size.width += 5.0 } @@ -957,6 +965,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { return Layout( spec: spec, backgroundColor: backgroundColor, + isDark: isDark, sideInsets: sideInsets, imageFrame: imageFrame, imageSize: boundingImageSize, @@ -1203,6 +1212,8 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { let starsEffectLayerFrame = CGRect(origin: CGPoint(), size: layout.size) animation.animator.updateFrame(layer: starsEffectLayer, frame: starsEffectLayerFrame, completion: nil) starsEffectLayer.update(size: starsEffectLayerFrame.size) + + starsEffectLayer.opacity = layout.isDark ? 0.55 : 1.0 } else { if let starsEffectLayer = self.starsEffectLayer { self.starsEffectLayer = nil @@ -1366,6 +1377,7 @@ public final class ReactionButtonComponent: Equatable { public var extractedSelectedForeground: UInt32 public var deselectedMediaPlaceholder: UInt32 public var selectedMediaPlaceholder: UInt32 + public var isDark: Bool public init( deselectedBackground: UInt32, @@ -1381,7 +1393,8 @@ public final class ReactionButtonComponent: Equatable { extractedForeground: UInt32, extractedSelectedForeground: UInt32, deselectedMediaPlaceholder: UInt32, - selectedMediaPlaceholder: UInt32 + selectedMediaPlaceholder: UInt32, + isDark: Bool ) { self.deselectedBackground = deselectedBackground self.selectedBackground = selectedBackground @@ -1397,6 +1410,7 @@ public final class ReactionButtonComponent: Equatable { self.extractedSelectedForeground = extractedSelectedForeground self.deselectedMediaPlaceholder = deselectedMediaPlaceholder self.selectedMediaPlaceholder = selectedMediaPlaceholder + self.isDark = isDark } } diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 77384b09b5..285b6e4699 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -73,7 +73,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case keepChatNavigationStack(PresentationTheme, Bool) case skipReadHistory(PresentationTheme, Bool) case alwaysDisplayTyping(Bool) - case dustEffect(Bool) + case debugRatingLayout(Bool) case crashOnSlowQueries(PresentationTheme, Bool) case crashOnMemoryPressure(PresentationTheme, Bool) case clearTips(PresentationTheme) @@ -132,7 +132,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .webViewInspection, .resetWebViewCache: return DebugControllerSection.web.rawValue - case .keepChatNavigationStack, .skipReadHistory, .alwaysDisplayTyping, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: + case .keepChatNavigationStack, .skipReadHistory, .alwaysDisplayTyping, .debugRatingLayout, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .allForumsHaveTabs, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .fakeAds, .enableLocalTranslation: return DebugControllerSection.experiments.rawValue @@ -185,7 +185,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 16 case .alwaysDisplayTyping: return 17 - case .dustEffect: + case .debugRatingLayout: return 18 case .crashOnSlowQueries: return 20 @@ -970,11 +970,11 @@ private enum DebugControllerEntry: ItemListNodeEntry { return settings }).start() }) - case let .dustEffect(value): - return ItemListSwitchItem(presentationData: presentationData, title: "Dust Debug", value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .debugRatingLayout(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Rating Debug", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in var settings = settings - settings.dustEffect = value + settings.debugRatingLayout = value return settings }).start() }) @@ -1506,7 +1506,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory)) #endif entries.append(.alwaysDisplayTyping(experimentalSettings.alwaysDisplayTyping)) - entries.append(.dustEffect(experimentalSettings.dustEffect)) + entries.append(.debugRatingLayout(experimentalSettings.debugRatingLayout)) } entries.append(.crashOnSlowQueries(presentationData.theme, experimentalSettings.crashOnLongQueries)) entries.append(.crashOnMemoryPressure(presentationData.theme, experimentalSettings.crashOnMemoryPressure)) diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index 361c840421..30f45efffa 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -532,7 +532,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveForeground: .clear, reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2), - reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsInactiveForeground: UIColor(rgb: 0xFFD738), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), @@ -548,7 +548,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveForeground: .clear, reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2), - reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsInactiveForeground: UIColor(rgb: 0xFFD738), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), @@ -570,7 +570,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveForeground: .clear, reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2), - reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsInactiveForeground: UIColor(rgb: 0xFFD738), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), @@ -586,7 +586,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveForeground: .clear, reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2), - reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsInactiveForeground: UIColor(rgb: 0xFFD738), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), @@ -605,7 +605,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveForeground: .clear, reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2), - reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsInactiveForeground: UIColor(rgb: 0xFFD738), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), @@ -621,7 +621,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveForeground: .clear, reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2), - reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), + reactionStarsInactiveForeground: UIColor(rgb: 0xFFD738), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index 0ce3db9a51..864920d628 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -376,7 +376,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb, extractedSelectedForeground: arguments.presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : arguments.presentationData.theme.theme.contextMenu.primaryColor.argb, deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb, - selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb + selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb, + isDark: arguments.presentationData.theme.theme.overallDarkAppearance ) case .BubbleOutgoing, .ImageOutgoing, .FreeOutgoing: let themeColors = bubbleColorComponents(theme: arguments.presentationData.theme.theme, incoming: false, wallpaper: !arguments.presentationData.theme.wallpaper.isEmpty) @@ -395,7 +396,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb, extractedSelectedForeground: arguments.presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : arguments.presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb, deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb, - selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb + selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb, + isDark: arguments.presentationData.theme.theme.overallDarkAppearance ) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift index 04a8eb3957..12db046140 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift @@ -87,7 +87,8 @@ public final class MessageReactionButtonsNode: ASDisplayNode { extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb, extractedSelectedForeground: presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb, deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb, - selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb + selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb, + isDark: presentationData.theme.theme.overallDarkAppearance ) case .outgoing: themeColors = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty) @@ -105,7 +106,8 @@ public final class MessageReactionButtonsNode: ASDisplayNode { extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb, extractedSelectedForeground: presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb, deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb, - selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb + selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb, + isDark: presentationData.theme.theme.overallDarkAppearance ) case .freeform: if presentationData.theme.wallpaper.isEmpty { @@ -128,7 +130,8 @@ public final class MessageReactionButtonsNode: ASDisplayNode { extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb, extractedSelectedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb, deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb, - selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb + selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb, + isDark: presentationData.theme.theme.overallDarkAppearance ) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift index c318f38baa..2d964015fb 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift @@ -5,25 +5,134 @@ import ComponentFlow import MultilineTextComponent import Svg +private func generateNumberOffsets() -> [CGPoint] { + return [ + CGPoint(x: 0.33, y: -0.33), + CGPoint(x: 0.24749999999999983, y: -0.495), + CGPoint(x: 0.0, y: -1.4025), + CGPoint(x: 0.5775, y: -0.495), + CGPoint(x: 0.33, y: 0.0), + CGPoint(x: 0.5775, y: 0.0), + CGPoint(x: 0.49500000000000005, y: 0.0), + CGPoint(x: 0.66, y: 0.0), + CGPoint(x: 0.66, y: 0.0), + CGPoint(x: 0.5775, y: 0.0), + CGPoint(x: 0.5775, y: 0.0), + CGPoint(x: 0.165, y: 0.0), + CGPoint(x: 0.5775, y: 0.0), + CGPoint(x: 0.5775, y: 0.0), + CGPoint(x: 0.5775, y: 0.0), + CGPoint(x: 0.5775, y: 0.0), + CGPoint(x: 0.66, y: 0.0), + CGPoint(x: 0.66, y: 0.0), + CGPoint(x: 0.5775, y: 0.0), + CGPoint(x: 0.495, y: 0.0), + CGPoint(x: 0.5775, y: 0.0), + CGPoint(x: 0.7425, y: 0.0), + CGPoint(x: 0.49500000000000005, y: 0.0), + CGPoint(x: 0.5775, y: 0.0), + CGPoint(x: 0.66, y: 0.0), + CGPoint(x: 0.41250000000000003, y: 0.0), + CGPoint(x: 0.49500000000000005, y: 0.0), + CGPoint(x: 0.2475, y: 0.0), + CGPoint(x: 0.5775, y: 0.0), + CGPoint(x: 1.2375, y: 0.0), + CGPoint(x: 1.0725, y: 0.0), + CGPoint(x: 0.7425, y: 0.0), + CGPoint(x: 0.7425, y: 0.0), + CGPoint(x: 1.32, y: 0.0), + CGPoint(x: 0.9900000000000001, y: 0.0), + CGPoint(x: 1.5675000000000001, y: 0.0), + CGPoint(x: 0.9075000000000001, y: 0.0), + CGPoint(x: 1.155, y: 0.0), + CGPoint(x: 1.155, y: 0.0), + CGPoint(x: 0.8250000000000001, y: 0.41250000000000003), + CGPoint(x: 0.66, y: 1.32), + CGPoint(x: 0.33, y: 1.32), + CGPoint(x: 0.41250000000000003, y: 0.41250000000000003), + CGPoint(x: 0.49500000000000005, y: 0.2475), + CGPoint(x: 0.41250000000000003, y: 0.33), + CGPoint(x: 0.5775, y: 0.49500000000000005), + CGPoint(x: 0.7425, y: 0.9075000000000001), + CGPoint(x: 0.41250000000000003, y: 0.49500000000000005), + CGPoint(x: 0.5775, y: 0.5775), + CGPoint(x: 1.32, y: -0.9075), + CGPoint(x: 0.49500000000000005, y: -0.41250000000000003), + CGPoint(x: 0.2475, y: -0.9075), + CGPoint(x: 0.7425, y: -0.66), + CGPoint(x: 0.9075000000000001, y: -0.49500000000000005), + CGPoint(x: 0.66, y: -0.165), + CGPoint(x: 1.155, y: -0.16499999999999998), + CGPoint(x: 0.9075000000000001, y: 0.0), + CGPoint(x: 0.8250000000000001, y: 0.0), + CGPoint(x: 0.9900000000000001, y: -0.0825), + CGPoint(x: 0.8250000000000001, y: 0.08249999999999998), + CGPoint(x: 0.41250000000000003, y: 0.0), + CGPoint(x: 0.41250000000000003, y: 0.0), + CGPoint(x: 1.2375, y: 0.0), + CGPoint(x: 0.9900000000000001, y: 0.0), + CGPoint(x: 0.9900000000000001, y: 0.0), + CGPoint(x: 1.0725, y: 0.0), + CGPoint(x: 0.8250000000000001, y: 0.0), + CGPoint(x: 0.8250000000000001, y: 0.0), + CGPoint(x: 0.7425, y: 0.0), + CGPoint(x: 0.9075000000000001, y: 0.0), + CGPoint(x: 0.5775, y: 0.0), + CGPoint(x: 0.7425, y: 0.0), + CGPoint(x: 0.7425, y: 0.0), + CGPoint(x: 0.7425, y: 0.0), + CGPoint(x: 0.7425, y: 0.0), + CGPoint(x: 0.99, y: 0.0), + CGPoint(x: 0.41250000000000003, y: 0.0), + CGPoint(x: 0.66, y: 0.0), + CGPoint(x: 0.7425, y: 0.0), + CGPoint(x: 0.9900000000000001, y: 0.0), + CGPoint(x: 0.66, y: 0.0), + CGPoint(x: 0.5775, y: 0.0), + CGPoint(x: 1.2375, y: 0.0), + CGPoint(x: 0.9900000000000001, y: 0.0), + CGPoint(x: 1.32, y: 0.0), + CGPoint(x: 1.155, y: 0.0), + CGPoint(x: 0.9900000000000001, y: 0.0), + CGPoint(x: 1.0725, y: 0.0), + CGPoint(x: 1.2375, y: 0.0), + CGPoint(x: 0.8250000000000001, y: 0.0), + CGPoint(x: 1.0725, y: 0.0), + CGPoint(x: 0.9075000000000001, y: 0.0), + CGPoint(x: 1.155, y: 0.0), + CGPoint(x: 0.8250000000000001, y: 0.0), + CGPoint(x: 1.155, y: 0.0), + CGPoint(x: 1.0725, y: 0.0), + CGPoint(x: 1.2375, y: 0.0), + CGPoint(x: 1.155, y: 0.0), + CGPoint(x: 1.32, y: 0.0), + ] +} + +let numberOffsets: [CGPoint] = generateNumberOffsets() + public final class PeerInfoRatingComponent: Component { let backgroundColor: UIColor let borderColor: UIColor let foregroundColor: UIColor let level: Int let action: () -> Void + let debugLevel: Bool public init( backgroundColor: UIColor, borderColor: UIColor, foregroundColor: UIColor, level: Int, - action: @escaping () -> Void + action: @escaping () -> Void, + debugLevel: Bool = false ) { self.backgroundColor = backgroundColor self.borderColor = borderColor self.foregroundColor = foregroundColor self.level = level self.action = action + self.debugLevel = debugLevel } public static func ==(lhs: PeerInfoRatingComponent, rhs: PeerInfoRatingComponent) -> Bool { @@ -39,6 +148,9 @@ public final class PeerInfoRatingComponent: Component { if lhs.level != rhs.level { return false } + if lhs.debugLevel != rhs.debugLevel { + return false + } return true } @@ -56,7 +168,7 @@ public final class PeerInfoRatingComponent: Component { private let borderLayer: SimpleLayer private let backgroundLayer: SimpleLayer - //private var tempLevel: Int = 1 + private var debugLevel: Int = 1 private var component: PeerInfoRatingComponent? private weak var state: EmptyComponentState? @@ -78,18 +190,23 @@ public final class PeerInfoRatingComponent: Component { } @objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) { + guard let component = self.component else { + return + } if case .ended = recognizer.state { - self.component?.action() - - /*if self.tempLevel < 10 { - self.tempLevel += 1 + if component.debugLevel { + if self.debugLevel < 10 { + self.debugLevel += 1 + } else { + self.debugLevel += 10 + } + if self.debugLevel >= 110 { + self.debugLevel = 1 + } + self.state?.updated(transition: .immediate) } else { - self.tempLevel += 10 + self.component?.action() } - if self.tempLevel >= 110 { - self.tempLevel = 1 - } - self.state?.updated(transition: .immediate)*/ } } @@ -102,14 +219,51 @@ public final class PeerInfoRatingComponent: Component { self.component = component self.state = state - let level = component.level - //let level = self.tempLevel + let level: Int + if component.debugLevel { + level = self.debugLevel + } else { + level = component.level + } let iconSize = CGSize(width: 26.0, height: 26.0) - if previousComponent?.level != level || previousComponent?.borderColor != component.borderColor || previousComponent?.foregroundColor != component.foregroundColor || previousComponent?.backgroundColor != component.backgroundColor || "".isEmpty { + let alwaysRedraw: Bool = component.debugLevel + + if previousComponent?.level != level || previousComponent?.borderColor != component.borderColor || previousComponent?.foregroundColor != component.foregroundColor || previousComponent?.backgroundColor != component.backgroundColor || alwaysRedraw { + let weight: CGFloat = UIFont.Weight.semibold.rawValue + let width: CGFloat = -0.1 + + let descriptor: UIFontDescriptor + if #available(iOS 14.0, *) { + descriptor = UIFont.systemFont(ofSize: 10.0).fontDescriptor + } else { + descriptor = UIFont.systemFont(ofSize: 10.0, weight: UIFont.Weight.semibold).fontDescriptor + } + + let symbolicTraits = descriptor.symbolicTraits + var updatedDescriptor: UIFontDescriptor? = descriptor.withSymbolicTraits(symbolicTraits) + updatedDescriptor = updatedDescriptor?.withDesign(.default) + if #available(iOS 14.0, *) { + updatedDescriptor = updatedDescriptor?.addingAttributes([ + UIFontDescriptor.AttributeName.traits: [UIFontDescriptor.TraitKey.weight: weight] + ]) + } + if #available(iOS 16.0, *) { + updatedDescriptor = updatedDescriptor?.addingAttributes([ + UIFontDescriptor.AttributeName.traits: [UIFontDescriptor.TraitKey.width: width] + ]) + } + + let font: UIFont + if let updatedDescriptor { + font = UIFont(descriptor: updatedDescriptor, size: 10.0) + } else { + font = UIFont(descriptor: descriptor, size: 10.0) + } + let attributedText = NSAttributedString(string: "\(level)", attributes: [ - NSAttributedString.Key.font: Font.semibold(10.0), + NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: component.foregroundColor ]) @@ -157,13 +311,25 @@ public final class PeerInfoRatingComponent: Component { } let levelIndex: Int - if level <= 10 { - levelIndex = max(0, component.level) + if level < 0 { + levelIndex = 1 + } else if level <= 10 { + levelIndex = max(1, level) } else if level <= 90 { levelIndex = (level / 10) * 10 } else { levelIndex = 90 } + + let backgroundOffsetsY: [Int: CGFloat] = [ + 3: -0.8250000000000001, + 7: 0.33, + 40: 1.4025, + 60: 0.2475, + 70: 0.33, + 80: 0.2475, + ] + let borderImage = generateImage(iconSize, rotatedContext: { size, context in UIGraphicsPushContext(context) defer { @@ -174,7 +340,7 @@ public final class PeerInfoRatingComponent: Component { 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(), size: size), blendMode: .normal, alpha: 1.0) + image.draw(in: CGRect(origin: CGPoint(x: 0.0, y: backgroundOffsetsY[levelIndex] ?? 0.0), size: size), blendMode: .normal, alpha: 1.0) } } }) @@ -196,7 +362,7 @@ public final class PeerInfoRatingComponent: Component { 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(), size: size), blendMode: .normal, alpha: 1.0) + image.draw(in: CGRect(origin: CGPoint(x: 0.0, y: backgroundOffsetsY[levelIndex] ?? 0.0), size: size), blendMode: .normal, alpha: 1.0) } } @@ -208,7 +374,15 @@ public final class PeerInfoRatingComponent: Component { if let textLayout { let titleScale: CGFloat - if level < 10 { + if level < 0 { + if abs(level) < 10 { + titleScale = 0.8 + } else if abs(level) < 100 { + titleScale = 0.6 + } else { + titleScale = 0.4 + } + } else if level < 10 { titleScale = 1.0 } else if level < 100 { titleScale = 0.8 @@ -216,19 +390,25 @@ public final class PeerInfoRatingComponent: Component { titleScale = 0.6 } - var textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textLayout.size.width) * 0.5), y: floorToScreenPixels((size.height - textLayout.size.height) * 0.5)), size: textLayout.size) - if level == 1 { - textFrame.origin.x += UIScreenPixel - } else { - textFrame.origin.x += 0.0 - } + let textFrame = CGRect(origin: CGPoint(x: (size.width - textLayout.size.width) * 0.5, y: (size.height - textLayout.size.height) * 0.5), size: textLayout.size) context.saveGState() context.translateBy(x: textFrame.midX, y: textFrame.midY) context.scaleBy(x: titleScale, y: titleScale) context.translateBy(x: -textFrame.midX, y: -textFrame.midY) - attributedText.draw(at: textFrame.origin) + var drawPoint: CGPoint + drawPoint = textFrame.origin + + if level >= 1 && level <= 99 { + let numberOffset = numberOffsets[level - 1] + drawPoint.x += numberOffset.x + drawPoint.y += numberOffset.y + } else { + drawPoint.x += -UIScreenPixel + -textLayout.opticalBounds.minX + (textFrame.width - textLayout.opticalBounds.width) * 0.5 + } + + attributedText.draw(at: drawPoint) context.restoreGState() } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index fed481f012..8ba2f1f94e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -831,7 +831,36 @@ final class PeerInfoHeaderNode: ASDisplayNode { headerButtonBackgroundColor = regularHeaderButtonBackgroundColor.mixedWith(collapsedHeaderButtonBackgroundColor, alpha: effectiveTransitionFraction) - if let status = peer?.emojiStatus, case let .starGift(_, _, _, _, _, innerColor, outerColor, _, _) = status.content { + 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) + + let innerColor = UIColor(rgb: UInt32(bitPattern: innerColor)) + let outerColor = UIColor(rgb: UInt32(bitPattern: outerColor)) + let backgroundColor = innerColor.mixedWith(outerColor, alpha: 0.8) + + let patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue)) + ratingBorderColor = patternColor.withAlphaComponent(0.3).blendOver(background: backgroundColor).mixedWith(.clear, alpha: effectiveTransitionFraction) + ratingForegroundColor = ratingBorderColor.mixedWith(presentationData.theme.list.itemCheckColors.foregroundColor, alpha: effectiveTransitionFraction) + } else if let profileColor = peer?.profileColor { + ratingBackgroundColor = UIColor(white: 1.0, alpha: 1.0).mixedWith(presentationData.theme.list.itemCheckColors.fillColor, alpha: effectiveTransitionFraction) + + let backgroundColors = self.context.peerNameColors.getProfile(profileColor, dark: presentationData.theme.overallDarkAppearance) + + let innerColor = backgroundColors.main + let outerColor = backgroundColors.secondary ?? backgroundColors.main + let backgroundColor = innerColor.mixedWith(outerColor, alpha: 0.8) + + let patternColor = UIColor(white: 0.0, alpha: 0.6) + ratingBorderColor = patternColor.withAlphaComponent(0.3).blendOver(background: backgroundColor).mixedWith(.clear, alpha: effectiveTransitionFraction) + ratingForegroundColor = ratingBorderColor.mixedWith(presentationData.theme.list.itemCheckColors.foregroundColor, alpha: effectiveTransitionFraction) + } else { + ratingBackgroundColor = presentationData.theme.list.itemCheckColors.fillColor + 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)) @@ -848,7 +877,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { ratingBackgroundColor = presentationData.theme.list.itemCheckColors.fillColor ratingBorderColor = UIColor.clear ratingForegroundColor = presentationData.theme.list.itemCheckColors.foregroundColor - } + }*/ } do { @@ -1959,7 +1988,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.currentPendingStarRating = cachedData.pendingStarRating #if DEBUG - self.currentPendingStarRating = TelegramStarPendingRating(rating: TelegramStarRating(level: starRating.level, currentLevelStars: starRating.currentLevelStars, stars: starRating.stars + 123, nextLevelStars: starRating.nextLevelStars), timestamp: Int32(Date().timeIntervalSince1970) + 60 * 60 * 24 * 3) + 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, nextLevelStars: starRating.nextLevelStars! * 2), timestamp: Int32(Date().timeIntervalSince1970) + 60 * 60 * 24 * 3) + } #endif } else { self.currentStarRating = nil @@ -1996,7 +2028,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { pendingStarRating: self.currentPendingStarRating, customTheme: self.presentationData?.theme )) - } + }, + debugLevel: self.context.sharedContext.immediateExperimentalUISettings.debugRatingLayout )), environment: {}, containerSize: CGSize(width: width - 12.0 * 2.0, height: 100.0) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index b9f558eba4..2a4449a56d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -2599,6 +2599,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if let listSource = self.listSource as? PeerStoryListContext { let _ = listSource.addToFolder(id: folderPreview.folder.id, items: [item]) } + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + //TODO:localize + let text: String + text = "Story added to folder." + self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: text, cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) }))) } @@ -2989,6 +2995,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.initialStoryFolderId = nil self.setStoryFolder(id: folder.id, assumeEmpty: false, animated: false) } else { + if self.initialStoryFolderId != nil && !storyFolders.isEmpty { + self.initialStoryFolderId = nil + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + //TODO:localize + self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: "The album is no longer available.", cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + self.currentListState = state var hasLocalItems = false @@ -3923,21 +3937,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.presentRenameStoryFolder(id: folder.id, title: folder.title) }) }))) + } + + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuShare, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + guard let self else { + c?.dismiss(completion: nil) + return + } - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuShare, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + c?.dismiss(completion: { [weak self] in guard let self else { - c?.dismiss(completion: nil) return } - - c?.dismiss(completion: { [weak self] in - guard let self else { - return - } - self.shareFolder(id: folder.id) - }) - }))) - } + self.shareFolder(id: folder.id) + }) + }))) if self.canManageStories { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuReorder, icon: { theme in @@ -5095,6 +5109,16 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } if let listSource = self.listSource as? PeerStoryListContext { let _ = listSource.addToFolder(id: folderId, items: items) + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + //TODO:localize + let text: String + if items.count == 1 { + text = "Story added to folder." + } else { + text = "\(items.count) stories added to folder." + } + self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: text, cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) } }) controller.navigationPresentation = .modal @@ -5392,6 +5416,64 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr collectibleItemInfo: nil ) self.parentController?.present(shareController, in: .window(.root)) + shareController.completed = { [weak self] peerIds in + guard let self else { + return + } + let _ = (self.context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in + guard let self else { + return + } + + let peers = peerList.compactMap { $0 } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + let text: String + var savedMessages = false + if peers.count == 1, let peer = peers.first { + let peerName = peer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.WebBrowser_LinkForwardTooltip_Chat_One(peerName).string + savedMessages = peer.id == self.context.account.peerId + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.WebBrowser_LinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.WebBrowser_LinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + + self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.parentController?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) + }) + } + return false + }), in: .current) + }) + } + shareController.actionCompleted = { [weak self] in + guard let self else { + return + } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } } } } diff --git a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/BUILD index 83001d32bd..e7197580ee 100644 --- a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/BUILD @@ -27,6 +27,9 @@ swift_library( "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/PremiumUI", "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath", + "//submodules/Components/HierarchyTrackingLayer", + "//submodules/TelegramUI/Components/AnimatedTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift index 5c1b03e34c..b121ca24ae 100644 --- a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift @@ -142,7 +142,7 @@ private final class ProfileLevelInfoScreenComponent: Component { self.addSubview(self.dimView) self.layer.addSublayer(self.backgroundLayer) - self.scrollView.delaysContentTouches = true + self.scrollView.delaysContentTouches = false self.scrollView.canCancelContentTouches = true self.scrollView.clipsToBounds = false self.scrollView.contentInsetAdjustmentBehavior = .never @@ -283,6 +283,8 @@ private final class ProfileLevelInfoScreenComponent: Component { self.isUpdating = false } + let isChangingPreview = transition.userData(TransitionHint.self)?.isChangingPreview ?? false + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.16) let environment = environment[ViewControllerComponentContainer.Environment.self].value @@ -354,13 +356,14 @@ private final class ProfileLevelInfoScreenComponent: Component { let pendingPoints = pendingStarRating.rating.stars - component.starRating.stars if self.isPreviewingPendingRating { + //TODO:localize secondaryDescriptionTextString = "This will be your rating in 21 days,\n after \(pendingPoints) points are added. [Back >]()" } else { let dayCount = (pendingStarRating.timestamp - timestamp) / (24 * 60 * 60) if dayCount == 0 { secondaryDescriptionTextString = environment.strings.ProfileLevelInfo_MyDescriptionToday(Int32(pendingPoints)) } else { - secondaryDescriptionTextString = environment.strings.ProfileLevelInfo_MyDescription(environment.strings.ProfileLevelInfo_MyDescriptionDays(Int32(dayCount)), environment.strings.ProfileLevelInfo_MyDescriptionPoints(Int32(pendingPoints))).string + secondaryDescriptionTextString = environment.strings.ProfileLevelInfo_MyDescriptionPreview(environment.strings.ProfileLevelInfo_MyDescriptionDays(Int32(dayCount)), environment.strings.ProfileLevelInfo_MyDescriptionPoints(Int32(pendingPoints))).string } } } @@ -398,8 +401,9 @@ private final class ProfileLevelInfoScreenComponent: Component { environment.theme.list.itemCheckColors.fillColor, environment.theme.list.itemCheckColors.fillColor ] + let _ = gradientColors - let levelFraction: CGFloat + var levelFraction: CGFloat let badgeText: String var badgeTextSuffix: String? @@ -413,8 +417,8 @@ private final class ProfileLevelInfoScreenComponent: Component { if let nextLevelStars = pendingStarRating.rating.nextLevelStars { badgeTextSuffix = " / \(starCountString(Int64(nextLevelStars), decimalSeparator: "."))" } - if let nextLevelStars = pendingStarRating.rating.nextLevelStars { - levelFraction = Double(pendingStarRating.rating.stars) / Double(nextLevelStars) + if let nextLevelStars = pendingStarRating.rating.nextLevelStars, nextLevelStars > pendingStarRating.rating.stars { + levelFraction = Double(pendingStarRating.rating.stars - pendingStarRating.rating.currentLevelStars) / Double(nextLevelStars - pendingStarRating.rating.currentLevelStars) } else { levelFraction = 1.0 } @@ -426,13 +430,17 @@ private final class ProfileLevelInfoScreenComponent: Component { badgeTextSuffix = " / \(starCountString(Int64(nextLevelStars), decimalSeparator: "."))" } if let nextLevelStars = component.starRating.nextLevelStars { - levelFraction = Double(component.starRating.stars) / Double(nextLevelStars) - } else { + 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 = max(0.0, levelFraction) - let levelInfoSize = self.levelInfo.update( + /*let levelInfoSize = self.levelInfo.update( transition: .immediate, component: AnyComponent(PremiumLimitDisplayComponent( inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5), @@ -453,18 +461,31 @@ private final class ProfileLevelInfoScreenComponent: Component { )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 200.0) + )*/ + let _ = levelFraction + 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)) } ?? "", + badgeValue: badgeText, + badgeTotal: badgeTextSuffix, + level: Int(currentLevel) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 110.0) ) if let levelInfoView = self.levelInfo.view { if levelInfoView.superview == nil { self.scrollContentView.addSubview(levelInfoView) } - levelInfoView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - levelInfoSize.width) * 0.5), y: contentHeight - 16.0), size: levelInfoSize) + levelInfoView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - levelInfoSize.width) * 0.5), y: contentHeight - 6.0), size: levelInfoSize) } contentHeight += 129.0 - let isChangingPreview = transition.userData(TransitionHint.self)?.isChangingPreview ?? false - if let secondaryDescriptionTextString { if isChangingPreview, let secondaryDescriptionTextView = self.secondaryDescriptionText?.view { self.secondaryDescriptionText = nil diff --git a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarBadge.swift b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarBadge.swift new file mode 100644 index 0000000000..bc4d34798e --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarBadge.swift @@ -0,0 +1,439 @@ +import Foundation +import UIKit +import Display +import TelegramPresentationData +import ComponentFlow +import RoundedRectWithTailPath +import AnimatedTextComponent +import MultilineTextComponent + +final class ProfileLevelRatingBarBadge: Component { + final class TransitionHint { + let animateText: Bool + + init(animateText: Bool) { + self.animateText = animateText + } + } + + let theme: PresentationTheme + let title: String + let suffix: String? + + init( + theme: PresentationTheme, + title: String, + suffix: String? + ) { + self.theme = theme + self.title = title + self.suffix = suffix + } + + static func ==(lhs: ProfileLevelRatingBarBadge, rhs: ProfileLevelRatingBarBadge) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.suffix != rhs.suffix { + return false + } + return true + } + + final class View: UIView { + private let badgeView: UIView + private let badgeMaskView: UIView + private let badgeShapeLayer = SimpleShapeLayer() + + private let badgeForeground: SimpleLayer + let badgeIcon: UIImageView + private let badgeLabel = ComponentView() + private let suffixLabel = ComponentView() + + private var badgeTailPosition: CGFloat = 0.0 + private var badgeShapeArguments: (Double, Double, CGSize, CGFloat, CGFloat)? + + private var component: ProfileLevelRatingBarBadge? + private var isUpdating: Bool = false + + private var previousAvailableSize: CGSize? + + override init(frame: CGRect) { + self.badgeView = UIView() + self.badgeView.alpha = 0.0 + + self.badgeShapeLayer.fillColor = UIColor.white.cgColor + self.badgeShapeLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + + self.badgeMaskView = UIView() + self.badgeMaskView.layer.addSublayer(self.badgeShapeLayer) + self.badgeView.mask = self.badgeMaskView + + self.badgeForeground = SimpleLayer() + self.badgeForeground.anchorPoint = CGPoint() + + self.badgeIcon = UIImageView() + self.badgeIcon.contentMode = .center + + super.init(frame: frame) + + self.addSubview(self.badgeView) + self.badgeView.layer.addSublayer(self.badgeForeground) + self.badgeView.addSubview(self.badgeIcon) + + self.isUserInteractionEnabled = false + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.badgeView.frame.contains(point) { + return self + } else { + return nil + } + } + + func update(component: ProfileLevelRatingBarBadge, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.component == nil { + 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) + } + + let badgeLabelSize = self.badgeLabel.update( + transition: labelsTransition, + component: AnyComponent(AnimatedTextComponent( + font: Font.with(size: 24.0, design: .round, weight: .semibold, traits: []), + color: component.theme.list.itemCheckColors.foregroundColor, + items: [AnimatedTextComponent.Item( + id: AnyHashable(0), + content: .text(component.title) + )] + )), + environment: {}, + containerSize: CGSize(width: 300.0, height: 100.0) + ) + + let badgeSuffixSpacing: CGFloat = 0.0 + + let badgeSuffixSize = self.suffixLabel.update( + transition: labelsTransition, + component: AnyComponent(AnimatedTextComponent( + font: Font.regular(22.0), + color: component.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.6), + items: [AnimatedTextComponent.Item( + id: AnyHashable(0), + content: .text(component.suffix ?? "") + )] + )), + environment: {}, + containerSize: CGSize(width: 300.0, height: 100.0) + ) + + var badgeWidth: CGFloat = badgeLabelSize.width + 3.0 + 54.0 + if component.suffix != nil { + badgeWidth += badgeSuffixSize.width + badgeSuffixSpacing + } + + 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: 10.0, y: 8.0, width: 30.0, height: 30.0) + + self.badgeView.alpha = 1.0 + + let size = badgeSize + + var badgeContentWidth: CGFloat = badgeLabelSize.width + if component.suffix != nil { + badgeContentWidth += badgeSuffixSpacing + badgeSuffixSize.width + } + + let badgeLabelFrame = CGRect(origin: CGPoint(x: 14.0 + floorToScreenPixels((badgeFullSize.width - badgeContentWidth) / 2.0), y: 9.0), size: badgeLabelSize) + if let badgeLabelView = self.badgeLabel.view { + if badgeLabelView.superview == nil { + self.badgeView.addSubview(badgeLabelView) + } + labelsTransition.setFrame(view: badgeLabelView, frame: badgeLabelFrame) + } + if let suffixLabelView = self.suffixLabel.view { + if suffixLabelView.superview == nil { + suffixLabelView.layer.anchorPoint = CGPoint() + self.badgeView.addSubview(suffixLabelView) + } + let badgeSuffixFrame = CGRect(origin: CGPoint(x: badgeLabelFrame.maxX + badgeSuffixSpacing, y: badgeLabelFrame.maxY - badgeSuffixSize.height), size: badgeSuffixSize) + labelsTransition.setPosition(view: suffixLabelView, position: badgeSuffixFrame.origin) + suffixLabelView.bounds = CGRect(origin: CGPoint(), size: badgeSuffixFrame.size) + } + + 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) { + 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 transition: ContainedViewLayoutTransition = .immediate + transition.updateAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: tailPositionFraction, y: 1.0)) + transition.updatePosition(layer: self.badgeView.layer, position: CGPoint(x: (tailPositionFraction - 0.5) * size.width, y: 0.0)) + } + + func updateBadgeAngle(angle: CGFloat) { + 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 { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private let labelWidth: CGFloat = 16.0 +private let labelHeight: CGFloat = 36.0 +private let labelSize = CGSize(width: labelWidth, height: labelHeight) +private let font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: []) + +private final class BadgeLabelView: UIView { + private class StackView: UIView { + var labels: [UILabel] = [] + + var currentValue: Int32 = 0 + + var color: UIColor = .white { + didSet { + for view in self.labels { + view.textColor = self.color + } + } + } + + init() { + super.init(frame: CGRect(origin: .zero, size: labelSize)) + + var height: CGFloat = -labelHeight + for i in -1 ..< 10 { + let label = UILabel() + if i == -1 { + label.text = "9" + } else { + label.text = "\(i)" + } + label.textColor = self.color + label.font = font + label.textAlignment = .center + label.frame = CGRect(x: 0, y: height, width: labelWidth, height: labelHeight) + self.addSubview(label) + self.labels.append(label) + + height += labelHeight + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(value: Int32, isFirst: Bool, isLast: Bool, transition: ComponentTransition) { + let previousValue = self.currentValue + self.currentValue = value + + self.labels[1].alpha = isFirst && !isLast ? 0.0 : 1.0 + + if previousValue == 9 && value < 9 { + self.bounds = CGRect( + origin: CGPoint( + x: 0.0, + y: -1.0 * labelSize.height + ), + size: labelSize + ) + } + + let bounds = CGRect( + origin: CGPoint( + x: 0.0, + y: CGFloat(value) * labelSize.height + ), + size: labelSize + ) + transition.setBounds(view: self, bounds: bounds) + } + } + + private var itemViews: [Int: StackView] = [:] + private var staticLabel = UILabel() + + init() { + super.init(frame: .zero) + + self.clipsToBounds = true + self.isUserInteractionEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var color: UIColor = .white { + didSet { + self.staticLabel.textColor = self.color + for (_, view) in self.itemViews { + view.color = self.color + } + } + } + + func update(value: String, transition: ComponentTransition) -> CGSize { + if value.contains(" ") { + for (_, view) in self.itemViews { + view.isHidden = true + } + + if self.staticLabel.superview == nil { + self.staticLabel.textColor = self.color + self.staticLabel.font = font + + self.addSubview(self.staticLabel) + } + + self.staticLabel.text = value + let size = self.staticLabel.sizeThatFits(CGSize(width: 100.0, height: 100.0)) + self.staticLabel.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: labelHeight)) + + return CGSize(width: ceil(self.staticLabel.bounds.width), height: ceil(self.staticLabel.bounds.height)) + } + + let string = value + let stringArray = Array(string.map { String($0) }.reversed()) + + let labelSpacing: CGFloat = 0.0 + + let totalWidth = CGFloat(stringArray.count) * labelWidth + CGFloat(stringArray.count - 1) * labelSpacing + + var validIds: [Int] = [] + for i in 0 ..< stringArray.count { + validIds.append(i) + + let itemView: StackView + var itemTransition = transition + if let current = self.itemViews[i] { + itemView = current + } else { + itemTransition = transition.withAnimation(.none) + itemView = StackView() + itemView.color = self.color + self.itemViews[i] = itemView + self.addSubview(itemView) + } + + let digit = Int32(stringArray[i]) ?? 0 + itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition) + + itemTransition.setFrame( + view: itemView, + frame: CGRect(x: totalWidth - labelWidth * CGFloat(i + 1) + labelSpacing * CGFloat(i), y: 0.0, width: labelWidth, height: labelHeight) + ) + } + + var removeIds: [Int] = [] + for (id, itemView) in self.itemViews { + if !validIds.contains(id) { + removeIds.append(id) + + transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in + itemView.removeFromSuperview() + }) + } + } + for id in removeIds { + self.itemViews.removeValue(forKey: id) + } + return CGSize(width: totalWidth, height: labelHeight) + } +} + diff --git a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarComponent.swift b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarComponent.swift new file mode 100644 index 0000000000..de5fdf751c --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelRatingBarComponent.swift @@ -0,0 +1,414 @@ +import Foundation +import UIKit +import Display +import TelegramPresentationData +import ComponentFlow +import MultilineTextComponent +import BundleIconComponent +import HierarchyTrackingLayer + +final class ProfileLevelRatingBarComponent: Component { + final class TransitionHint { + let animate: Bool + + init(animate: Bool) { + self.animate = animate + } + } + + let theme: PresentationTheme + let value: CGFloat + let leftLabel: String + let rightLabel: String + let badgeValue: String + let badgeTotal: String? + let level: Int + + init( + theme: PresentationTheme, + value: CGFloat, + leftLabel: String, + rightLabel: String, + badgeValue: String, + badgeTotal: String?, + level: Int + ) { + self.theme = theme + self.value = value + self.leftLabel = leftLabel + self.rightLabel = rightLabel + self.badgeValue = badgeValue + self.badgeTotal = badgeTotal + self.level = level + } + + static func ==(lhs: ProfileLevelRatingBarComponent, rhs: ProfileLevelRatingBarComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.leftLabel != rhs.leftLabel { + return false + } + if lhs.rightLabel != rhs.rightLabel { + return false + } + if lhs.badgeValue != rhs.badgeValue { + return false + } + if lhs.badgeTotal != rhs.badgeTotal { + return false + } + if lhs.level != rhs.level { + return false + } + return true + } + + private final class AnimationState { + let fromValue: CGFloat + let toValue: CGFloat + let fromBadgeSize: CGSize + let startTime: Double + let duration: Double + let isWraparound: Bool + + init(fromValue: CGFloat, toValue: CGFloat, fromBadgeSize: CGSize, startTime: Double, duration: Double, isWraparound: Bool) { + self.fromValue = fromValue + self.toValue = toValue + self.fromBadgeSize = fromBadgeSize + self.startTime = startTime + self.duration = duration + self.isWraparound = isWraparound + } + + func timeFraction(at timestamp: Double) -> CGFloat { + var fraction = CGFloat((timestamp - self.startTime) / self.duration) + fraction = max(0.0, min(1.0, fraction)) + return fraction + } + + func fraction(at timestamp: Double) -> CGFloat { + return listViewAnimationCurveSystem(self.timeFraction(at: timestamp)) + } + + func value(at timestamp: Double) -> CGFloat { + let fraction = self.fraction(at: timestamp) + 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 + } else { + let halfFraction = (fraction - 0.5) / 0.5 + return halfFraction * self.toValue + } + } + + func badgeSize(at timestamp: Double, endValue: CGSize) -> CGSize { + let fraction = self.fraction(at: timestamp) + return CGSize( + width: (1.0 - fraction) * self.fromBadgeSize.width + fraction * endValue.width, + height: endValue.height + ) + } + } + + final class View: UIView { + private let barBackground: UIImageView + private let backgroundClippingContainer: UIView + private let foregroundClippingContainer: UIView + private let barForeground: UIImageView + + private let backgroundLeftLabel = ComponentView() + private let backgroundRightLabel = ComponentView() + private let foregroundLeftLabel = ComponentView() + private let foregroundRightLabel = ComponentView() + + private let badge = ComponentView() + + private var component: ProfileLevelRatingBarComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + + private var hierarchyTracker: HierarchyTrackingLayer? + private var animationLink: SharedDisplayLinkDriver.Link? + + private var animationState: AnimationState? + + override init(frame: CGRect) { + self.barBackground = UIImageView() + self.backgroundClippingContainer = UIView() + self.backgroundClippingContainer.clipsToBounds = true + self.foregroundClippingContainer = UIView() + self.foregroundClippingContainer.clipsToBounds = true + self.barForeground = UIImageView() + + super.init(frame: frame) + + let hierarchyTracker = HierarchyTrackingLayer() + self.hierarchyTracker = hierarchyTracker + self.layer.addSublayer(hierarchyTracker) + + self.hierarchyTracker?.isInHierarchyUpdated = { [weak self] value in + guard let self else { + return + } + self.updateAnimations() + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + private func updateAnimations() { + if let hierarchyTracker = self.hierarchyTracker, hierarchyTracker.isInHierarchy { + if self.animationState != nil { + if self.animationLink == nil { + self.animationLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in + guard let self else { + return + } + self.updateAnimations() + }) + } + } else { + self.animationLink?.invalidate() + self.animationLink = nil + self.animationState = nil + } + } else { + self.animationLink?.invalidate() + self.animationLink = nil + self.animationState = nil + } + + if let animationState = self.animationState { + if animationState.timeFraction(at: CACurrentMediaTime()) >= 1.0 { + self.animationState = nil + self.updateAnimations() + } + } + + if self.animationState != nil && !self.isUpdating { + self.state?.updated(transition: .immediate, isLocal: true) + } + } + + func update(component: ProfileLevelRatingBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let barHeight: CGFloat = 30.0 + + self.isUpdating = true + defer { + self.isUpdating = false + } + + var labelsTransition = transition + if let previousComponent = self.component, let hint = transition.userData(TransitionHint.self), hint.animate { + labelsTransition = .spring(duration: 0.4) + + let fromValue: CGFloat + if let animationState = self.animationState { + fromValue = animationState.value(at: CACurrentMediaTime()) + } else { + fromValue = previousComponent.value + } + let fromBadgeSize: CGSize + if let badgeView = self.badge.view as? ProfileLevelRatingBarBadge.View { + fromBadgeSize = badgeView.bounds.size + } else { + fromBadgeSize = CGSize() + } + self.animationState = AnimationState( + fromValue: fromValue, + toValue: component.value, + fromBadgeSize: fromBadgeSize, + startTime: CACurrentMediaTime(), + duration: 0.4 * UIView.animationDurationFactor(), + isWraparound: false//previousComponent.level < component.level + ) + self.updateAnimations() + } + + self.component = component + self.state = state + + if self.barBackground.image == nil { + self.barBackground.image = generateStretchableFilledCircleImage(diameter: 12.0, color: .white)?.withRenderingMode(.alwaysTemplate) + 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.foregroundClippingContainer) + self.foregroundClippingContainer.addSubview(self.barForeground) + } + + let progressValue: CGFloat + if let animationState = self.animationState { + progressValue = animationState.value(at: CACurrentMediaTime()) + } else { + progressValue = component.value + } + + 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 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)) + } + 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)) + 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.setFrame(view: self.barForeground, frame: CGRect(origin: CGPoint(), size: barBackgroundFrame.size)) + + let labelFont = Font.semibold(14.0) + + let leftLabelSize = self.backgroundLeftLabel.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.leftLabel, font: labelFont, textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0) + ) + let _ = self.foregroundLeftLabel.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.leftLabel, font: labelFont, textColor: component.theme.list.itemCheckColors.foregroundColor)) + )), + environment: {}, + containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0) + ) + let rightLabelSize = self.backgroundRightLabel.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.rightLabel, font: labelFont, textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0) + ) + let _ = self.foregroundRightLabel.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.rightLabel, font: labelFont, textColor: component.theme.list.itemCheckColors.foregroundColor)) + )), + environment: {}, + containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0) + ) + + let leftLabelFrame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((barBackgroundFrame.height - leftLabelSize.height) * 0.5)), size: leftLabelSize) + let rightLabelFrame = CGRect(origin: CGPoint(x: barBackgroundFrame.width - 12.0 - rightLabelSize.width, y: floorToScreenPixels((barBackgroundFrame.height - rightLabelSize.height) * 0.5)), size: rightLabelSize) + + if let backgroundLeftLabelView = self.backgroundLeftLabel.view { + if backgroundLeftLabelView.superview == nil { + backgroundLeftLabelView.layer.anchorPoint = CGPoint() + self.backgroundClippingContainer.addSubview(backgroundLeftLabelView) + } + transition.setPosition(view: backgroundLeftLabelView, position: leftLabelFrame.origin) + backgroundLeftLabelView.bounds = CGRect(origin: CGPoint(), size: leftLabelFrame.size) + } + if let foregroundLeftLabelView = self.foregroundLeftLabel.view { + if foregroundLeftLabelView.superview == nil { + foregroundLeftLabelView.layer.anchorPoint = CGPoint() + self.foregroundClippingContainer.addSubview(foregroundLeftLabelView) + } + transition.setPosition(view: foregroundLeftLabelView, position: leftLabelFrame.origin) + foregroundLeftLabelView.bounds = CGRect(origin: CGPoint(), size: leftLabelFrame.size) + } + if let backgroundRightLabelView = self.backgroundRightLabel.view { + if backgroundRightLabelView.superview == nil { + backgroundRightLabelView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) + self.backgroundClippingContainer.addSubview(backgroundRightLabelView) + } + transition.setPosition(view: backgroundRightLabelView, position: CGPoint(x: rightLabelFrame.maxX, y: rightLabelFrame.minY)) + backgroundRightLabelView.bounds = CGRect(origin: CGPoint(), size: rightLabelFrame.size) + } + if let foregroundRightLabelView = self.foregroundRightLabel.view { + if foregroundRightLabelView.superview == nil { + foregroundRightLabelView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) + self.foregroundClippingContainer.addSubview(foregroundRightLabelView) + } + transition.setPosition(view: foregroundRightLabelView, position: CGPoint(x: rightLabelFrame.maxX, y: rightLabelFrame.minY)) + foregroundRightLabelView.bounds = CGRect(origin: CGPoint(), size: rightLabelFrame.size) + } + + let badgeSize = self.badge.update( + transition: transition.withUserData(ProfileLevelRatingBarBadge.TransitionHint(animateText: !labelsTransition.animation.isImmediate)), + component: AnyComponent(ProfileLevelRatingBarBadge( + theme: component.theme, + title: "\(component.badgeValue)", + suffix: component.badgeTotal + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + + var badgeFrame = CGRect(origin: CGPoint(x: barBackgroundFrame.minX + barForegroundFrame.width, y: barBackgroundFrame.minY - 7.0), size: badgeSize) + if let badgeView = self.badge.view as? ProfileLevelRatingBarBadge.View { + if badgeView.superview == nil { + self.addSubview(badgeView) + } + + let apparentBadgeSize: CGSize + var apparentBadgeOffset: CGFloat = 0.0 + if let animationState = self.animationState { + apparentBadgeSize = animationState.badgeSize(at: CACurrentMediaTime(), endValue: badgeSize) + apparentBadgeOffset = (animationState.fromBadgeSize.width - badgeSize.width) * (1.0 - animationState.fraction(at: CACurrentMediaTime())) + apparentBadgeOffset = -apparentBadgeOffset * 0.25 + } else { + apparentBadgeSize = badgeSize + } + + badgeFrame.size = apparentBadgeSize + + let badgeSideInset: CGFloat = 0.0 + + let badgeOverflowWidth: CGFloat + if badgeFrame.minX - apparentBadgeSize.width * 0.5 < badgeSideInset { + badgeOverflowWidth = badgeSideInset - (badgeFrame.minX - apparentBadgeSize.width * 0.5) + } else if badgeFrame.minX + apparentBadgeSize.width * 0.5 > availableSize.width - badgeSideInset { + badgeOverflowWidth = availableSize.width - badgeSideInset - (badgeFrame.minX + apparentBadgeSize.width * 0.5) + } else { + badgeOverflowWidth = 0.0 + } + + badgeFrame.origin.x += badgeOverflowWidth + apparentBadgeOffset + badgeView.frame = badgeFrame + + badgeView.adjustTail(size: apparentBadgeSize, overflowWidth: -badgeOverflowWidth, transition: transition) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 6744d31a2e..693d64930e 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3941,6 +3941,7 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation var switchToRecommendedChannels = false var switchToGifts = false var switchToGroupsInCommon = false + var switchToStoryFolder: Int64? switch mode { case let .forumTopic(thread): forumTopicThread = thread @@ -3950,10 +3951,12 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation switchToGifts = true case .groupsInCommon: switchToGroupsInCommon = true + case let .storyAlbum(id): + switchToStoryFolder = id default: break } - return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [], forumTopicThread: forumTopicThread, switchToRecommendedChannels: switchToRecommendedChannels, switchToGifts: switchToGifts, switchToGroupsInCommon: switchToGroupsInCommon) + return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [], forumTopicThread: forumTopicThread, switchToRecommendedChannels: switchToRecommendedChannels, switchToGifts: switchToGifts, switchToGroupsInCommon: switchToGroupsInCommon, switchToStoryFolder: switchToStoryFolder) } else if peer is TelegramUser { var nearbyPeerDistance: Int32? var reactionSourceMessageId: MessageId? diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 085147992c..c58025f9a6 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -68,6 +68,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var conferenceDebug: Bool public var checkSerializedData: Bool public var allForumsHaveTabs: Bool + public var debugRatingLayout: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -113,7 +114,8 @@ public struct ExperimentalUISettings: Codable, Equatable { fakeAds: false, conferenceDebug: false, checkSerializedData: false, - allForumsHaveTabs: false + allForumsHaveTabs: false, + debugRatingLayout: false ) } @@ -160,7 +162,8 @@ public struct ExperimentalUISettings: Codable, Equatable { fakeAds: Bool, conferenceDebug: Bool, checkSerializedData: Bool, - allForumsHaveTabs: Bool + allForumsHaveTabs: Bool, + debugRatingLayout: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -205,6 +208,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.conferenceDebug = conferenceDebug self.checkSerializedData = checkSerializedData self.allForumsHaveTabs = allForumsHaveTabs + self.debugRatingLayout = debugRatingLayout } public init(from decoder: Decoder) throws { @@ -253,6 +257,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.conferenceDebug = try container.decodeIfPresent(Bool.self, forKey: "conferenceDebug") ?? false self.checkSerializedData = try container.decodeIfPresent(Bool.self, forKey: "checkSerializedData") ?? false self.allForumsHaveTabs = try container.decodeIfPresent(Bool.self, forKey: "allForumsHaveTabs") ?? false + self.debugRatingLayout = try container.decodeIfPresent(Bool.self, forKey: "debugRatingLayout") ?? false } public func encode(to encoder: Encoder) throws { @@ -301,6 +306,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encodeIfPresent(self.conferenceDebug, forKey: "conferenceDebug") try container.encodeIfPresent(self.checkSerializedData, forKey: "checkSerializedData") try container.encodeIfPresent(self.allForumsHaveTabs, forKey: "allForumsHaveTabs") + try container.encodeIfPresent(self.debugRatingLayout, forKey: "debugRatingLayout") } }