Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2025-08-01 16:58:19 +02:00
commit cefc76d4fc
16 changed files with 1280 additions and 78 deletions

View File

@ -14845,7 +14845,7 @@ Sorry for the inconvenience.";
"ProfileLevelInfo.MyDescriptionDays_any" = "%@ days"; "ProfileLevelInfo.MyDescriptionDays_any" = "%@ days";
"ProfileLevelInfo.MyDescriptionPoints_1" = "%@ point is"; "ProfileLevelInfo.MyDescriptionPoints_1" = "%@ point is";
"ProfileLevelInfo.MyDescriptionPoints_any" = "%@ points are"; "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.OtherDescription" = "The rating reflects **%@'s** activity on Telegram. What affects it:";
"ProfileLevelInfo.LevelIndex_1" = "Level %@"; "ProfileLevelInfo.LevelIndex_1" = "Level %@";
"ProfileLevelInfo.LevelIndex_any" = "Level %@"; "ProfileLevelInfo.LevelIndex_any" = "Level %@";

View File

@ -2580,7 +2580,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
foundRemotePeers = .single(([], [], [], false)) foundRemotePeers = .single(([], [], [], false))
} }
let searchLocations: [SearchMessagesLocation] 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 { 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)] 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 { } else if let (peerId, _, _) = options.peer {

View File

@ -783,6 +783,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
let spec: Spec let spec: Spec
let backgroundColor: UInt32 let backgroundColor: UInt32
let isDark: Bool
let sideInsets: CGFloat let sideInsets: CGFloat
let imageFrame: CGRect let imageFrame: CGRect
@ -799,6 +800,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
init( init(
spec: Spec, spec: Spec,
backgroundColor: UInt32, backgroundColor: UInt32,
isDark: Bool,
sideInsets: CGFloat, sideInsets: CGFloat,
imageFrame: CGRect, imageFrame: CGRect,
imageSize: CGSize, imageSize: CGSize,
@ -810,6 +812,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
) { ) {
self.spec = spec self.spec = spec
self.backgroundColor = backgroundColor self.backgroundColor = backgroundColor
self.isDark = isDark
self.sideInsets = sideInsets self.sideInsets = sideInsets
self.imageFrame = imageFrame self.imageFrame = imageFrame
self.imageSize = imageSize 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 backgroundColor = spec.component.chosenOrder != nil ? spec.component.colors.selectedBackground : spec.component.colors.deselectedBackground
let isDark = spec.component.colors.isDark
let imageFrame: CGRect let imageFrame: CGRect
if spec.component.isTag { if spec.component.isTag {
@ -892,7 +896,11 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
) )
} }
counterLayout = counterValue counterLayout = counterValue
if spec.component.count != 0 {
size.width += spacing + counterValue.size.width size.width += spacing + counterValue.size.width
} else {
size.width -= 1.0
}
if spec.component.isTag { if spec.component.isTag {
size.width += 5.0 size.width += 5.0
} }
@ -957,6 +965,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
return Layout( return Layout(
spec: spec, spec: spec,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
isDark: isDark,
sideInsets: sideInsets, sideInsets: sideInsets,
imageFrame: imageFrame, imageFrame: imageFrame,
imageSize: boundingImageSize, imageSize: boundingImageSize,
@ -1203,6 +1212,8 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
let starsEffectLayerFrame = CGRect(origin: CGPoint(), size: layout.size) let starsEffectLayerFrame = CGRect(origin: CGPoint(), size: layout.size)
animation.animator.updateFrame(layer: starsEffectLayer, frame: starsEffectLayerFrame, completion: nil) animation.animator.updateFrame(layer: starsEffectLayer, frame: starsEffectLayerFrame, completion: nil)
starsEffectLayer.update(size: starsEffectLayerFrame.size) starsEffectLayer.update(size: starsEffectLayerFrame.size)
starsEffectLayer.opacity = layout.isDark ? 0.55 : 1.0
} else { } else {
if let starsEffectLayer = self.starsEffectLayer { if let starsEffectLayer = self.starsEffectLayer {
self.starsEffectLayer = nil self.starsEffectLayer = nil
@ -1366,6 +1377,7 @@ public final class ReactionButtonComponent: Equatable {
public var extractedSelectedForeground: UInt32 public var extractedSelectedForeground: UInt32
public var deselectedMediaPlaceholder: UInt32 public var deselectedMediaPlaceholder: UInt32
public var selectedMediaPlaceholder: UInt32 public var selectedMediaPlaceholder: UInt32
public var isDark: Bool
public init( public init(
deselectedBackground: UInt32, deselectedBackground: UInt32,
@ -1381,7 +1393,8 @@ public final class ReactionButtonComponent: Equatable {
extractedForeground: UInt32, extractedForeground: UInt32,
extractedSelectedForeground: UInt32, extractedSelectedForeground: UInt32,
deselectedMediaPlaceholder: UInt32, deselectedMediaPlaceholder: UInt32,
selectedMediaPlaceholder: UInt32 selectedMediaPlaceholder: UInt32,
isDark: Bool
) { ) {
self.deselectedBackground = deselectedBackground self.deselectedBackground = deselectedBackground
self.selectedBackground = selectedBackground self.selectedBackground = selectedBackground
@ -1397,6 +1410,7 @@ public final class ReactionButtonComponent: Equatable {
self.extractedSelectedForeground = extractedSelectedForeground self.extractedSelectedForeground = extractedSelectedForeground
self.deselectedMediaPlaceholder = deselectedMediaPlaceholder self.deselectedMediaPlaceholder = deselectedMediaPlaceholder
self.selectedMediaPlaceholder = selectedMediaPlaceholder self.selectedMediaPlaceholder = selectedMediaPlaceholder
self.isDark = isDark
} }
} }

View File

@ -73,7 +73,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case keepChatNavigationStack(PresentationTheme, Bool) case keepChatNavigationStack(PresentationTheme, Bool)
case skipReadHistory(PresentationTheme, Bool) case skipReadHistory(PresentationTheme, Bool)
case alwaysDisplayTyping(Bool) case alwaysDisplayTyping(Bool)
case dustEffect(Bool) case debugRatingLayout(Bool)
case crashOnSlowQueries(PresentationTheme, Bool) case crashOnSlowQueries(PresentationTheme, Bool)
case crashOnMemoryPressure(PresentationTheme, Bool) case crashOnMemoryPressure(PresentationTheme, Bool)
case clearTips(PresentationTheme) case clearTips(PresentationTheme)
@ -132,7 +132,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.logging.rawValue return DebugControllerSection.logging.rawValue
case .webViewInspection, .resetWebViewCache: case .webViewInspection, .resetWebViewCache:
return DebugControllerSection.web.rawValue return DebugControllerSection.web.rawValue
case .keepChatNavigationStack, .skipReadHistory, .alwaysDisplayTyping, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: case .keepChatNavigationStack, .skipReadHistory, .alwaysDisplayTyping, .debugRatingLayout, .crashOnSlowQueries, .crashOnMemoryPressure:
return DebugControllerSection.experiments.rawValue 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: 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 return DebugControllerSection.experiments.rawValue
@ -185,7 +185,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 16 return 16
case .alwaysDisplayTyping: case .alwaysDisplayTyping:
return 17 return 17
case .dustEffect: case .debugRatingLayout:
return 18 return 18
case .crashOnSlowQueries: case .crashOnSlowQueries:
return 20 return 20
@ -970,11 +970,11 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return settings return settings
}).start() }).start()
}) })
case let .dustEffect(value): case let .debugRatingLayout(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Dust Debug", value: value, sectionId: self.section, style: .blocks, updated: { value in 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 let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in
var settings = settings var settings = settings
settings.dustEffect = value settings.debugRatingLayout = value
return settings return settings
}).start() }).start()
}) })
@ -1506,7 +1506,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory)) entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory))
#endif #endif
entries.append(.alwaysDisplayTyping(experimentalSettings.alwaysDisplayTyping)) entries.append(.alwaysDisplayTyping(experimentalSettings.alwaysDisplayTyping))
entries.append(.dustEffect(experimentalSettings.dustEffect)) entries.append(.debugRatingLayout(experimentalSettings.debugRatingLayout))
} }
entries.append(.crashOnSlowQueries(presentationData.theme, experimentalSettings.crashOnLongQueries)) entries.append(.crashOnSlowQueries(presentationData.theme, experimentalSettings.crashOnLongQueries))
entries.append(.crashOnMemoryPressure(presentationData.theme, experimentalSettings.crashOnMemoryPressure)) entries.append(.crashOnMemoryPressure(presentationData.theme, experimentalSettings.crashOnMemoryPressure))

