diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift index 9d815f9921..81aae9de5b 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift @@ -592,9 +592,9 @@ public final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource return nil } self.animation = animation - let frameCount = Int(animation.frameCount) + let frameCount = max(1, Int(animation.frameCount)) self.frameCount = frameCount - self.frameRate = Int(animation.frameRate) + self.frameRate = max(1, Int(animation.frameRate)) self.cache = cachePathPrefix.flatMap { cachePathPrefix in AnimatedStickerDirectFrameSourceCache(queue: queue, pathPrefix: cachePathPrefix, width: width, height: height, frameCount: frameCount, fitzModifier: fitzModifier, useHardware: useMetalCache) diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index bdc23e1a58..0b323ae9fc 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -903,10 +903,17 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz guard let publicToken = nativeParams["public_token"] as? String else { return } + + var customTokenizeUrl: String? + if let value = nativeParams["public_token"] as? String, let url = URL(string: value), let host = url.host { + if url.scheme == "https" && (host == "smart-glocal.com" || host.hasSuffix(".smart-glocal.com")) { + customTokenizeUrl = value + } + } var dismissImpl: (() -> Void)? let canSave = paymentForm.canSaveCredentials || paymentForm.passwordMissing - let controller = BotCheckoutNativeCardEntryController(context: strongSelf.context, provider: .smartglobal(isTesting: paymentForm.invoice.isTest, publicToken: publicToken), completion: { method in + let controller = BotCheckoutNativeCardEntryController(context: strongSelf.context, provider: .smartglobal(isTesting: paymentForm.invoice.isTest, publicToken: publicToken, customTokenizeUrl: customTokenizeUrl), completion: { method in guard let strongSelf = self else { return } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift index ab4254bd9f..74af758711 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift @@ -30,7 +30,7 @@ struct BotCheckoutNativeCardEntryAdditionalFields: OptionSet { final class BotCheckoutNativeCardEntryController: ViewController { enum Provider { case stripe(additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String) - case smartglobal(isTesting: Bool, publicToken: String) + case smartglobal(isTesting: Bool, publicToken: String, customTokenizeUrl: String?) } private var controllerNode: BotCheckoutNativeCardEntryControllerNode { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift index 0602b878ef..9f497c90b9 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift @@ -310,9 +310,11 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, })) self.updateDone() - case let .smartglobal(isTesting, publicToken): + case let .smartglobal(isTesting, publicToken, customTokenizeUrl): let url: String - if isTesting { + if let customTokenizeUrl { + url = customTokenizeUrl + } else if isTesting { url = "https://tgb-playground.smart-glocal.com/cds/v1/tokenize/card" } else { url = "https://tgb.smart-glocal.com/cds/v1/tokenize/card" diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 1aacf0f996..7cac497f41 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2477,7 +2477,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) } else if peer.isFake { currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) - } else if case let .user(user) = peer, let emojiStatus = user.emojiStatus, !premiumConfiguration.isPremiumDisabled { + } else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { currentCredibilityIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if peer.isVerified { currentCredibilityIconContent = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) @@ -2493,7 +2493,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) } else if peer.isFake { currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) - } else if case let .user(user) = peer, let emojiStatus = user.emojiStatus, !premiumConfiguration.isPremiumDisabled { + } else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { currentCredibilityIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if peer.isVerified { currentCredibilityIconContent = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index 6c1e332766..1b70a3a8b6 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -609,7 +609,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent currentCredibilityIcon = .text(color: presentationData.theme.chat.message.incoming.scamColor, string: presentationData.strings.Message_ScamAccount.uppercased()) } else if item.peer.isFake { currentCredibilityIcon = .text(color: presentationData.theme.chat.message.incoming.scamColor, string: presentationData.strings.Message_FakeAccount.uppercased()) - } else if case let .user(user) = item.peer, let emojiStatus = user.emojiStatus { + } else if let emojiStatus = item.peer.emojiStatus { currentCredibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: UIColor(white: 0.0, alpha: 0.1), themeColor: presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if item.peer.isVerified { currentCredibilityIcon = .verified(fillColor: presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index d24d1e2a7c..aedc76242e 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -225,12 +225,25 @@ open class ViewControllerComponentContainer: ViewController { public var wasDismissed: (() -> Void)? - public init(context: AccountContext, component: C, navigationBarAppearance: NavigationBarAppearance, statusBarStyle: StatusBarStyle = .default, presentationMode: PresentationMode = .default, theme: Theme = .default) where C.EnvironmentType == ViewControllerComponentContainer.Environment { + public init( + context: AccountContext, + component: C, + navigationBarAppearance: NavigationBarAppearance, + statusBarStyle: StatusBarStyle = .default, + presentationMode: PresentationMode = .default, + theme: Theme = .default, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil + ) where C.EnvironmentType == ViewControllerComponentContainer.Environment { self.context = context self.component = AnyComponent(component) self.theme = theme - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let presentationData: PresentationData + if let updatedPresentationData { + presentationData = updatedPresentationData.initial + } else { + presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + } let navigationBarPresentationData: NavigationBarPresentationData? switch navigationBarAppearance { @@ -243,7 +256,7 @@ open class ViewControllerComponentContainer: ViewController { } super.init(navigationBarPresentationData: navigationBarPresentationData) - self.presentationDataDisposable = (self.context.sharedContext.presentationData + self.presentationDataDisposable = ((updatedPresentationData?.signal ?? self.context.sharedContext.presentationData) |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { var theme = presentationData.theme @@ -266,6 +279,19 @@ open class ViewControllerComponentContainer: ViewController { strongSelf.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style } + let navigationBarPresentationData: NavigationBarPresentationData? + switch navigationBarAppearance { + case .none: + navigationBarPresentationData = nil + case .transparent: + navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData, hideBackground: true, hideBadge: false, hideSeparator: true) + case .default: + navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData) + } + if let navigationBarPresentationData { + strongSelf.navigationBar?.updatePresentationData(navigationBarPresentationData) + } + if let layout = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, transition: .immediate) } diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 54165ebd7d..4361932dd8 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -699,7 +699,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) } else if peer.isFake { credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) - } else if case let .user(user) = peer, let emojiStatus = user.emojiStatus { + } else if let emojiStatus = peer.emojiStatus { credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if peer.isVerified { credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) diff --git a/submodules/Display/Source/GenerateImage.swift b/submodules/Display/Source/GenerateImage.swift index 05454b192d..610628a96e 100644 --- a/submodules/Display/Source/GenerateImage.swift +++ b/submodules/Display/Source/GenerateImage.swift @@ -428,6 +428,7 @@ public func generateScaledImage(image: UIImage?, size: CGSize, opaque: Bool = tr public func generateSingleColorImage(size: CGSize, color: UIColor, scale: CGFloat = 0.0) -> UIImage? { return generateImage(size, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(color.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) }, scale: scale) diff --git a/submodules/Display/Source/ListViewItemNode.swift b/submodules/Display/Source/ListViewItemNode.swift index 7386ec3f17..bec5fbf86c 100644 --- a/submodules/Display/Source/ListViewItemNode.swift +++ b/submodules/Display/Source/ListViewItemNode.swift @@ -52,17 +52,19 @@ public enum ListViewItemNodeVisibility: Equatable { case visible(CGFloat, CGRect) } -public struct ListViewItemLayoutParams { +public struct ListViewItemLayoutParams: Equatable { public let width: CGFloat public let leftInset: CGFloat public let rightInset: CGFloat public let availableHeight: CGFloat + public let isStandalone: Bool - public init(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, availableHeight: CGFloat) { + public init(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, availableHeight: CGFloat, isStandalone: Bool = false) { self.width = width self.leftInset = leftInset self.rightInset = rightInset self.availableHeight = availableHeight + self.isStandalone = isStandalone } } diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 093c11e33e..7a17a43cf3 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -913,7 +913,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) } else if item.peer.isFake { credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) - } else if case let .user(user) = item.peer, let emojiStatus = user.emojiStatus { + } else if let emojiStatus = item.peer.emojiStatus { credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if item.peer.isVerified { credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) diff --git a/submodules/ItemListUI/Sources/ItemListItem.swift b/submodules/ItemListUI/Sources/ItemListItem.swift index 7cede4afcb..1cadb4b7d7 100644 --- a/submodules/ItemListUI/Sources/ItemListItem.swift +++ b/submodules/ItemListUI/Sources/ItemListItem.swift @@ -122,6 +122,10 @@ public func itemListNeighborsPlainInsets(_ neighbors: ItemListNeighbors) -> UIEd } public func itemListNeighborsGroupedInsets(_ neighbors: ItemListNeighbors, _ params: ListViewItemLayoutParams) -> UIEdgeInsets { + if params.isStandalone { + return UIEdgeInsets() + } + let topInset: CGFloat switch neighbors.top { case .none: diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift index 5b03157cac..6059880d4b 100644 --- a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -599,7 +599,12 @@ private final class VariableBlurView: UIVisualEffectView { fatalError("init(coder:) has not been implemented") } - override func updateTraitsIfNeeded() { + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + if #available(iOS 13.0, *) { + if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + self.resetEffect() + } + } } private func resetEffect() { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index a61a4d9ec1..0a2304340b 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -589,6 +589,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { strings: strongSelf.presentationData.strings, deviceMetrics: DeviceMetrics.iPhone13, emojiContent: emojiContent, + color: nil, backgroundColor: .clear, separatorColor: strongSelf.presentationData.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5), hideTopPanel: hideTopPanel, @@ -1215,6 +1216,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { strings: self.presentationData.strings, deviceMetrics: DeviceMetrics.iPhone13, emojiContent: emojiContent, + color: nil, backgroundColor: .clear, separatorColor: self.presentationData.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5), hideTopPanel: hideTopPanel, diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 471983403b..373ff7b42e 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -117,6 +117,8 @@ swift_library( "//submodules/TelegramUI/Components/Settings/PeerNameColorScreen", "//submodules/ManagedAnimationNode:ManagedAnimationNode", "//submodules/TelegramUI/Components/Settings/QuickReactionSetupController", + "//submodules/TelegramUI/Components/Settings/ThemeCarouselItem", + "//submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 06dad8b58c..a0591b67c4 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -307,10 +307,10 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) - let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) + let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) - let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) + let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) let peer6: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let timestamp = self.referenceTimestamp diff --git a/submodules/SettingsUI/Sources/ThemePickerController.swift b/submodules/SettingsUI/Sources/ThemePickerController.swift index ea45e2a190..97198c6691 100644 --- a/submodules/SettingsUI/Sources/ThemePickerController.swift +++ b/submodules/SettingsUI/Sources/ThemePickerController.swift @@ -19,6 +19,7 @@ import ContextUI import UndoUI import ItemListPeerActionItem import AnimationUI +import ThemeSettingsThemeItem private final class ThemePickerControllerArguments { let context: AccountContext diff --git a/submodules/SettingsUI/Sources/ThemePickerGridItem.swift b/submodules/SettingsUI/Sources/ThemePickerGridItem.swift index b2d3d19c4d..a1dec15db2 100644 --- a/submodules/SettingsUI/Sources/ThemePickerGridItem.swift +++ b/submodules/SettingsUI/Sources/ThemePickerGridItem.swift @@ -18,7 +18,7 @@ import AnimatedStickerNode import TelegramAnimatedStickerNode import ShimmerEffect import StickerResources - +import ThemeCarouselItem private var cachedBorderImages: [String: UIImage] = [:] private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selected: Bool) -> UIImage? { @@ -500,7 +500,7 @@ class ThemeGridThemeItemNode: ListViewItemNode, ItemListItemNode { for theme in item.themes { let selected = item.currentTheme.index == theme.index - let iconItem = ThemeCarouselThemeIconItem(context: item.context, emojiFile: theme.emoticon.flatMap { item.animatedEmojiStickers[$0]?.first?.file }, themeReference: theme, nightMode: item.nightMode, themeSpecificAccentColors: item.themeSpecificAccentColors, themeSpecificChatWallpapers: item.themeSpecificChatWallpapers, selected: selected, theme: item.theme, strings: item.strings, wallpaper: nil, action: { theme in + let iconItem = ThemeCarouselThemeIconItem(context: item.context, emojiFile: theme.emoticon.flatMap { item.animatedEmojiStickers[$0]?.first?.file }, themeReference: theme, nightMode: item.nightMode, channelMode: false, themeSpecificAccentColors: item.themeSpecificAccentColors, themeSpecificChatWallpapers: item.themeSpecificChatWallpapers, selected: selected, theme: item.theme, strings: item.strings, wallpaper: nil, action: { theme in item.updatedTheme(theme) }, contextAction: nil) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index aff01e5072..59ab83e09a 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -948,7 +948,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) - let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) + let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift index e1b9d76ff0..2f8ca0cdab 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift @@ -14,6 +14,7 @@ import DeviceLocationManager import Geocoding import WallpaperResources import Sunrise +import ThemeSettingsThemeItem private enum TriggerMode { case system diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 59ebed6a4b..168b3ca003 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -455,10 +455,10 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) - let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) + let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) - let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) + let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) let peer6: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer7: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(6)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_7_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index a7740847bd..08e5dc5254 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -19,32 +19,7 @@ import ContextUI import UndoUI import PremiumUI import PeerNameColorScreen - -func themeDisplayName(strings: PresentationStrings, reference: PresentationThemeReference) -> String { - let name: String - switch reference { - case let .builtin(theme): - switch theme { - case .dayClassic: - name = strings.Appearance_ThemeCarouselClassic - case .day: - name = strings.Appearance_ThemeCarouselDay - case .night: - name = strings.Appearance_ThemeCarouselNewNight - case .nightAccent: - name = strings.Appearance_ThemeCarouselTintedNight - } - case let .local(theme): - name = theme.title - case let .cloud(theme): - if let emoticon = theme.theme.emoticon { - name = emoticon - } else { - name = theme.theme.title - } - } - return name -} +import ThemeCarouselItem private final class ThemeSettingsControllerArguments { let context: AccountContext diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 719c1db1ec..5366d566a0 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -1262,7 +1262,7 @@ public extension Api { return parser(reader) } else { - telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + telegramApiLog("Type constructor \(String(UInt32(bitPattern: signature), radix: 16, uppercase: false)) not found") return nil } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift index 1ed491fde8..3ab4562186 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift @@ -839,7 +839,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) } else if item.peer.isFake { credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) - } else if case let .user(user) = item.peer, let emojiStatus = user.emojiStatus, !premiumConfiguration.isPremiumDisabled { + } else if let emojiStatus = item.peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if item.peer.isVerified { credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) diff --git a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift index d0d5b3a45c..a288585d95 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift @@ -61,7 +61,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: "", photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) case let .chatForbidden(id, title): return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: title, photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) - case let .channel(flags, flags2, id, accessHash, title, username, photo, date, restrictionReason, adminRights, bannedRights, defaultBannedRights, _, usernames, _, color, profileColor, _, _): + case let .channel(flags, flags2, id, accessHash, title, username, photo, date, restrictionReason, adminRights, bannedRights, defaultBannedRights, _, usernames, _, color, profileColor, emojiStatus, boostLevel): let isMin = (flags & (1 << 12)) != 0 let participationStatus: TelegramChannelParticipationStatus @@ -173,7 +173,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { } } - return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: accessHashValue, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: date, version: 0, participationStatus: participationStatus, info: info, flags: channelFlags, restrictionInfo: restrictionInfo, adminRights: adminRights.flatMap(TelegramChatAdminRights.init), bannedRights: bannedRights.flatMap(TelegramChatBannedRights.init), defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId) + return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: accessHashValue, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: date, version: 0, participationStatus: participationStatus, info: info, flags: channelFlags, restrictionInfo: restrictionInfo, adminRights: adminRights.flatMap(TelegramChatAdminRights.init), bannedRights: bannedRights.flatMap(TelegramChatBannedRights.init), defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), approximateBoostLevel: boostLevel) case let .channelForbidden(flags, id, accessHash, title, untilDate): let info: TelegramChannelInfo if (flags & Int32(1 << 8)) != 0 { @@ -182,7 +182,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { info = .broadcast(TelegramChannelBroadcastInfo(flags: [])) } - return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: .personal(accessHash), title: title, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .kicked, info: info, flags: TelegramChannelFlags(), restrictionInfo: nil, adminRights: nil, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: untilDate ?? Int32.max), defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: .personal(accessHash), title: title, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .kicked, info: info, flags: TelegramChannelFlags(), restrictionInfo: nil, adminRights: nil, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: untilDate ?? Int32.max), defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil) } } @@ -190,7 +190,7 @@ func mergeGroupOrChannel(lhs: Peer?, rhs: Api.Chat) -> Peer? { switch rhs { case .chat, .chatEmpty, .chatForbidden, .channelForbidden: return parseTelegramGroupOrChannel(chat: rhs) - case let .channel(flags, flags2, _, accessHash, title, username, photo, _, _, _, _, defaultBannedRights, _, usernames, _, color, profileColor, _, _): + case let .channel(flags, flags2, _, accessHash, title, username, photo, _, _, _, _, defaultBannedRights, _, usernames, _, color, profileColor, emojiStatus, boostLevel): let isMin = (flags & (1 << 12)) != 0 if accessHash != nil && !isMin { return parseTelegramGroupOrChannel(chat: rhs) @@ -252,7 +252,9 @@ func mergeGroupOrChannel(lhs: Peer?, rhs: Api.Chat) -> Peer? { } } - return TelegramChannel(id: lhs.id, accessHash: lhs.accessHash, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: lhs.creationDate, version: lhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: lhs.restrictionInfo, adminRights: lhs.adminRights, bannedRights: lhs.bannedRights, defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId) + let parsedEmojiStatus = emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)) + + return TelegramChannel(id: lhs.id, accessHash: lhs.accessHash, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: lhs.creationDate, version: lhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: lhs.restrictionInfo, adminRights: lhs.adminRights, bannedRights: lhs.bannedRights, defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: parsedEmojiStatus, approximateBoostLevel: boostLevel) } else { return parseTelegramGroupOrChannel(chat: rhs) } @@ -306,6 +308,6 @@ func mergeChannel(lhs: TelegramChannel?, rhs: TelegramChannel) -> TelegramChanne let storiesHidden: Bool? = rhs.storiesHidden ?? lhs.storiesHidden - return TelegramChannel(id: lhs.id, accessHash: accessHash, title: rhs.title, username: rhs.username, photo: rhs.photo, creationDate: rhs.creationDate, version: rhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: rhs.restrictionInfo, adminRights: rhs.adminRights, bannedRights: rhs.bannedRights, defaultBannedRights: rhs.defaultBannedRights, usernames: rhs.usernames, storiesHidden: storiesHidden, nameColor: rhs.nameColor, backgroundEmojiId: rhs.backgroundEmojiId, profileColor: rhs.profileColor, profileBackgroundEmojiId: rhs.profileBackgroundEmojiId) + return TelegramChannel(id: lhs.id, accessHash: accessHash, title: rhs.title, username: rhs.username, photo: rhs.photo, creationDate: rhs.creationDate, version: rhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: rhs.restrictionInfo, adminRights: rhs.adminRights, bannedRights: rhs.bannedRights, defaultBannedRights: rhs.defaultBannedRights, usernames: rhs.usernames, storiesHidden: storiesHidden, nameColor: rhs.nameColor, backgroundEmojiId: rhs.backgroundEmojiId, profileColor: rhs.profileColor, profileBackgroundEmojiId: rhs.profileBackgroundEmojiId, emojiStatus: rhs.emojiStatus, approximateBoostLevel: rhs.approximateBoostLevel) } diff --git a/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift index daf468b03b..bcaf3dbe7e 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift @@ -1876,6 +1876,12 @@ private func sendStandaloneMessage(auxiliaryMethods: AccountAuxiliaryMethods, po if let value = contents.media { media.append(value) } + + var attributes = contents.attributes + if !attributes.contains(where: { $0 is AutoremoveTimeoutMessageAttribute }), let messageAutoremoveTimeout = state.messageAutoremoveTimeout { + attributes.append(AutoclearTimeoutMessageAttribute(timeout: messageAutoremoveTimeout, countdownBeginTime: nil)) + } + let message = Message( stableId: 1, stableVersion: 0, @@ -1892,7 +1898,7 @@ private func sendStandaloneMessage(auxiliaryMethods: AccountAuxiliaryMethods, po forwardInfo: nil, author: nil, text: contents.text, - attributes: contents.attributes, + attributes: attributes, media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), @@ -1967,7 +1973,7 @@ private func sendStandaloneMessage(auxiliaryMethods: AccountAuxiliaryMethods, po } let entitiesAttribute = message.textEntitiesAttribute - let (tags, globalTags) = tagsForStoreMessage(incoming: false, attributes: contents.attributes, media: updatedMedia, textEntities: entitiesAttribute?.entities, isPinned: false) + let (tags, globalTags) = tagsForStoreMessage(incoming: false, attributes: attributes, media: updatedMedia, textEntities: entitiesAttribute?.entities, isPinned: false) let storedMessage = StoreMessage( peerId: peerId, diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StickerPackCollectionInfo.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StickerPackCollectionInfo.swift index f0898df6e3..2c40edf798 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StickerPackCollectionInfo.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StickerPackCollectionInfo.swift @@ -30,6 +30,9 @@ public struct StickerPackCollectionInfoFlags: OptionSet { if flags.contains(StickerPackCollectionInfoFlags.isEmoji) { rawValue |= StickerPackCollectionInfoFlags.isEmoji.rawValue } + if flags.contains(StickerPackCollectionInfoFlags.isAvailableAsChannelStatus) { + rawValue |= StickerPackCollectionInfoFlags.isAvailableAsChannelStatus.rawValue + } self.rawValue = rawValue } @@ -39,6 +42,7 @@ public struct StickerPackCollectionInfoFlags: OptionSet { public static let isAnimated = StickerPackCollectionInfoFlags(rawValue: 1 << 2) public static let isVideo = StickerPackCollectionInfoFlags(rawValue: 1 << 3) public static let isEmoji = StickerPackCollectionInfoFlags(rawValue: 1 << 4) + public static let isAvailableAsChannelStatus = StickerPackCollectionInfoFlags(rawValue: 1 << 5) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift index 08abf2fdab..717c902f4e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift @@ -171,6 +171,8 @@ public final class TelegramChannel: Peer, Equatable { public let backgroundEmojiId: Int64? public let profileColor: PeerNameColor? public let profileBackgroundEmojiId: Int64? + public let emojiStatus: PeerEmojiStatus? + public let approximateBoostLevel: Int32? public var indexName: PeerIndexNameRepresentation { var addressNames = self.usernames.map { $0.username } @@ -181,8 +183,19 @@ public final class TelegramChannel: Peer, Equatable { } public var associatedMediaIds: [MediaId]? { - if let backgroundEmojiId = self.backgroundEmojiId { - return [MediaId(namespace: Namespaces.Media.CloudFile, id: backgroundEmojiId)] + if let emojiStatus = self.emojiStatus, let backgroundEmojiId = self.backgroundEmojiId { + return [ + MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId), + MediaId(namespace: Namespaces.Media.CloudFile, id: backgroundEmojiId) + ] + } else if let emojiStatus = self.emojiStatus { + return [ + MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId) + ] + } else if let backgroundEmojiId = self.backgroundEmojiId { + return [ + MediaId(namespace: Namespaces.Media.CloudFile, id: backgroundEmojiId) + ] } else { return nil } @@ -191,7 +204,17 @@ public final class TelegramChannel: Peer, Equatable { public let associatedPeerId: PeerId? = nil public let notificationSettingsPeerId: PeerId? = nil - public var timeoutAttribute: UInt32? { return nil } + public var timeoutAttribute: UInt32? { + if let emojiStatus = self.emojiStatus { + if let expirationDate = emojiStatus.expirationDate { + return UInt32(max(0, expirationDate)) + } else { + return nil + } + } else { + return nil + } + } public init( id: PeerId, @@ -213,7 +236,9 @@ public final class TelegramChannel: Peer, Equatable { nameColor: PeerNameColor?, backgroundEmojiId: Int64?, profileColor: PeerNameColor?, - profileBackgroundEmojiId: Int64? + profileBackgroundEmojiId: Int64?, + emojiStatus: PeerEmojiStatus?, + approximateBoostLevel: Int32? ) { self.id = id self.accessHash = accessHash @@ -235,6 +260,8 @@ public final class TelegramChannel: Peer, Equatable { self.backgroundEmojiId = backgroundEmojiId self.profileColor = profileColor self.profileBackgroundEmojiId = profileBackgroundEmojiId + self.emojiStatus = emojiStatus + self.approximateBoostLevel = approximateBoostLevel } public init(decoder: PostboxDecoder) { @@ -268,6 +295,8 @@ public final class TelegramChannel: Peer, Equatable { self.backgroundEmojiId = decoder.decodeOptionalInt64ForKey("bgem") self.profileColor = decoder.decodeOptionalInt32ForKey("pclr").flatMap { PeerNameColor(rawValue: $0) } self.profileBackgroundEmojiId = decoder.decodeOptionalInt64ForKey("pgem") + self.emojiStatus = decoder.decode(PeerEmojiStatus.self, forKey: "emjs") + self.approximateBoostLevel = decoder.decodeOptionalInt32ForKey("abl") } public func encode(_ encoder: PostboxEncoder) { @@ -347,6 +376,18 @@ public final class TelegramChannel: Peer, Equatable { } else { encoder.encodeNil(forKey: "pgem") } + + if let emojiStatus = self.emojiStatus { + encoder.encode(emojiStatus, forKey: "emjs") + } else { + encoder.encodeNil(forKey: "emjs") + } + + if let approximateBoostLevel = self.approximateBoostLevel { + encoder.encodeInt32(approximateBoostLevel, forKey: "abl") + } else { + encoder.encodeNil(forKey: "abl") + } } public func isEqual(_ other: Peer) -> Bool { @@ -399,43 +440,57 @@ public final class TelegramChannel: Peer, Equatable { if lhs.profileBackgroundEmojiId != rhs.profileBackgroundEmojiId { return false } + if lhs.emojiStatus != rhs.emojiStatus { + return false + } + if lhs.approximateBoostLevel != rhs.approximateBoostLevel { + return false + } return true } public func withUpdatedAddressName(_ addressName: String?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: addressName, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: addressName, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) } public func withUpdatedAddressNames(_ addressNames: [TelegramPeerUsername]) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: addressNames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: addressNames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) } public func withUpdatedDefaultBannedRights(_ defaultBannedRights: TelegramChatBannedRights?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) } public func withUpdatedFlags(_ flags: TelegramChannelFlags) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) } public func withUpdatedStoriesHidden(_ storiesHidden: Bool?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) } public func withUpdatedNameColor(_ nameColor: PeerNameColor?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) } public func withUpdatedBackgroundEmojiId(_ backgroundEmojiId: Int64?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) } public func withUpdatedProfileColor(_ profileColor: PeerNameColor?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) } public func withUpdatedProfileBackgroundEmojiId(_ profileBackgroundEmojiId: Int64?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + } + + public func withUpdatedEmojiStatus(_ emojiStatus: PeerEmojiStatus?) -> TelegramChannel { + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + } + + public func withUpdatedApproximateBoostLevel(_ approximateBoostLevel: Int32?) -> TelegramChannel { + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: approximateBoostLevel) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index 493c924e6c..f35211a3e9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -315,7 +315,9 @@ private class AdMessagesHistoryContextImpl { nameColor: invite.nameColor, backgroundEmojiId: nil, profileColor: nil, - profileBackgroundEmojiId: nil + profileBackgroundEmojiId: nil, + emojiStatus: nil, + approximateBoostLevel: nil ) case let .webPage(webPage): author = TelegramChannel( @@ -338,7 +340,9 @@ private class AdMessagesHistoryContextImpl { nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, - profileBackgroundEmojiId: nil + profileBackgroundEmojiId: nil, + emojiStatus: nil, + approximateBoostLevel: nil ) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 07ed846288..07d8b77419 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -1423,6 +1423,13 @@ public func _internal_pollPeerStories(postbox: Postbox, network: Network, accoun guard let inputPeer = inputPeer else { return .complete() } + + #if DEBUG + if "".isEmpty { + return .complete() + } + #endif + return network.request(Api.functions.stories.getPeerStories(peer: inputPeer)) |> map(Optional.init) |> `catch` { _ -> Signal in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift index 8f6fca61b8..06cbf99f62 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift @@ -514,6 +514,10 @@ public extension EnginePeer { return self._asPeer().profileColor } + var emojiStatus: PeerEmojiStatus? { + return self._asPeer().emojiStatus + } + var backgroundEmojiId: Int64? { return self._asPeer().backgroundEmojiId } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 7bde5e753a..c9ada3511e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -718,6 +718,10 @@ public extension TelegramEngine { return _internal_updatePeerNameColorAndEmoji(account: self.account, peerId: peerId, nameColor: nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId) } + public func updatePeerEmojiStatus(peerId: EnginePeer.Id, fileId: Int64?, expirationDate: Int32?) -> Signal { + return _internal_updatePeerEmojiStatus(account: self.account, peerId: peerId, fileId: fileId, expirationDate: expirationDate) + } + public func getChatListPeers(filterPredicate: ChatListFilterPredicate) -> Signal<[EnginePeer], NoError> { return self.account.postbox.transaction { transaction -> [EnginePeer] in return transaction.getChatListPeers(groupId: .root, filterPredicate: filterPredicate, additionalFilter: nil).map(EnginePeer.init) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift index 9a76f169a4..74a3fcb96d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift @@ -100,9 +100,9 @@ func _internal_updatePeerNameColorAndEmoji(account: Account, peerId: EnginePeer. if let peer = peer as? TelegramChannel, let inputChannel = apiInputChannel(peer) { let flagsReplies: Int32 = (1 << 0) | (1 << 2) - var flagsProfile: Int32 = (1 << 0) | (1 << 2) + var flagsProfile: Int32 = (1 << 0) | (1 << 1) if profileColor != nil { - flagsProfile |= (1 << 1) + flagsProfile |= (1 << 2) } return combineLatest( @@ -161,3 +161,45 @@ func _internal_updatePeerNameColorAndEmoji(account: Account, peerId: EnginePeer. |> castError(UpdatePeerNameColorAndEmojiError.self) |> switchToLatest } + +public enum UpdatePeerEmojiStatusError { + case generic +} + +func _internal_updatePeerEmojiStatus(account: Account, peerId: PeerId, fileId: Int64?, expirationDate: Int32?) -> Signal { + return account.postbox.transaction { transaction -> Api.InputChannel? in + let updatedStatus = fileId.flatMap { + PeerEmojiStatus(fileId: $0, expirationDate: expirationDate) + } + if let peer = transaction.getPeer(peerId) as? TelegramChannel { + updatePeersCustom(transaction: transaction, peers: [peer.withUpdatedEmojiStatus(updatedStatus)], update: { _, updated in updated }) + } + + return transaction.getPeer(peerId).flatMap(apiInputChannel) + } + |> castError(UpdatePeerEmojiStatusError.self) + |> mapToSignal { inputChannel -> Signal in + guard let inputChannel = inputChannel else { + return .fail(.generic) + } + let mappedStatus: Api.EmojiStatus + if let fileId = fileId { + if let expirationDate = expirationDate { + mappedStatus = .emojiStatusUntil(documentId: fileId, until: expirationDate) + } else { + mappedStatus = .emojiStatus(documentId: fileId) + } + } else { + mappedStatus = .emojiStatusEmpty + } + return account.network.request(Api.functions.channels.updateEmojiStatus(channel: inputChannel, emojiStatus: mappedStatus)) + |> ignoreValues + |> `catch` { error -> Signal in + if error.errorDescription == "CHAT_NOT_MODIFIED" { + return .complete() + } else { + return .fail(.generic) + } + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift index 0f5ea32a5a..13283901eb 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift @@ -49,6 +49,9 @@ extension StickerPackCollectionInfo { if (flags & (1 << 7)) != 0 { setFlags.insert(.isEmoji) } + if (flags & (1 << 10)) != 0 { + setFlags.insert(.isAvailableAsChannelStatus) + } var thumbnailRepresentation: TelegramMediaImageRepresentation? var immediateThumbnailData: Data? diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index 53aaa0e3bb..9a07ce5ac0 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -255,6 +255,17 @@ public extension Peer { return false } + var emojiStatus: PeerEmojiStatus? { + switch self { + case let user as TelegramUser: + return user.emojiStatus + case let channel as TelegramChannel: + return channel.emojiStatus + default: + return nil + } + } + var backgroundEmojiId: Int64? { switch self { case let user as TelegramUser: diff --git a/submodules/TelegramPresentationData/Sources/PresentationData.swift b/submodules/TelegramPresentationData/Sources/PresentationData.swift index 002043cccf..bcbb763eee 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationData.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationData.swift @@ -875,3 +875,29 @@ public extension PresentationData { return PresentationData(strings: strings, theme: self.theme, autoNightModeTriggered: self.autoNightModeTriggered, chatWallpaper: self.chatWallpaper, chatFontSize: self.chatFontSize, chatBubbleCorners: chatBubbleCorners, listsFontSize: self.listsFontSize, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, nameSortOrder: self.nameSortOrder, reduceMotion: self.reduceMotion, largeEmoji: self.largeEmoji) } } + +public func themeDisplayName(strings: PresentationStrings, reference: PresentationThemeReference) -> String { + let name: String + switch reference { + case let .builtin(theme): + switch theme { + case .dayClassic: + name = strings.Appearance_ThemeCarouselClassic + case .day: + name = strings.Appearance_ThemeCarouselDay + case .night: + name = strings.Appearance_ThemeCarouselNewNight + case .nightAccent: + name = strings.Appearance_ThemeCarouselTintedNight + } + case let .local(theme): + name = theme.title + case let .cloud(theme): + if let emoticon = theme.theme.emoticon { + name = emoticon + } else { + name = theme.theme.title + } + } + return name +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 81ca15be32..b34c6c8ec8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -1953,11 +1953,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI authorNameColor = color if case let .peer(peerId) = item.chatLocation, let authorPeerId = item.message.author?.id, authorPeerId == peerId { + if effectiveAuthor is TelegramChannel, let emojiStatus = effectiveAuthor.emojiStatus { + currentCredibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor, themeColor: color.withMultipliedAlpha(0.4), loopMode: .count(2)) + } } else if effectiveAuthor.isScam { currentCredibilityIcon = .text(color: incoming ? item.presentationData.theme.theme.chat.message.incoming.scamColor : item.presentationData.theme.theme.chat.message.outgoing.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) } else if effectiveAuthor.isFake { currentCredibilityIcon = .text(color: incoming ? item.presentationData.theme.theme.chat.message.incoming.scamColor : item.presentationData.theme.theme.chat.message.outgoing.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) - } else if let user = effectiveAuthor as? TelegramUser, let emojiStatus = user.emojiStatus { + } else if let emojiStatus = effectiveAuthor.emojiStatus { currentCredibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor, themeColor: color.withMultipliedAlpha(0.4), loopMode: .count(2)) } else if effectiveAuthor.isVerified { currentCredibilityIcon = .verified(fillColor: item.presentationData.theme.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index 4f62d1263b..0dea6e479e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -69,7 +69,7 @@ private func filterOriginalMessageFlags(_ message: Message) -> Message { private func filterMessageChannelPeer(_ peer: Peer) -> Peer { if let peer = peer as? TelegramChannel { - return TelegramChannel(id: peer.id, accessHash: peer.accessHash, title: peer.title, username: peer.username, photo: peer.photo, creationDate: peer.creationDate, version: peer.version, participationStatus: peer.participationStatus, info: .group(TelegramChannelGroupInfo(flags: [])), flags: peer.flags, restrictionInfo: peer.restrictionInfo, adminRights: peer.adminRights, bannedRights: peer.bannedRights, defaultBannedRights: peer.defaultBannedRights, usernames: peer.usernames, storiesHidden: peer.storiesHidden, nameColor: peer.nameColor, backgroundEmojiId: peer.backgroundEmojiId, profileColor: peer.profileColor, profileBackgroundEmojiId: peer.profileBackgroundEmojiId) + return TelegramChannel(id: peer.id, accessHash: peer.accessHash, title: peer.title, username: peer.username, photo: peer.photo, creationDate: peer.creationDate, version: peer.version, participationStatus: peer.participationStatus, info: .group(TelegramChannelGroupInfo(flags: [])), flags: peer.flags, restrictionInfo: peer.restrictionInfo, adminRights: peer.adminRights, bannedRights: peer.bannedRights, defaultBannedRights: peer.defaultBannedRights, usernames: peer.usernames, storiesHidden: peer.storiesHidden, nameColor: peer.nameColor, backgroundEmojiId: peer.backgroundEmojiId, profileColor: peer.profileColor, profileBackgroundEmojiId: peer.profileBackgroundEmojiId, emojiStatus: peer.emojiStatus, approximateBoostLevel: peer.approximateBoostLevel) } return peer } diff --git a/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift b/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift index 14f5bb3d83..e09b349b29 100644 --- a/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift +++ b/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift @@ -334,7 +334,6 @@ private final class LineView: UIView { let _ = previousParams - self.backgroundView.tintColor = primaryColor if let secondaryColor { @@ -406,6 +405,10 @@ private final class LineView: UIView { self.dashBackgroundView = nil dashBackgroundView.removeFromSuperview() } + if let dashThirdBackgroundView = self.dashThirdBackgroundView { + self.dashThirdBackgroundView = nil + dashThirdBackgroundView.removeFromSuperview() + } self.backgroundView.alpha = 1.0 } diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index f708e2baa0..98e17f2e40 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -257,7 +257,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { titleCredibilityIcon = .fake } else if peer.isScam { titleCredibilityIcon = .scam - } else if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus, !premiumConfiguration.isPremiumDisabled { + } else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { titleCredibilityIcon = .emojiStatus(emojiStatus) } else if peer.isVerified { titleCredibilityIcon = .verified diff --git a/submodules/TelegramUI/Components/DynamicCornerRadiusView/BUILD b/submodules/TelegramUI/Components/DynamicCornerRadiusView/BUILD new file mode 100644 index 0000000000..a6c83e6f0e --- /dev/null +++ b/submodules/TelegramUI/Components/DynamicCornerRadiusView/BUILD @@ -0,0 +1,18 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "DynamicCornerRadiusView", + module_name = "DynamicCornerRadiusView", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/ComponentFlow" + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/DynamicCornerRadiusView/Sources/DynamicCornerRadiusView.swift b/submodules/TelegramUI/Components/DynamicCornerRadiusView/Sources/DynamicCornerRadiusView.swift new file mode 100644 index 0000000000..531a3bb4a8 --- /dev/null +++ b/submodules/TelegramUI/Components/DynamicCornerRadiusView/Sources/DynamicCornerRadiusView.swift @@ -0,0 +1,90 @@ +import Foundation +import UIKit +import ComponentFlow + +private func generatePath(size: CGSize, corners: DynamicCornerRadiusView.Corners) -> CGPath { + let path = CGMutablePath() + + var corners = corners + corners.minXMinY = max(0.01, corners.minXMinY) + corners.maxXMinY = max(0.01, corners.maxXMinY) + corners.minXMaxY = max(0.01, corners.minXMaxY) + corners.maxXMaxY = max(0.01, corners.maxXMaxY) + + path.move(to: CGPoint(x: 0.0, y: corners.minXMinY)) + path.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: corners.minXMinY, y: 0.0), radius: corners.minXMinY) + path.addLine(to: CGPoint(x: size.width - corners.maxXMinY, y: 0.0)) + path.addArc(tangent1End: CGPoint(x: size.width, y: 0.0), tangent2End: CGPoint(x: size.width, y: corners.maxXMinY), radius: corners.maxXMinY) + path.addLine(to: CGPoint(x: size.width, y: size.height - corners.maxXMaxY)) + path.addArc(tangent1End: CGPoint(x: size.width, y: size.height), tangent2End: CGPoint(x: size.width - corners.maxXMaxY, y: size.height), radius: corners.maxXMaxY) + path.addLine(to: CGPoint(x: corners.minXMaxY, y: size.height)) + path.addArc(tangent1End: CGPoint(x: 0.0, y: size.height), tangent2End: CGPoint(x: 0.0, y: size.height - corners.minXMaxY), radius: corners.minXMaxY) + path.closeSubpath() + + return path +} + +open class DynamicCornerRadiusView: UIView { + override public static var layerClass: AnyClass { + return CAShapeLayer.self + } + + public struct Corners: Equatable { + public var minXMinY: CGFloat + public var maxXMinY: CGFloat + public var minXMaxY: CGFloat + public var maxXMaxY: CGFloat + + public init(minXMinY: CGFloat, maxXMinY: CGFloat, minXMaxY: CGFloat, maxXMaxY: CGFloat) { + self.minXMinY = minXMinY + self.maxXMinY = maxXMinY + self.minXMaxY = minXMaxY + self.maxXMaxY = maxXMaxY + } + } + + private struct Params: Equatable { + var size: CGSize + var corners: Corners + + init(size: CGSize, corners: Corners) { + self.size = size + self.corners = corners + } + } + + private var params: Params? + + override public init(frame: CGRect) { + super.init(frame: frame) + + if let shapeLayer = self.layer as? CAShapeLayer { + shapeLayer.strokeColor = nil + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func update(size: CGSize, corners: Corners, transition: Transition) { + let params = Params(size: size, corners: corners) + if self.params == params { + return + } + self.params = params + self.update(params: params, transition: transition) + } + + public func updateColor(color: UIColor, transition: Transition) { + if let shapeLayer = self.layer as? CAShapeLayer { + transition.setShapeLayerFillColor(layer: shapeLayer, color: color) + } + } + + private func update(params: Params, transition: Transition) { + if let shapeLayer = self.layer as? CAShapeLayer { + transition.setShapeLayerPath(layer: shapeLayer, path: generatePath(size: params.size, corners: params.corners)) + } + } +} diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 8df436290e..f2057cc3e8 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -64,6 +64,7 @@ public final class EmojiStatusSelectionComponent: Component { public let emojiContent: EmojiPagerContentComponent public let backgroundColor: UIColor public let separatorColor: UIColor + public let color: UIColor? public let hideTopPanel: Bool public let disableTopPanel: Bool public let hideTopPanelUpdated: (Bool, Transition) -> Void @@ -73,6 +74,7 @@ public final class EmojiStatusSelectionComponent: Component { strings: PresentationStrings, deviceMetrics: DeviceMetrics, emojiContent: EmojiPagerContentComponent, + color: UIColor?, backgroundColor: UIColor, separatorColor: UIColor, hideTopPanel: Bool, @@ -83,6 +85,7 @@ public final class EmojiStatusSelectionComponent: Component { self.strings = strings self.deviceMetrics = deviceMetrics self.emojiContent = emojiContent + self.color = color self.backgroundColor = backgroundColor self.separatorColor = separatorColor self.hideTopPanel = hideTopPanel @@ -103,6 +106,9 @@ public final class EmojiStatusSelectionComponent: Component { if lhs.emojiContent != rhs.emojiContent { return false } + if lhs.color != rhs.color { + return false + } if lhs.backgroundColor != rhs.backgroundColor { return false } @@ -169,7 +175,7 @@ public final class EmojiStatusSelectionComponent: Component { isContentInFocus: true, containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: 0.0, bottom: 0.0, right: 0.0), topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0), - emojiContent: component.emojiContent, + emojiContent: component.emojiContent.withCustomTintColor(component.color), stickerContent: nil, maskContent: nil, gifContent: nil, @@ -200,7 +206,8 @@ public final class EmojiStatusSelectionComponent: Component { isExpanded: false, clipContentToTopPanel: false, useExternalSearchContainer: false, - hidePanels: component.disableTopPanel + hidePanels: component.disableTopPanel, + customTintColor: component.color )), environment: {}, containerSize: availableSize @@ -277,6 +284,7 @@ public final class EmojiStatusSelectionController: ViewController { private var validLayout: ContainerViewLayout? private let currentSelection: Int64? + private let color: UIColor? private var emojiContentDisposable: Disposable? private var emojiContent: EmojiPagerContentComponent? @@ -312,10 +320,11 @@ public final class EmojiStatusSelectionController: ViewController { private var isReactionSearchActive: Bool = false - init(controller: EmojiStatusSelectionController, context: AccountContext, sourceView: UIView?, emojiContent: Signal, currentSelection: Int64?) { + init(controller: EmojiStatusSelectionController, context: AccountContext, sourceView: UIView?, emojiContent: Signal, currentSelection: Int64?, color: UIColor?) { self.controller = controller self.context = context self.currentSelection = currentSelection + self.color = color if let sourceView = sourceView { self.globalSourceRect = sourceView.convert(sourceView.bounds, to: nil) @@ -826,17 +835,26 @@ public final class EmojiStatusSelectionController: ViewController { renderer: animationRenderer, placeholderColor: UIColor(white: 0.0, alpha: 0.0), pointSize: CGSize(width: 32.0, height: 32.0), - dynamicColor: self.presentationData.theme.list.itemAccentColor + dynamicColor: self.color ?? self.presentationData.theme.list.itemAccentColor ) - switch item.tintMode { - case let .custom(color): - baseItemLayer.contentTintColor = color - case .accent: - baseItemLayer.contentTintColor = self.presentationData.theme.list.itemAccentColor - case .primary: - baseItemLayer.contentTintColor = self.presentationData.theme.list.itemPrimaryTextColor - case .none: - break + if let color = self.color { + switch item.tintMode { + case .none: + break + default: + baseItemLayer.contentTintColor = color + } + } else { + switch item.tintMode { + case let .custom(color): + baseItemLayer.contentTintColor = color + case .accent: + baseItemLayer.contentTintColor = self.presentationData.theme.list.itemAccentColor + case .primary: + baseItemLayer.contentTintColor = self.presentationData.theme.list.itemPrimaryTextColor + case .none: + break + } } if let sublayers = animationLayer.sublayers { @@ -988,6 +1006,7 @@ public final class EmojiStatusSelectionController: ViewController { strings: self.presentationData.strings, deviceMetrics: layout.deviceMetrics, emojiContent: emojiContent, + color: self.color, backgroundColor: listBackgroundColor, separatorColor: separatorColor, hideTopPanel: self.isReactionSearchActive, @@ -1024,11 +1043,18 @@ public final class EmojiStatusSelectionController: ViewController { sourceOrigin = CGPoint(x: layout.size.width / 2.0, y: floor(layout.size.height / 2.0 - componentSize.height)) } + var componentFrame: CGRect + let pointsToTop: Bool if sourceOrigin.y + 5.0 + componentSize.height > layout.size.height - layout.insets(options: []).bottom { - sourceOrigin.y = layout.size.height - layout.insets(options: []).bottom - componentSize.height - 5.0 + componentFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - componentSize.width) / 2.0), y: sourceOrigin.y - 25.0 - componentSize.height), size: componentSize) + pointsToTop = false + } else { + componentFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - componentSize.width) / 2.0), y: sourceOrigin.y + 5.0), size: componentSize) + pointsToTop = true + } + if componentFrame.minY < layout.insets(options: [.statusBar]).top { + componentFrame.origin.y = layout.insets(options: [.statusBar]).top } - - let componentFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - componentSize.width) / 2.0), y: sourceOrigin.y + 5.0), size: componentSize) if self.componentShadowLayer.bounds.size != componentFrame.size { let componentShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: componentFrame.size), cornerRadius: 24.0).cgPath @@ -1038,7 +1064,12 @@ public final class EmojiStatusSelectionController: ViewController { let cloudOffset0: CGFloat = 30.0 let cloudSize0: CGFloat = 16.0 - var cloudFrame0 = CGRect(origin: CGPoint(x: floor(sourceOrigin.x + cloudOffset0 - cloudSize0 / 2.0), y: componentFrame.minY - cloudSize0 / 2.0), size: CGSize(width: cloudSize0, height: cloudSize0)) + var cloudFrame0: CGRect + if pointsToTop { + cloudFrame0 = CGRect(origin: CGPoint(x: floor(sourceOrigin.x + cloudOffset0 - cloudSize0 / 2.0), y: componentFrame.minY - cloudSize0 / 2.0), size: CGSize(width: cloudSize0, height: cloudSize0)) + } else { + cloudFrame0 = CGRect(origin: CGPoint(x: floor(sourceOrigin.x + cloudOffset0 - cloudSize0 / 2.0), y: componentFrame.maxY - cloudSize0 / 2.0), size: CGSize(width: cloudSize0, height: cloudSize0)) + } var invertX = false if cloudFrame0.maxX >= layout.size.width - layout.safeInsets.right - 32.0 { cloudFrame0.origin.x = floor(sourceOrigin.x - cloudSize0 - cloudOffset0 + cloudSize0 / 2.0) @@ -1055,7 +1086,12 @@ public final class EmojiStatusSelectionController: ViewController { let cloudOffset1 = CGPoint(x: -9.0, y: -14.0) let cloudSize1: CGFloat = 8.0 - var cloudFrame1 = CGRect(origin: CGPoint(x: floor(cloudFrame0.midX + cloudOffset1.x - cloudSize1 / 2.0), y: floor(cloudFrame0.midY + cloudOffset1.y - cloudSize1 / 2.0)), size: CGSize(width: cloudSize1, height: cloudSize1)) + var cloudFrame1: CGRect + if pointsToTop { + cloudFrame1 = CGRect(origin: CGPoint(x: floor(cloudFrame0.midX + cloudOffset1.x - cloudSize1 / 2.0), y: floor(cloudFrame0.midY + cloudOffset1.y - cloudSize1 / 2.0)), size: CGSize(width: cloudSize1, height: cloudSize1)) + } else { + cloudFrame1 = CGRect(origin: CGPoint(x: floor(cloudFrame0.midX + cloudOffset1.x - cloudSize1 / 2.0), y: floor(cloudFrame0.midY - cloudOffset1.y - cloudSize1 / 2.0)), size: CGSize(width: cloudSize1, height: cloudSize1)) + } if invertX { cloudFrame1.origin.x = floor(cloudFrame0.midX - cloudSize1 - cloudOffset1.x + cloudSize1 / 2.0) } @@ -1076,7 +1112,7 @@ public final class EmojiStatusSelectionController: ViewController { let contentDuration: Double = 0.3 let contentDelay: Double = 0.14 - let initialContentFrame = CGRect(origin: CGPoint(x: cloudFrame0.midX - 24.0, y: componentFrame.minY), size: CGSize(width: 24.0 * 2.0, height: 24.0 * 2.0)) + let initialContentFrame = CGRect(origin: CGPoint(x: cloudFrame0.midX - 24.0, y: pointsToTop ? componentFrame.minY : (componentFrame.maxY - 24.0 * 2.0)), size: CGSize(width: 24.0 * 2.0, height: 24.0 * 2.0)) if let emojiView = self.componentHost.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View { emojiView.animateIn(fromLocation: self.view.convert(initialContentFrame.center, to: emojiView)) @@ -1084,7 +1120,7 @@ public final class EmojiStatusSelectionController: ViewController { componentView.layer.animatePosition(from: initialContentFrame.center, to: componentFrame.center, duration: contentDuration, delay: contentDelay, timingFunction: kCAMediaTimingFunctionSpring) componentView.layer.animateBounds(from: CGRect(origin: CGPoint(x: -(componentFrame.minX - initialContentFrame.minX), y: -(componentFrame.minY - initialContentFrame.minY)), size: initialContentFrame.size), to: CGRect(origin: CGPoint(), size: componentFrame.size), duration: contentDuration, delay: contentDelay, timingFunction: kCAMediaTimingFunctionSpring) - self.componentShadowLayer.animateFrame(from: CGRect(origin: CGPoint(x: cloudFrame0.midX - 24.0, y: componentFrame.minY), size: CGSize(width: 24.0 * 2.0, height: 24.0 * 2.0)), to: componentView.frame, duration: contentDuration, delay: contentDelay, timingFunction: kCAMediaTimingFunctionSpring) + self.componentShadowLayer.animateFrame(from: CGRect(origin: CGPoint(x: cloudFrame0.midX - 24.0, y: pointsToTop ? componentFrame.minY : (componentFrame.maxY - 24.0 * 2.0)), size: CGSize(width: 24.0 * 2.0, height: 24.0 * 2.0)), to: componentView.frame, duration: contentDuration, delay: contentDelay, timingFunction: kCAMediaTimingFunctionSpring) componentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04, delay: contentDelay) self.componentShadowLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04, delay: contentDelay) @@ -1264,7 +1300,17 @@ public final class EmojiStatusSelectionController: ViewController { return } - if let _ = item, let destinationView = controller.destinationItemView() { + var animateOutToView = false + switch controller.mode { + case .backgroundSelection, .customStatusSelection, .quickReactionSelection: + if let itemFile = item?.itemFile, itemFile.fileId.id != 0 { + animateOutToView = true + } + case .statusSelection: + animateOutToView = true + } + + if animateOutToView, item != nil, let destinationView = controller.destinationItemView() { if let snapshotView = destinationView.snapshotView(afterScreenUpdates: false) { snapshotView.frame = destinationView.frame destinationView.superview?.insertSubview(snapshotView, belowSubview: destinationView) @@ -1279,6 +1325,10 @@ public final class EmojiStatusSelectionController: ViewController { case .statusSelection: let _ = (self.context.engine.accountData.setEmojiStatus(file: item?.itemFile, expirationDate: nil) |> deliverOnMainQueue).start() + case let .backgroundSelection(completion): + completion(item?.itemFile) + case let .customStatusSelection(completion): + completion(item?.itemFile, nil) case let .quickReactionSelection(completion): if let item = item, let itemFile = item.itemFile { var selectedReaction: MessageReaction.Reaction? @@ -1304,7 +1354,7 @@ public final class EmojiStatusSelectionController: ViewController { completion() } - if let item = item, let destinationView = controller.destinationItemView() { + if animateOutToView, let item = item, let destinationView = controller.destinationItemView() { var emojiString: String? if let itemFile = item.itemFile { attributeLoop: for attribute in itemFile.attributes { @@ -1364,6 +1414,8 @@ public final class EmojiStatusSelectionController: ViewController { public enum Mode { case statusSelection + case backgroundSelection(completion: (TelegramMediaFile?) -> Void) + case customStatusSelection(completion: (TelegramMediaFile?, Int32?) -> Void) case quickReactionSelection(completion: () -> Void) } @@ -1371,6 +1423,7 @@ public final class EmojiStatusSelectionController: ViewController { private weak var sourceView: UIView? private let emojiContent: Signal private let currentSelection: Int64? + private let color: UIColor? private let mode: Mode private let destinationItemView: () -> UIView? @@ -1383,12 +1436,13 @@ public final class EmojiStatusSelectionController: ViewController { return true } - public init(context: AccountContext, mode: Mode, sourceView: UIView, emojiContent: Signal, currentSelection: Int64?, destinationItemView: @escaping () -> UIView?) { + public init(context: AccountContext, mode: Mode, sourceView: UIView, emojiContent: Signal, currentSelection: Int64?, color: UIColor? = nil, destinationItemView: @escaping () -> UIView?) { self.context = context self.mode = mode self.sourceView = sourceView self.emojiContent = emojiContent self.currentSelection = currentSelection + self.color = color self.destinationItemView = destinationItemView super.init(navigationBarPresentationData: nil) @@ -1418,7 +1472,7 @@ public final class EmojiStatusSelectionController: ViewController { } override public func loadDisplayNode() { - self.displayNode = Node(controller: self, context: self.context, sourceView: self.sourceView, emojiContent: self.emojiContent, currentSelection: self.currentSelection) + self.displayNode = Node(controller: self, context: self.context, sourceView: self.sourceView, emojiContent: self.emojiContent, currentSelection: self.currentSelection, color: self.color) super.displayNodeDidLoad() } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index e661064f2b..ffd8d3fb03 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -7188,6 +7188,7 @@ public final class EmojiPagerContentComponent: Component { public enum Subject: Equatable { case generic case status + case channelStatus case reaction(onlyTop: Bool) case emoji case topicIcon @@ -7239,6 +7240,19 @@ public final class EmojiPagerContentComponent: Component { orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedStatusEmoji) orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentStatusEmoji) + iconStatusEmoji = context.engine.stickers.loadedStickerPack(reference: .iconStatusEmoji, forceActualized: false) + |> map { result -> [TelegramMediaFile] in + switch result { + case let .result(_, items, _): + return items.map(\.file) + default: + return [] + } + } + |> take(1) + } else if case .channelStatus = subject { + orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedStatusEmoji) + iconStatusEmoji = context.engine.stickers.loadedStickerPack(reference: .iconStatusEmoji, forceActualized: false) |> map { result -> [TelegramMediaFile] in switch result { @@ -7283,6 +7297,8 @@ public final class EmojiPagerContentComponent: Component { searchCategories = context.engine.stickers.emojiSearchCategories(kind: .emoji) } else if case .status = subject { searchCategories = context.engine.stickers.emojiSearchCategories(kind: .status) + } else if case .channelStatus = subject { + searchCategories = context.engine.stickers.emojiSearchCategories(kind: .status) } else if [.profilePhoto, .groupPhoto].contains(subject) { searchCategories = context.engine.stickers.emojiSearchCategories(kind: .avatar) } else { @@ -7564,6 +7580,166 @@ public final class EmojiPagerContentComponent: Component { } } + if let recentStatusEmoji = recentStatusEmoji { + for item in recentStatusEmoji.items { + guard let item = item.contents.get(RecentMediaItem.self) else { + continue + } + + let file = item.media + if existingIds.contains(file.fileId) { + continue + } + existingIds.insert(file.fileId) + + var tintMode: Item.TintMode = .none + if file.isCustomTemplateEmoji { + tintMode = .accent + } + for attribute in file.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute { + switch packReference { + case let .id(id, _): + if id == 773947703670341676 || id == 2964141614563343 { + tintMode = .accent + } + default: + break + } + } + } + + let resultItem: EmojiPagerContentComponent.Item + + let animationData = EntityKeyboardAnimationData(file: file) + resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: file, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + if let groupIndex = itemGroupIndexById[groupId] { + if itemGroups[groupIndex].items.count >= (5 + 8) * 8 { + break + } + + itemGroups[groupIndex].items.append(resultItem) + } + } + } + if let featuredStatusEmoji = featuredStatusEmoji { + for item in featuredStatusEmoji.items { + guard let item = item.contents.get(RecentMediaItem.self) else { + continue + } + + let file = item.media + if existingIds.contains(file.fileId) { + continue + } + existingIds.insert(file.fileId) + + let resultItem: EmojiPagerContentComponent.Item + + var tintMode: Item.TintMode = .none + if file.isCustomTemplateEmoji { + tintMode = .accent + } + for attribute in file.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute { + switch packReference { + case let .id(id, _): + if id == 773947703670341676 || id == 2964141614563343 { + tintMode = .accent + } + default: + break + } + } + } + + let animationData = EntityKeyboardAnimationData(file: file) + resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: file, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + if let groupIndex = itemGroupIndexById[groupId] { + if itemGroups[groupIndex].items.count >= (5 + 8) * 8 { + break + } + + itemGroups[groupIndex].items.append(resultItem) + } + } + } + } else if case .channelStatus = subject { + let resultItem = EmojiPagerContentComponent.Item( + animationData: nil, + content: .icon(.stop), + itemFile: nil, + subgroupId: nil, + icon: .none, + tintMode: .accent + ) + + let groupId = "recent" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: topStatusTitle?.uppercased(), subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) + } + + var existingIds = Set() + + for file in iconStatusEmoji.prefix(7) { + if existingIds.contains(file.fileId) { + continue + } + existingIds.insert(file.fileId) + + var tintMode: Item.TintMode = .none + if file.isCustomTemplateEmoji { + tintMode = .accent + } + for attribute in file.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute { + switch packReference { + case let .id(id, _): + if id == 773947703670341676 || id == 2964141614563343 { + tintMode = .accent + } + default: + break + } + } + } + + let resultItem: EmojiPagerContentComponent.Item + + let animationData = EntityKeyboardAnimationData(file: file) + resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: file, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } + } + if let recentStatusEmoji = recentStatusEmoji { for item in recentStatusEmoji.items { guard let item = item.contents.get(RecentMediaItem.self) else { @@ -8143,7 +8319,7 @@ public final class EmojiPagerContentComponent: Component { var isTemplate = false var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { - if [.status, .backgroundIcon].contains(subject) { + if [.status, .channelStatus, .backgroundIcon].contains(subject) { if let backgroundIconColor { tintMode = .custom(backgroundIconColor) } else { @@ -8229,7 +8405,7 @@ public final class EmojiPagerContentComponent: Component { for item in featuredEmojiPack.topItems { var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { - if [.status, .backgroundIcon].contains(subject) { + if [.status, .channelStatus, .backgroundIcon].contains(subject) { if let backgroundIconColor { tintMode = .custom(backgroundIconColor) } else { @@ -8361,8 +8537,8 @@ public final class EmojiPagerContentComponent: Component { ) } - let warpContentsOnEdges = [.reaction(onlyTop: true), .reaction(onlyTop: false), .quickReaction, .status, .profilePhoto, .groupPhoto, .backgroundIcon].contains(subject) - let enableLongPress = [.reaction(onlyTop: true), .reaction(onlyTop: false), .status].contains(subject) + let warpContentsOnEdges = [.reaction(onlyTop: true), .reaction(onlyTop: false), .quickReaction, .status, .channelStatus, .profilePhoto, .groupPhoto, .backgroundIcon].contains(subject) + let enableLongPress = [.reaction(onlyTop: true), .reaction(onlyTop: false), .status, .channelStatus].contains(subject) return EmojiPagerContentComponent( id: "emoji", diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/BUILD b/submodules/TelegramUI/Components/ListActionItemComponent/BUILD new file mode 100644 index 0000000000..dae202b12d --- /dev/null +++ b/submodules/TelegramUI/Components/ListActionItemComponent/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ListActionItemComponent", + module_name = "ListActionItemComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUI/Components/ListSectionComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift new file mode 100644 index 0000000000..fb5467ad0a --- /dev/null +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -0,0 +1,187 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import ListSectionComponent + +public final class ListActionItemComponent: Component { + public let theme: PresentationTheme + public let title: AnyComponent + public let icon: AnyComponentWithIdentity? + public let hasArrow: Bool + public let action: ((UIView) -> Void)? + + public init( + theme: PresentationTheme, + title: AnyComponent, + icon: AnyComponentWithIdentity?, + hasArrow: Bool = true, + action: ((UIView) -> Void)? + ) { + self.theme = theme + self.title = title + self.icon = icon + self.hasArrow = hasArrow + self.action = action + } + + public static func ==(lhs: ListActionItemComponent, rhs: ListActionItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.icon != rhs.icon { + return false + } + if lhs.hasArrow != rhs.hasArrow { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + return true + } + + public final class View: HighlightTrackingButton, ListSectionComponent.ChildView { + private let title = ComponentView() + private var icon: ComponentView? + + private let arrowView: UIImageView + + private var component: ListActionItemComponent? + + public var iconView: UIView? { + return self.icon?.view + } + + public var customUpdateIsHighlighted: ((Bool) -> Void)? + + public override init(frame: CGRect) { + self.arrowView = UIImageView() + + super.init(frame: CGRect()) + + self.addSubview(self.arrowView) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.internalHighligthedChanged = { [weak self] isHighlighted in + guard let self else { + return + } + if let customUpdateIsHighlighted = self.customUpdateIsHighlighted { + customUpdateIsHighlighted(isHighlighted) + } + } + } + + required public init?(coder: NSCoder) { + preconditionFailure() + } + + @objc private func pressed() { + self.component?.action?(self) + } + + func update(component: ListActionItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousComponent = self.component + self.component = component + + self.isEnabled = component.action != nil + + let verticalInset: CGFloat = 11.0 + + let contentLeftInset: CGFloat = 16.0 + let contentRightInset: CGFloat = component.hasArrow ? 30.0 : 16.0 + + var contentHeight: CGFloat = 0.0 + contentHeight += verticalInset + + let titleSize = self.title.update( + transition: transition, + component: component.title, + environment: {}, + containerSize: CGSize(width: availableSize.width - contentLeftInset, height: availableSize.height) + ) + let titleFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: verticalInset), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + contentHeight += titleSize.height + + contentHeight += verticalInset + + if let iconValue = component.icon { + if previousComponent?.icon?.id != iconValue.id, let icon = self.icon { + self.icon = nil + if let iconView = icon.view { + transition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in + iconView?.removeFromSuperview() + }) + } + } + + var iconTransition = transition + let icon: ComponentView + if let current = self.icon { + icon = current + } else { + iconTransition = iconTransition.withAnimation(.none) + icon = ComponentView() + self.icon = icon + } + + let iconSize = icon.update( + transition: iconTransition, + component: iconValue.component, + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availableSize.height) + ) + let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - contentRightInset - iconSize.width, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize) + if let iconView = icon.view { + if iconView.superview == nil { + iconView.isUserInteractionEnabled = false + self.addSubview(iconView) + transition.animateAlpha(view: iconView, from: 0.0, to: 1.0) + } + iconTransition.setFrame(view: iconView, frame: iconFrame) + } + } else { + if let icon = self.icon { + self.icon = nil + if let iconView = icon.view { + transition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in + iconView?.removeFromSuperview() + }) + } + } + } + + if self.arrowView.image == nil { + self.arrowView.image = PresentationResourcesItemList.disclosureArrowImage(component.theme)?.withRenderingMode(.alwaysTemplate) + } + self.arrowView.tintColor = component.theme.list.disclosureArrowColor + if let image = self.arrowView.image { + let arrowFrame = CGRect(origin: CGPoint(x: availableSize.width - 7.0 - image.size.width, y: floor((contentHeight - image.size.height) * 0.5)), size: image.size) + transition.setFrame(view: self.arrowView, frame: arrowFrame) + } + transition.setAlpha(view: self.arrowView, alpha: component.hasArrow ? 1.0 : 0.0) + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ListItemComponentAdaptor/BUILD b/submodules/TelegramUI/Components/ListItemComponentAdaptor/BUILD new file mode 100644 index 0000000000..1ebd9ea2ec --- /dev/null +++ b/submodules/TelegramUI/Components/ListItemComponentAdaptor/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ListItemComponentAdaptor", + module_name = "ListItemComponentAdaptor", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ListItemComponentAdaptor/Sources/ListItemComponentAdaptor.swift b/submodules/TelegramUI/Components/ListItemComponentAdaptor/Sources/ListItemComponentAdaptor.swift new file mode 100644 index 0000000000..ba4e12d70e --- /dev/null +++ b/submodules/TelegramUI/Components/ListItemComponentAdaptor/Sources/ListItemComponentAdaptor.swift @@ -0,0 +1,131 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import ComponentDisplayAdapters + +public protocol _ListItemComponentAdaptorItemGenerator: AnyObject, Equatable { + func item() -> ListViewItem +} + +public final class ListItemComponentAdaptor: Component { + public typealias ItemGenerator = _ListItemComponentAdaptorItemGenerator + + private let itemGenerator: AnyObject + private let isEqualImpl: (AnyObject) -> Bool + private let itemImpl: () -> ListViewItem + private let params: ListViewItemLayoutParams + + public init( + itemGenerator: ItemGeneratorType, + params: ListViewItemLayoutParams + ) { + self.itemGenerator = itemGenerator + self.isEqualImpl = { other in + if let other = other as? ItemGeneratorType, itemGenerator == other { + return true + } else { + return false + } + } + self.itemImpl = { + return itemGenerator.item() + } + self.params = params + } + + public static func ==(lhs: ListItemComponentAdaptor, rhs: ListItemComponentAdaptor) -> Bool { + if !lhs.isEqualImpl(rhs.itemGenerator) { + return false + } + if lhs.params != rhs.params { + return false + } + return true + } + + public final class View: UIView { + private var itemNode: ListViewItemNode? + + func update(component: ListItemComponentAdaptor, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let item = component.itemImpl() + + if let itemNode = self.itemNode { + let mappedAnimation: ListViewItemUpdateAnimation + switch transition.animation { + case .none: + mappedAnimation = .None + case let .curve(duration, curve): + mappedAnimation = .System(duration: duration, transition: ControlledTransition(duration: duration, curve: curve.containedViewLayoutTransitionCurve, interactive: false)) + } + + var resultSize: CGSize? + item.updateNode( + async: { f in f() }, + node: { return itemNode }, + params: component.params, + previousItem: nil, + nextItem: nil, + animation: mappedAnimation, + completion: { [weak itemNode] layout, apply in + resultSize = layout.size + + guard let itemNode else { + return + } + + let nodeFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + + apply(ListViewItemApply(isOnScreen: true)) + } + ) + if let resultSize { + transition.setFrame(view: itemNode.view, frame: CGRect(origin: CGPoint(), size: resultSize)) + return resultSize + } else { + #if DEBUG + assertionFailure() + #endif + return self.bounds.size + } + } else { + var itemNode: ListViewItemNode? + item.nodeConfiguredForParams( + async: { f in f() }, + params: component.params, + synchronousLoads: true, + previousItem: nil, + nextItem: nil, + completion: { result, apply in + itemNode = result + apply().1(ListViewItemApply(isOnScreen: true)) + } + ) + if let itemNode { + self.itemNode = itemNode + self.addSubnode(itemNode) + + return itemNode.bounds.size + } else { + #if DEBUG + assertionFailure() + #endif + return self.bounds.size + } + } + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ListSectionComponent/BUILD b/submodules/TelegramUI/Components/ListSectionComponent/BUILD new file mode 100644 index 0000000000..e216e0bb5d --- /dev/null +++ b/submodules/TelegramUI/Components/ListSectionComponent/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ListSectionComponent", + module_name = "ListSectionComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUI/Components/DynamicCornerRadiusView", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift new file mode 100644 index 0000000000..5612d0f692 --- /dev/null +++ b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift @@ -0,0 +1,288 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import DynamicCornerRadiusView + +public protocol ListSectionComponentChildView: AnyObject { + var customUpdateIsHighlighted: ((Bool) -> Void)? { get set } +} + +public final class ListSectionComponent: Component { + public typealias ChildView = ListSectionComponentChildView + + public enum Background: Equatable { + case none + case all + case range(from: AnyHashable, corners: DynamicCornerRadiusView.Corners) + } + + public let theme: PresentationTheme + public let background: Background + public let header: AnyComponent? + public let footer: AnyComponent? + public let items: [AnyComponentWithIdentity] + + public init( + theme: PresentationTheme, + background: Background = .all, + header: AnyComponent?, + footer: AnyComponent?, + items: [AnyComponentWithIdentity] + ) { + self.theme = theme + self.background = background + self.header = header + self.footer = footer + self.items = items + } + + public static func ==(lhs: ListSectionComponent, rhs: ListSectionComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.background != rhs.background { + return false + } + if lhs.header != rhs.header { + return false + } + if lhs.footer != rhs.footer { + return false + } + if lhs.items != rhs.items { + return false + } + return true + } + + public final class View: UIView { + private let contentView: UIView + private let contentBackgroundView: DynamicCornerRadiusView + + private var header: ComponentView? + private var footer: ComponentView? + private var itemViews: [AnyHashable: ComponentView] = [:] + + private var isHighlighted: Bool = false + + private var component: ListSectionComponent? + + public override init(frame: CGRect) { + self.contentView = UIView() + self.contentView.layer.cornerRadius = 11.0 + self.contentView.clipsToBounds = true + + self.contentBackgroundView = DynamicCornerRadiusView() + + super.init(frame: CGRect()) + + self.addSubview(self.contentBackgroundView) + self.addSubview(self.contentView) + } + + required public init?(coder: NSCoder) { + preconditionFailure() + } + + private func updateIsHighlighted(isHighlighted: Bool) { + if self.isHighlighted == isHighlighted { + return + } + self.isHighlighted = isHighlighted + + guard let component = self.component else { + return + } + + let transition: Transition + let backgroundColor: UIColor + if isHighlighted { + transition = .immediate + backgroundColor = component.theme.list.itemHighlightedBackgroundColor + } else { + transition = .easeInOut(duration: 0.2) + backgroundColor = component.theme.list.itemBlocksBackgroundColor + } + self.contentBackgroundView.updateColor(color: backgroundColor, transition: transition) + } + + func update(component: ListSectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let backgroundColor: UIColor + if self.isHighlighted { + backgroundColor = component.theme.list.itemHighlightedBackgroundColor + } else { + backgroundColor = component.theme.list.itemBlocksBackgroundColor + } + self.contentBackgroundView.updateColor(color: backgroundColor, transition: transition) + + let headerSideInset: CGFloat = 16.0 + + var contentHeight: CGFloat = 0.0 + + if let headerValue = component.header { + let header: ComponentView + var headerTransition = transition + if let current = self.header { + header = current + } else { + headerTransition = headerTransition.withAnimation(.none) + header = ComponentView() + self.header = header + } + + let headerSize = header.update( + transition: headerTransition, + component: headerValue, + environment: {}, + containerSize: CGSize(width: availableSize.width - headerSideInset * 2.0, height: availableSize.height) + ) + if let headerView = header.view { + if headerView.superview == nil { + self.addSubview(headerView) + } + headerTransition.setFrame(view: headerView, frame: CGRect(origin: CGPoint(x: headerSideInset, y: contentHeight), size: headerSize)) + } + contentHeight += headerSize.height + } else { + if let header = self.header { + self.header = nil + header.view?.removeFromSuperview() + } + } + + var innerContentHeight: CGFloat = 0.0 + var validItemIds: [AnyHashable] = [] + for item in component.items { + validItemIds.append(item.id) + + let itemView: ComponentView + var itemTransition = transition + if let current = self.itemViews[item.id] { + itemView = current + } else { + itemTransition = itemTransition.withAnimation(.none) + itemView = ComponentView() + self.itemViews[item.id] = itemView + } + + let itemSize = itemView.update( + transition: itemTransition, + component: item.component, + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availableSize.height) + ) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: innerContentHeight), size: itemSize) + if let itemComponentView = itemView.view { + if itemComponentView.superview == nil { + self.contentView.addSubview(itemComponentView) + transition.animateAlpha(view: itemComponentView, from: 0.0, to: 1.0) + + if let itemComponentView = itemComponentView as? ChildView { + itemComponentView.customUpdateIsHighlighted = { [weak self] isHighlighted in + guard let self else { + return + } + self.updateIsHighlighted(isHighlighted: isHighlighted) + } + } + } + itemTransition.setFrame(view: itemComponentView, frame: itemFrame) + } + innerContentHeight += itemSize.height + } + var removedItemIds: [AnyHashable] = [] + for (id, itemView) in self.itemViews { + if !validItemIds.contains(id) { + removedItemIds.append(id) + + if let itemComponentView = itemView.view { + transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in + itemComponentView?.removeFromSuperview() + }) + } + } + } + for id in removedItemIds { + self.itemViews.removeValue(forKey: id) + } + + if innerContentHeight != 0.0 && contentHeight != 0.0 { + contentHeight += 7.0 + } + + let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: availableSize.width, height: innerContentHeight)) + transition.setFrame(view: self.contentView, frame: contentFrame) + + let backgroundFrame: CGRect + var backgroundAlpha: CGFloat = 1.0 + switch component.background { + case .none: + backgroundFrame = contentFrame + backgroundAlpha = 0.0 + self.contentBackgroundView.update(size: backgroundFrame.size, corners: DynamicCornerRadiusView.Corners(minXMinY: 11.0, maxXMinY: 11.0, minXMaxY: 11.0, maxXMaxY: 11.0), transition: transition) + case .all: + backgroundFrame = contentFrame + self.contentBackgroundView.update(size: backgroundFrame.size, corners: DynamicCornerRadiusView.Corners(minXMinY: 11.0, maxXMinY: 11.0, minXMaxY: 11.0, maxXMaxY: 11.0), transition: transition) + case let .range(from, corners): + if let itemComponentView = self.itemViews[from]?.view, itemComponentView.frame.minY < contentFrame.height { + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: contentFrame.minY + itemComponentView.frame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height - itemComponentView.frame.minY)) + } else { + backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minY, y: contentFrame.height), size: CGSize(width: contentFrame.width, height: 0.0)) + } + self.contentBackgroundView.update(size: backgroundFrame.size, corners: corners, transition: transition) + } + transition.setFrame(view: self.contentBackgroundView, frame: backgroundFrame) + transition.setAlpha(view: self.contentBackgroundView, alpha: backgroundAlpha) + + contentHeight += innerContentHeight + + if let footerValue = component.footer { + let footer: ComponentView + var footerTransition = transition + if let current = self.footer { + footer = current + } else { + footerTransition = footerTransition.withAnimation(.none) + footer = ComponentView() + self.footer = footer + } + + let footerSize = footer.update( + transition: footerTransition, + component: footerValue, + environment: {}, + containerSize: CGSize(width: availableSize.width - headerSideInset * 2.0, height: availableSize.height) + ) + if contentHeight != 0.0 { + contentHeight += 7.0 + } + if let footerView = footer.view { + if footerView.superview == nil { + self.addSubview(footerView) + } + footerTransition.setFrame(view: footerView, frame: CGRect(origin: CGPoint(x: headerSideInset, y: contentHeight), size: footerSize)) + } + contentHeight += footerSize.height + } else { + if let footer = self.footer { + self.footer = nil + footer.view?.removeFromSuperview() + } + } + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift b/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift index 3f7c37cfd7..472def405a 100644 --- a/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift +++ b/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift @@ -332,7 +332,7 @@ private func notificationsPeerCategoryEntries(peerId: EnginePeer.Id, notificatio } } existingThreadIds.insert(value.threadId) - entries.append(.exception(Int32(index), presentationData.dateTimeFormat, presentationData.nameDisplayOrder, .channel(TelegramChannel(id: peerId, accessHash: nil, title: "", username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(TelegramChannelGroupInfo(flags: [])), flags: [.isForum], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)), value.threadId, value.info, title, value.notificationSettings._asNotificationSettings(), state.editing, state.revealedThreadId == value.threadId)) + entries.append(.exception(Int32(index), presentationData.dateTimeFormat, presentationData.nameDisplayOrder, .channel(TelegramChannel(id: peerId, accessHash: nil, title: "", username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(TelegramChannelGroupInfo(flags: [])), flags: [.isForum], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)), value.threadId, value.info, title, value.notificationSettings._asNotificationSettings(), state.editing, state.revealedThreadId == value.threadId)) index += 1 } diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/BUILD b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/BUILD index 631aee7b7d..da94a4fdb1 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/BUILD @@ -26,6 +26,7 @@ swift_library( "//submodules/Markdown", "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent", "//submodules/Components/BundleIconComponent", "//submodules/Components/PagerComponent", "//submodules/PremiumUI", diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index a3c415cb82..031539d733 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -20,82 +20,7 @@ import BundleIconComponent import AnimatedTextComponent import TextFormat import AudioToolbox - -private final class ButtonSubtitleComponent: CombinedComponent { - let count: Int - let theme: PresentationTheme - let strings: PresentationStrings - - init(count: Int, theme: PresentationTheme, strings: PresentationStrings) { - self.count = count - self.theme = theme - self.strings = strings - } - - static func ==(lhs: ButtonSubtitleComponent, rhs: ButtonSubtitleComponent) -> Bool { - if lhs.count != rhs.count { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.strings !== rhs.strings { - return false - } - return true - } - - static var body: Body { - let icon = Child(BundleIconComponent.self) - let text = Child(AnimatedTextComponent.self) - - return { context in - let icon = icon.update( - component: BundleIconComponent( - name: "Chat/Input/Accessory Panels/TextLockIcon", - tintColor: context.component.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7), - maxSize: CGSize(width: 10.0, height: 10.0) - ), - availableSize: CGSize(width: 100.0, height: 100.0), - transition: context.transition - ) - var textItems: [AnimatedTextComponent.Item] = [] - - let levelString = context.component.strings.ChannelReactions_LevelRequiredLabel("") - var previousIndex = 0 - let nsLevelString = levelString.string as NSString - for range in levelString.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) { - if range.range.lowerBound > previousIndex { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable(range.index), content: .text(nsLevelString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex))))) - } - if range.index == 0 { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable(range.index), content: .number(context.component.count, minDigits: 1))) - } - previousIndex = range.range.upperBound - } - if nsLevelString.length > previousIndex { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable(100), content: .text(nsLevelString.substring(with: NSRange(location: previousIndex, length: nsLevelString.length - previousIndex))))) - } - - let text = text.update( - component: AnimatedTextComponent(font: Font.medium(11.0), color: context.component.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7), items: textItems), - availableSize: CGSize(width: context.availableSize.width - 20.0, height: 100.0), - transition: context.transition - ) - - let spacing: CGFloat = 3.0 - let size = CGSize(width: icon.size.width + spacing + text.size.width, height: text.size.height) - context.add(icon - .position(icon.size.centered(in: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: icon.size.width, height: size.height))).center) - ) - context.add(text - .position(text.size.centered(in: CGRect(origin: CGPoint(x: icon.size.width + spacing, y: 0.0), size: text.size)).center) - ) - - return size - } - } -} +import PremiumLockButtonSubtitleComponent final class PeerAllowedReactionsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -904,7 +829,7 @@ final class PeerAllowedReactionsScreenComponent: Component { }).count : 0 if let boostStatus = self.boostStatus, customReactionCount > boostStatus.level { - buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(ButtonSubtitleComponent( + buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(PremiumLockButtonSubtitleComponent( count: customReactionCount, theme: environment.theme, strings: environment.strings diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift index dcf1946b95..369f19bb3e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift @@ -34,15 +34,17 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { let id: AnyHashable let label: Label let additionalBadgeLabel: String? + let additionalBadgeIcon: UIImage? let text: String let icon: UIImage? let iconSignal: Signal? let action: (() -> Void)? - init(id: AnyHashable, label: Label = .none, additionalBadgeLabel: String? = nil, text: String, icon: UIImage? = nil, iconSignal: Signal? = nil, action: (() -> Void)?) { + init(id: AnyHashable, label: Label = .none, additionalBadgeLabel: String? = nil, additionalBadgeIcon: UIImage? = nil, text: String, icon: UIImage? = nil, iconSignal: Signal? = nil, action: (() -> Void)?) { self.id = id self.label = label self.additionalBadgeLabel = additionalBadgeLabel + self.additionalBadgeIcon = additionalBadgeIcon self.text = text self.icon = icon self.iconSignal = iconSignal @@ -61,6 +63,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { private let labelBadgeNode: ASImageNode private let labelNode: ImmediateTextNode private var additionalLabelNode: ImmediateTextNode? + private var additionalLabelBadgeNode: ASImageNode? private let textNode: ImmediateTextNode private let arrowNode: ASImageNode private let bottomSeparatorNode: ASDisplayNode @@ -249,6 +252,24 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { self.labelBadgeNode.removeFromSupernode() } + if let additionalBadgeIcon = item.additionalBadgeIcon { + let additionalLabelBadgeNode: ASImageNode + if let current = self.additionalLabelBadgeNode { + additionalLabelBadgeNode = current + } else { + additionalLabelBadgeNode = ASImageNode() + additionalLabelBadgeNode.isUserInteractionEnabled = false + self.additionalLabelBadgeNode = additionalLabelBadgeNode + self.insertSubnode(additionalLabelBadgeNode, belowSubnode: self.labelNode) + } + additionalLabelBadgeNode.image = additionalBadgeIcon + } else { + if let additionalLabelBadgeNode = self.additionalLabelBadgeNode { + self.additionalLabelBadgeNode = nil + additionalLabelBadgeNode.removeFromSupernode() + } + } + var badgeWidth = max(badgeDiameter, labelSize.width + 10.0) if case .semitransparentBadge = item.label { badgeWidth += 2.0 @@ -283,6 +304,11 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { additionalLabelNode.removeFromSupernode() } + if let additionalLabelBadgeNode = self.additionalLabelBadgeNode, let image = additionalLabelBadgeNode.image { + let additionalLabelSize = image.size + additionalLabelBadgeNode.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 6.0, y: floor((height - additionalLabelSize.height) / 2.0) + 1.0), size: additionalLabelSize) + } + let labelBadgeNodeFrame: CGRect if case let .image(_, imageSize) = item.label { labelBadgeNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - imageSize.width, y: floorToScreenPixels(textFrame.midY - imageSize.height / 2.0)), size:imageSize) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 0903e86ae9..3571c93393 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -476,7 +476,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { credibilityIcon = .fake } else if peer.isScam { credibilityIcon = .scam - } else if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus, !premiumConfiguration.isPremiumDisabled { + } else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { credibilityIcon = .emojiStatus(emojiStatus) } else if peer.isVerified { credibilityIcon = .verified diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 7319f76151..cd230137fd 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1675,7 +1675,12 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL } let colorImage = generateSettingsMenuPeerColorsLabelIcon(colors: colors) - items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerColor, label: .image(colorImage, colorImage.size), text: presentationData.strings.Channel_ChannelColor, icon: UIImage(bundleImageName: "Chat/Info/NameColorIcon"), action: { + //TODO:localize + var boostIcon: UIImage? + if let approximateBoostLevel = channel.approximateBoostLevel, approximateBoostLevel < 1 { + boostIcon = generateDisclosureActionBoostLevelBadgeImage(text: "Level 1+") + } + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerColor, label: .image(colorImage, colorImage.size), additionalBadgeIcon: boostIcon, text: "Appearance", icon: UIImage(bundleImageName: "Chat/Info/NameColorIcon"), action: { interaction.editingOpenNameColorSetup() })) @@ -3835,7 +3840,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var currentSelectedFileId: Int64? var topStatusTitle = strongSelf.presentationData.strings.PeerStatusSetup_NoTimerTitle if let peer = strongSelf.data?.peer { - if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus { + if let emojiStatus = peer.emojiStatus { selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId)) currentSelectedFileId = emojiStatus.fileId @@ -7200,9 +7205,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if self.peerId == self.context.account.peerId { let controller = PeerNameColorScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, subject: .account) self.controller?.push(controller) - } else { - let controller = PeerNameColorScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, subject: .channel(self.peerId)) - self.controller?.push(controller) + } else if let peer = self.data?.peer, peer is TelegramChannel { + self.controller?.push(ChannelAppearanceScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId)) } } @@ -12218,7 +12222,7 @@ private final class AccountPeerContextItemNode: ASDisplayNode, ContextMenuCustom self.avatarNode.setPeer(context: self.item.context, account: self.item.account, theme: self.presentationData.theme, peer: self.item.peer) - if case let .user(user) = self.item.peer, let _ = user.emojiStatus { + if self.item.peer.emojiStatus != nil { rightTextInset += 32.0 } @@ -12236,6 +12240,10 @@ private final class AccountPeerContextItemNode: ASDisplayNode, ContextMenuCustom } else if user.isPremium { iconContent = .premium(color: self.presentationData.theme.list.itemAccentColor) } + } else if case let .channel(channel) = self.item.peer { + if let emojiStatus = channel.emojiStatus { + iconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 28.0, height: 28.0), placeholderColor: self.presentationData.theme.list.mediaPlaceholderColor, themeColor: self.presentationData.theme.list.itemAccentColor, loopMode: .forever) + } } if let iconContent { let emojiStatusSize = self.emojiStatusView.update( diff --git a/submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent/BUILD b/submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent/BUILD new file mode 100644 index 0000000000..b98e6369ae --- /dev/null +++ b/submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent/BUILD @@ -0,0 +1,22 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PremiumLockButtonSubtitleComponent", + module_name = "PremiumLockButtonSubtitleComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent/Sources/PremiumLockButtonSubtitleComponent.swift b/submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent/Sources/PremiumLockButtonSubtitleComponent.swift new file mode 100644 index 0000000000..8e868f52f7 --- /dev/null +++ b/submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent/Sources/PremiumLockButtonSubtitleComponent.swift @@ -0,0 +1,83 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import BundleIconComponent +import AnimatedTextComponent + +public final class PremiumLockButtonSubtitleComponent: CombinedComponent { + public let count: Int + public let theme: PresentationTheme + public let strings: PresentationStrings + + public init(count: Int, theme: PresentationTheme, strings: PresentationStrings) { + self.count = count + self.theme = theme + self.strings = strings + } + + public static func ==(lhs: PremiumLockButtonSubtitleComponent, rhs: PremiumLockButtonSubtitleComponent) -> Bool { + if lhs.count != rhs.count { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + return true + } + + public static var body: Body { + let icon = Child(BundleIconComponent.self) + let text = Child(AnimatedTextComponent.self) + + return { context in + let icon = icon.update( + component: BundleIconComponent( + name: "Chat/Input/Accessory Panels/TextLockIcon", + tintColor: context.component.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7), + maxSize: CGSize(width: 10.0, height: 10.0) + ), + availableSize: CGSize(width: 100.0, height: 100.0), + transition: context.transition + ) + var textItems: [AnimatedTextComponent.Item] = [] + + let levelString = context.component.strings.ChannelReactions_LevelRequiredLabel("") + var previousIndex = 0 + let nsLevelString = levelString.string as NSString + for range in levelString.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) { + if range.range.lowerBound > previousIndex { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable(range.index), content: .text(nsLevelString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex))))) + } + if range.index == 0 { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable(range.index), content: .number(context.component.count, minDigits: 1))) + } + previousIndex = range.range.upperBound + } + if nsLevelString.length > previousIndex { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable(100), content: .text(nsLevelString.substring(with: NSRange(location: previousIndex, length: nsLevelString.length - previousIndex))))) + } + + let text = text.update( + component: AnimatedTextComponent(font: Font.medium(11.0), color: context.component.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7), items: textItems), + availableSize: CGSize(width: context.availableSize.width - 20.0, height: 100.0), + transition: context.transition + ) + + let spacing: CGFloat = 3.0 + let size = CGSize(width: icon.size.width + spacing + text.size.width, height: text.size.height) + context.add(icon + .position(icon.size.centered(in: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: icon.size.width, height: size.height))).center) + ) + context.add(text + .position(text.size.centered(in: CGRect(origin: CGPoint(x: icon.size.width + spacing, y: 0.0), size: text.size)).center) + ) + + return size + } + } +} diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD index dd8d1627db..778642e431 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD @@ -21,6 +21,7 @@ swift_library( "//submodules/PresentationDataUtils", "//submodules/UndoUI", "//submodules/WallpaperBackgroundNode", + "//submodules/ComponentFlow", "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/TelegramUI/Components/EntityKeyboard", "//submodules/SolidRoundedButtonNode", @@ -28,6 +29,20 @@ swift_library( "//submodules/PremiumUI", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent", "//submodules/AvatarNode", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent", + "//submodules/TelegramUI/Components/ListItemComponentAdaptor", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/Settings/ThemeCarouselItem", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/EmojiStatusSelectionComponent", + "//submodules/TelegramUI/Components/DynamicCornerRadiusView", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/WallpaperResources", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift new file mode 100644 index 0000000000..52caa07c99 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift @@ -0,0 +1,1455 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import UndoUI +import EntityKeyboard +import PremiumUI +import ComponentFlow +import BundleIconComponent +import AnimatedTextComponent +import ViewControllerComponent +import ButtonComponent +import PremiumLockButtonSubtitleComponent +import ListItemComponentAdaptor +import ListSectionComponent +import MultilineTextComponent +import ThemeCarouselItem +import ListActionItemComponent +import EmojiStatusSelectionComponent +import EmojiStatusComponent +import DynamicCornerRadiusView +import ComponentDisplayAdapters +import WallpaperResources + +private final class EmojiActionIconComponent: Component { + let context: AccountContext + let color: UIColor + let fileId: Int64? + let file: TelegramMediaFile? + + init( + context: AccountContext, + color: UIColor, + fileId: Int64?, + file: TelegramMediaFile? + ) { + self.context = context + self.color = color + self.fileId = fileId + self.file = file + } + + static func ==(lhs: EmojiActionIconComponent, rhs: EmojiActionIconComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.color != rhs.color { + return false + } + if lhs.fileId != rhs.fileId { + return false + } + if lhs.file != rhs.file { + return false + } + return true + } + + final class View: UIView { + private var icon: ComponentView? + + func update(component: EmojiActionIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let size = CGSize(width: 24.0, height: 24.0) + + if let fileId = component.fileId { + let icon: ComponentView + if let current = self.icon { + icon = current + } else { + icon = ComponentView() + self.icon = icon + } + let _ = icon.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: .animation( + content: .customEmoji(fileId: fileId), + size: size, + placeholderColor: .lightGray, + themeColor: component.color, + loopMode: .forever + ), + isVisibleForAnimations: false, + action: nil + )), + environment: {}, + containerSize: size + ) + let iconFrame = CGRect(origin: CGPoint(), size: size) + if let iconView = icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconView.frame = iconFrame + } + } else { + if let icon = self.icon { + self.icon = nil + icon.view?.removeFromSuperview() + } + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public func generateDisclosureActionBoostLevelBadgeImage(text: String) -> UIImage { + let attributedText = NSAttributedString(string: text, attributes: [ + .font: Font.medium(12.0), + .foregroundColor: UIColor.white + ]) + let bounds = attributedText.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + let leftInset: CGFloat = 16.0 + let rightInset: CGFloat = 4.0 + let size = CGSize(width: leftInset + rightInset + ceil(bounds.width), height: 20.0) + return generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 6.0).cgPath) + context.clip() + + var locations: [CGFloat] = [0.0, 1.0] + let colors: [CGColor] = [UIColor(rgb: 0x9076FF).cgColor, UIColor(rgb: 0xB86DEA).cgColor] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) + + context.resetClip() + + UIGraphicsPushContext(context) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white) { + let imageFit: CGFloat = 14.0 + let imageSize = image.size.aspectFitted(CGSize(width: imageFit, height: imageFit)) + let imageRect = CGRect(origin: CGPoint(x: 2.0, y: UIScreenPixel + floorToScreenPixels((size.height - imageSize.height) * 0.5)), size: imageSize) + image.draw(in: imageRect) + } + + attributedText.draw(at: CGPoint(x: leftInset, y: floorToScreenPixels((size.height - bounds.height) * 0.5))) + + UIGraphicsPopContext() + })! +} + +private final class BoostLevelIconComponent: Component { + let strings: PresentationStrings + let level: Int + + init( + strings: PresentationStrings, + level: Int + ) { + self.strings = strings + self.level = level + } + + static func ==(lhs: BoostLevelIconComponent, rhs: BoostLevelIconComponent) -> Bool { + if lhs.strings !== rhs.strings { + return false + } + if lhs.level != rhs.level { + return false + } + return true + } + + final class View: UIView { + private let imageView: UIImageView + + private var component: BoostLevelIconComponent? + + override init(frame: CGRect) { + self.imageView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.imageView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BoostLevelIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if self.component != component { + //TODO:localize + self.imageView.image = generateDisclosureActionBoostLevelBadgeImage(text: "Level \(component.level)") + } + self.component = component + + if let image = self.imageView.image { + self.imageView.frame = CGRect(origin: CGPoint(), size: image.size) + return image.size + } else { + return CGSize(width: 1.0, height: 20.0) + } + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class ChannelAppearanceScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let peerId: EnginePeer.Id + + init( + context: AccountContext, + peerId: EnginePeer.Id + ) { + self.context = context + self.peerId = peerId + } + + static func ==(lhs: ChannelAppearanceScreenComponent, rhs: ChannelAppearanceScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peerId != rhs.peerId { + return false + } + + return true + } + + private final class ContentsData { + let peer: EnginePeer? + let subscriberCount: Int? + let availableThemes: [TelegramTheme] + + init(peer: EnginePeer?, subscriberCount: Int?, availableThemes: [TelegramTheme]) { + self.peer = peer + self.subscriberCount = subscriberCount + self.availableThemes = availableThemes + } + + static func get(context: AccountContext, peerId: EnginePeer.Id) -> Signal { + return combineLatest( + context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), + TelegramEngine.EngineData.Item.Peer.ParticipantCount(id: peerId) + ), + telegramThemes(postbox: context.account.postbox, network: context.account.network, accountManager: context.sharedContext.accountManager) + ) + |> map { peerData, cloudThemes -> ContentsData in + let (peer, subscriberCount) = peerData + return ContentsData( + peer: peer, + subscriberCount: subscriberCount, + availableThemes: cloudThemes + ) + } + } + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + private struct ResolvedState { + var nameColor: PeerNameColor + var profileColor: PeerNameColor? + var replyFileId: Int64? + var backgroundFileId: Int64? + var emojiStatus: PeerEmojiStatus? + + var hasChanges: Bool + + init(nameColor: PeerNameColor, profileColor: PeerNameColor?, replyFileId: Int64?, backgroundFileId: Int64?, emojiStatus: PeerEmojiStatus?, hasChanges: Bool) { + self.nameColor = nameColor + self.profileColor = profileColor + self.replyFileId = replyFileId + self.backgroundFileId = backgroundFileId + self.emojiStatus = emojiStatus + self.hasChanges = hasChanges + } + } + + final class View: UIView, UIScrollViewDelegate { + private let scrollView: ScrollView + private let actionButton = ComponentView() + private let bottomPanelBackgroundView: BlurredBackgroundView + private let bottomPanelSeparator: SimpleLayer + + private let replySection = ComponentView() + private let wallpaperSection = ComponentView() + private let bannerSection = ComponentView() + private let resetColorSection = ComponentView() + private let emojiStatusSection = ComponentView() + + private var chatPreviewItemNode: PeerNameColorChatPreviewItemNode? + + private var isUpdating: Bool = false + + private var component: ChannelAppearanceScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + let isReady = ValuePromise(false, ignoreRepeated: true) + private var contentsData: ContentsData? + private var contentsDataDisposable: Disposable? + + private var cachedIconFiles: [Int64: TelegramMediaFile] = [:] + + private var updatedPeerNameColor: PeerNameColor? + private var updatedPeerNameEmoji: Int64?? + private var updatedPeerProfileColor: PeerNameColor?? + private var updatedPeerProfileEmoji: Int64?? + private var updatedPeerStatus: PeerEmojiStatus?? + + private var requiredLevel: Int? + + private var currentTheme: PresentationThemeReference? + private var resolvedCurrentTheme: (reference: PresentationThemeReference, isDark: Bool, theme: PresentationTheme, wallpaper: TelegramWallpaper?)? + private var resolvingCurrentTheme: (reference: PresentationThemeReference, isDark: Bool, disposable: Disposable)? + + private var boostLevel: Int? + private var boostStatus: ChannelBoostStatus? + private var boostStatusDisposable: Disposable? + + private var isApplyingSettings: Bool = false + private var applyDisposable: Disposable? + + private weak var emojiStatusSelectionController: ViewController? + private weak var currentUndoController: UndoOverlayController? + + override init(frame: CGRect) { + self.scrollView = ScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.alwaysBounceVertical = true + + self.bottomPanelBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.bottomPanelSeparator = SimpleLayer() + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.addSubview(self.bottomPanelBackgroundView) + self.layer.addSublayer(self.bottomPanelSeparator) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.contentsDataDisposable?.dispose() + self.applyDisposable?.dispose() + self.boostStatusDisposable?.dispose() + self.resolvingCurrentTheme?.disposable.dispose() + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + return true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrolling(transition: .immediate) + } + + private func updateScrolling(transition: Transition) { + let navigationAlphaDistance: CGFloat = 16.0 + let navigationAlpha: CGFloat = max(0.0, min(1.0, self.scrollView.contentOffset.y / navigationAlphaDistance)) + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) + transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) + } + + let bottomNavigationAlphaDistance: CGFloat = 16.0 + let bottomNavigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentSize.height - self.scrollView.bounds.maxY) / bottomNavigationAlphaDistance)) + + transition.setAlpha(view: self.bottomPanelBackgroundView, alpha: bottomNavigationAlpha) + transition.setAlpha(layer: self.bottomPanelSeparator, alpha: bottomNavigationAlpha) + } + + private func resolveState() -> ResolvedState? { + guard let contentsData = self.contentsData, let peer = contentsData.peer else { + return nil + } + + var hasChanges = false + + let nameColor: PeerNameColor + if let updatedPeerNameColor = self.updatedPeerNameColor { + nameColor = updatedPeerNameColor + } else if let peerNameColor = peer.nameColor { + nameColor = peerNameColor + } else { + nameColor = .blue + } + if nameColor != peer.nameColor { + hasChanges = true + } + + let profileColor: PeerNameColor? + if case let .some(value) = self.updatedPeerProfileColor { + profileColor = value + } else if let peerProfileColor = peer.profileColor { + profileColor = peerProfileColor + } else { + profileColor = nil + } + if profileColor != peer.profileColor { + hasChanges = true + } + + let replyFileId: Int64? + if case let .some(value) = self.updatedPeerNameEmoji { + replyFileId = value + } else { + replyFileId = peer.backgroundEmojiId + } + if replyFileId != peer.backgroundEmojiId { + hasChanges = true + } + + let backgroundFileId: Int64? + if case let .some(value) = self.updatedPeerProfileEmoji { + backgroundFileId = value + } else { + backgroundFileId = peer.profileBackgroundEmojiId + } + if backgroundFileId != peer.profileBackgroundEmojiId { + hasChanges = true + } + + let emojiStatus: PeerEmojiStatus? + if case let .some(value) = self.updatedPeerStatus { + emojiStatus = value + } else { + emojiStatus = peer.emojiStatus + } + if emojiStatus != peer.emojiStatus { + hasChanges = true + } + + return ResolvedState( + nameColor: nameColor, + profileColor: profileColor, + replyFileId: replyFileId, + backgroundFileId: backgroundFileId, + emojiStatus: emojiStatus, + hasChanges: hasChanges + ) + } + + private func applySettings() { + guard let component = self.component, let resolvedState = self.resolveState(), let requiredLevel = self.requiredLevel else { + return + } + if self.isApplyingSettings { + return + } + + if let boostLevel = self.boostLevel, requiredLevel > boostLevel { + self.displayPremiumScreen(requiredLevel: requiredLevel) + return + } + + if !resolvedState.hasChanges { + self.environment?.controller()?.dismiss() + return + } + + self.isApplyingSettings = true + self.state?.updated(transition: .immediate) + + self.applyDisposable?.dispose() + + let statusFileId = resolvedState.emojiStatus?.fileId + + enum ApplyError { + case generic + } + + self.applyDisposable = (combineLatest([ + component.context.engine.peers.updatePeerNameColorAndEmoji(peerId: component.peerId, nameColor: resolvedState.nameColor, backgroundEmojiId: resolvedState.replyFileId, profileColor: resolvedState.profileColor, profileBackgroundEmojiId: resolvedState.backgroundFileId) + |> ignoreValues + |> mapError { _ -> ApplyError in + return .generic + }, + component.context.engine.peers.updatePeerEmojiStatus(peerId: component.peerId, fileId: statusFileId, expirationDate: nil) + |> mapError { _ -> ApplyError in + return .generic + } + ]) + |> deliverOnMainQueue).start(error: { [weak self] _ in + guard let self, let component = self.component else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + self.isApplyingSettings = false + self.state?.updated(transition: .immediate) + }, completed: { [weak self] in + guard let self else { + return + } + self.environment?.controller()?.dismiss() + }) + } + + private func displayPremiumScreen(requiredLevel: Int) { + guard let component = self.component else { + return + } + + let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId)) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in + guard let self, let component = self.component, let peer, let status = self.boostStatus else { + return + } + + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) + + let link = status.url + let controller = PremiumLimitScreen(context: component.context, subject: .storiesChannelBoost(peer: peer, boostSubject: .channelReactions(reactionCount: requiredLevel), isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, myBoostCount: 0, canBoostAgain: false), count: Int32(status.boosts), action: { [weak self] in + guard let self, let component = self.component else { + return true + } + + UIPasteboard.general.string = link + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current) + return true + }, openStats: { [weak self] in + guard let self else { + return + } + self.openBoostStats() + }, openGift: premiumConfiguration.giveawayGiftsPurchaseAvailable ? { [weak self] in + guard let self, let component = self.component else { + return + } + let controller = createGiveawayController(context: component.context, peerId: component.peerId, subject: .generic) + self.environment?.controller()?.push(controller) + } : nil) + self.environment?.controller()?.push(controller) + + HapticFeedback().impact(.light) + }) + } + + private func openBoostStats() { + guard let component = self.component, let boostStatus = self.boostStatus else { + return + } + let statsController = component.context.sharedContext.makeChannelStatsController(context: component.context, updatedPresentationData: nil, peerId: component.peerId, boosts: true, boostStatus: boostStatus) + self.environment?.controller()?.push(statsController) + } + + private enum EmojiSetupSubject { + case reply + case profile + case status + } + + private var previousEmojiSetupTimestamp: Double? + private func openEmojiSetup(sourceView: UIView, currentFileId: Int64?, color: UIColor?, subject: EmojiSetupSubject) { + guard let component = self.component, let environment = self.environment else { + return + } + + let currentTimestamp = CACurrentMediaTime() + if let previousTimestamp = self.previousEmojiSetupTimestamp, currentTimestamp < previousTimestamp + 1.0 { + return + } + self.previousEmojiSetupTimestamp = currentTimestamp + + self.emojiStatusSelectionController?.dismiss() + var selectedItems = Set() + if let currentFileId { + selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: currentFileId)) + } + + let mappedSubject: EmojiPagerContentComponent.Subject + switch subject { + case .reply, .profile: + mappedSubject = .backgroundIcon + case .status: + mappedSubject = .channelStatus + } + + let mappedMode: EmojiStatusSelectionController.Mode + switch subject { + case .status: + mappedMode = .customStatusSelection(completion: { [weak self] result, timestamp in + guard let self else { + return + } + if let result { + self.cachedIconFiles[result.fileId.id] = result + } + switch subject { + case .status: + self.updatedPeerStatus = (result?.fileId.id).flatMap { PeerEmojiStatus(fileId: $0, expirationDate: timestamp) } + default: + break + } + self.state?.updated(transition: .spring(duration: 0.4)) + }) + default: + mappedMode = .backgroundSelection(completion: { [weak self] result in + guard let self else { + return + } + if let result { + self.cachedIconFiles[result.fileId.id] = result + } + switch subject { + case .reply: + self.updatedPeerNameEmoji = (result?.fileId.id) + case .profile: + self.updatedPeerProfileEmoji = (result?.fileId.id) + case .status: + self.updatedPeerStatus = (result?.fileId.id).flatMap { PeerEmojiStatus(fileId: $0, expirationDate: nil) } + } + self.state?.updated(transition: .spring(duration: 0.4)) + }) + } + + let controller = EmojiStatusSelectionController( + context: component.context, + mode: mappedMode, + sourceView: sourceView, + emojiContent: EmojiPagerContentComponent.emojiInputData( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + isStandalone: false, + subject: mappedSubject, + hasTrending: false, + topReactionItems: [], + areUnicodeEmojiEnabled: false, + areCustomEmojiEnabled: true, + chatPeerId: component.context.account.peerId, + selectedItems: selectedItems, + topStatusTitle: nil, + backgroundIconColor: color + ), + currentSelection: currentFileId, + color: color, + destinationItemView: { [weak sourceView] in + guard let sourceView else { + return nil + } + return sourceView + } + ) + self.emojiStatusSelectionController = controller + environment.controller()?.present(controller, in: .window(.root)) + } + + func update(component: ChannelAppearanceScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + self.component = component + self.state = state + + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + } + + if self.contentsDataDisposable == nil { + self.contentsDataDisposable = (ContentsData.get(context: component.context, peerId: component.peerId) + |> deliverOnMainQueue).start(next: { [weak self] contentsData in + guard let self else { + return + } + if self.contentsData == nil, case let .channel(channel) = contentsData.peer { + self.boostLevel = channel.approximateBoostLevel.flatMap(Int.init) + } + self.contentsData = contentsData + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + self.isReady.set(true) + }) + } + if self.boostStatusDisposable == nil { + self.boostStatusDisposable = (component.context.engine.peers.getChannelBoostStatus(peerId: component.peerId) + |> deliverOnMainQueue).start(next: { [weak self] boostStatus in + guard let self else { + return + } + self.boostLevel = boostStatus?.level + self.boostStatus = boostStatus + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + }) + } + + guard let contentsData = self.contentsData, var peer = contentsData.peer, let resolvedState = self.resolveState() else { + return availableSize + } + + var requiredLevel = 1 + + let replyIconLevel = 5 + var profileIconLevel = 7 + var emojiStatusLevel = 8 + let themeLevel = 9 + + if let data = component.context.currentAppConfiguration.with({ $0 }).data { + if let value = data["channel_profile_color_level_min"] as? Double { + profileIconLevel = Int(value) + } + if let value = data["channel_emoji_status_level_min"] as? Double { + emojiStatusLevel = Int(value) + } + } + + let profileColor = resolvedState.profileColor + + let replyFileId = resolvedState.replyFileId + if replyFileId != nil { + requiredLevel = max(requiredLevel, replyIconLevel) + } + + let backgroundFileId = resolvedState.backgroundFileId + if profileColor != nil || backgroundFileId != nil { + requiredLevel = max(requiredLevel, profileIconLevel) + } + + let emojiStatus = resolvedState.emojiStatus + + if emojiStatus != nil { + requiredLevel = max(requiredLevel, emojiStatusLevel) + } + let statusFileId = emojiStatus?.fileId + + let cloudThemes: [PresentationThemeReference] = contentsData.availableThemes.map { .cloud(PresentationCloudTheme(theme: $0, resolvedWallpaper: nil, creatorAccountId: $0.isCreator ? component.context.account.id : nil)) } + var chatThemes = cloudThemes.filter { $0.emoticon != nil } + chatThemes.insert(.builtin(.dayClassic), at: 0) + + if !chatThemes.isEmpty { + if self.currentTheme == nil { + self.currentTheme = chatThemes[0] + } + } + + if let currentTheme = self.currentTheme, (self.resolvedCurrentTheme?.reference != currentTheme || self.resolvedCurrentTheme?.isDark != environment.theme.overallDarkAppearance), (self.resolvingCurrentTheme?.reference != currentTheme || self.resolvingCurrentTheme?.isDark != environment.theme.overallDarkAppearance) { + self.resolvingCurrentTheme?.disposable.dispose() + + let disposable = MetaDisposable() + self.resolvingCurrentTheme = (currentTheme, environment.theme.overallDarkAppearance, disposable) + + var presentationTheme: PresentationTheme? + switch currentTheme { + case .builtin: + presentationTheme = makePresentationTheme(mediaBox: component.context.sharedContext.accountManager.mediaBox, themeReference: .builtin(environment.theme.overallDarkAppearance ? .night : .dayClassic)) + case let .cloud(cloudTheme): + presentationTheme = makePresentationTheme(cloudTheme: cloudTheme.theme, dark: environment.theme.overallDarkAppearance) + default: + presentationTheme = makePresentationTheme(mediaBox: component.context.sharedContext.accountManager.mediaBox, themeReference: currentTheme) + } + if let presentationTheme { + let resolvedWallpaper: Signal + if case let .file(file) = presentationTheme.chat.defaultWallpaper, file.id == 0 { + resolvedWallpaper = cachedWallpaper(account: component.context.account, slug: file.slug, settings: file.settings) + |> map { wallpaper -> TelegramWallpaper? in + return wallpaper?.wallpaper + } + } else { + resolvedWallpaper = .single(presentationTheme.chat.defaultWallpaper) + } + disposable.set((resolvedWallpaper + |> deliverOnMainQueue).startStrict(next: { [weak self] resolvedWallpaper in + guard let self, let environment = self.environment else { + return + } + self.resolvedCurrentTheme = (currentTheme, environment.theme.overallDarkAppearance, presentationTheme, resolvedWallpaper) + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + })) + } + } + + if self.currentTheme != nil && self.currentTheme != chatThemes.first { + requiredLevel = max(requiredLevel, themeLevel) + } + + if case let .user(user) = peer { + peer = .user(user + .withUpdatedNameColor(resolvedState.nameColor) + .withUpdatedProfileColor(profileColor) + .withUpdatedEmojiStatus(emojiStatus) + .withUpdatedBackgroundEmojiId(replyFileId) + .withUpdatedProfileBackgroundEmojiId(backgroundFileId) + ) + } else if case let .channel(channel) = peer { + peer = .channel(channel + .withUpdatedNameColor(resolvedState.nameColor) + .withUpdatedProfileColor(profileColor) + .withUpdatedEmojiStatus(emojiStatus) + .withUpdatedBackgroundEmojiId(replyFileId) + .withUpdatedProfileBackgroundEmojiId(backgroundFileId) + ) + } + + self.requiredLevel = requiredLevel + + let topInset: CGFloat = 24.0 + let bottomContentInset: CGFloat = 24.0 + let bottomInset: CGFloat = 8.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let sectionSpacing: CGFloat = 32.0 + + let listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) + + var contentHeight: CGFloat = 0.0 + contentHeight += environment.navigationHeight + contentHeight += topInset + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + //TODO:localize + let messageItem = PeerNameColorChatPreviewItem.MessageItem( + outgoing: false, + peerId: EnginePeer.Id(namespace: peer.id.namespace, id: PeerId.Id._internalFromInt64Value(0)), + author: peer.compactDisplayTitle, + photo: peer.profileImageRepresentations, + nameColor: resolvedState.nameColor, + backgroundEmojiId: replyFileId, + reply: (peer.compactDisplayTitle, "Reply to your channel"), + linkPreview: ("Telegram", "Link Preview", "This preview will also be tinted."), + text: "The color you select will be used for the channel's name" + ) + + var replyLogoContents: [AnyComponentWithIdentity] = [] + replyLogoContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Replies Logo", //TODO:localize + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + )))) + if replyFileId != nil, let boostLevel = self.boostLevel, boostLevel < replyIconLevel { + replyLogoContents.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(BoostLevelIconComponent( + strings: environment.strings, + level: replyIconLevel + )))) + } + + var chatPreviewTheme: PresentationTheme = environment.theme + var chatPreviewWallpaper: TelegramWallpaper = presentationData.chatWallpaper + if let resolvedCurrentTheme = self.resolvedCurrentTheme { + chatPreviewTheme = resolvedCurrentTheme.theme + if let wallpaper = resolvedCurrentTheme.wallpaper { + chatPreviewWallpaper = wallpaper + } + } + + let replySectionSize = self.replySection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Choose a color for the name of your channel, the link it sends, and replies to its messages.", //TODO:localize + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: PeerNameColorChatPreviewItem( + context: component.context, + theme: chatPreviewTheme, + componentTheme: chatPreviewTheme, + strings: environment.strings, + sectionId: 0, + fontSize: presentationData.chatFontSize, + chatBubbleCorners: presentationData.chatBubbleCorners, + wallpaper: chatPreviewWallpaper, + dateTimeFormat: environment.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + messageItems: [messageItem] + ), + params: listItemParams + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: PeerNameColorItem( + theme: environment.theme, + colors: component.context.peerNameColors, + isProfile: false, + currentColor: resolvedState.nameColor, + updated: { [weak self] value in + guard let self else { + return + } + self.updatedPeerNameColor = value + self.state?.updated(transition: .spring(duration: 0.4)) + }, + sectionId: 0 + ), + params: listItemParams + ))), + AnyComponentWithIdentity(id: 2, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(HStack(replyLogoContents, spacing: 6.0)), + icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( + context: component.context, + color: component.context.peerNameColors.get(resolvedState.nameColor, dark: environment.theme.overallDarkAppearance).main, + fileId: replyFileId, + file: replyFileId.flatMap { self.cachedIconFiles[$0] } + ))), + action: { [weak self] view in + guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { + return + } + + self.openEmojiSetup(sourceView: iconView, currentFileId: resolvedState.replyFileId, color: component.context.peerNameColors.get(resolvedState.nameColor, dark: environment.theme.overallDarkAppearance).main, subject: .reply) + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let replySectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: replySectionSize) + if let replySectionView = self.replySection.view { + if replySectionView.superview == nil { + self.scrollView.addSubview(replySectionView) + } + transition.setFrame(view: replySectionView, frame: replySectionFrame) + } + contentHeight += replySectionSize.height + + contentHeight += sectionSpacing + + if !chatThemes.isEmpty, let currentTheme { + var wallpaperLogoContents: [AnyComponentWithIdentity] = [] + wallpaperLogoContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Channel Wallpaper", //TODO:localize + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + )))) + if currentTheme != chatThemes[0], let boostLevel = self.boostLevel, boostLevel < themeLevel { + wallpaperLogoContents.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(BoostLevelIconComponent( + strings: environment.strings, + level: themeLevel + )))) + } + + let wallpaperSectionSize = self.wallpaperSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Set a wallpaper that will be visible to everyone reading your channel.", //TODO:localize + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: ThemeCarouselThemeItem( + context: component.context, + theme: environment.theme, + strings: environment.strings, + sectionId: 0, + themes: chatThemes, + animatedEmojiStickers: component.context.animatedEmojiStickers, + themeSpecificAccentColors: [:], + themeSpecificChatWallpapers: [:], + nightMode: environment.theme.overallDarkAppearance, + channelMode: true, + currentTheme: currentTheme, + updatedTheme: { [weak self] value in + guard let self else { + return + } + self.currentTheme = value + self.state?.updated(transition: .spring(duration: 0.4)) + }, + contextAction: nil + ), + params: listItemParams + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(HStack(wallpaperLogoContents, spacing: 6.0)), + icon: nil, + action: { [weak self] view in + guard let self else { + return + } + let _ = self + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let wallpaperSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: wallpaperSectionSize) + if let wallpaperSectionView = self.wallpaperSection.view { + if wallpaperSectionView.superview == nil { + self.scrollView.addSubview(wallpaperSectionView) + } + transition.setFrame(view: wallpaperSectionView, frame: wallpaperSectionFrame) + } + contentHeight += wallpaperSectionSize.height + contentHeight += sectionSpacing + } + + var profileLogoContents: [AnyComponentWithIdentity] = [] + profileLogoContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Profile Logo", //TODO:localize + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + )))) + if backgroundFileId != nil, let boostLevel = self.boostLevel, boostLevel < profileIconLevel { + profileLogoContents.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(BoostLevelIconComponent( + strings: environment.strings, + level: profileIconLevel + )))) + } + + let bannerBackground: ListSectionComponent.Background + if profileColor != nil { + bannerBackground = .range(from: 1, corners: DynamicCornerRadiusView.Corners(minXMinY: 0.0, maxXMinY: 0.0, minXMaxY: 11.0, maxXMaxY: 11.0)) + } else { + bannerBackground = .range(from: 1, corners: DynamicCornerRadiusView.Corners(minXMinY: 11.0, maxXMinY: 11.0, minXMaxY: 11.0, maxXMaxY: 11.0)) + } + let bannerSectionSize = self.bannerSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + background: bannerBackground, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "PROFILE PAGE COLOR", //TODO:localize + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Choose a color and a logo for the channel's profile.", //TODO:localize + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: PeerNameColorProfilePreviewItem( + context: component.context, + theme: environment.theme, + componentTheme: environment.theme, + strings: environment.strings, + sectionId: 0, + peer: peer, + subtitleString: contentsData.subscriberCount.flatMap { environment.strings.Conversation_StatusSubscribers(Int32($0)) }, + files: self.cachedIconFiles, + nameDisplayOrder: presentationData.nameDisplayOrder + ), + params: listItemParams + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: PeerNameColorItem( + theme: environment.theme, + colors: component.context.peerNameColors, + isProfile: true, + currentColor: profileColor, + updated: { [weak self] value in + guard let self else { + return + } + self.updatedPeerProfileColor = value + self.state?.updated(transition: .spring(duration: 0.4)) + }, + sectionId: 0 + ), + params: listItemParams + ))), + AnyComponentWithIdentity(id: 2, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(HStack(profileLogoContents, spacing: 6.0)), + icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( + context: component.context, + color: profileColor.flatMap { profileColor in + component.context.peerNameColors.getProfile(profileColor, dark: environment.theme.overallDarkAppearance, subject: .palette).main + } ?? environment.theme.list.itemAccentColor, + fileId: backgroundFileId, + file: backgroundFileId.flatMap { self.cachedIconFiles[$0] } + ))), + action: { [weak self] view in + guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { + return + } + + self.openEmojiSetup(sourceView: iconView, currentFileId: resolvedState.backgroundFileId, color: resolvedState.profileColor.flatMap { + component.context.peerNameColors.getProfile($0, dark: environment.theme.overallDarkAppearance, subject: .palette).main + } ?? environment.theme.list.itemAccentColor, subject: .profile) + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let bannerSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: bannerSectionSize) + if let bannerSectionView = self.bannerSection.view { + if bannerSectionView.superview == nil { + self.scrollView.addSubview(bannerSectionView) + } + transition.setFrame(view: bannerSectionView, frame: bannerSectionFrame) + } + contentHeight += bannerSectionSize.height + contentHeight += sectionSpacing + + var emojiStatusContents: [AnyComponentWithIdentity] = [] + emojiStatusContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Channel Emoji Status", //TODO:localize + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + )))) + if emojiStatus != nil, let boostLevel = self.boostLevel, boostLevel < emojiStatusLevel { + emojiStatusContents.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(BoostLevelIconComponent( + strings: environment.strings, + level: emojiStatusLevel + )))) + } + + let resetColorSectionSize = self.resetColorSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Reset Profile Color", //TODO:localize + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemAccentColor + )), + maximumNumberOfLines: 0 + )), + icon: nil, + hasArrow: false, + action: { [weak self] view in + guard let self else { + return + } + + self.updatedPeerProfileColor = .some(nil) + self.updatedPeerProfileEmoji = .some(nil) + self.state?.updated(transition: .spring(duration: 0.4)) + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + + let displayResetProfileColor = profileColor != nil || backgroundFileId != nil + + let resetColorSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: resetColorSectionSize) + if let resetColorSectionView = self.resetColorSection.view { + if resetColorSectionView.superview == nil { + self.scrollView.addSubview(resetColorSectionView) + } + transition.setPosition(view: resetColorSectionView, position: resetColorSectionFrame.center) + transition.setBounds(view: resetColorSectionView, bounds: CGRect(origin: CGPoint(), size: resetColorSectionFrame.size)) + transition.setScale(view: resetColorSectionView, scale: displayResetProfileColor ? 1.0 : 0.001) + transition.setAlpha(view: resetColorSectionView, alpha: displayResetProfileColor ? 1.0 : 0.0) + } + if displayResetProfileColor { + contentHeight += resetColorSectionSize.height + contentHeight += sectionSpacing + } + + let emojiStatusSectionSize = self.emojiStatusSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Choose a status that will be shown next to the channel's name.", //TODO:localize + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(HStack(emojiStatusContents, spacing: 6.0)), + icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( + context: component.context, + color: environment.theme.list.itemAccentColor, + fileId: statusFileId, + file: statusFileId.flatMap { self.cachedIconFiles[$0] } + ))), + action: { [weak self] view in + guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { + return + } + + self.openEmojiSetup(sourceView: iconView, currentFileId: resolvedState.emojiStatus?.fileId, color: nil, subject: .status) + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let emojiStatusSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: emojiStatusSectionSize) + if let emojiStatusSectionView = self.emojiStatusSection.view { + if emojiStatusSectionView.superview == nil { + self.scrollView.addSubview(emojiStatusSectionView) + } + transition.setFrame(view: emojiStatusSectionView, frame: emojiStatusSectionFrame) + } + contentHeight += emojiStatusSectionSize.height + + contentHeight += bottomContentInset + + var buttonContents: [AnyComponentWithIdentity] = [] + //TODO:localize + buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + Text(text: "Apply Changes", font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) + ))) + + if let boostLevel = self.boostLevel, requiredLevel > boostLevel { + buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(PremiumLockButtonSubtitleComponent( + count: requiredLevel, + theme: environment.theme, + strings: environment.strings + )))) + } + + let buttonSize = self.actionButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + VStack(buttonContents, spacing: 3.0) + )), + isEnabled: true, + tintWhenDisabled: false, + displaysProgress: self.isApplyingSettings, + action: { [weak self] in + guard let self else { + return + } + self.applySettings() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + contentHeight += buttonSize.height + + contentHeight += bottomInset + contentHeight += environment.safeInsets.bottom + + let buttonY = availableSize.height - bottomInset - environment.safeInsets.bottom - buttonSize.height + + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: buttonY), size: buttonSize) + if let buttonView = self.actionButton.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: buttonFrame) + transition.setAlpha(view: buttonView, alpha: 1.0) + } + + let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: buttonY - 8.0), size: CGSize(width: availableSize.width, height: availableSize.height - buttonY + 8.0)) + transition.setFrame(view: self.bottomPanelBackgroundView, frame: bottomPanelFrame) + self.bottomPanelBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.bottomPanelBackgroundView.update(size: bottomPanelFrame.size, transition: transition.containedViewLayoutTransition) + + self.bottomPanelSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + transition.setFrame(layer: self.bottomPanelSeparator, frame: CGRect(origin: CGPoint(x: bottomPanelFrame.minX, y: bottomPanelFrame.minY), size: CGSize(width: bottomPanelFrame.width, height: UIScreenPixel))) + + let previousBounds = self.scrollView.bounds + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: availableSize.height - bottomPanelFrame.minY, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class ChannelAppearanceScreen: ViewControllerComponentContainer { + private let context: AccountContext + + private var didSetReady: Bool = false + + public init( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)?, + peerId: EnginePeer.Id + ) { + self.context = context + + super.init(context: context, component: ChannelAppearanceScreenComponent( + context: context, + peerId: peerId + ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: updatedPresentationData) + + //TODO:localize + self.title = "Appearance" + + self.ready.set(.never()) + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? ChannelAppearanceScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? ChannelAppearanceScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + if let componentView = self.node.hostView.componentView as? ChannelAppearanceScreenComponent.View { + if !self.didSetReady { + self.didSetReady = true + self.ready.set(componentView.isReady.get()) + } + } + } +} diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift index 920bbc1b46..3133579374 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift @@ -11,10 +11,11 @@ import ItemListUI import PresentationDataUtils import AccountContext import WallpaperBackgroundNode +import ListItemComponentAdaptor -final class PeerNameColorChatPreviewItem: ListViewItem, ItemListItem { +final class PeerNameColorChatPreviewItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { struct MessageItem: Equatable { - static func == (lhs: MessageItem, rhs: MessageItem) -> Bool { + static func ==(lhs: MessageItem, rhs: MessageItem) -> Bool { if lhs.outgoing != rhs.outgoing { return false } @@ -118,6 +119,44 @@ final class PeerNameColorChatPreviewItem: ListViewItem, ItemListItem { } } } + + public func item() -> ListViewItem { + return self + } + + public static func ==(lhs: PeerNameColorChatPreviewItem, rhs: PeerNameColorChatPreviewItem) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.componentTheme !== rhs.componentTheme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.fontSize != rhs.fontSize { + return false + } + if lhs.chatBubbleCorners != rhs.chatBubbleCorners { + return false + } + if lhs.wallpaper != rhs.wallpaper { + return false + } + if lhs.dateTimeFormat != rhs.dateTimeFormat { + return false + } + if lhs.nameDisplayOrder != rhs.nameDisplayOrder { + return false + } + if lhs.messageItems != rhs.messageItems { + return false + } + return true + } } final class PeerNameColorChatPreviewItemNode: ListViewItemNode { @@ -260,7 +299,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) - if let currentItem, currentItem.messageItems.first?.nameColor != item.messageItems.first?.nameColor || currentItem.messageItems.first?.backgroundEmojiId != item.messageItems.first?.backgroundEmojiId { + if let currentItem, currentItem.messageItems.first?.nameColor != item.messageItems.first?.nameColor || currentItem.messageItems.first?.backgroundEmojiId != item.messageItems.first?.backgroundEmojiId || currentItem.theme !== item.theme || currentItem.wallpaper != item.wallpaper { if let snapshot = strongSelf.view.snapshotView(afterScreenUpdates: false) { snapshot.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: snapshot.frame.size) strongSelf.view.addSubview(snapshot) @@ -329,31 +368,42 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { strongSelf.insertSubnode(strongSelf.maskNode, at: 3) } - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - switch neighbors.top { + if params.isStandalone { + strongSelf.topStripeNode.isHidden = true + strongSelf.bottomStripeNode.isHidden = true + strongSelf.maskNode.isHidden = true + } else { + let hasCorners = itemListHasRoundedBlockLayout(params) + + var hasTopCorners = false + var hasBottomCorners = false + + switch neighbors.top { case .sameSection(false): strongSelf.topStripeNode.isHidden = true default: hasTopCorners = true strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = 0.0 + bottomStripeOffset = -separatorHeight + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } - let bottomStripeInset: CGFloat - let bottomStripeOffset: CGFloat - switch neighbors.bottom { - case .sameSection(false): - bottomStripeInset = 0.0 - bottomStripeOffset = -separatorHeight - strongSelf.bottomStripeNode.isHidden = false - default: - bottomStripeInset = 0.0 - bottomStripeOffset = 0.0 - hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners - } - - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) @@ -377,8 +427,6 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { backgroundNode.updateLayout(size: backgroundNode.bounds.size, displayMode: displayMode, transition: .immediate) } strongSelf.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0) - strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } }) } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift index 25c36e87b9..8d10567b47 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift @@ -10,6 +10,7 @@ import MergeLists import ItemListUI import PresentationDataUtils import AccountContext +import ListItemComponentAdaptor private enum PeerNameColorEntryId: Hashable { case color(Int32) @@ -313,7 +314,7 @@ private final class PeerNameColorIconItemNode : ListViewItemNode { } } -final class PeerNameColorItem: ListViewItem, ItemListItem { +final class PeerNameColorItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { var sectionId: ItemListSectionId let theme: PresentationTheme @@ -365,6 +366,27 @@ final class PeerNameColorItem: ListViewItem, ItemListItem { } } } + + public func item() -> ListViewItem { + return self + } + + public static func ==(lhs: PeerNameColorItem, rhs: PeerNameColorItem) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.colors != rhs.colors { + return false + } + if lhs.isProfile != rhs.isProfile { + return false + } + if lhs.currentColor != rhs.currentColor { + return false + } + + return true + } } private struct PeerNameColorItemNodeTransition { @@ -528,24 +550,30 @@ final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { strongSelf.containerNode.insertSubnode(strongSelf.maskNode, at: 3) } - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - if item.currentColor != nil { - switch neighbors.top { - case .sameSection(false): - strongSelf.topStripeNode.isHidden = true - default: - hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners - } - } else { + if params.isStandalone { + strongSelf.backgroundNode.isHidden = true strongSelf.topStripeNode.isHidden = true - hasTopCorners = true - } - let bottomStripeInset: CGFloat - let bottomStripeOffset: CGFloat - switch neighbors.bottom { + strongSelf.bottomStripeNode.isHidden = true + strongSelf.maskNode.isHidden = true + } else { + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + if item.currentColor != nil { + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + } else { + strongSelf.topStripeNode.isHidden = true + hasTopCorners = true + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { case .sameSection(false): bottomStripeInset = params.leftInset + 16.0 bottomStripeOffset = -separatorHeight @@ -555,15 +583,17 @@ final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { bottomStripeOffset = 0.0 hasBottomCorners = true strongSelf.bottomStripeNode.isHidden = hasCorners + } + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } strongSelf.containerNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: contentSize.height) - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) var listInsets = UIEdgeInsets() listInsets.top += params.leftInset + 8.0 diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift index 9db8346dd7..32ef02063c 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift @@ -14,24 +14,28 @@ import ComponentFlow import PeerInfoCoverComponent import AvatarNode import EmojiStatusComponent +import ListItemComponentAdaptor +import ComponentDisplayAdapters -final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem { +final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { let context: AccountContext let theme: PresentationTheme let componentTheme: PresentationTheme let strings: PresentationStrings let sectionId: ItemListSectionId let peer: EnginePeer? + let subtitleString: String? let files: [Int64: TelegramMediaFile] let nameDisplayOrder: PresentationPersonNameOrder - init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, peer: EnginePeer?, files: [Int64: TelegramMediaFile], nameDisplayOrder: PresentationPersonNameOrder) { + init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, peer: EnginePeer?, subtitleString: String? = nil, files: [Int64: TelegramMediaFile], nameDisplayOrder: PresentationPersonNameOrder) { self.context = context self.theme = theme self.componentTheme = componentTheme self.strings = strings self.sectionId = sectionId self.peer = peer + self.subtitleString = subtitleString self.files = files self.nameDisplayOrder = nameDisplayOrder } @@ -46,7 +50,7 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem { Queue.mainQueue().async { completion(node, { - return (nil, { _ in apply() }) + return (nil, { _ in apply(.None) }) }) } } @@ -61,13 +65,43 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem { let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { _ in - apply() + apply(animation) }) } } } } } + + func item() -> ListViewItem { + return self + } + + static func ==(lhs: PeerNameColorProfilePreviewItem, rhs: PeerNameColorProfilePreviewItem) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.componentTheme !== rhs.componentTheme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.files != rhs.files { + return false + } + if lhs.nameDisplayOrder != rhs.nameDisplayOrder { + return false + } + + return true + } } final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { @@ -104,7 +138,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { deinit { } - func asyncLayout() -> (_ item: PeerNameColorProfilePreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: PeerNameColorProfilePreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { return { [weak self] item, params, neighbors in let separatorHeight = UIScreenPixel @@ -117,7 +151,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size - return (layout, { [weak self] in + return (layout, { [weak self] animation in guard let self else { return } @@ -139,32 +173,41 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { if self.maskNode.supernode == nil { self.addSubnode(self.maskNode) } - - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - switch neighbors.top { + + if params.isStandalone { + self.topStripeNode.isHidden = true + self.bottomStripeNode.isHidden = true + self.maskNode.isHidden = true + } else { + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { case .sameSection(false): self.topStripeNode.isHidden = true default: hasTopCorners = true self.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = 0.0 + bottomStripeOffset = -separatorHeight + self.bottomStripeNode.isHidden = item.peer?.profileColor == nil + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + self.bottomStripeNode.isHidden = hasCorners + } + + self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + self.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } - let bottomStripeInset: CGFloat - let bottomStripeOffset: CGFloat - switch neighbors.bottom { - case .sameSection(false): - bottomStripeInset = 0.0 - bottomStripeOffset = -separatorHeight - self.bottomStripeNode.isHidden = item.peer?.profileColor == nil - default: - bottomStripeInset = 0.0 - bottomStripeOffset = 0.0 - hasBottomCorners = true - self.bottomStripeNode.isHidden = hasCorners - } - - self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) @@ -217,6 +260,61 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { } self.avatarNode.frame = avatarFrame.offsetBy(dx: coverFrame.minX, dy: coverFrame.minY) + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 }) + + enum CredibilityIcon { + case none + case premium + case verified + case fake + case scam + case emojiStatus(PeerEmojiStatus) + } + + let credibilityIcon: CredibilityIcon + if let peer = item.peer { + if peer.isFake { + credibilityIcon = .fake + } else if peer.isScam { + credibilityIcon = .scam + } else if case let .user(user) = peer, let emojiStatus = user.emojiStatus, !premiumConfiguration.isPremiumDisabled { + credibilityIcon = .emojiStatus(emojiStatus) + } else if case let .channel(channel) = peer, let emojiStatus = channel.emojiStatus, !premiumConfiguration.isPremiumDisabled { + credibilityIcon = .emojiStatus(emojiStatus) + } else if peer.isVerified { + credibilityIcon = .verified + } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled && (peer.id != item.context.account.peerId) { + credibilityIcon = .premium + } else { + credibilityIcon = .none + } + } else { + credibilityIcon = .none + } + + let statusColor: UIColor + if let peer = item.peer, peer.profileColor != nil { + statusColor = .white + } else { + statusColor = item.theme.list.itemCheckColors.fillColor + } + + let emojiStatusContent: EmojiStatusComponent.Content + switch credibilityIcon { + case .none: + emojiStatusContent = .none + case .premium: + emojiStatusContent = .premium(color: statusColor) + case .verified: + emojiStatusContent = .verified(fillColor: statusColor, foregroundColor: .clear, sizeType: .large) + case .fake: + emojiStatusContent = .text(color: item.theme.chat.message.incoming.scamColor, string: item.strings.Message_FakeAccount.uppercased()) + case .scam: + emojiStatusContent = .text(color: item.theme.chat.message.incoming.scamColor, string: item.strings.Message_ScamAccount.uppercased()) + case let .emojiStatus(emojiStatus): + emojiStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: item.theme.list.mediaPlaceholderColor, themeColor: statusColor, loopMode: .forever) + } + let backgroundColor: UIColor let titleColor: UIColor let subtitleColor: UIColor @@ -239,15 +337,64 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { environment: {}, containerSize: CGSize(width: coverFrame.width - 16.0, height: 100.0) ) - let titleFrame = CGRect(origin: CGPoint(x: coverFrame.minX + floor((coverFrame.width - titleSize.width) * 0.5), y: avatarFrame.maxY + 10.0), size: titleSize) + + var titleContentWidth = titleSize.width + var hasStatusIcon = false + if case .none = emojiStatusContent { + } else { + hasStatusIcon = true + titleContentWidth += 4.0 + 34.0 + } + let _ = hasStatusIcon + + let titleFrame = CGRect(origin: CGPoint(x: coverFrame.minX + floor((coverFrame.width - titleContentWidth) * 0.5), y: avatarFrame.maxY + 10.0), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { + titleView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.0) self.view.addSubview(titleView) } - titleView.frame = titleFrame + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + animation.animator.updatePosition(layer: titleView.layer, position: titleFrame.origin, completion: nil) } - let subtitleString: String = item.strings.LastSeen_JustNow + let icon: ComponentView + if let current = self.icon { + icon = current + } else { + icon = ComponentView() + self.icon = icon + } + let iconSize = CGSize(width: 34.0, height: 34.0) + let _ = icon.update( + transition: Transition(animation.transition), + component: AnyComponent(EmojiStatusComponent( + context: item.context, + animationCache: item.context.animationCache, + animationRenderer: item.context.animationRenderer, + content: emojiStatusContent, + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: iconSize + ) + if let iconView = icon.view { + if iconView.superview == nil { + self.view.addSubview(iconView) + } + let iconFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - iconSize.height) * 0.5)), size: iconSize) + iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + animation.animator.updatePosition(layer: iconView.layer, position: iconFrame.center, completion: nil) + } + + let subtitleString: String + if let value = item.subtitleString { + subtitleString = value + } else if case .channel = item.peer { + subtitleString = item.strings.Channel_Status + } else { + subtitleString = item.strings.LastSeen_JustNow + } let subtitleSize = self.subtitle.update( transition: .immediate, component: AnyComponent(Text( @@ -265,8 +412,6 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { } self.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0) - self.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) - self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) }) } } diff --git a/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/BUILD b/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/BUILD new file mode 100644 index 0000000000..7e7c767224 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/BUILD @@ -0,0 +1,37 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ThemeCarouselItem", + module_name = "ThemeCarouselItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/MergeLists", + "//submodules/TelegramUIPreferences", + "//submodules/ItemListUI", + "//submodules/PresentationDataUtils", + "//submodules/WallpaperResources", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/ContextUI", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", + "//submodules/ShimmerEffect", + "//submodules/StickerResources", + "//submodules/TelegramUI/Components/ListItemComponentAdaptor", + "//submodules/HexColor", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/SettingsUI/Sources/ThemeCarouselItem.swift b/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/Sources/ThemeCarouselItem.swift similarity index 82% rename from submodules/SettingsUI/Sources/ThemeCarouselItem.swift rename to submodules/TelegramUI/Components/Settings/ThemeCarouselItem/Sources/ThemeCarouselItem.swift index 7744e5a771..2b0dc7d7f4 100644 --- a/submodules/SettingsUI/Sources/ThemeCarouselItem.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/Sources/ThemeCarouselItem.swift @@ -18,12 +18,15 @@ import AnimatedStickerNode import TelegramAnimatedStickerNode import ShimmerEffect import StickerResources +import ListItemComponentAdaptor +import HexColor private struct ThemeCarouselThemeEntry: Comparable, Identifiable { let index: Int let emojiFile: TelegramMediaFile? let themeReference: PresentationThemeReference let nightMode: Bool + let channelMode: Bool let themeSpecificAccentColors: [Int64: PresentationThemeAccentColor] let themeSpecificChatWallpapers: [Int64: TelegramWallpaper] var selected: Bool @@ -48,6 +51,9 @@ private struct ThemeCarouselThemeEntry: Comparable, Identifiable { if lhs.nightMode != rhs.nightMode { return false } + if lhs.channelMode != rhs.channelMode { + return false + } if lhs.themeSpecificAccentColors != rhs.themeSpecificAccentColors { return false } @@ -74,30 +80,32 @@ private struct ThemeCarouselThemeEntry: Comparable, Identifiable { } func item(context: AccountContext, action: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem { - return ThemeCarouselThemeIconItem(context: context, emojiFile: self.emojiFile, themeReference: self.themeReference, nightMode: self.nightMode, themeSpecificAccentColors: self.themeSpecificAccentColors, themeSpecificChatWallpapers: self.themeSpecificChatWallpapers, selected: self.selected, theme: self.theme, strings: self.strings, wallpaper: self.wallpaper, action: action, contextAction: contextAction) + return ThemeCarouselThemeIconItem(context: context, emojiFile: self.emojiFile, themeReference: self.themeReference, nightMode: self.nightMode, channelMode: self.channelMode, themeSpecificAccentColors: self.themeSpecificAccentColors, themeSpecificChatWallpapers: self.themeSpecificChatWallpapers, selected: self.selected, theme: self.theme, strings: self.strings, wallpaper: self.wallpaper, action: action, contextAction: contextAction) } } -class ThemeCarouselThemeIconItem: ListViewItem { - let context: AccountContext - let emojiFile: TelegramMediaFile? - let themeReference: PresentationThemeReference - let nightMode: Bool - let themeSpecificAccentColors: [Int64: PresentationThemeAccentColor] - let themeSpecificChatWallpapers: [Int64: TelegramWallpaper] - let selected: Bool - let theme: PresentationTheme - let strings: PresentationStrings - let wallpaper: TelegramWallpaper? - let action: (PresentationThemeReference) -> Void - let contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)? +public class ThemeCarouselThemeIconItem: ListViewItem { + public let context: AccountContext + public let emojiFile: TelegramMediaFile? + public let themeReference: PresentationThemeReference + public let nightMode: Bool + public let channelMode: Bool + public let themeSpecificAccentColors: [Int64: PresentationThemeAccentColor] + public let themeSpecificChatWallpapers: [Int64: TelegramWallpaper] + public let selected: Bool + public let theme: PresentationTheme + public let strings: PresentationStrings + public let wallpaper: TelegramWallpaper? + public let action: (PresentationThemeReference) -> Void + public let contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)? - public init(context: AccountContext, emojiFile: TelegramMediaFile?, themeReference: PresentationThemeReference, nightMode: Bool, themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], selected: Bool, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper?, action: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?) { + public init(context: AccountContext, emojiFile: TelegramMediaFile?, themeReference: PresentationThemeReference, nightMode: Bool, channelMode: Bool, themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], selected: Bool, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper?, action: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?) { self.context = context self.emojiFile = emojiFile self.themeReference = themeReference self.nightMode = nightMode + self.channelMode = channelMode self.themeSpecificAccentColors = themeSpecificAccentColors self.themeSpecificChatWallpapers = themeSpecificChatWallpapers self.selected = selected @@ -148,7 +156,6 @@ class ThemeCarouselThemeIconItem: ListViewItem { } } - private let textFont = Font.regular(12.0) private let selectedTextFont = Font.bold(12.0) @@ -338,6 +345,7 @@ private final class ThemeCarouselThemeItemIconNode : ListViewItemNode { var updatedThemeReference = false var updatedTheme = false var updatedNightMode = false + var updatedChannelMode = false var updatedWallpaper = false var updatedSelected = false @@ -347,6 +355,9 @@ private final class ThemeCarouselThemeItemIconNode : ListViewItemNode { if currentItem?.nightMode != item.nightMode { updatedNightMode = true } + if currentItem?.channelMode != item.channelMode { + updatedChannelMode = true + } if currentItem?.wallpaper != item.wallpaper { updatedWallpaper = true } @@ -372,7 +383,7 @@ private final class ThemeCarouselThemeItemIconNode : ListViewItemNode { if let strongSelf = self { strongSelf.item = item - if updatedThemeReference || updatedWallpaper || updatedNightMode { + if updatedThemeReference || updatedWallpaper || updatedNightMode || updatedChannelMode { var themeReference = item.themeReference if case .builtin = themeReference, item.nightMode { themeReference = .builtin(.night) @@ -381,7 +392,7 @@ private final class ThemeCarouselThemeItemIconNode : ListViewItemNode { let color = item.themeSpecificAccentColors[themeReference.index] let wallpaper = item.themeSpecificChatWallpapers[themeReference.index] - strongSelf.imageNode.setSignal(themeIconImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, theme: themeReference, color: color, wallpaper: wallpaper ?? item.wallpaper, nightMode: item.nightMode, emoticon: true)) + strongSelf.imageNode.setSignal(themeIconImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, theme: themeReference, color: color, wallpaper: wallpaper ?? item.wallpaper, nightMode: item.nightMode, channelMode: item.channelMode, emoticon: true)) strongSelf.imageNode.backgroundColor = nil } @@ -507,23 +518,24 @@ private final class ThemeCarouselThemeItemIconNode : ListViewItemNode { } } -class ThemeCarouselThemeItem: ListViewItem, ItemListItem { - var sectionId: ItemListSectionId +public class ThemeCarouselThemeItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { + public var sectionId: ItemListSectionId - let context: AccountContext - let theme: PresentationTheme - let strings: PresentationStrings - let themes: [PresentationThemeReference] - let animatedEmojiStickers: [String: [StickerPackItem]] - let themeSpecificAccentColors: [Int64: PresentationThemeAccentColor] - let themeSpecificChatWallpapers: [Int64: TelegramWallpaper] - let nightMode: Bool - let currentTheme: PresentationThemeReference - let updatedTheme: (PresentationThemeReference) -> Void - let contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)? - let tag: ItemListItemTag? + public let context: AccountContext + public let theme: PresentationTheme + public let strings: PresentationStrings + public let themes: [PresentationThemeReference] + public let animatedEmojiStickers: [String: [StickerPackItem]] + public let themeSpecificAccentColors: [Int64: PresentationThemeAccentColor] + public let themeSpecificChatWallpapers: [Int64: TelegramWallpaper] + public let nightMode: Bool + public let channelMode: Bool + public let currentTheme: PresentationThemeReference + public let updatedTheme: (PresentationThemeReference) -> Void + public let contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)? + public let tag: ItemListItemTag? - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, themes: [PresentationThemeReference], animatedEmojiStickers: [String: [StickerPackItem]], themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], nightMode: Bool, currentTheme: PresentationThemeReference, updatedTheme: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil) { + public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, themes: [PresentationThemeReference], animatedEmojiStickers: [String: [StickerPackItem]], themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], nightMode: Bool, channelMode: Bool = false, currentTheme: PresentationThemeReference, updatedTheme: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil) { self.context = context self.theme = theme self.strings = strings @@ -532,6 +544,7 @@ class ThemeCarouselThemeItem: ListViewItem, ItemListItem { self.themeSpecificAccentColors = themeSpecificAccentColors self.themeSpecificChatWallpapers = themeSpecificChatWallpapers self.nightMode = nightMode + self.channelMode = channelMode self.currentTheme = currentTheme self.updatedTheme = updatedTheme self.contextAction = contextAction @@ -539,7 +552,7 @@ class ThemeCarouselThemeItem: ListViewItem, ItemListItem { self.sectionId = sectionId } - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = ThemeCarouselThemeItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) @@ -555,7 +568,7 @@ class ThemeCarouselThemeItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { if let nodeValue = node() as? ThemeCarouselThemeItemNode { let makeLayout = nodeValue.asyncLayout() @@ -571,6 +584,45 @@ class ThemeCarouselThemeItem: ListViewItem, ItemListItem { } } } + + public func item() -> ListViewItem { + return self + } + + public static func ==(lhs: ThemeCarouselThemeItem, rhs: ThemeCarouselThemeItem) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.themes != rhs.themes { + return false + } + if lhs.animatedEmojiStickers != rhs.animatedEmojiStickers { + return false + } + if lhs.themeSpecificAccentColors != rhs.themeSpecificAccentColors { + return false + } + if lhs.themeSpecificChatWallpapers != rhs.themeSpecificChatWallpapers { + return false + } + if lhs.nightMode != rhs.nightMode { + return false + } + if lhs.channelMode != rhs.channelMode { + return false + } + if lhs.currentTheme != rhs.currentTheme { + return false + } + + return true + } } private struct ThemeCarouselThemeItemNodeTransition { @@ -609,7 +661,7 @@ private func ensureThemeVisible(listNode: ListView, themeReference: Presentation } } -class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { +public class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { private let containerNode: ASDisplayNode private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -627,11 +679,11 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { private var tapping = false - var tag: ItemListItemTag? { + public var tag: ItemListItemTag? { return self.item?.tag } - init() { + public init() { self.containerNode = ASDisplayNode() self.backgroundNode = ASDisplayNode() @@ -654,7 +706,7 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { self.addSubnode(self.listNode) } - override func didLoad() { + override public func didLoad() { super.didLoad() self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true } @@ -695,7 +747,7 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { }) } - func asyncLayout() -> (_ item: ThemeCarouselThemeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + public func asyncLayout() -> (_ item: ThemeCarouselThemeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { return { item, params, neighbors in let contentSize: CGSize let insets: UIEdgeInsets @@ -728,20 +780,26 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { if strongSelf.maskNode.supernode == nil { strongSelf.containerNode.insertSubnode(strongSelf.maskNode, at: 3) } - - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - switch neighbors.top { + + if params.isStandalone { + strongSelf.topStripeNode.isHidden = true + strongSelf.bottomStripeNode.isHidden = true + strongSelf.maskNode.isHidden = true + strongSelf.backgroundNode.isHidden = true + } else { + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { case .sameSection(false): strongSelf.topStripeNode.isHidden = true default: hasTopCorners = true strongSelf.topStripeNode.isHidden = hasCorners - } - let bottomStripeInset: CGFloat - let bottomStripeOffset: CGFloat - switch neighbors.bottom { + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { case .sameSection(false): bottomStripeInset = params.leftInset + 16.0 bottomStripeOffset = -separatorHeight @@ -751,17 +809,20 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { bottomStripeOffset = 0.0 hasBottomCorners = true strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.bottomStripeNode.isHidden = true + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } - - strongSelf.bottomStripeNode.isHidden = true strongSelf.containerNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: contentSize.height) - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) - strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) var listInsets = UIEdgeInsets() listInsets.top += params.leftInset + 12.0 @@ -781,12 +842,12 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { hasCurrentTheme = true } let emojiFile = theme.emoticon.flatMap { item.animatedEmojiStickers[$0]?.first?.file } - entries.append(ThemeCarouselThemeEntry(index: index, emojiFile: emojiFile, themeReference: theme, nightMode: item.nightMode, themeSpecificAccentColors: item.themeSpecificAccentColors, themeSpecificChatWallpapers: item.themeSpecificChatWallpapers, selected: selected, theme: item.theme, strings: item.strings, wallpaper: nil)) + entries.append(ThemeCarouselThemeEntry(index: index, emojiFile: emojiFile, themeReference: theme, nightMode: item.nightMode, channelMode: item.channelMode, themeSpecificAccentColors: item.themeSpecificAccentColors, themeSpecificChatWallpapers: item.themeSpecificChatWallpapers, selected: selected, theme: item.theme, strings: item.strings, wallpaper: nil)) index += 1 } if !hasCurrentTheme { - entries.append(ThemeCarouselThemeEntry(index: index, emojiFile: nil, themeReference: item.currentTheme, nightMode: false, themeSpecificAccentColors: item.themeSpecificAccentColors, themeSpecificChatWallpapers: item.themeSpecificChatWallpapers, selected: true, theme: item.theme, strings: item.strings, wallpaper: nil)) + entries.append(ThemeCarouselThemeEntry(index: index, emojiFile: nil, themeReference: item.currentTheme, nightMode: false, channelMode: item.channelMode, themeSpecificAccentColors: item.themeSpecificAccentColors, themeSpecificChatWallpapers: item.themeSpecificChatWallpapers, selected: true, theme: item.theme, strings: item.strings, wallpaper: nil)) } let action: (PresentationThemeReference) -> Void = { [weak self] themeReference in @@ -810,15 +871,15 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } - func prepareCrossfadeTransition() { + public func prepareCrossfadeTransition() { guard self.snapshotView == nil else { return } @@ -836,7 +897,7 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { } } - func animateCrossfadeTransition() { + public func animateCrossfadeTransition() { guard self.snapshotView?.layer.animationKeys()?.isEmpty ?? true else { return } diff --git a/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/BUILD b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/BUILD new file mode 100644 index 0000000000..f0350af4f8 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/BUILD @@ -0,0 +1,33 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ThemeSettingsThemeItem", + module_name = "ThemeSettingsThemeItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/MergeLists", + "//submodules/TelegramUIPreferences", + "//submodules/ItemListUI", + "//submodules/PresentationDataUtils", + "//submodules/WallpaperResources", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/ContextUI", + "//submodules/HexColor", + "//submodules/TelegramUI/Components/ListItemComponentAdaptor", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsThemeItem.swift b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift similarity index 85% rename from submodules/SettingsUI/Sources/Themes/ThemeSettingsThemeItem.swift rename to submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift index 6fa79773ef..64b0b365d3 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsThemeItem.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift @@ -14,6 +14,8 @@ import WallpaperResources import AccountContext import AppBundle import ContextUI +import ListItemComponentAdaptor +import HexColor private struct ThemeSettingsThemeEntry: Comparable, Identifiable { let index: Int @@ -369,24 +371,24 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { } } -class ThemeSettingsThemeItem: ListViewItem, ItemListItem { - var sectionId: ItemListSectionId +public class ThemeSettingsThemeItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { + public var sectionId: ItemListSectionId - let context: AccountContext - let theme: PresentationTheme - let strings: PresentationStrings - let themes: [PresentationThemeReference] - let allThemes: [PresentationThemeReference] - let displayUnsupported: Bool - let themeSpecificAccentColors: [Int64: PresentationThemeAccentColor] - let themeSpecificChatWallpapers: [Int64: TelegramWallpaper] - let themePreferredBaseTheme: [Int64: TelegramBaseTheme] - let currentTheme: PresentationThemeReference - let updatedTheme: (PresentationThemeReference) -> Void - let contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)? - let tag: ItemListItemTag? + public let context: AccountContext + public let theme: PresentationTheme + public let strings: PresentationStrings + public let themes: [PresentationThemeReference] + public let allThemes: [PresentationThemeReference] + public let displayUnsupported: Bool + public let themeSpecificAccentColors: [Int64: PresentationThemeAccentColor] + public let themeSpecificChatWallpapers: [Int64: TelegramWallpaper] + public let themePreferredBaseTheme: [Int64: TelegramBaseTheme] + public let currentTheme: PresentationThemeReference + public let updatedTheme: (PresentationThemeReference) -> Void + public let contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)? + public let tag: ItemListItemTag? - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, themes: [PresentationThemeReference], allThemes: [PresentationThemeReference], displayUnsupported: Bool, themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], themePreferredBaseTheme: [Int64: TelegramBaseTheme], currentTheme: PresentationThemeReference, updatedTheme: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil) { + public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, themes: [PresentationThemeReference], allThemes: [PresentationThemeReference], displayUnsupported: Bool, themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], themePreferredBaseTheme: [Int64: TelegramBaseTheme], currentTheme: PresentationThemeReference, updatedTheme: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil) { self.context = context self.theme = theme self.strings = strings @@ -403,7 +405,7 @@ class ThemeSettingsThemeItem: ListViewItem, ItemListItem { self.sectionId = sectionId } - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = ThemeSettingsThemeItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) @@ -419,7 +421,7 @@ class ThemeSettingsThemeItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { if let nodeValue = node() as? ThemeSettingsThemeItemNode { let makeLayout = nodeValue.asyncLayout() @@ -435,6 +437,42 @@ class ThemeSettingsThemeItem: ListViewItem, ItemListItem { } } } + + public func item() -> ListViewItem { + return self + } + + public static func ==(lhs: ThemeSettingsThemeItem, rhs: ThemeSettingsThemeItem) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.themes != rhs.themes { + return false + } + if lhs.displayUnsupported != rhs.displayUnsupported { + return false + } + if lhs.themeSpecificAccentColors != rhs.themeSpecificAccentColors { + return false + } + if lhs.themeSpecificChatWallpapers != rhs.themeSpecificChatWallpapers { + return false + } + if lhs.themePreferredBaseTheme != rhs.themePreferredBaseTheme { + return false + } + if lhs.currentTheme != rhs.currentTheme { + return false + } + + return true + } } private struct ThemeSettingsThemeItemNodeTransition { @@ -472,7 +510,7 @@ private func ensureThemeVisible(listNode: ListView, themeReference: Presentation } } -class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { +public class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { private let containerNode: ASDisplayNode private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -488,13 +526,13 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { private var item: ThemeSettingsThemeItem? private var layoutParams: ListViewItemLayoutParams? - var tag: ItemListItemTag? { + public var tag: ItemListItemTag? { return self.item?.tag } private var tapping = false - init() { + public init() { self.containerNode = ASDisplayNode() self.backgroundNode = ASDisplayNode() @@ -517,7 +555,7 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { self.addSubnode(self.listNode) } - override func didLoad() { + override public func didLoad() { super.didLoad() self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true } @@ -558,7 +596,7 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { }) } - func asyncLayout() -> (_ item: ThemeSettingsThemeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + public func asyncLayout() -> (_ item: ThemeSettingsThemeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { return { item, params, neighbors in let contentSize: CGSize let insets: UIEdgeInsets @@ -591,20 +629,26 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { if strongSelf.maskNode.supernode == nil { strongSelf.containerNode.insertSubnode(strongSelf.maskNode, at: 3) } - - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - switch neighbors.top { + + if params.isStandalone { + strongSelf.topStripeNode.isHidden = true + strongSelf.bottomStripeNode.isHidden = true + strongSelf.maskNode.isHidden = true + strongSelf.backgroundNode.isHidden = true + } else { + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { case .sameSection(false): strongSelf.topStripeNode.isHidden = true default: hasTopCorners = true strongSelf.topStripeNode.isHidden = hasCorners - } - let bottomStripeInset: CGFloat - let bottomStripeOffset: CGFloat - switch neighbors.bottom { + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { case .sameSection(false): bottomStripeInset = params.leftInset + 16.0 bottomStripeOffset = -separatorHeight @@ -614,15 +658,18 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { bottomStripeOffset = 0.0 hasBottomCorners = true strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } strongSelf.containerNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: contentSize.height) - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) - strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) var listInsets = UIEdgeInsets() listInsets.top += params.leftInset + 4.0 @@ -694,15 +741,15 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } - func prepareCrossfadeTransition() { + public func prepareCrossfadeTransition() { guard self.snapshotView == nil else { return } @@ -719,7 +766,7 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { } } - func animateCrossfadeTransition() { + public func animateCrossfadeTransition() { guard self.snapshotView?.layer.animationKeys()?.isEmpty ?? true else { return } diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index e69c895c20..3deab23b96 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -604,7 +604,7 @@ public final class PeerListItemComponent: Component { statusIcon = .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_ScamAccount.uppercased()) } else if peer.isFake { statusIcon = .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_FakeAccount.uppercased()) - } else if case let .user(user) = peer, let emojiStatus = user.emojiStatus { + } else if let emojiStatus = peer.emojiStatus { statusIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.theme.list.itemAccentColor, loopMode: .count(2)) } else if peer.isVerified { statusIcon = .verified(fillColor: component.theme.list.itemCheckColors.fillColor, foregroundColor: component.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index eb4fed6df6..4fd10e2b54 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -368,6 +368,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.titleAccessoryPanelContainer = ChatControllerTitlePanelNodeContainer() self.titleAccessoryPanelContainer.clipsToBounds = true + setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat) + self.inputContextPanelContainer = ChatControllerTitlePanelNodeContainer() self.inputContextOverTextPanelContainer = ChatControllerTitlePanelNodeContainer() @@ -1012,7 +1014,11 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } if let historyNodeContainer = self.historyNodeContainer as? HistoryNodeContainer { - historyNodeContainer.isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat + let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat + if historyNodeContainer.isSecret != isSecret { + historyNodeContainer.isSecret = isSecret + setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret) + } } var previousListBottomInset: CGFloat? diff --git a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift index d04a66cebb..5f758642c5 100644 --- a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift @@ -540,6 +540,11 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { } else { emojiStatus = emojiStatusValue } + } else if let channel = interfaceState.renderedPeer?.peer as? TelegramChannel, let emojiStatusValue = channel.emojiStatus { + if channel.isFake || channel.isScam { + } else { + emojiStatus = emojiStatusValue + } } /*#if DEBUG diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 09fa79e5df..5b6dc4d37e 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -363,6 +363,18 @@ public final class SharedAccountContextImpl: SharedAccountContext { } if themeUpdated { updateLegacyTheme() + + /*if #available(iOS 13.0, *) { + let userInterfaceStyle: UIUserInterfaceStyle + if strongSelf.currentPresentationData.with({ $0 }).theme.overallDarkAppearance { + userInterfaceStyle = .dark + } else { + userInterfaceStyle = .light + } + if let eventView = strongSelf.mainWindow?.hostView.eventView, eventView.overrideUserInterfaceStyle != userInterfaceStyle { + eventView.overrideUserInterfaceStyle = userInterfaceStyle + } + }*/ } if themeNameUpdated { strongSelf.presentCrossfadeController() @@ -962,6 +974,18 @@ public final class SharedAccountContextImpl: SharedAccountContext { } }) } + + /*if #available(iOS 13.0, *) { + let userInterfaceStyle: UIUserInterfaceStyle + if self.currentPresentationData.with({ $0 }).theme.overallDarkAppearance { + userInterfaceStyle = .dark + } else { + userInterfaceStyle = .light + } + if let eventView = self.mainWindow?.hostView.eventView, eventView.overrideUserInterfaceStyle != userInterfaceStyle { + eventView.overrideUserInterfaceStyle = userInterfaceStyle + } + }*/ } deinit { diff --git a/submodules/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift index fac8add04e..d3a39df2eb 100644 --- a/submodules/WallpaperResources/Sources/WallpaperResources.swift +++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift @@ -1398,7 +1398,7 @@ private let messageImage: UIImage = { return messageBubbleImage(maxCornerRadius: 16.0, minCornerRadius: 16.0, incoming: true, fillColor: .white, strokeColor: .clear, neighbors: .none, shadow: nil, wallpaper: .color(0x000000), knockout: false) }() -public func themeIconImage(account: Account, accountManager: AccountManager, theme: PresentationThemeReference, color: PresentationThemeAccentColor?, wallpaper: TelegramWallpaper? = nil, nightMode: Bool? = nil, emoticon: Bool = false, large: Bool = false, qr: Bool = false, message: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func themeIconImage(account: Account, accountManager: AccountManager, theme: PresentationThemeReference, color: PresentationThemeAccentColor?, wallpaper: TelegramWallpaper? = nil, nightMode: Bool? = nil, channelMode: Bool? = nil, emoticon: Bool = false, large: Bool = false, qr: Bool = false, message: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let colorsSignal: Signal<((UIColor, UIColor?, [UInt32]), [UIColor], [UIColor], UIImage?, Bool, Bool, CGFloat, Int32?), NoError> var reference: MediaResourceReference? @@ -1693,6 +1693,7 @@ public func themeIconImage(account: Account, accountManager: AccountManager 1 { c.clip() @@ -1748,7 +1749,7 @@ public func themeIconImage(account: Account, accountManager: AccountManagertotalFrame(); + _frameCount = MAX(1, _frameCount); _frameRate = (int32_t)_animation->frameRate(); + _frameRate = MAX(1, _frameRate); size_t width = 0; size_t height = 0; @@ -59,6 +61,9 @@ return nil; } + width = MAX(1, width); + height = MAX(1, height); + _dimensions = CGSizeMake(width, height); if ((_frameRate > 360) || _animation->duration() > 9.0) {