View File

@ -532,7 +532,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0),
reactionActiveForeground: .clear, reactionActiveForeground: .clear,
reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2), reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2),
reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsInactiveForeground: UIColor(rgb: 0xFFD738),
reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0),
reactionStarsActiveForeground: .white, reactionStarsActiveForeground: .white,
reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1),
@ -548,7 +548,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0),
reactionActiveForeground: .clear, reactionActiveForeground: .clear,
reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2), reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2),
reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsInactiveForeground: UIColor(rgb: 0xFFD738),
reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0),
reactionStarsActiveForeground: .white, reactionStarsActiveForeground: .white,
reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1),
@ -570,7 +570,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0),
reactionActiveForeground: .clear, reactionActiveForeground: .clear,
reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2), reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2),
reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsInactiveForeground: UIColor(rgb: 0xFFD738),
reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0),
reactionStarsActiveForeground: .white, reactionStarsActiveForeground: .white,
reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1),
@ -586,7 +586,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0),
reactionActiveForeground: .clear, reactionActiveForeground: .clear,
reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2), reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2),
reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsInactiveForeground: UIColor(rgb: 0xFFD738),
reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0),
reactionStarsActiveForeground: .white, reactionStarsActiveForeground: .white,
reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1),
@ -605,7 +605,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0),
reactionActiveForeground: .clear, reactionActiveForeground: .clear,
reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2), reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2),
reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsInactiveForeground: UIColor(rgb: 0xFFD738),
reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0),
reactionStarsActiveForeground: .white, reactionStarsActiveForeground: .white,
reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1),
@ -621,7 +621,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0),
reactionActiveForeground: .clear, reactionActiveForeground: .clear,
reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2), reactionStarsInactiveBackground: UIColor(rgb: 0xD3720A, alpha: 0.2),
reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsInactiveForeground: UIColor(rgb: 0xFFD738),
reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0),
reactionStarsActiveForeground: .white, reactionStarsActiveForeground: .white,
reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1),

View File

@ -376,7 +376,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb, extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb,
extractedSelectedForeground: arguments.presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : 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, deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb,
isDark: arguments.presentationData.theme.theme.overallDarkAppearance
) )
case .BubbleOutgoing, .ImageOutgoing, .FreeOutgoing: case .BubbleOutgoing, .ImageOutgoing, .FreeOutgoing:
let themeColors = bubbleColorComponents(theme: arguments.presentationData.theme.theme, incoming: false, wallpaper: !arguments.presentationData.theme.wallpaper.isEmpty) 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, 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, extractedSelectedForeground: arguments.presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : arguments.presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb,
deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb, deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb,
isDark: arguments.presentationData.theme.theme.overallDarkAppearance
) )
} }

View File

@ -87,7 +87,8 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb, extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb,
extractedSelectedForeground: presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb, extractedSelectedForeground: presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb,
deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb, deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb,
isDark: presentationData.theme.theme.overallDarkAppearance
) )
case .outgoing: case .outgoing:
themeColors = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty) 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, extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb,
extractedSelectedForeground: presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb, extractedSelectedForeground: presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb,
deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb, deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb,
isDark: presentationData.theme.theme.overallDarkAppearance
) )
case .freeform: case .freeform:
if presentationData.theme.wallpaper.isEmpty { if presentationData.theme.wallpaper.isEmpty {
@ -128,7 +130,8 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb, extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb,
extractedSelectedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb, extractedSelectedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb,
deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb, deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb,
isDark: presentationData.theme.theme.overallDarkAppearance
) )
} }

View File

@ -5,25 +5,134 @@ import ComponentFlow
import MultilineTextComponent import MultilineTextComponent
import Svg 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 { public final class PeerInfoRatingComponent: Component {
let backgroundColor: UIColor let backgroundColor: UIColor
let borderColor: UIColor let borderColor: UIColor
let foregroundColor: UIColor let foregroundColor: UIColor
let level: Int let level: Int
let action: () -> Void let action: () -> Void
let debugLevel: Bool
public init( public init(
backgroundColor: UIColor, backgroundColor: UIColor,
borderColor: UIColor, borderColor: UIColor,
foregroundColor: UIColor, foregroundColor: UIColor,
level: Int, level: Int,
action: @escaping () -> Void action: @escaping () -> Void,
debugLevel: Bool = false
) { ) {
self.backgroundColor = backgroundColor self.backgroundColor = backgroundColor
self.borderColor = borderColor self.borderColor = borderColor
self.foregroundColor = foregroundColor self.foregroundColor = foregroundColor
self.level = level self.level = level
self.action = action self.action = action
self.debugLevel = debugLevel
} }
public static func ==(lhs: PeerInfoRatingComponent, rhs: PeerInfoRatingComponent) -> Bool { public static func ==(lhs: PeerInfoRatingComponent, rhs: PeerInfoRatingComponent) -> Bool {
@ -39,6 +148,9 @@ public final class PeerInfoRatingComponent: Component {
if lhs.level != rhs.level { if lhs.level != rhs.level {
return false return false
} }
if lhs.debugLevel != rhs.debugLevel {
return false
}
return true return true
} }
@ -56,7 +168,7 @@ public final class PeerInfoRatingComponent: Component {
private let borderLayer: SimpleLayer private let borderLayer: SimpleLayer
private let backgroundLayer: SimpleLayer private let backgroundLayer: SimpleLayer
//private var tempLevel: Int = 1 private var debugLevel: Int = 1
private var component: PeerInfoRatingComponent? private var component: PeerInfoRatingComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
@ -78,18 +190,23 @@ public final class PeerInfoRatingComponent: Component {
} }
@objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) { @objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) {
guard let component = self.component else {
return
}
if case .ended = recognizer.state { if case .ended = recognizer.state {
self.component?.action() if component.debugLevel {
if self.debugLevel < 10 {
/*if self.tempLevel < 10 { self.debugLevel += 1
self.tempLevel += 1
} else { } else {
self.tempLevel += 10 self.debugLevel += 10
} }
if self.tempLevel >= 110 { if self.debugLevel >= 110 {
self.tempLevel = 1 self.debugLevel = 1
}
self.state?.updated(transition: .immediate)
} else {
self.component?.action()
} }
self.state?.updated(transition: .immediate)*/
} }
} }
@ -102,14 +219,51 @@ public final class PeerInfoRatingComponent: Component {
self.component = component self.component = component
self.state = state self.state = state
let level = component.level let level: Int
//let level = self.tempLevel if component.debugLevel {
level = self.debugLevel
} else {
level = component.level
}
let iconSize = CGSize(width: 26.0, height: 26.0) 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: [ let attributedText = NSAttributedString(string: "\(level)", attributes: [
NSAttributedString.Key.font: Font.semibold(10.0), NSAttributedString.Key.font: font,
NSAttributedString.Key.foregroundColor: component.foregroundColor NSAttributedString.Key.foregroundColor: component.foregroundColor
]) ])
@ -157,13 +311,25 @@ public final class PeerInfoRatingComponent: Component {
} }
let levelIndex: Int let levelIndex: Int
if level <= 10 { if level < 0 {
levelIndex = max(0, component.level) levelIndex = 1
} else if level <= 10 {
levelIndex = max(1, level)
} else if level <= 90 { } else if level <= 90 {
levelIndex = (level / 10) * 10 levelIndex = (level / 10) * 10
} else { } else {
levelIndex = 90 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 let borderImage = generateImage(iconSize, rotatedContext: { size, context in
UIGraphicsPushContext(context) UIGraphicsPushContext(context)
defer { 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 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) { 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 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) { 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 { if let textLayout {
let titleScale: CGFloat 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 titleScale = 1.0
} else if level < 100 { } else if level < 100 {
titleScale = 0.8 titleScale = 0.8
@ -216,19 +390,25 @@ public final class PeerInfoRatingComponent: Component {
titleScale = 0.6 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) let textFrame = CGRect(origin: CGPoint(x: (size.width - textLayout.size.width) * 0.5, y: (size.height - textLayout.size.height) * 0.5), size: textLayout.size)
if level == 1 {
textFrame.origin.x += UIScreenPixel
} else {
textFrame.origin.x += 0.0
}
context.saveGState() context.saveGState()
context.translateBy(x: textFrame.midX, y: textFrame.midY) context.translateBy(x: textFrame.midX, y: textFrame.midY)
context.scaleBy(x: titleScale, y: titleScale) context.scaleBy(x: titleScale, y: titleScale)
context.translateBy(x: -textFrame.midX, y: -textFrame.midY) 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() context.restoreGState()
} }

View File

@ -831,7 +831,36 @@ final class PeerInfoHeaderNode: ASDisplayNode {
headerButtonBackgroundColor = regularHeaderButtonBackgroundColor.mixedWith(collapsedHeaderButtonBackgroundColor, alpha: effectiveTransitionFraction) 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.1).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.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
ratingBorderColor = UIColor.clear
ratingForegroundColor = presentationData.theme.list.itemCheckColors.foregroundColor
}
/*if let status = peer?.emojiStatus, case let .starGift(_, _, _, _, _, innerColor, outerColor, _, _) = status.content {
let _ = outerColor let _ = outerColor
let mainColor = UIColor(rgb: UInt32(bitPattern: innerColor)) let mainColor = UIColor(rgb: UInt32(bitPattern: innerColor))
@ -848,7 +877,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
ratingBackgroundColor = presentationData.theme.list.itemCheckColors.fillColor ratingBackgroundColor = presentationData.theme.list.itemCheckColors.fillColor
ratingBorderColor = UIColor.clear ratingBorderColor = UIColor.clear
ratingForegroundColor = presentationData.theme.list.itemCheckColors.foregroundColor ratingForegroundColor = presentationData.theme.list.itemCheckColors.foregroundColor
} }*/
} }
do { do {
@ -1959,7 +1988,10 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.currentPendingStarRating = cachedData.pendingStarRating self.currentPendingStarRating = cachedData.pendingStarRating
#if DEBUG #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 #endif
} else { } else {
self.currentStarRating = nil self.currentStarRating = nil
@ -1996,7 +2028,8 @@ final class PeerInfoHeaderNode: ASDisplayNode {
pendingStarRating: self.currentPendingStarRating, pendingStarRating: self.currentPendingStarRating,
customTheme: self.presentationData?.theme customTheme: self.presentationData?.theme
)) ))
} },
debugLevel: self.context.sharedContext.immediateExperimentalUISettings.debugRatingLayout
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: width - 12.0 * 2.0, height: 100.0) containerSize: CGSize(width: width - 12.0 * 2.0, height: 100.0)

View File

@ -2599,6 +2599,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
if let listSource = self.listSource as? PeerStoryListContext { if let listSource = self.listSource as? PeerStoryListContext {
let _ = listSource.addToFolder(id: folderPreview.folder.id, items: [item]) 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.initialStoryFolderId = nil
self.setStoryFolder(id: folder.id, assumeEmpty: false, animated: false) self.setStoryFolder(id: folder.id, assumeEmpty: false, animated: false)
} else { } 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 self.currentListState = state
var hasLocalItems = false var hasLocalItems = false
@ -3923,6 +3937,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
self.presentRenameStoryFolder(id: folder.id, title: folder.title) 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 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 { guard let self else {
@ -3937,7 +3952,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
self.shareFolder(id: folder.id) self.shareFolder(id: folder.id)
}) })
}))) })))
}
if self.canManageStories { if self.canManageStories {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuReorder, icon: { theme in 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 { if let listSource = self.listSource as? PeerStoryListContext {
let _ = listSource.addToFolder(id: folderId, items: items) 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 controller.navigationPresentation = .modal
@ -5392,6 +5416,64 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
collectibleItemInfo: nil collectibleItemInfo: nil
) )
self.parentController?.present(shareController, in: .window(.root)) 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))
}
} }
} }
} }

View File

@ -27,6 +27,9 @@ swift_library(
"//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/PremiumUI", "//submodules/PremiumUI",
"//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath",
"//submodules/Components/HierarchyTrackingLayer",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -142,7 +142,7 @@ private final class ProfileLevelInfoScreenComponent: Component {
self.addSubview(self.dimView) self.addSubview(self.dimView)
self.layer.addSublayer(self.backgroundLayer) self.layer.addSublayer(self.backgroundLayer)
self.scrollView.delaysContentTouches = true self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false self.scrollView.clipsToBounds = false
self.scrollView.contentInsetAdjustmentBehavior = .never self.scrollView.contentInsetAdjustmentBehavior = .never
@ -283,6 +283,8 @@ private final class ProfileLevelInfoScreenComponent: Component {
self.isUpdating = false self.isUpdating = false
} }
let isChangingPreview = transition.userData(TransitionHint.self)?.isChangingPreview ?? false
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.16) let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.16)
let environment = environment[ViewControllerComponentContainer.Environment.self].value let environment = environment[ViewControllerComponentContainer.Environment.self].value
@ -354,13 +356,14 @@ private final class ProfileLevelInfoScreenComponent: Component {
let pendingPoints = pendingStarRating.rating.stars - component.starRating.stars let pendingPoints = pendingStarRating.rating.stars - component.starRating.stars
if self.isPreviewingPendingRating { if self.isPreviewingPendingRating {
//TODO:localize
secondaryDescriptionTextString = "This will be your rating in 21 days,\n after \(pendingPoints) points are added. [Back >]()" secondaryDescriptionTextString = "This will be your rating in 21 days,\n after \(pendingPoints) points are added. [Back >]()"
} else { } else {
let dayCount = (pendingStarRating.timestamp - timestamp) / (24 * 60 * 60) let dayCount = (pendingStarRating.timestamp - timestamp) / (24 * 60 * 60)
if dayCount == 0 { if dayCount == 0 {
secondaryDescriptionTextString = environment.strings.ProfileLevelInfo_MyDescriptionToday(Int32(pendingPoints)) secondaryDescriptionTextString = environment.strings.ProfileLevelInfo_MyDescriptionToday(Int32(pendingPoints))
} else { } 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,
environment.theme.list.itemCheckColors.fillColor environment.theme.list.itemCheckColors.fillColor
] ]
let _ = gradientColors
let levelFraction: CGFloat var levelFraction: CGFloat
let badgeText: String let badgeText: String
var badgeTextSuffix: String? var badgeTextSuffix: String?
@ -413,8 +417,8 @@ private final class ProfileLevelInfoScreenComponent: Component {
if let nextLevelStars = pendingStarRating.rating.nextLevelStars { if let nextLevelStars = pendingStarRating.rating.nextLevelStars {
badgeTextSuffix = " / \(starCountString(Int64(nextLevelStars), decimalSeparator: "."))" badgeTextSuffix = " / \(starCountString(Int64(nextLevelStars), decimalSeparator: "."))"
} }
if let nextLevelStars = pendingStarRating.rating.nextLevelStars { if let nextLevelStars = pendingStarRating.rating.nextLevelStars, nextLevelStars > pendingStarRating.rating.stars {
levelFraction = Double(pendingStarRating.rating.stars) / Double(nextLevelStars) levelFraction = Double(pendingStarRating.rating.stars - pendingStarRating.rating.currentLevelStars) / Double(nextLevelStars - pendingStarRating.rating.currentLevelStars)
} else { } else {
levelFraction = 1.0 levelFraction = 1.0
} }
@ -426,13 +430,17 @@ private final class ProfileLevelInfoScreenComponent: Component {
badgeTextSuffix = " / \(starCountString(Int64(nextLevelStars), decimalSeparator: "."))" badgeTextSuffix = " / \(starCountString(Int64(nextLevelStars), decimalSeparator: "."))"
} }
if let nextLevelStars = component.starRating.nextLevelStars { if let nextLevelStars = component.starRating.nextLevelStars {
levelFraction = Double(component.starRating.stars) / Double(nextLevelStars) levelFraction = Double(component.starRating.stars - component.starRating.currentLevelStars) / Double(nextLevelStars - component.starRating.currentLevelStars)
} else { } else if component.starRating.stars > 0 {
levelFraction = 1.0 levelFraction = 1.0
} else {
levelFraction = 0.0
} }
} }
let levelInfoSize = self.levelInfo.update( levelFraction = max(0.0, levelFraction)
/*let levelInfoSize = self.levelInfo.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(PremiumLimitDisplayComponent( component: AnyComponent(PremiumLimitDisplayComponent(
inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5), inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5),
@ -453,18 +461,31 @@ private final class ProfileLevelInfoScreenComponent: Component {
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 200.0) 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 let levelInfoView = self.levelInfo.view {
if levelInfoView.superview == nil { if levelInfoView.superview == nil {
self.scrollContentView.addSubview(levelInfoView) 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 contentHeight += 129.0
let isChangingPreview = transition.userData(TransitionHint.self)?.isChangingPreview ?? false
if let secondaryDescriptionTextString { if let secondaryDescriptionTextString {
if isChangingPreview, let secondaryDescriptionTextView = self.secondaryDescriptionText?.view { if isChangingPreview, let secondaryDescriptionTextView = self.secondaryDescriptionText?.view {
self.secondaryDescriptionText = nil self.secondaryDescriptionText = nil

View File

@ -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<Empty>()
private let suffixLabel = ComponentView<Empty>()
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<Empty>, 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<Empty>, 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)
}
}

View File

@ -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<Empty>()
private let backgroundRightLabel = ComponentView<Empty>()
private let foregroundLeftLabel = ComponentView<Empty>()
private let foregroundRightLabel = ComponentView<Empty>()
private let badge = ComponentView<Empty>()
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<Empty>, 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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -3941,6 +3941,7 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation
var switchToRecommendedChannels = false var switchToRecommendedChannels = false
var switchToGifts = false var switchToGifts = false
var switchToGroupsInCommon = false var switchToGroupsInCommon = false
var switchToStoryFolder: Int64?
switch mode { switch mode {
case let .forumTopic(thread): case let .forumTopic(thread):
forumTopicThread = thread forumTopicThread = thread
@ -3950,10 +3951,12 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation
switchToGifts = true switchToGifts = true
case .groupsInCommon: case .groupsInCommon:
switchToGroupsInCommon = true switchToGroupsInCommon = true
case let .storyAlbum(id):
switchToStoryFolder = id
default: default:
break 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 { } else if peer is TelegramUser {
var nearbyPeerDistance: Int32? var nearbyPeerDistance: Int32?
var reactionSourceMessageId: MessageId? var reactionSourceMessageId: MessageId?

View File

@ -68,6 +68,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
public var conferenceDebug: Bool public var conferenceDebug: Bool
public var checkSerializedData: Bool public var checkSerializedData: Bool
public var allForumsHaveTabs: Bool public var allForumsHaveTabs: Bool
public var debugRatingLayout: Bool
public static var defaultSettings: ExperimentalUISettings { public static var defaultSettings: ExperimentalUISettings {
return ExperimentalUISettings( return ExperimentalUISettings(
@ -113,7 +114,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
fakeAds: false, fakeAds: false,
conferenceDebug: false, conferenceDebug: false,
checkSerializedData: false, checkSerializedData: false,
allForumsHaveTabs: false allForumsHaveTabs: false,
debugRatingLayout: false
) )
} }
@ -160,7 +162,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
fakeAds: Bool, fakeAds: Bool,
conferenceDebug: Bool, conferenceDebug: Bool,
checkSerializedData: Bool, checkSerializedData: Bool,
allForumsHaveTabs: Bool allForumsHaveTabs: Bool,
debugRatingLayout: Bool
) { ) {
self.keepChatNavigationStack = keepChatNavigationStack self.keepChatNavigationStack = keepChatNavigationStack
self.skipReadHistory = skipReadHistory self.skipReadHistory = skipReadHistory
@ -205,6 +208,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
self.conferenceDebug = conferenceDebug self.conferenceDebug = conferenceDebug
self.checkSerializedData = checkSerializedData self.checkSerializedData = checkSerializedData
self.allForumsHaveTabs = allForumsHaveTabs self.allForumsHaveTabs = allForumsHaveTabs
self.debugRatingLayout = debugRatingLayout
} }
public init(from decoder: Decoder) throws { 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.conferenceDebug = try container.decodeIfPresent(Bool.self, forKey: "conferenceDebug") ?? false
self.checkSerializedData = try container.decodeIfPresent(Bool.self, forKey: "checkSerializedData") ?? false self.checkSerializedData = try container.decodeIfPresent(Bool.self, forKey: "checkSerializedData") ?? false
self.allForumsHaveTabs = try container.decodeIfPresent(Bool.self, forKey: "allForumsHaveTabs") ?? 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 { 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.conferenceDebug, forKey: "conferenceDebug")
try container.encodeIfPresent(self.checkSerializedData, forKey: "checkSerializedData") try container.encodeIfPresent(self.checkSerializedData, forKey: "checkSerializedData")
try container.encodeIfPresent(self.allForumsHaveTabs, forKey: "allForumsHaveTabs") try container.encodeIfPresent(self.allForumsHaveTabs, forKey: "allForumsHaveTabs")
try container.encodeIfPresent(self.debugRatingLayout, forKey: "debugRatingLayout")
} }
} }