From 4f183fa7fe1c166895419d98e0ca34fc53d385ee Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 17 Oct 2023 01:41:53 +0400 Subject: [PATCH 1/6] Name colors improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 17 + .../Display/Source/ListViewItemHeader.swift | 4 +- .../Sources/ItemListController.swift | 1 + .../Sources/ItemListControllerNode.swift | 14 +- .../Items/ItemListDisclosureItem.swift | 29 +- submodules/SettingsUI/BUILD | 1 + .../Themes/ThemeSettingsController.swift | 106 ++-- .../SyncCore/SyncCore_CachedUserData.swift | 14 +- .../AccountData/UpdateAccountPeerName.swift | 25 +- .../Sources/TelegramEngine/Peers/Peer.swift | 4 + .../Sources/Utils/PeerUtils.swift | 19 + .../Sources/PeerNameColor.swift | 46 ++ .../Resources/PresentationResourceKey.swift | 1 + .../Resources/PresentationResourcesChat.swift | 22 + .../Sources/FlashTintControlComponent.swift | 6 +- .../ChatMessageAttachedContentNode.swift | 100 ++-- .../Sources/ChatMessageBubbleItemNode.swift | 4 +- .../Sources/ChatMessageReplyInfoNode.swift | 25 +- .../Settings/PeerNameColorScreen/BUILD | 31 + .../Sources/ApplyColorFooterItem.swift | 134 +++++ .../Sources/BackgroundEmojiItem.swift | 429 ++++++++++++++ .../PeerNameColorChatPreviewItem.swift | 373 ++++++++++++ .../Sources/PeerNameColorItem.swift | 538 ++++++++++++++++++ .../Sources/PeerNameColorScreen.swift | 471 +++++++++++++++ .../Sources/UndoOverlayControllerNode.swift | 2 +- 25 files changed, 2299 insertions(+), 117 deletions(-) create mode 100644 submodules/TelegramPresentationData/Sources/PeerNameColor.swift create mode 100644 submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD create mode 100644 submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ApplyColorFooterItem.swift create mode 100644 submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/BackgroundEmojiItem.swift create mode 100644 submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift create mode 100644 submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift create mode 100644 submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 5fe90a20ef..8cef70f22e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10123,3 +10123,20 @@ Sorry for the inconvenience."; "Channel.AdminLog.MessageChangedNameColorSet" = "%1$@ set name color to %2$@"; "Channel.AdminLog.MessageChangedBackgroundEmojiSet" = "%1$@ set background emoji to %2$@"; + +"Appearance.NameColor" = "Your Name Color"; + +"NameColor.Title.Account" = "Your Name Color"; +"NameColor.Title.Channel" = "Channel Title Color"; + +"NameColor.ChatPreview.Title" = "COLOR PREVIEW"; +"NameColor.ChatPreview.ReplyText" = "Reply to your message"; +"NameColor.ChatPreview.MessageText" = "Your name and replies to your messages will be shown in the selected color."; +"NameColor.ChatPreview.LinkSite" = "Telegram"; +"NameColor.ChatPreview.LinkTitle" = "Link Preview"; +"NameColor.ChatPreview.LinkText" = "Your selected color will also tint the link preview."; +"NameColor.ChatPreview.Description.Account" = "You can choose an individual color to tint your name, the links you send, and replies to your messages."; + +"NameColor.ApplyColor" = "Apply Color"; + +"NameColor.TooltipPremium.Account" = "Subscribe to [Telegram Premium]() to choose a custom color for your name."; diff --git a/submodules/Display/Source/ListViewItemHeader.swift b/submodules/Display/Source/ListViewItemHeader.swift index 01f0f96e8c..a2551cdb0d 100644 --- a/submodules/Display/Source/ListViewItemHeader.swift +++ b/submodules/Display/Source/ListViewItemHeader.swift @@ -29,7 +29,7 @@ open class ListViewItemHeaderNode: ASDisplayNode { private var isFlashingOnScrolling = false weak var attachedToItemNode: ListViewItemNode? - var item: ListViewItemHeader? + public var item: ListViewItemHeader? func updateInternalStickLocationDistanceFactor(_ factor: CGFloat, animated: Bool) { self.internalStickLocationDistanceFactor = factor @@ -124,7 +124,7 @@ open class ListViewItemHeaderNode: ASDisplayNode { private var cachedLayout: (CGSize, CGFloat, CGFloat)? - func updateLayoutInternal(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + public func updateLayoutInternal(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { var update = false if let cachedLayout = self.cachedLayout { if cachedLayout.0 != size || cachedLayout.1 != leftInset || cachedLayout.2 != rightInset { diff --git a/submodules/ItemListUI/Sources/ItemListController.swift b/submodules/ItemListUI/Sources/ItemListController.swift index d26f110ac1..ea6a8cb633 100644 --- a/submodules/ItemListUI/Sources/ItemListController.swift +++ b/submodules/ItemListUI/Sources/ItemListController.swift @@ -264,6 +264,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable self.isOpaqueWhenInOverlay = true self.blocksBackgroundWhenInOverlay = true + self.automaticallyControlPresentationContextLayout = false self.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style diff --git a/submodules/ItemListUI/Sources/ItemListControllerNode.swift b/submodules/ItemListUI/Sources/ItemListControllerNode.swift index 74abf80506..0eacb7ad40 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerNode.swift @@ -233,6 +233,8 @@ public final class ItemListControllerNodeView: UITracingLayerView { } open class ItemListControllerNode: ASDisplayNode { + private weak var controller: ItemListController? + private var _ready = ValuePromise() open var ready: Signal { return self._ready.get() @@ -295,6 +297,7 @@ open class ItemListControllerNode: ASDisplayNode { private var previousContentOffset: ListViewVisibleContentOffset? public init(controller: ItemListController?, navigationBar: NavigationBar, state: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError>) { + self.controller = controller self.navigationBar = navigationBar self.listNode = ListView() @@ -585,9 +588,10 @@ open class ItemListControllerNode: ASDisplayNode { insets.top += headerHeight } + var footerHeight: CGFloat = 0.0 if let footerItemNode = self.footerItemNode { - let footerHeight = footerItemNode.updateLayout(layout: layout, transition: transition) - insets.bottom += footerHeight + footerHeight = footerItemNode.updateLayout(layout: layout, transition: transition) + insets.bottom = footerHeight } self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) @@ -616,6 +620,12 @@ open class ItemListControllerNode: ASDisplayNode { self.dequeueTransitions() } + var layout = layout + layout.intrinsicInsets.left = 4.0 + layout.intrinsicInsets.right = 4.0 + layout.intrinsicInsets.bottom = insets.bottom + self.controller?.presentationContext.containerLayoutUpdated(layout, transition: transition) + if !self.afterLayoutActions.isEmpty { let afterLayoutActions = self.afterLayoutActions self.afterLayoutActions = [] diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index ae6f0a9275..5dcf593bd1 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -35,6 +35,7 @@ public enum ItemListDisclosureLabelStyle { case multilineDetailText case badge(UIColor) case color(UIColor) + case semitransparentBadge(UIColor) case image(image: UIImage, size: CGSize) } @@ -125,6 +126,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { } private let badgeFont = Font.regular(15.0) +private let boldBadgeFont = Font.semibold(14.0) public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode @@ -256,11 +258,21 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { var updatedLabelBadgeImage: UIImage? var updatedLabelImage: UIImage? + var badgeDiameter: CGFloat = 20.0 var badgeColor: UIColor? + var badgeColorUpdated = false if case let .badge(color) = item.labelStyle { if item.label.count > 0 { badgeColor = color } + } else if case let .semitransparentBadge(color) = item.labelStyle { + badgeDiameter = 24.0 + badgeColor = color.withAlphaComponent(0.1) + + badgeColorUpdated = true + if let currentItem = currentItem, case let .semitransparentBadge(previousColor) = currentItem.labelStyle, color.isEqual(previousColor) { + badgeColorUpdated = false + } } if case let .color(color) = item.labelStyle { var updatedColor = true @@ -277,7 +289,6 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { updatedLabelImage = image } - let badgeDiameter: CGFloat = 20.0 if currentItem?.presentationData.theme !== item.presentationData.theme { updatedTheme = item.presentationData.theme switch item.disclosureStyle { @@ -289,7 +300,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { if let badgeColor = badgeColor { updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor) } - } else if let badgeColor = badgeColor, !currentHasBadge { + } else if let badgeColor = badgeColor, !currentHasBadge || badgeColorUpdated { updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor) } @@ -313,7 +324,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { var additionalTextRightInset: CGFloat = 0.0 switch item.labelStyle { - case .badge: + case .badge, .semitransparentBadge: additionalTextRightInset += 44.0 default: break @@ -355,6 +366,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { case .badge: labelBadgeColor = item.presentationData.theme.list.itemCheckColors.foregroundColor labelFont = badgeFont + case let .semitransparentBadge(color): + labelBadgeColor = color + labelFont = boldBadgeFont case .detailText, .multilineDetailText: labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor labelFont = detailFont @@ -578,14 +592,19 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { strongSelf.labelBadgeNode.removeFromSupernode() } - let badgeWidth = max(badgeDiameter, labelLayout.size.width + 10.0) + var badgeWidth = max(badgeDiameter, labelLayout.size.width + 10.0) + if case .semitransparentBadge = item.labelStyle { + badgeWidth += 2.0 + } let badgeFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth, y: floor((contentSize.height - badgeDiameter) / 2.0)), size: CGSize(width: badgeWidth, height: badgeDiameter)) strongSelf.labelBadgeNode.frame = badgeFrame let labelFrame: CGRect switch item.labelStyle { case .badge: - labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: badgeFrame.minY + 1), size: labelLayout.size) + labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: badgeFrame.minY + 1.0), size: labelLayout.size) + case .semitransparentBadge: + labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: badgeFrame.minY + 1.0 - UIScreenPixel + floorToScreenPixels((badgeDiameter - labelLayout.size.height) / 2.0)), size: labelLayout.size) case .detailText, .multilineDetailText: labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size) default: diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 6781ddea57..837b1ee9e1 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -114,6 +114,7 @@ swift_library( "//submodules/ImageBlur:ImageBlur", "//submodules/AttachmentUI:AttachmentUI", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen", + "//submodules/TelegramUI/Components/Settings/PeerNameColorScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index d0cdb5c54f..0fc2326ca0 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -18,6 +18,7 @@ import AccountContext import ContextUI import UndoUI import PremiumUI +import PeerNameColorScreen func themeDisplayName(strings: PresentationStrings, reference: PresentationThemeReference) -> String { let name: String @@ -50,6 +51,7 @@ private final class ThemeSettingsControllerArguments { let selectTheme: (PresentationThemeReference) -> Void let openThemeSettings: () -> Void let openWallpaperSettings: () -> Void + let openNameColorSettings: () -> Void let selectAccentColor: (PresentationThemeAccentColor?) -> Void let openAccentColorPicker: (PresentationThemeReference, Bool) -> Void let toggleNightTheme: (Bool) -> Void @@ -64,11 +66,12 @@ private final class ThemeSettingsControllerArguments { let themeContextAction: (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void let colorContextAction: (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void - init(context: AccountContext, selectTheme: @escaping (PresentationThemeReference) -> Void, openThemeSettings: @escaping () -> Void, openWallpaperSettings: @escaping () -> Void, selectAccentColor: @escaping (PresentationThemeAccentColor?) -> Void, openAccentColorPicker: @escaping (PresentationThemeReference, Bool) -> Void, toggleNightTheme: @escaping (Bool) -> Void, openAutoNightTheme: @escaping () -> Void, openTextSize: @escaping () -> Void, openBubbleSettings: @escaping () -> Void, openPowerSavingSettings: @escaping () -> Void, openStickersAndEmoji: @escaping () -> Void, toggleShowNextMediaOnTap: @escaping (Bool) -> Void, selectAppIcon: @escaping (PresentationAppIcon) -> Void, editTheme: @escaping (PresentationCloudTheme) -> Void, themeContextAction: @escaping (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void, colorContextAction: @escaping (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void) { + init(context: AccountContext, selectTheme: @escaping (PresentationThemeReference) -> Void, openThemeSettings: @escaping () -> Void, openWallpaperSettings: @escaping () -> Void, openNameColorSettings: @escaping () -> Void, selectAccentColor: @escaping (PresentationThemeAccentColor?) -> Void, openAccentColorPicker: @escaping (PresentationThemeReference, Bool) -> Void, toggleNightTheme: @escaping (Bool) -> Void, openAutoNightTheme: @escaping () -> Void, openTextSize: @escaping () -> Void, openBubbleSettings: @escaping () -> Void, openPowerSavingSettings: @escaping () -> Void, openStickersAndEmoji: @escaping () -> Void, toggleShowNextMediaOnTap: @escaping (Bool) -> Void, selectAppIcon: @escaping (PresentationAppIcon) -> Void, editTheme: @escaping (PresentationCloudTheme) -> Void, themeContextAction: @escaping (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void, colorContextAction: @escaping (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void) { self.context = context self.selectTheme = selectTheme self.openThemeSettings = openThemeSettings self.openWallpaperSettings = openWallpaperSettings + self.openNameColorSettings = openNameColorSettings self.selectAccentColor = selectAccentColor self.openAccentColorPicker = openAccentColorPicker self.toggleNightTheme = toggleNightTheme @@ -119,6 +122,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { case themes(PresentationTheme, PresentationStrings, [PresentationThemeReference], PresentationThemeReference, Bool, [String: [StickerPackItem]], [Int64: PresentationThemeAccentColor], [Int64: TelegramWallpaper]) case chatTheme(PresentationTheme, String) case wallpaper(PresentationTheme, String) + case nameColor(PresentationTheme, String, String, UIColor) case autoNight(PresentationTheme, String, Bool, Bool) case autoNightTheme(PresentationTheme, String, String) case textSize(PresentationTheme, String, String) @@ -133,7 +137,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { - case .themeListHeader, .chatPreview, .themes, .chatTheme, .wallpaper: + case .themeListHeader, .chatPreview, .themes, .chatTheme, .wallpaper, .nameColor: return ThemeSettingsControllerSection.chatPreview.rawValue case .autoNight, .autoNightTheme: return ThemeSettingsControllerSection.nightMode.rawValue @@ -150,38 +154,40 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { var stableId: Int32 { switch self { - case .themeListHeader: - return 0 - case .chatPreview: - return 1 - case .themes: - return 2 - case .chatTheme: - return 3 - case .wallpaper: - return 4 - case .autoNight: - return 5 - case .autoNightTheme: - return 6 - case .textSize: - return 7 - case .bubbleSettings: - return 8 - case .powerSaving: - return 9 - case .stickersAndEmoji: - return 10 - case .iconHeader: - return 11 - case .iconItem: - return 12 - case .otherHeader: - return 13 - case .showNextMediaOnTap: - return 14 - case .showNextMediaOnTapInfo: - return 15 + case .themeListHeader: + return 0 + case .chatPreview: + return 1 + case .themes: + return 2 + case .chatTheme: + return 3 + case .wallpaper: + return 4 + case .nameColor: + return 5 + case .autoNight: + return 6 + case .autoNightTheme: + return 7 + case .textSize: + return 8 + case .bubbleSettings: + return 9 + case .powerSaving: + return 10 + case .stickersAndEmoji: + return 11 + case .iconHeader: + return 12 + case .iconItem: + return 13 + case .otherHeader: + return 14 + case .showNextMediaOnTap: + return 15 + case .showNextMediaOnTapInfo: + return 16 } } @@ -211,6 +217,12 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { } else { return false } + case let .nameColor(lhsTheme, lhsText, lhsName, lhsColor): + if case let .nameColor(rhsTheme, rhsText, rhsName, rhsColor) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsName == rhsName, lhsColor == rhsColor { + return true + } else { + return false + } case let .autoNight(lhsTheme, lhsText, lhsValue, lhsEnabled): if case let .autoNight(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { return true @@ -297,10 +309,10 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { return ThemeSettingsChatPreviewItem(context: arguments.context, theme: theme, componentTheme: theme, strings: strings, sectionId: self.section, fontSize: fontSize, chatBubbleCorners: chatBubbleCorners, wallpaper: wallpaper, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, messageItems: items) case let .themes(theme, strings, chatThemes, currentTheme, nightMode, animatedEmojiStickers, themeSpecificAccentColors, themeSpecificChatWallpapers): return ThemeCarouselThemeItem(context: arguments.context, theme: theme, strings: strings, sectionId: self.section, themes: chatThemes, animatedEmojiStickers: animatedEmojiStickers, themeSpecificAccentColors: themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, nightMode: nightMode, currentTheme: currentTheme, updatedTheme: { theme in - arguments.selectTheme(theme) - }, contextAction: { theme, node, gesture in - arguments.themeContextAction(false, theme, node, gesture) - }, tag: ThemeSettingsEntryTag.theme) + arguments.selectTheme(theme) + }, contextAction: { theme, node, gesture in + arguments.themeContextAction(false, theme, node, gesture) + }, tag: ThemeSettingsEntryTag.theme) case let .chatTheme(_, text): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openThemeSettings() @@ -309,6 +321,10 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openWallpaperSettings() }) + case let .nameColor(_, text, name, color): + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: name, labelStyle: .semitransparentBadge(color), sectionId: self.section, style: .blocks, action: { + arguments.openNameColorSettings() + }) case let .autoNight(_, title, value, enabled): return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleNightTheme(value) @@ -353,7 +369,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { } } -private func themeSettingsControllerEntries(presentationData: PresentationData, presentationThemeSettings: PresentationThemeSettings, mediaSettings: MediaDisplaySettings, themeReference: PresentationThemeReference, availableThemes: [PresentationThemeReference], availableAppIcons: [PresentationAppIcon], currentAppIconName: String?, isPremium: Bool, chatThemes: [PresentationThemeReference], animatedEmojiStickers: [String: [StickerPackItem]]) -> [ThemeSettingsControllerEntry] { +private func themeSettingsControllerEntries(presentationData: PresentationData, presentationThemeSettings: PresentationThemeSettings, mediaSettings: MediaDisplaySettings, themeReference: PresentationThemeReference, availableThemes: [PresentationThemeReference], availableAppIcons: [PresentationAppIcon], currentAppIconName: String?, isPremium: Bool, chatThemes: [PresentationThemeReference], animatedEmojiStickers: [String: [StickerPackItem]], accountPeer: EnginePeer?) -> [ThemeSettingsControllerEntry] { var entries: [ThemeSettingsControllerEntry] = [] let strings = presentationData.strings @@ -365,6 +381,10 @@ private func themeSettingsControllerEntries(presentationData: PresentationData, entries.append(.chatTheme(presentationData.theme, strings.Settings_ChatThemes)) entries.append(.wallpaper(presentationData.theme, strings.Settings_ChatBackground)) + if let accountPeer, case let .user(user) = accountPeer { + entries.append(.nameColor(presentationData.theme, strings.Appearance_NameColor, accountPeer.compactDisplayTitle, (user.nameColor ?? .blue).color)) + } + entries.append(.autoNight(presentationData.theme, strings.Appearance_NightTheme, presentationThemeSettings.automaticThemeSwitchSetting.force, !presentationData.autoNightModeTriggered || presentationThemeSettings.automaticThemeSwitchSetting.force)) let autoNightMode: String switch presentationThemeSettings.automaticThemeSwitchSetting.trigger { @@ -488,6 +508,8 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The pushControllerImpl?(themePickerController(context: context)) }, openWallpaperSettings: { pushControllerImpl?(ThemeGridController(context: context)) + }, openNameColorSettings: { + pushControllerImpl?(PeerNameColorScreen(context: context, subject: .account)) }, selectAccentColor: { accentColor in selectAccentColorImpl?(accentColor) }, openAccentColorPicker: { themeReference, create in @@ -1000,8 +1022,8 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The }) }) - let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings, SharedDataKeys.chatThemes, ApplicationSpecificSharedDataKeys.mediaDisplaySettings]), cloudThemes.get(), availableAppIcons, currentAppIconName.get(), removedThemeIndexesPromise.get(), animatedEmojiStickers, context.account.postbox.peerView(id: context.account.peerId)) - |> map { presentationData, sharedData, cloudThemes, availableAppIcons, currentAppIconName, removedThemeIndexes, animatedEmojiStickers, peerView -> (ItemListControllerState, (ItemListNodeState, Any)) in + let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings, SharedDataKeys.chatThemes, ApplicationSpecificSharedDataKeys.mediaDisplaySettings]), cloudThemes.get(), availableAppIcons, currentAppIconName.get(), removedThemeIndexesPromise.get(), animatedEmojiStickers, context.account.postbox.peerView(id: context.account.peerId), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))) + |> map { presentationData, sharedData, cloudThemes, availableAppIcons, currentAppIconName, removedThemeIndexes, animatedEmojiStickers, peerView, accountPeer -> (ItemListControllerState, (ItemListNodeState, Any)) in let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings let mediaSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaDisplaySettings]?.get(MediaDisplaySettings.self) ?? MediaDisplaySettings.defaultSettings @@ -1042,7 +1064,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The chatThemes.insert(.builtin(.dayClassic), at: 0) let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Appearance_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(presentationData: presentationData, presentationThemeSettings: settings, mediaSettings: mediaSettings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName, isPremium: isPremium, chatThemes: chatThemes, animatedEmojiStickers: animatedEmojiStickers), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(presentationData: presentationData, presentationThemeSettings: settings, mediaSettings: mediaSettings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName, isPremium: isPremium, chatThemes: chatThemes, animatedEmojiStickers: animatedEmojiStickers, accountPeer: accountPeer), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index 1c5c7f3126..6be99834da 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -114,7 +114,7 @@ public struct CachedPremiumGiftOption: Equatable, PostboxCoding { } } -public enum PeerNameColor: Int32 { +public enum PeerNameColor: Int32, CaseIterable { case red case orange case violet @@ -122,12 +122,12 @@ public enum PeerNameColor: Int32 { case cyan case blue case pink - case other7 - case other8 - case other9 - case other10 - case other11 - case other12 + case redDash + case orangeDash + case violetDash + case greenDash + case cyanDash + case blueDash case other13 case other14 case other15 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/UpdateAccountPeerName.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/UpdateAccountPeerName.swift index e201a45ff0..13d45fb782 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/UpdateAccountPeerName.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/UpdateAccountPeerName.swift @@ -53,23 +53,24 @@ public enum UpdateNameColorAndEmojiError { func _internal_updateNameColorAndEmoji(account: Account, nameColor: PeerNameColor, backgroundEmojiId: Int64?) -> Signal { let flags: Int32 = (1 << 0) - return account.postbox.loadedPeerWithId(account.peerId) - |> castError(UpdateNameColorAndEmojiError.self) - |> mapToSignal { accountPeer -> Signal in - guard let accountPeer = accountPeer as? TelegramUser else { - return .fail(.generic) + return account.postbox.transaction { transaction -> Signal in + guard let peer = transaction.getPeer(account.peerId) as? TelegramUser else { + return .complete() } + updatePeersCustom(transaction: transaction, peers: [peer.withUpdatedNameColor(nameColor).withUpdatedBackgroundEmojiId(backgroundEmojiId)], update: { _, updated in + return updated + }) + return .single(peer) + } + |> switchToLatest + |> castError(UpdateNameColorAndEmojiError.self) + |> mapToSignal { _ -> Signal in return account.network.request(Api.functions.account.updateColor(flags: flags, color: nameColor.rawValue, backgroundEmojiId: backgroundEmojiId ?? 0)) |> mapError { _ -> UpdateNameColorAndEmojiError in return .generic } - |> mapToSignal { apiUser -> Signal in - return account.postbox.transaction { transaction -> Void in - updatePeersCustom(transaction: transaction, peers: [accountPeer.withUpdatedNameColor(nameColor).withUpdatedBackgroundEmojiId(backgroundEmojiId)], update: { _, updated in - return updated - }) - } - |> castError(UpdateNameColorAndEmojiError.self) + |> mapToSignal { _ -> Signal in + return .complete() } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift index 16eafaf8af..6bc1f5c1f5 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift @@ -505,6 +505,10 @@ public extension EnginePeer { } return false } + + var nameColor: PeerNameColor? { + return self._asPeer().nameColor + } } public extension EnginePeer { diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index 725f889a33..95351ed979 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -216,6 +216,25 @@ public extension Peer { return false } } + + var nameColor: PeerNameColor? { + switch self { + case let user as TelegramUser: + if let nameColor = user.nameColor { + return nameColor + } else { + return PeerNameColor(rawValue: Int32(self.id.id._internalGetInt64Value() % 7)) ?? .blue + } + case let channel as TelegramChannel: + if let nameColor = channel.nameColor { + return nameColor + } else { + return PeerNameColor(rawValue: Int32(self.id.id._internalGetInt64Value() % 7)) ?? .blue + } + default: + return nil + } + } } public extension TelegramPeerUsername { diff --git a/submodules/TelegramPresentationData/Sources/PeerNameColor.swift b/submodules/TelegramPresentationData/Sources/PeerNameColor.swift new file mode 100644 index 0000000000..e848fc8484 --- /dev/null +++ b/submodules/TelegramPresentationData/Sources/PeerNameColor.swift @@ -0,0 +1,46 @@ +import Foundation +import UIKit +import TelegramCore + +public extension PeerNameColor { + var color: UIColor { + return self.dashColors.0 + } + + var dashColors: (UIColor, UIColor?) { + switch self { + case .red: + return (UIColor(rgb: 0xCC5049), nil) + case .orange: + return (UIColor(rgb: 0xD67722), nil) + case .violet: + return (UIColor(rgb: 0x955CDB), nil) + case .green: + return (UIColor(rgb: 0x40A920), nil) + case .cyan: + return (UIColor(rgb: 0x309EBA), nil) + case .blue: + return (UIColor(rgb: 0x368AD1), nil) + case .pink: + return (UIColor(rgb: 0xC7508B), nil) + case .redDash: + return (UIColor(rgb: 0xE15052), UIColor(rgb: 0xF9AE63)) + case .orangeDash: + return (UIColor(rgb: 0xE0802B), UIColor(rgb: 0xFAC534)) + case .violetDash: + return (UIColor(rgb: 0xA05FF3), UIColor(rgb: 0xF48FFF)) + case .greenDash: + return (UIColor(rgb: 0x27A910), UIColor(rgb: 0xA7DC57)) + case .cyanDash: + return (UIColor(rgb: 0x27ACCE), UIColor(rgb: 0x82E8D6)) + case .blueDash: + return (UIColor(rgb: 0x3391D4), UIColor(rgb: 0x7DD3F0)) + case .other13: + return (.black, nil) + case .other14: + return (.black, nil) + case .other15: + return (.black, nil) + } + } +} diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 5f67d72241..a8749e2f94 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -300,6 +300,7 @@ public enum PresentationResourceKey: Int32 { case chatReplyBackgroundTemplateImage case chatReplyServiceBackgroundTemplateImage + case chatReplyLineDashTemplateImage } public enum ChatExpiredStoryIndicatorType: Hashable { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index bceb49c555..1d115137da 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -1316,4 +1316,26 @@ public struct PresentationResourcesChat { })?.stretchableImage(withLeftCapWidth: Int(radius), topCapHeight: Int(radius)).withRenderingMode(.alwaysTemplate) }) } + + public static func chatReplyLineDashTemplateImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatReplyLineDashTemplateImage.rawValue, { theme in + let radius: CGFloat = 3.0 + let offset: CGFloat = 5.0 + + return generateImage(CGSize(width: radius, height: radius * 6.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.move(to: CGPoint(x: size.width, y: offset)) + context.addLine(to: CGPoint(x: size.width, y: offset + radius * 3.0)) + context.addLine(to: CGPoint(x: 0.0, y: offset + radius * 4.0)) + context.addLine(to: CGPoint(x: 0.0, y: offset + radius)) + context.closePath() + + context.setFillColor(UIColor.white.cgColor) + context.fillPath() + })?.resizableImage(withCapInsets: .zero, resizingMode: .tile).withRenderingMode(.alwaysTemplate) + }) + } + +//chatReplyLineDashTemplateImage } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift index 3956cd276d..9f2b676bd0 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift @@ -81,7 +81,7 @@ private final class FlashColorComponent: Component { func update(component: FlashColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component - let contentSize = CGSize(width: 24.0, height: 24.0) + let contentSize = CGSize(width: 30.0, height: 30.0) self.contentView.frame = CGRect(origin: .zero, size: contentSize) let bounds = CGRect(origin: .zero, size: contentSize) @@ -192,7 +192,7 @@ final class FlashTintControlComponent: Component { let isFirstTime = self.component == nil self.component = component - let size = CGSize(width: 160.0, height: 40.0) + let size = CGSize(width: 184.0, height: 92.0) if isFirstTime { self.maskLayer.path = generateRoundedRectWithTailPath(rectSize: size, cornerRadius: 10.0, tailSize: CGSize(width: 18, height: 7.0), tailRadius: 1.0, tailPosition: 0.8, transformTail: false).cgPath self.maskLayer.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: size.height + 7.0)) @@ -264,7 +264,7 @@ final class FlashTintControlComponent: Component { if view.superview == nil { self.containerView.addSubview(view) } - view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - swatchesSize.width) / 2.0), y: floorToScreenPixels((size.height - swatchesSize.height) / 2.0)), size: swatchesSize) + view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - swatchesSize.width) / 2.0), y: 8.0), size: swatchesSize) } self.dismissView.frame = CGRect(origin: .zero, size: availableSize) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index c7a37b6233..71d91da7f1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -53,6 +53,7 @@ public struct ChatMessageAttachedContentNodeMediaFlags: OptionSet { public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode { private var backgroundView: UIImageView? + private var lineDashView: UIImageView? private let topTitleNode: TextNode private let textNode: TextNodeWithEntities private let inlineImageNode: TransformImageNode @@ -251,13 +252,47 @@ public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode { let string = NSMutableAttributedString() var notEmpty = false - let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing + let mainColor: UIColor + var secondaryColor: UIColor? + if !incoming { + mainColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor + } else { + var authorNameColor: UIColor? + let author = message.author + if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace), author?.id.namespace == Namespaces.Peer.CloudUser { + authorNameColor = author?.nameColor?.color + secondaryColor = author?.nameColor?.dashColors.1 +// if let rawAuthorNameColor = authorNameColor { +// var dimColors = false +// switch presentationData.theme.theme.name { +// case .builtin(.nightAccent), .builtin(.night): +// dimColors = true +// default: +// break +// } +// if dimColors { +// var hue: CGFloat = 0.0 +// var saturation: CGFloat = 0.0 +// var brightness: CGFloat = 0.0 +// rawAuthorNameColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil) +// authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0) +// } +// } + } + + if let authorNameColor { + mainColor = authorNameColor + } else { + mainColor = presentationData.theme.theme.chat.message.incoming.accentTextColor + } + } + let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing if let title = title, !title.isEmpty { if titleBeforeMedia { - topTitleString.append(NSAttributedString(string: title, font: titleFont, textColor: messageTheme.accentTextColor)) + topTitleString.append(NSAttributedString(string: title, font: titleFont, textColor: incoming ? mainColor : messageTheme.accentTextColor)) } else { - string.append(NSAttributedString(string: title, font: titleFont, textColor: messageTheme.accentTextColor)) + string.append(NSAttributedString(string: title, font: titleFont, textColor: incoming ? mainColor : messageTheme.accentTextColor)) notEmpty = true } } @@ -565,7 +600,6 @@ public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode { let upatedTextCutout = textCutout - let (topTitleLayout, topTitleApply) = topTitleAsyncLayout(TextNodeLayoutArguments(attributedString: topTitleString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (textLayout, textApply) = textAsyncLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: upatedTextCutout, insets: UIEdgeInsets())) @@ -603,40 +637,7 @@ public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode { var textFrame = CGRect(origin: CGPoint(), size: textLayout.size) textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top) - - let mainColor: UIColor - if !incoming { - mainColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor - } else { - var authorNameColor: UIColor? - let author = message.author - if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace), author?.id.namespace == Namespaces.Peer.CloudUser { - authorNameColor = author.flatMap { chatMessagePeerIdColors[Int(clamping: $0.id.id._internalGetInt64Value() % 7)] } - if let rawAuthorNameColor = authorNameColor { - var dimColors = false - switch presentationData.theme.theme.name { - case .builtin(.nightAccent), .builtin(.night): - dimColors = true - default: - break - } - if dimColors { - var hue: CGFloat = 0.0 - var saturation: CGFloat = 0.0 - var brightness: CGFloat = 0.0 - rawAuthorNameColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil) - authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0) - } - } - } - - if let authorNameColor { - mainColor = authorNameColor - } else { - mainColor = presentationData.theme.theme.chat.message.incoming.accentTextColor - } - } - + var boundingSize = textFrame.size if titleBeforeMedia { boundingSize.height += topTitleLayout.size.height + 4.0 @@ -854,10 +855,29 @@ public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode { backgroundView.image = PresentationResourcesChat.chatReplyBackgroundTemplateImage(presentationData.theme.theme) } backgroundView.tintColor = mainColor - - animation.animator.updateFrame(layer: backgroundView.layer, frame: CGRect(origin: CGPoint(x: 11.0, y: insets.top - 3.0), size: CGSize(width: adjustedBoundingSize.width - 4.0 - insets.right, height: adjustedBoundingSize.height - insets.top - insets.bottom + 4.0)), completion: nil) + + let backgroundFrame = CGRect(origin: CGPoint(x: 11.0, y: insets.top - 3.0), size: CGSize(width: adjustedBoundingSize.width - 4.0 - insets.right, height: adjustedBoundingSize.height - insets.top - insets.bottom + 4.0)) + animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil) backgroundView.isHidden = !displayLine + if let secondaryColor { + let lineDashView: UIImageView + if let current = strongSelf.lineDashView { + lineDashView = current + } else { + lineDashView = UIImageView(image: PresentationResourcesChat.chatReplyLineDashTemplateImage(presentationData.theme.theme)) + strongSelf.lineDashView = lineDashView + strongSelf.view.insertSubview(lineDashView, aboveSubview: backgroundView) + } + lineDashView.tintColor = secondaryColor + lineDashView.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 3.0, height: backgroundFrame.height)) + } else { + if let lineDashView = strongSelf.lineDashView { + strongSelf.lineDashView = nil + lineDashView.removeFromSuperview() + } + } + //strongSelf.borderColor = UIColor.red.cgColor //strongSelf.borderWidth = 2.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 04852957e3..c874618bc3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -1836,13 +1836,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if initialDisplayHeader && displayAuthorInfo { if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info, item.content.firstMessage.adAttribute == nil { authorNameString = EnginePeer(peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) - authorNameColor = chatMessagePeerIdColors[Int(clamping: peer.id.id._internalGetInt64Value() % 7)] + authorNameColor = (peer as Peer).nameColor?.color } else if let effectiveAuthor = effectiveAuthor { authorNameString = EnginePeer(effectiveAuthor).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) let nameColor: UIColor if incoming { - nameColor = chatMessagePeerIdColors[Int(clamping: effectiveAuthor.id.id._internalGetInt64Value() % 7)] + nameColor = (effectiveAuthor.nameColor ?? .blue).color } else { nameColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift index c57e44066d..92742864f4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift @@ -100,6 +100,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } private let backgroundView: UIImageView + private var lineDashView: UIImageView? private var quoteIconView: UIImageView? private let contentNode: ASDisplayNode private var titleNode: TextNode? @@ -216,11 +217,13 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { let dustColor: UIColor var authorNameColor: UIColor? + var dashSecondaryColor: UIColor? let author = arguments.message?.effectiveAuthor if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(arguments.parentMessage.id.peerId.namespace) && author?.id.namespace == Namespaces.Peer.CloudUser { - authorNameColor = author.flatMap { chatMessagePeerIdColors[Int(clamping: $0.id.id._internalGetInt64Value() % 7)] } + authorNameColor = author?.nameColor?.color + dashSecondaryColor = author?.nameColor?.dashColors.1 if let rawAuthorNameColor = authorNameColor { var dimColors = false switch arguments.presentationData.theme.theme.name { @@ -240,6 +243,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } let mainColor: UIColor + var secondaryColor: UIColor? switch arguments.type { case let .bubble(incoming): @@ -247,6 +251,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { if incoming { if let authorNameColor { mainColor = authorNameColor + secondaryColor = dashSecondaryColor } else { mainColor = arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor } @@ -608,6 +613,24 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { node.backgroundView.tintColor = mainColor node.backgroundView.frame = backgroundFrame + if let secondaryColor { + let lineDashView: UIImageView + if let current = node.lineDashView { + lineDashView = current + } else { + lineDashView = UIImageView(image: PresentationResourcesChat.chatReplyLineDashTemplateImage(arguments.presentationData.theme.theme)) + node.lineDashView = lineDashView + node.contentNode.view.addSubview(lineDashView) + } + lineDashView.tintColor = secondaryColor + lineDashView.frame = CGRect(origin: .zero, size: CGSize(width: 3.0, height: backgroundFrame.height)) + } else { + if let lineDashView = node.lineDashView { + node.lineDashView = nil + lineDashView.removeFromSuperview() + } + } + if arguments.quote != nil || arguments.replyForward?.quote != nil { let quoteIconView: UIImageView if let current = node.quoteIconView { diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD new file mode 100644 index 0000000000..83c7ebc642 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD @@ -0,0 +1,31 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PeerNameColorScreen", + module_name = "PeerNameColorScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/ItemListUI", + "//submodules/PresentationDataUtils", + "//submodules/UndoUI", + "//submodules/WallpaperBackgroundNode", + "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/SolidRoundedButtonNode", + "//submodules/AppBundle", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ApplyColorFooterItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ApplyColorFooterItem.swift new file mode 100644 index 0000000000..e78a2d57da --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ApplyColorFooterItem.swift @@ -0,0 +1,134 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import SolidRoundedButtonNode +import AppBundle + +final class ApplyColorFooterItem: ItemListControllerFooterItem { + let theme: PresentationTheme + let title: String + let locked: Bool + let action: () -> Void + + init(theme: PresentationTheme, title: String, locked: Bool, action: @escaping () -> Void) { + self.theme = theme + self.title = title + self.locked = locked + self.action = action + } + + func isEqual(to: ItemListControllerFooterItem) -> Bool { + if let item = to as? ApplyColorFooterItem { + return self.theme === item.theme && self.title == item.title && self.locked == item.locked + } else { + return false + } + } + + func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode { + if let current = current as? ApplyColorFooterItemNode { + current.item = self + return current + } else { + return ApplyColorFooterItemNode(item: self) + } + } +} + +final class ApplyColorFooterItemNode: ItemListControllerFooterItemNode { + private let backgroundNode: NavigationBackgroundNode + private let separatorNode: ASDisplayNode + private let buttonNode: SolidRoundedButtonNode + + private var validLayout: ContainerViewLayout? + + var item: ApplyColorFooterItem { + didSet { + self.updateItem() + if let layout = self.validLayout { + let _ = self.updateLayout(layout: layout, transition: .immediate) + } + } + } + + init(item: ApplyColorFooterItem) { + self.item = item + + self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.tabBar.backgroundColor) + self.separatorNode = ASDisplayNode() + + self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), height: 50.0, cornerRadius: 11.0) + self.buttonNode.icon = item.locked ? UIImage(bundleImageName: "Chat/Stickers/Lock") : nil + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.buttonNode) + + self.updateItem() + } + + private func updateItem() { + self.backgroundNode.updateColor(color: self.item.theme.rootController.tabBar.backgroundColor, transition: .immediate) + self.separatorNode.backgroundColor = self.item.theme.rootController.tabBar.separatorColor + + let backgroundColor = self.item.theme.list.itemCheckColors.fillColor + let textColor = self.item.theme.list.itemCheckColors.foregroundColor + + self.buttonNode.updateTheme(SolidRoundedButtonTheme(backgroundColor: backgroundColor, backgroundColors: [], foregroundColor: textColor), animated: true) + self.buttonNode.title = self.item.title + self.buttonNode.icon = self.item.locked ? UIImage(bundleImageName: "Chat/Stickers/Lock") : nil + + self.buttonNode.pressed = { [weak self] in + self?.item.action() + } + } + + override func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) { + transition.updateAlpha(node: self.backgroundNode, alpha: alpha) + transition.updateAlpha(node: self.separatorNode, alpha: alpha) + } + + override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = layout + + let buttonInset: CGFloat = 16.0 + let buttonWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - buttonInset * 2.0 + let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition) + let inset: CGFloat = 9.0 + + let insets = layout.insets(options: [.input]) + + var panelHeight: CGFloat = buttonHeight + inset * 2.0 + let totalPanelHeight: CGFloat + if let inputHeight = layout.inputHeight, inputHeight > 0.0 { + totalPanelHeight = panelHeight + insets.bottom + } else { + panelHeight += insets.bottom + totalPanelHeight = panelHeight + } + + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - totalPanelHeight), size: CGSize(width: layout.size.width, height: panelHeight)) + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonInset, y: panelFrame.minY + inset), size: CGSize(width: buttonWidth, height: buttonHeight))) + + transition.updateFrame(node: self.backgroundNode, frame: panelFrame) + self.backgroundNode.update(size: panelFrame.size, transition: transition) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: panelFrame.origin, size: CGSize(width: panelFrame.width, height: UIScreenPixel))) + + return panelHeight + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if self.backgroundNode.frame.contains(point) { + return true + } else { + return false + } + } +} diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/BackgroundEmojiItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/BackgroundEmojiItem.swift new file mode 100644 index 0000000000..6847d92576 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/BackgroundEmojiItem.swift @@ -0,0 +1,429 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import Postbox +import TelegramCore +import ItemListUI +import EmojiStatusComponent +import ComponentFlow +import AccountContext + +enum ItemListReactionArrowStyle { + case arrow + case none +} + +final class BackgroundEmojiItem: ListViewItem, ItemListItem { + let context: AccountContext + let presentationData: ItemListPresentationData + let icon: UIImage? + let title: String + let arrowStyle: ItemListReactionArrowStyle + let reaction: MessageReaction.Reaction + let availableReactions: AvailableReactions? + public let sectionId: ItemListSectionId + let style: ItemListStyle + let action: (() -> Void)? + public let tag: ItemListItemTag? + + public init(context: AccountContext, presentationData: ItemListPresentationData, icon: UIImage? = nil, title: String, arrowStyle: ItemListReactionArrowStyle = .none, reaction: MessageReaction.Reaction, availableReactions: AvailableReactions?, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, tag: ItemListItemTag? = nil) { + self.context = context + self.presentationData = presentationData + self.icon = icon + self.title = title + self.arrowStyle = arrowStyle + self.reaction = reaction + self.availableReactions = availableReactions + self.sectionId = sectionId + self.style = style + self.action = action + self.tag = tag + } + + 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 { + Queue.mainQueue().async { + let node = BackgroundEmojiItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + 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? BackgroundEmojiItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } + + public var selectable: Bool = true + + public func selected(listView: ListView){ + listView.clearHighlightAnimated(true) + self.action?() + } +} + +private let badgeFont = Font.regular(15.0) + +final class BackgroundEmojiItemNode: ListViewItemNode, ItemListItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let maskNode: ASImageNode + + let iconNode: ASImageNode + let titleNode: TextNode + let arrowNode: ASImageNode + let iconView: ComponentHostView + + private let activateArea: AccessibilityAreaNode + + private var item: BackgroundEmojiItem? + private var fileDisposable: Disposable? + private var file: TelegramMediaFile? + + override var canBeSelected: Bool { + if let item = self.item, let _ = item.action { + return true + } else { + return false + } + } + + var tag: ItemListItemTag? { + return self.item?.tag + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displaysAsynchronously = false + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + + self.iconView = ComponentHostView() + + self.arrowNode = ASImageNode() + self.arrowNode.displayWithoutProcessing = true + self.arrowNode.displaysAsynchronously = false + self.arrowNode.isLayerBacked = true + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + self.activateArea = AccessibilityAreaNode() + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.titleNode) + self.view.addSubview(self.iconView) + self.addSubnode(self.arrowNode) + + self.addSubnode(self.activateArea) + } + + deinit { + self.fileDisposable?.dispose() + } + + func asyncLayout() -> (_ item: BackgroundEmojiItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + + let currentItem = self.item + + return { item, params, neighbors in + var rightInset: CGFloat + rightInset = 34.0 + params.rightInset + let _ = rightInset + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + var updatedTheme: PresentationTheme? + var updateArrowImage: UIImage? + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) + } + + var updateIcon = false + if currentItem?.icon != item.icon { + updateIcon = true + } + + var leftInset = 16.0 + params.leftInset + if item.icon != nil { + leftInset += 43.0 + } + + var additionalTextRightInset: CGFloat = 0.0 + additionalTextRightInset += 44.0 + if item.arrowStyle == .arrow { + additionalTextRightInset += 24.0 + } + + let titleColor: UIColor = item.presentationData.theme.list.itemPrimaryTextColor + + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - additionalTextRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let verticalInset: CGFloat = 11.0 + + let height: CGFloat + height = verticalInset * 2.0 + titleLayout.size.height + + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: height) + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: height) + insets = itemListNeighborsGroupedInsets(neighbors, params) + } + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + strongSelf.activateArea.accessibilityLabel = item.title + + strongSelf.activateArea.accessibilityTraits = [] + + if let icon = item.icon { + if strongSelf.iconNode.supernode == nil { + strongSelf.addSubnode(strongSelf.iconNode) + } + if updateIcon { + strongSelf.iconNode.image = icon + } + let iconY = floor((layout.contentSize.height - icon.size.height) / 2.0) + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - icon.size.width) / 2.0), y: iconY), size: icon.size) + } else if strongSelf.iconNode.supernode != nil { + strongSelf.iconNode.image = nil + strongSelf.iconNode.removeFromSupernode() + } + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + + } + + if let updateArrowImage = updateArrowImage { + strongSelf.arrowNode.image = updateArrowImage + } + + let _ = titleApply() + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + if strongSelf.maskNode.supernode != nil { + strongSelf.maskNode.removeFromSupernode() + } + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + 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 + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.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: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + } + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) + strongSelf.titleNode.frame = titleFrame + + var animationContent: EmojiStatusComponent.AnimationContent? + switch item.reaction { + case .builtin: + if let availableReactions = item.availableReactions { + for reaction in availableReactions.reactions { + if reaction.value == item.reaction { + animationContent = .file(file: reaction.selectAnimation) + break + } + } + } + case let .custom(fileId): + animationContent = .customEmoji(fileId: fileId) + } + + + var rightInset: CGFloat = 0.0 + if let arrowImage = strongSelf.arrowNode.image, item.arrowStyle == .arrow { + let arrowRightOffset: CGFloat = 7.0 + strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - arrowRightOffset - arrowImage.size.width, y: floorToScreenPixels((height - arrowImage.size.height) / 2.0)), size: arrowImage.size) + rightInset += arrowRightOffset + arrowImage.size.width + strongSelf.arrowNode.isHidden = false + } else { + strongSelf.arrowNode.isHidden = true + } + + if let animationContent = animationContent { + let iconBoundingSize = CGSize(width: 28.0, height: 28.0) + let iconOffsetX: CGFloat = 0.0 + let iconSize = strongSelf.iconView.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: item.context, + animationCache: item.context.animationCache, + animationRenderer: item.context.animationRenderer, + content: .animation(content: animationContent, size: iconBoundingSize, placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .forever), + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: iconBoundingSize + ) + strongSelf.iconView.isUserInteractionEnabled = false + strongSelf.iconView.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - iconSize.width + iconOffsetX - rightInset, y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize) + } + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: height + UIScreenPixel)) + } + }) + } + } + + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift new file mode 100644 index 0000000000..3766da300f --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift @@ -0,0 +1,373 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import Postbox +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import WallpaperBackgroundNode + +class PeerNameColorChatPreviewItem: ListViewItem, ItemListItem { + struct MessageItem: Equatable { + static func == (lhs: MessageItem, rhs: MessageItem) -> Bool { + if lhs.outgoing != rhs.outgoing { + return false + } + if lhs.peerId != rhs.peerId { + return false + } + if lhs.author != rhs.author { + return false + } + if lhs.photo != rhs.photo { + return false + } + if lhs.nameColor != rhs.nameColor { + return false + } + if lhs.backgroundEmojiId != rhs.backgroundEmojiId { + return false + } + if let lhsReply = lhs.reply, let rhsReply = rhs.reply, lhsReply.0 != rhsReply.0 || lhsReply.1 != rhsReply.1 { + return false + } else if (lhs.reply == nil) != (rhs.reply == nil) { + return false + } + if let lhsLinkPreview = lhs.linkPreview, let rhsLinkPreview = rhs.linkPreview, lhsLinkPreview.0 != rhsLinkPreview.0 || lhsLinkPreview.1 != rhsLinkPreview.1 || lhsLinkPreview.2 != rhsLinkPreview.2 { + return false + } else if (lhs.linkPreview == nil) != (rhs.linkPreview == nil) { + return false + } + if lhs.text != rhs.text { + return false + } + return true + } + + let outgoing: Bool + let peerId: EnginePeer.Id + let author: String + let photo: [TelegramMediaImageRepresentation] + let nameColor: PeerNameColor + let backgroundEmojiId: Int64? + let reply: (String, String)? + let linkPreview: (String, String, String)? + let text: String + } + + let context: AccountContext + let theme: PresentationTheme + let componentTheme: PresentationTheme + let strings: PresentationStrings + let sectionId: ItemListSectionId + let fontSize: PresentationFontSize + let chatBubbleCorners: PresentationChatBubbleCorners + let wallpaper: TelegramWallpaper + let dateTimeFormat: PresentationDateTimeFormat + let nameDisplayOrder: PresentationPersonNameOrder + let messageItems: [MessageItem] + + init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, messageItems: [MessageItem]) { + self.context = context + self.theme = theme + self.componentTheme = componentTheme + self.strings = strings + self.sectionId = sectionId + self.fontSize = fontSize + self.chatBubbleCorners = chatBubbleCorners + self.wallpaper = wallpaper + self.dateTimeFormat = dateTimeFormat + self.nameDisplayOrder = nameDisplayOrder + self.messageItems = messageItems + } + + 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 = PeerNameColorChatPreviewItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + 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? PeerNameColorChatPreviewItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } +} + +final class PeerNameColorChatPreviewItemNode: ListViewItemNode { + private var backgroundNode: WallpaperBackgroundNode? + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private let containerNode: ASDisplayNode + private var messageNodes: [ListViewItemNode]? + private var itemHeaderNodes: [ListViewItemNode.HeaderId: ListViewItemHeaderNode] = [:] + + private var item: PeerNameColorChatPreviewItem? + private var finalImage = true + + private let disposable = MetaDisposable() + + init() { + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.containerNode = ASDisplayNode() + self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + + super.init(layerBacked: false, dynamicBounce: false) + + self.clipsToBounds = true + self.isUserInteractionEnabled = false + + self.addSubnode(self.containerNode) + } + + deinit { + self.disposable.dispose() + } + + func asyncLayout() -> (_ item: PeerNameColorChatPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentNodes = self.messageNodes + + var currentBackgroundNode = self.backgroundNode + + return { item, params, neighbors in + if currentBackgroundNode == nil { + currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false) + } + currentBackgroundNode?.update(wallpaper: item.wallpaper) + currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.componentTheme, bubbleCorners: item.chatBubbleCorners) + + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1)) + + var items: [ListViewItem] = [] + for messageItem in item.messageItems.reversed() { + let authorPeerId = messageItem.peerId + + var peers = SimpleDictionary() + var messages = SimpleDictionary() + + peers[authorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: messageItem.author, lastName: "", username: nil, phone: nil, photo: messageItem.photo, botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId) + + let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) + if let (_, text) = messageItem.reply { + messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[authorPeerId], text: text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + } + + var media: [Media] = [] + if let (site, title, text) = messageItem.linkPreview { + media.append(TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "", displayUrl: "", hash: 0, type: nil, websiteName: site, title: title, text: text, embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil, displayOptions: TelegramMediaWebpageDisplayOptions.default)))) + } + + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[authorPeerId], text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil)] : [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, isCentered: false)) + } + + var nodes: [ListViewItemNode] = [] + if let messageNodes = currentNodes { + nodes = messageNodes + for i in 0 ..< items.count { + let itemNode = messageNodes[i] + items[i].updateNode(async: { $0() }, node: { + return itemNode + }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + itemNode.isUserInteractionEnabled = false + + apply(ListViewItemApply(isOnScreen: true)) + }) + } + } else { + var messageNodes: [ListViewItemNode] = [] + for i in 0 ..< items.count { + var itemNode: ListViewItemNode? + items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in + itemNode = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) + itemNode!.isUserInteractionEnabled = false + messageNodes.append(itemNode!) + } + nodes = messageNodes + } + + var contentSize = CGSize(width: params.width, height: 4.0 + 4.0) + for node in nodes { + contentSize.height += node.frame.size.height + } + insets = itemListNeighborsGroupedInsets(neighbors, params) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + let leftInset = params.leftInset + let rightInset = params.leftInset + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) + + strongSelf.messageNodes = nodes + var topOffset: CGFloat = 4.0 + for node in nodes { + if node.supernode == nil { + strongSelf.containerNode.addSubnode(node) + } + node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: node.frame.size), within: layoutSize) + topOffset += node.frame.size.height + + if let header = node.headers()?.last { + let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: 7.0), size: CGSize(width: layoutSize.width, height: header.height)) + let stickLocationDistanceFactor: CGFloat = 0.0 + + let id = header.id + let headerNode: ListViewItemHeaderNode + if let current = strongSelf.itemHeaderNodes[id] { + headerNode = current + headerNode.updateFrame(headerFrame, within: layoutSize) + + if headerNode.item !== header { + header.updateNode(headerNode, previous: nil, next: nil) + headerNode.item = header + } + headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset) + headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) + } else { + headerNode = header.node(synchronousLoad: false) + if headerNode.item !== header { + header.updateNode(headerNode, previous: nil, next: nil) + headerNode.item = header + } + headerNode.frame = headerFrame + headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset) + strongSelf.itemHeaderNodes[id] = headerNode + + strongSelf.containerNode.addSubnode(headerNode) + headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) + } + } + } + + if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode { + strongSelf.backgroundNode = currentBackgroundNode + strongSelf.insertSubnode(currentBackgroundNode, at: 0) + } + + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + 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 + + let backgroundFrame = 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))) + + let displayMode: WallpaperDisplayMode + if abs(params.availableHeight - params.width) < 100.0, params.availableHeight > 700.0 { + displayMode = .halfAspectFill + } else { + if backgroundFrame.width > backgroundFrame.height * 4.0 { + if params.availableHeight < 700.0 { + displayMode = .halfAspectFill + } else { + displayMode = .aspectFill + } + } else { + displayMode = .aspectFill + } + } + + if let backgroundNode = strongSelf.backgroundNode { + backgroundNode.frame = backgroundFrame.insetBy(dx: 0.0, dy: -100.0) + 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)) + } + }) + } + } + + override 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) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift new file mode 100644 index 0000000000..6c4357b347 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift @@ -0,0 +1,538 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import MergeLists +import ItemListUI +import PresentationDataUtils + +private enum PeerNameColorEntryId: Hashable { + case color(Int32) +} + +private enum PeerNameColorEntry: Comparable, Identifiable { + case color(Int, PeerNameColor, Bool) + + var stableId: PeerNameColorEntryId { + switch self { + case let .color(_, color, _): + return .color(color.rawValue) + } + } + + static func ==(lhs: PeerNameColorEntry, rhs: PeerNameColorEntry) -> Bool { + switch lhs { + case let .color(lhsIndex, lhsAccentColor, lhsSelected): + if case let .color(rhsIndex, rhsAccentColor, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsAccentColor == rhsAccentColor, lhsSelected == rhsSelected { + return true + } else { + return false + } + } + } + + static func <(lhs: PeerNameColorEntry, rhs: PeerNameColorEntry) -> Bool { + switch lhs { + case let .color(lhsIndex, _, _): + switch rhs { + case let .color(rhsIndex, _, _): + return lhsIndex < rhsIndex + } + } + } + + func item(action: @escaping (PeerNameColor) -> Void) -> ListViewItem { + switch self { + case let .color(_, color, selected): + return PeerNameColorIconItem(color: color, selected: selected, action: action) + } + } +} + + +private class PeerNameColorIconItem: ListViewItem { + let color: PeerNameColor + let selected: Bool + let action: (PeerNameColor) -> Void + + public init(color: PeerNameColor, selected: Bool, action: @escaping (PeerNameColor) -> Void) { + self.color = color + self.selected = selected + self.action = action + } + + 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 = PeerNameColorIconItemNode() + let (nodeLayout, apply) = node.asyncLayout()(self, params) + node.insets = nodeLayout.insets + node.contentSize = nodeLayout.contentSize + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in + apply(false) + }) + }) + } + } + } + + 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 { + assert(node() is PeerNameColorIconItemNode) + if let nodeValue = node() as? PeerNameColorIconItemNode { + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params) + Queue.mainQueue().async { + completion(nodeLayout, { _ in + let animated: Bool + if case .Crossfade = animation { + animated = true + } else { + animated = false + } + apply(animated) + }) + } + } + } + } + } + + public var selectable = true + public func selected(listView: ListView) { + self.action(self.color) + } +} + +private func generateRingImage(nameColor: PeerNameColor) -> UIImage? { + return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setStrokeColor(nameColor.color.cgColor) + context.setLineWidth(2.0) + context.strokeEllipse(in: bounds.insetBy(dx: 1.0, dy: 1.0)) + }) +} + +private func generateFillImage(nameColor: PeerNameColor) -> UIImage? { + return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let circleBounds = bounds + context.addEllipse(in: circleBounds) + context.clip() + + let (firstColor, secondColor) = nameColor.dashColors + if let secondColor { + context.setFillColor(secondColor.cgColor) + context.fill(circleBounds) + + context.move(to: .zero) + context.addLine(to: CGPoint(x: size.width, y: 0.0)) + context.addLine(to: CGPoint(x: 0.0, y: size.height)) + context.closePath() + context.setFillColor(firstColor.cgColor) + context.fillPath() + } else { + context.setFillColor(firstColor.cgColor) + context.fill(circleBounds) + } + }) +} + +private final class PeerNameColorIconItemNode : ListViewItemNode { + private let containerNode: ContextControllerSourceNode + private let fillNode: ASImageNode + private let ringNode: ASImageNode + + var item: PeerNameColorIconItem? + + init() { + self.containerNode = ContextControllerSourceNode() + + self.fillNode = ASImageNode() + self.fillNode.displaysAsynchronously = false + self.fillNode.displayWithoutProcessing = true + + self.ringNode = ASImageNode() + self.ringNode.displaysAsynchronously = false + self.ringNode.displayWithoutProcessing = true + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.ringNode) + self.containerNode.addSubnode(self.fillNode) + } + + override func didLoad() { + super.didLoad() + + self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + } + + func setSelected(_ selected: Bool, animated: Bool = false) { + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate + if selected { + transition.updateTransformScale(node: self.fillNode, scale: 0.8) + } else { + transition.updateTransformScale(node: self.fillNode, scale: 1.0) + } + } + + func asyncLayout() -> (PeerNameColorIconItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let currentItem = self.item + + return { [weak self] item, params in + var updatedAccentColor = false + var updatedSelected = false + + if currentItem == nil || currentItem?.color != item.color { + updatedAccentColor = true + } + if currentItem?.selected != item.selected { + updatedSelected = true + } + + let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 60.0, height: 56.0), insets: UIEdgeInsets()) + return (itemLayout, { animated in + if let strongSelf = self { + strongSelf.item = item + + if updatedAccentColor { + strongSelf.fillNode.image = generateFillImage(nameColor: item.color) + strongSelf.ringNode.image = generateRingImage(nameColor: item.color) + } + + let center = CGPoint(x: 30.0, y: 28.0) + let bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 40.0, height: 40.0)) + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: itemLayout.contentSize) + + strongSelf.fillNode.position = center + strongSelf.ringNode.position = center + + strongSelf.fillNode.bounds = bounds + strongSelf.ringNode.bounds = bounds + + if updatedSelected { + strongSelf.setSelected(item.selected, animated: !updatedAccentColor && currentItem != nil) + } + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + super.animateRemoved(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + super.animateAdded(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } +} + +final class PeerNameColorItem: ListViewItem, ItemListItem { + var sectionId: ItemListSectionId + + let theme: PresentationTheme + let colors: [PeerNameColor] + let currentColor: PeerNameColor + let updated: (PeerNameColor) -> Void + let tag: ItemListItemTag? + + init(theme: PresentationTheme, colors: [PeerNameColor], currentColor: PeerNameColor, updated: @escaping (PeerNameColor) -> Void, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId) { + self.theme = theme + self.colors = colors + self.currentColor = currentColor + self.updated = updated + self.tag = tag + 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) { + async { + let node = PeerNameColorItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + 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? PeerNameColorItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } +} + +private struct PeerNameColorItemNodeTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let updatePosition: Bool +} + +private func preparedTransition(action: @escaping (PeerNameColor) -> Void, from fromEntries: [PeerNameColorEntry], to toEntries: [PeerNameColorEntry], updatePosition: Bool) -> PeerNameColorItemNodeTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(action: action), directionHint: .Down) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(action: action), directionHint: nil) } + + return PeerNameColorItemNodeTransition(deletions: deletions, insertions: insertions, updates: updates, updatePosition: updatePosition) +} + +private func ensureColorVisible(listNode: ListView, color: PeerNameColor, animated: Bool) -> Bool { + var resultNode: PeerNameColorIconItemNode? + listNode.forEachItemNode { node in + if resultNode == nil, let node = node as? PeerNameColorIconItemNode { + if node.item?.color == color { + resultNode = node + } + } + } + if let resultNode = resultNode { + listNode.ensureItemNodeVisible(resultNode, animated: animated, overflow: 24.0) + return true + } else { + return false + } +} + +final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { + private let containerNode: ASDisplayNode + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private let listNode: ListView + private var entries: [PeerNameColorEntry]? + private var enqueuedTransitions: [PeerNameColorItemNodeTransition] = [] + private var initialized = false + + private var item: PeerNameColorItem? + private var layoutParams: ListViewItemLayoutParams? + + private var tapping = false + + var tag: ItemListItemTag? { + return self.item?.tag + } + + init() { + self.containerNode = ASDisplayNode() + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.listNode = ListView() + self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.containerNode) + self.addSubnode(self.listNode) + } + + override func didLoad() { + super.didLoad() + self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true + } + + private func enqueueTransition(_ transition: PeerNameColorItemNodeTransition) { + self.enqueuedTransitions.append(transition) + + if let _ = self.item { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + guard let item = self.item, let transition = self.enqueuedTransitions.first else { + return + } + self.enqueuedTransitions.remove(at: 0) + + let options = ListViewDeleteAndInsertOptions() + var scrollToItem: ListViewScrollToItem? + if !self.initialized || transition.updatePosition || !self.tapping { + if let index = item.colors.firstIndex(where: { $0 == item.currentColor }) { + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(-70.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down) + self.initialized = true + } + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in + }) + } + + func asyncLayout() -> (_ item: PeerNameColorItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + + return { item, params, neighbors in + var themeUpdated = false + if currentItem?.theme !== item.theme { + themeUpdated = true + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + contentSize = CGSize(width: params.width, height: 60.0) + insets = itemListNeighborsGroupedInsets(neighbors, params) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + if themeUpdated { + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + } + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.containerNode.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.containerNode.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.containerNode.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + 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 { + 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 = params.leftInset + 16.0 + bottomStripeOffset = -separatorHeight + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + 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 + listInsets.bottom += params.rightInset + 8.0 + + strongSelf.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width) + strongSelf.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0) + strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + var entries: [PeerNameColorEntry] = [] + + var index: Int = 0 + for color in item.colors { + entries.append(.color(index, color, color == item.currentColor)) + index += 1 + } + + let action: (PeerNameColor) -> Void = { [weak self] color in + guard let self else { + return + } + self.tapping = true + item.updated(color) + Queue.mainQueue().after(0.4) { + self.tapping = false + } + let _ = ensureColorVisible(listNode: self.listNode, color: color, animated: true) + } + + let previousEntries = strongSelf.entries ?? [] + let updatePosition = currentItem != nil && previousEntries.count != entries.count + let transition = preparedTransition(action: action, from: previousEntries, to: entries, updatePosition: updatePosition) + strongSelf.enqueueTransition(transition) + + strongSelf.entries = entries + } + }) + } + } + + override 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) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift new file mode 100644 index 0000000000..5c2edf9f06 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift @@ -0,0 +1,471 @@ +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 + +private final class PeerNameColorScreenArguments { + let context: AccountContext + let updateNameColor: (PeerNameColor) -> Void + let openBackgroundEmoji: () -> Void + + init( + context: AccountContext, + updateNameColor: @escaping (PeerNameColor) -> Void, + openBackgroundEmoji: @escaping () -> Void + ) { + self.context = context + self.updateNameColor = updateNameColor + self.openBackgroundEmoji = openBackgroundEmoji + } +} + +private enum PeerNameColorScreenSection: Int32 { + case nameColor + case backgroundEmoji +} + +private enum PeerNameColorScreenEntry: ItemListNodeEntry { + enum StableId: Hashable { + case colorHeader + case colorMessage + case colorPicker + case colorDescription + case backgroundEmoji + case backgroundEmojiDescription + } + + case colorHeader(String) + case colorMessage(wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, items: [PeerNameColorChatPreviewItem.MessageItem]) + case colorPicker(colors: [PeerNameColor], currentColor: PeerNameColor) + case colorDescription(String) + case backgroundEmoji(String, MessageReaction.Reaction, AvailableReactions) + case backgroundEmojiDescription(String) + + var section: ItemListSectionId { + switch self { + case .colorHeader, .colorMessage, .colorPicker, .colorDescription: + return PeerNameColorScreenSection.nameColor.rawValue + case .backgroundEmoji, .backgroundEmojiDescription: + return PeerNameColorScreenSection.backgroundEmoji.rawValue + } + } + + var stableId: StableId { + switch self { + case .colorHeader: + return .colorHeader + case .colorMessage: + return .colorMessage + case .colorPicker: + return .colorPicker + case .colorDescription: + return .colorDescription + case .backgroundEmoji: + return .backgroundEmoji + case .backgroundEmojiDescription: + return .backgroundEmojiDescription + } + } + + var sortId: Int { + switch self { + case .colorHeader: + return 0 + case .colorMessage: + return 1 + case .colorPicker: + return 2 + case .colorDescription: + return 3 + case .backgroundEmoji: + return 4 + case .backgroundEmojiDescription: + return 5 + } + } + + static func ==(lhs: PeerNameColorScreenEntry, rhs: PeerNameColorScreenEntry) -> Bool { + switch lhs { + case let .colorHeader(text): + if case .colorHeader(text) = rhs { + return true + } else { + return false + } + case let .colorMessage(lhsWallpaper, lhsFontSize, lhsBubbleCorners, lhsDateTimeFormat, lhsNameDisplayOrder, lhsItems): + if case let .colorMessage(rhsWallpaper, rhsFontSize, rhsBubbleCorners, rhsDateTimeFormat, rhsNameDisplayOrder, rhsItems) = rhs, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsBubbleCorners == rhsBubbleCorners, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, lhsItems == rhsItems { + return true + } else { + return false + } + case let .colorPicker(lhsColors, lhsCurrentColor): + if case let .colorPicker(rhsColors, rhsCurrentColor) = rhs, lhsColors == rhsColors, lhsCurrentColor == rhsCurrentColor { + return true + } else { + return false + } + case let .colorDescription(text): + if case .colorDescription(text) = rhs { + return true + } else { + return false + } + case let .backgroundEmoji(lhsText, lhsReaction, lhsAvailableReactions): + if case let .backgroundEmoji(rhsText, rhsReaction, rhsAvailableReactions) = rhs, lhsText == rhsText, lhsReaction == rhsReaction, lhsAvailableReactions == rhsAvailableReactions { + return true + } else { + return false + } + case let .backgroundEmojiDescription(text): + if case .backgroundEmojiDescription(text) = rhs { + return true + } else { + return false + } + } + } + + static func <(lhs: PeerNameColorScreenEntry, rhs: PeerNameColorScreenEntry) -> Bool { + return lhs.sortId < rhs.sortId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! PeerNameColorScreenArguments + switch self { + case let .colorHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .colorMessage(wallpaper, fontSize, chatBubbleCorners, dateTimeFormat, nameDisplayOrder, items): + return PeerNameColorChatPreviewItem( + context: arguments.context, + theme: presentationData.theme, + componentTheme: presentationData.theme, + strings: presentationData.strings, + sectionId: self.section, + fontSize: fontSize, + chatBubbleCorners: chatBubbleCorners, + wallpaper: wallpaper, + dateTimeFormat: dateTimeFormat, + nameDisplayOrder: nameDisplayOrder, + messageItems: items) + case let .colorPicker(colors, currentColor): + return PeerNameColorItem( + theme: presentationData.theme, + colors: colors, + currentColor: currentColor, + updated: { color in + arguments.updateNameColor(color) + }, + sectionId: self.section + ) + case let .colorDescription(text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .backgroundEmoji(title, reaction, availableReactions): + return BackgroundEmojiItem( + context: arguments.context, + presentationData: presentationData, + title: title, + reaction: reaction, + availableReactions: availableReactions, + sectionId: self.section, + style: .blocks, + action: { + arguments.openBackgroundEmoji() + }) + case let .backgroundEmojiDescription(text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + } + } +} + +private struct PeerNameColorScreenState: Equatable { + var updatedNameColor: PeerNameColor? +} + +private func peerNameColorScreenEntries( + presentationData: PresentationData, + state: PeerNameColorScreenState, + peer: EnginePeer?, + isPremium: Bool +) -> [PeerNameColorScreenEntry] { + var entries: [PeerNameColorScreenEntry] = [] + + if let peer { + var allColors: [PeerNameColor] = [ + .blue + ] + allColors.append(contentsOf: PeerNameColor.allCases.filter { $0 != .blue}) + allColors.removeLast(3) + + let nameColor: PeerNameColor + if let updatedNameColor = state.updatedNameColor { + nameColor = updatedNameColor + } else if let peerNameColor = peer.nameColor { + nameColor = peerNameColor + } else { + nameColor = .blue + } + let messageItem = PeerNameColorChatPreviewItem.MessageItem( + outgoing: false, + peerId: peer.id, + author: peer.compactDisplayTitle, + photo: peer.profileImageRepresentations, + nameColor: nameColor, + backgroundEmojiId: nil, + reply: (peer.compactDisplayTitle, presentationData.strings.NameColor_ChatPreview_ReplyText), + linkPreview: (presentationData.strings.NameColor_ChatPreview_LinkSite, presentationData.strings.NameColor_ChatPreview_LinkTitle, presentationData.strings.NameColor_ChatPreview_LinkText), + text: presentationData.strings.NameColor_ChatPreview_MessageText + ) + + entries.append(.colorHeader(presentationData.strings.NameColor_ChatPreview_Title)) + entries.append(.colorMessage( + wallpaper: presentationData.chatWallpaper, + fontSize: presentationData.chatFontSize, + bubbleCorners: presentationData.chatBubbleCorners, + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + items: [messageItem] + )) + entries.append(.colorPicker( + colors: allColors, + currentColor: nameColor + )) + entries.append(.colorDescription(presentationData.strings.NameColor_ChatPreview_Description_Account)) + } + +// entries.append(.backgroundEmoji(presentationData.strings.Settings_QuickReactionSetup_ChooseQuickReaction, reactionSettings.quickReaction, availableReactions)) +// entries.append(.backgroundEmojiDescription(presentationData.strings.Settings_QuickReactionSetup_ChooseQuickReactionInfo)) + + return entries +} + +public enum PeerNameColorScreenSubject { + case account + case channel(EnginePeer.Id) +} + +public func PeerNameColorScreen( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + subject: PeerNameColorScreenSubject +) -> ViewController { + let statePromise = ValuePromise(PeerNameColorScreenState(), ignoreRepeated: true) + let stateValue = Atomic(value: PeerNameColorScreenState()) + let updateState: ((PeerNameColorScreenState) -> PeerNameColorScreenState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var presentImpl: ((ViewController) -> Void)? + var pushImpl: ((ViewController) -> Void)? + var dismissImpl: (() -> Void)? + +// var openQuickReactionImpl: (() -> Void)? + + let actionsDisposable = DisposableSet() + + let arguments = PeerNameColorScreenArguments( + context: context, + updateNameColor: { color in + updateState { state in + var updatedState = state + updatedState.updatedNameColor = color + return updatedState + } + }, + openBackgroundEmoji: { + + } + ) + + let peerId: EnginePeer.Id + switch subject { + case .account: + peerId = context.account.peerId + case let .channel(channelId): + peerId = channelId + } + + let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData + let signal = combineLatest(queue: .mainQueue(), + presentationData, + statePromise.get(), + context.engine.stickers.availableReactions(), + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + ) + |> deliverOnMainQueue + |> map { presentationData, state, availableReactions, peer -> (ItemListControllerState, (ItemListNodeState, Any)) in + let isPremium = peer?.isPremium ?? false + let title: String + switch subject { + case .account: + title = presentationData.strings.NameColor_Title_Account + case .channel: + title = presentationData.strings.NameColor_Title_Channel + } + + let footerItem = ApplyColorFooterItem( + theme: presentationData.theme, + title: presentationData.strings.NameColor_ApplyColor, + locked: !isPremium, + action: { + if isPremium { + let state = stateValue.with { $0 } + if let nameColor = state.updatedNameColor { + let _ = context.engine.accountData.updateNameColorAndEmoji(nameColor: nameColor, backgroundEmojiId: nil).startStandalone() + } + dismissImpl?() + } else { + let controller = UndoOverlayController( + presentationData: presentationData, + content: .premiumPaywall( + title: nil, + text: presentationData.strings.NameColor_TooltipPremium_Account, + customUndoText: nil, + timeout: nil, + linkAction: nil + ), + elevatedLayout: false, + action: { action in + if case .info = action { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesSuggestedReactions, forceDark: false, dismissed: nil) + pushImpl?(controller) + } + return true + } + ) + presentImpl?(controller) + } + } + ) + + let entries = peerNameColorScreenEntries( + presentationData: presentationData, + state: state, + peer: peer, + isPremium: isPremium + ) + + let controllerState = ItemListControllerState( + presentationData: ItemListPresentationData(presentationData), + title: .text(title), + leftNavigationButton: nil, + rightNavigationButton: nil, + backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), + animateChanges: false + ) + let listState = ItemListNodeState( + presentationData: ItemListPresentationData(presentationData), + entries: entries, + style: .blocks, + footerItem: footerItem, + animateChanges: true + ) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(context: context, state: signal) +// openQuickReactionImpl = { [weak controller] in +// let _ = (combineLatest(queue: .mainQueue(), +// settings, +// context.engine.stickers.availableReactions() +// ) +// |> take(1) +// |> deliverOnMainQueue).start(next: { settings, availableReactions in +// var currentSelectedFileId: MediaId? +// switch settings.quickReaction { +// case .builtin: +// if let availableReactions = availableReactions { +// if let reaction = availableReactions.reactions.first(where: { $0.value == settings.quickReaction }) { +// currentSelectedFileId = reaction.selectAnimation.fileId +// break +// } +// } +// case let .custom(fileId): +// currentSelectedFileId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) +// } +// +// var selectedItems = Set() +// if let currentSelectedFileId = currentSelectedFileId { +// selectedItems.insert(currentSelectedFileId) +// } +// +// guard let controller = controller else { +// return +// } +// var sourceItemNode: ItemListReactionItemNode? +// controller.forEachItemNode { itemNode in +// if let itemNode = itemNode as? ItemListReactionItemNode { +// sourceItemNode = itemNode +// } +// } +// +// if let sourceItemNode = sourceItemNode { +// controller.present(EmojiStatusSelectionController( +// context: context, +// mode: .quickReactionSelection(completion: { +// updateState { state in +// var state = state +// state.hasReaction = false +// return state +// } +// }), +// sourceView: sourceItemNode.iconView, +// emojiContent: EmojiPagerContentComponent.emojiInputData( +// context: context, +// animationCache: context.animationCache, +// animationRenderer: context.animationRenderer, +// isStandalone: false, +// isStatusSelection: false, +// isReactionSelection: true, +// isEmojiSelection: false, +// hasTrending: false, +// isQuickReactionSelection: true, +// topReactionItems: [], +// areUnicodeEmojiEnabled: false, +// areCustomEmojiEnabled: true, +// chatPeerId: context.account.peerId, +// selectedItems: selectedItems +// ), +// currentSelection: nil, +// destinationItemView: { [weak sourceItemNode] in +// return sourceItemNode?.iconView +// } +// ), in: .window(.root)) +// } +// }) +// } + + presentImpl = { [weak controller] c in + guard let controller else { + return + } + controller.present(c, in: .current) + } + pushImpl = { [weak controller] c in + guard let controller else { + return + } + controller.push(c) + } + dismissImpl = { [weak controller] in + guard let controller else { + return + } + controller.dismiss() + } + + return controller +} + diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index c0dc53d556..5b67c73eb6 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -1312,7 +1312,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { var contentHeight: CGFloat = 20.0 let margin: CGFloat = 12.0 - let leftMargin = 12.0 + layout.insets(options: []).left + let leftMargin = margin + layout.insets(options: []).left let buttonTextSize = self.undoButtonTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) let buttonMinX: CGFloat From b6f0d7ee7bc0b389111f89e19c43e05e667b1726 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 17 Oct 2023 02:25:15 +0400 Subject: [PATCH 2/6] Name colors improvements --- .../AvatarNode/Sources/AvatarNode.swift | 30 +++++++++++-------- .../Sources/AvatarVideoNode.swift | 12 ++++---- .../Sources/ChatMessageDateHeader.swift | 12 +++++++- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 79369bc56c..d9b2958c69 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -63,7 +63,7 @@ private class AvatarNodeParameters: NSObject { } } -private func calculateColors(explicitColorIndex: Int?, peerId: EnginePeer.Id?, icon: AvatarNodeIcon, theme: PresentationTheme?) -> [UIColor] { +private func calculateColors(explicitColorIndex: Int?, peerId: EnginePeer.Id?, nameColor: PeerNameColor?, icon: AvatarNodeIcon, theme: PresentationTheme?) -> [UIColor] { let colorIndex: Int if let explicitColorIndex = explicitColorIndex { colorIndex = explicitColorIndex @@ -110,7 +110,11 @@ private func calculateColors(explicitColorIndex: Int?, peerId: EnginePeer.Id?, i colors = AvatarNode.grayscaleColors } } else { - colors = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count] + if let nameColor { + colors = AvatarNode.gradientColors[Int(nameColor.rawValue) % AvatarNode.gradientColors.count] + } else { + colors = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count] + } } return colors @@ -122,7 +126,7 @@ public enum AvatarNodeExplicitIcon { private enum AvatarNodeState: Equatable { case empty - case peerAvatar(EnginePeer.Id, [String], TelegramMediaImageRepresentation?, AvatarNodeClipStyle) + case peerAvatar(EnginePeer.Id, PeerNameColor?, [String], TelegramMediaImageRepresentation?, AvatarNodeClipStyle) case custom(letter: [String], explicitColorIndex: Int?, explicitIcon: AvatarNodeExplicitIcon?) } @@ -130,8 +134,8 @@ private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool { switch (lhs, rhs) { case (.empty, .empty): return true - case let (.peerAvatar(lhsPeerId, lhsLetters, lhsPhotoRepresentations, lhsClipStyle), .peerAvatar(rhsPeerId, rhsLetters, rhsPhotoRepresentations, rhsClipStyle)): - return lhsPeerId == rhsPeerId && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations && lhsClipStyle == rhsClipStyle + case let (.peerAvatar(lhsPeerId, lhsPeerNameColor, lhsLetters, lhsPhotoRepresentations, lhsClipStyle), .peerAvatar(rhsPeerId, rhsPeerNameColor, rhsLetters, rhsPhotoRepresentations, rhsClipStyle)): + return lhsPeerId == rhsPeerId && lhsPeerNameColor == rhsPeerNameColor && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations && lhsClipStyle == rhsClipStyle case let (.custom(lhsLetters, lhsIndex, lhsIcon), .custom(rhsLetters, rhsIndex, rhsIcon)): return lhsLetters == rhsLetters && lhsIndex == rhsIndex && lhsIcon == rhsIcon default: @@ -450,7 +454,7 @@ public final class AvatarNode: ASDisplayNode { } else if peer?.restrictionText(platform: "ios", contentSettings: contentSettings) == nil { representation = peer?.smallProfileImage } - let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.displayLetters ?? [], representation, clipStyle) + let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.nameColor, peer?.displayLetters ?? [], representation, clipStyle) if updatedState != self.state || overrideImage != self.overrideImage || theme !== self.theme { self.state = updatedState self.overrideImage = overrideImage @@ -485,7 +489,7 @@ public final class AvatarNode: ASDisplayNode { self.editOverlayNode?.isHidden = true } - parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer.id, colors: calculateColors(explicitColorIndex: nil, peerId: peer.id, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) + parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer.id, colors: calculateColors(explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) } else { self.imageReady.set(.single(true)) self.displaySuspended = false @@ -494,7 +498,7 @@ public final class AvatarNode: ASDisplayNode { } self.editOverlayNode?.isHidden = true - let colors = calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), icon: icon, theme: theme) + let colors = calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme) parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle) if let badgeView = self.badgeView { @@ -614,7 +618,7 @@ public final class AvatarNode: ASDisplayNode { } else if peer?.restrictionText(platform: "ios", contentSettings: genericContext.currentContentSettings.with { $0 }) == nil { representation = peer?.smallProfileImage } - let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.displayLetters ?? [], representation, clipStyle) + let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.nameColor, peer?.displayLetters ?? [], representation, clipStyle) if updatedState != self.state || overrideImage != self.overrideImage || theme !== self.theme { self.state = updatedState self.overrideImage = overrideImage @@ -651,7 +655,7 @@ public final class AvatarNode: ASDisplayNode { self.editOverlayNode?.isHidden = true } - parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, colors: calculateColors(explicitColorIndex: nil, peerId: peer.id, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) + parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, colors: calculateColors(explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) } else { self.imageReady.set(.single(true)) self.displaySuspended = false @@ -660,7 +664,7 @@ public final class AvatarNode: ASDisplayNode { } self.editOverlayNode?.isHidden = true - let colors = calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), icon: icon, theme: theme) + let colors = calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme) parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle) if let badgeView = self.badgeView { @@ -697,9 +701,9 @@ public final class AvatarNode: ASDisplayNode { let parameters: AvatarNodeParameters if let icon = icon, case .phone = icon { - parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(explicitColorIndex: explicitIndex, peerId: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) + parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) } else { - parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(explicitColorIndex: explicitIndex, peerId: nil, icon: .none, theme: nil), letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) + parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .none, theme: nil), letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) } self.displaySuspended = true diff --git a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift index b603ca5173..6b17989a2e 100644 --- a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift +++ b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift @@ -25,7 +25,7 @@ public final class AvatarVideoNode: ASDisplayNode { private var emojiMarkup: TelegramMediaImage.EmojiMarkup? - private var fileDisposable: Disposable? + private var fileDisposable = MetaDisposable() private var animationFile: TelegramMediaFile? private var itemLayer: EmojiPagerContentComponent.View.ItemLayer? private var useAnimationNode = false @@ -55,7 +55,7 @@ public final class AvatarVideoNode: ASDisplayNode { } deinit { - self.fileDisposable?.dispose() + self.fileDisposable.dispose() self.stickerFetchedDisposable.dispose() self.playbackStartDisposable.dispose() } @@ -174,15 +174,15 @@ public final class AvatarVideoNode: ASDisplayNode { switch markup.content { case let .emoji(fileId): - self.fileDisposable = (self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) + self.fileDisposable.set((self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) |> deliverOnMainQueue).startStrict(next: { [weak self] files in if let strongSelf = self, let file = files.values.first { strongSelf.animationFile = file strongSelf.setupAnimation() } - }).strict() + })) case let .sticker(packReference, fileId): - self.fileDisposable = (self.context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: false) + self.fileDisposable.set((self.context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: false) |> map { pack -> TelegramMediaFile? in if case let .result(_, items, _) = pack, let item = items.first(where: { $0.file.fileId.id == fileId }) { return item.file @@ -194,7 +194,7 @@ public final class AvatarVideoNode: ASDisplayNode { strongSelf.animationFile = file strongSelf.setupAnimation() } - }).strict() + })) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index 9253684a1f..17821e75d8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -425,6 +425,9 @@ public final class ChatMessageAvatarHeader: ListViewItemHeader { return } node.updatePresentationData(self.presentationData, context: self.context) + if let peer = self.peer { + node.updatePeer(peer: peer) + } node.updateStoryStats(storyStats: self.storyStats, theme: self.presentationData.theme.theme, force: false) } } @@ -441,7 +444,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode { private let peerId: PeerId private let messageReference: MessageReference? - private let peer: Peer? + private var peer: Peer? private let adMessageId: EngineMessage.Id? private let containerNode: ContextControllerSourceNode @@ -518,6 +521,13 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode { self.avatarNode.setCustomLetters(letters, icon: !letters.isEmpty ? nil : .phone) } + + public func updatePeer(peer: Peer) { + if let previousPeer = self.peer, previousPeer.nameColor != peer.nameColor { + self.peer = peer + self.avatarNode.setPeer(context: self.context, theme: self.presentationData.theme.theme, peer: EnginePeer(peer), authorOfMessage: self.messageReference, overrideImage: nil, emptyColor: .black, synchronousLoad: false, displayDimensions: CGSize(width: 38.0, height: 38.0)) + } + } public func setPeer(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, peer: Peer, authorOfMessage: MessageReference?, emptyColor: UIColor) { self.containerNode.isGestureEnabled = peer.smallProfileImage != nil From 194b07d1d846aeacfd12e5eac95377cb1aa5a753 Mon Sep 17 00:00:00 2001 From: Mike Renoir <> Date: Tue, 17 Oct 2023 09:08:19 +0400 Subject: [PATCH 3/6] namecolor --- .../TelegramCore/Sources/Utils/PeerUtils.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index 725f889a33..87fb4f0475 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -52,6 +52,18 @@ public extension Peer { return nil } } + var nameColor: PeerNameColor? { + switch self { + case let user as TelegramUser: + return user.nameColor + case let group as TelegramGroup: + return group.nameColor + case let channel as TelegramChannel: + return channel.nameColor + default: + return nil + } + } var usernames: [TelegramPeerUsername] { switch self { From 1b50b16bee0f0fd957a63c8294cfbe4275c1d455 Mon Sep 17 00:00:00 2001 From: Mike Renoir <> Date: Tue, 17 Oct 2023 13:15:19 +0400 Subject: [PATCH 4/6] giftcodes for invoice --- .../Payments/BotPaymentForm.swift | 86 +++++++++++-------- .../TelegramEngine/Payments/GiftCodes.swift | 18 +++- .../Sources/Utils/PeerUtils.swift | 12 --- 3 files changed, 64 insertions(+), 52 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 12926a2549..65ac82ad77 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -7,8 +7,10 @@ import TelegramApi public enum BotPaymentInvoiceSource { case message(MessageId) case slug(String) + case premiumGiftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, quantity: Int32, option: PremiumGiftCodeOption) } + public struct BotPaymentInvoiceFields: OptionSet { public var rawValue: Int32 @@ -203,17 +205,51 @@ extension BotPaymentRequestedInfo { } } +private func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInvoiceSource) -> Api.InputInvoice? { + switch source { + case let .message(messageId): + guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { + return nil + } + return .inputInvoiceMessage(peer: inputPeer, msgId: messageId.id) + case let .slug(slug): + return .inputInvoiceSlug(slug: slug) + case let .premiumGiftCode(peerIds, boostPeerId, quantity, option): + + var flags: Int32 = 0 + var apiBoostPeer: Api.InputPeer? + var apiInputUsers: [Api.InputUser] = [] + + for peerId in peerIds { + if let user = transaction.getPeer(peerId), let apiUser = apiInputUser(user) { + apiInputUsers.append(apiUser) + } + } + + if let boostPeerId = boostPeerId, let boostPeer = transaction.getPeer(boostPeerId), let apiPeer = apiInputPeer(boostPeer) { + apiBoostPeer = apiPeer + flags |= (1 << 0) + } + let input: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: option.currency, amount: option.amount) + + flags = 0 + + if let _ = option.storeProductId { + flags |= (1 << 0) + } + if option.storeQuantity > 0 { + flags |= (1 << 1) + } + + let option: Api.PremiumGiftCodeOption = .premiumGiftCodeOption(flags: flags, users: quantity, months: option.months, storeProduct: option.storeProductId, storeQuantity: option.storeQuantity, currency: option.currency, amount: option.amount) + + return .inputInvoicePremiumGiftCode(purpose: input, option: option) + } +} + func _internal_fetchBotPaymentInvoice(postbox: Postbox, network: Network, source: BotPaymentInvoiceSource) -> Signal { return postbox.transaction { transaction -> Api.InputInvoice? in - switch source { - case let .message(messageId): - guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { - return nil - } - return .inputInvoiceMessage(peer: inputPeer, msgId: messageId.id) - case let .slug(slug): - return .inputInvoiceSlug(slug: slug) - } + return _internal_parseInputInvoice(transaction: transaction, source: source) } |> castError(BotPaymentFormRequestError.self) |> mapToSignal { invoice -> Signal in @@ -251,15 +287,7 @@ func _internal_fetchBotPaymentInvoice(postbox: Postbox, network: Network, source func _internal_fetchBotPaymentForm(accountPeerId: PeerId, postbox: Postbox, network: Network, source: BotPaymentInvoiceSource, themeParams: [String: Any]?) -> Signal { return postbox.transaction { transaction -> Api.InputInvoice? in - switch source { - case let .message(messageId): - guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { - return nil - } - return .inputInvoiceMessage(peer: inputPeer, msgId: messageId.id) - case let .slug(slug): - return .inputInvoiceSlug(slug: slug) - } + return _internal_parseInputInvoice(transaction: transaction, source: source) } |> castError(BotPaymentFormRequestError.self) |> mapToSignal { invoice -> Signal in @@ -354,15 +382,7 @@ extension BotPaymentShippingOption { func _internal_validateBotPaymentForm(account: Account, saveInfo: Bool, source: BotPaymentInvoiceSource, formInfo: BotPaymentRequestedInfo) -> Signal { return account.postbox.transaction { transaction -> Api.InputInvoice? in - switch source { - case let .message(messageId): - guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { - return nil - } - return .inputInvoiceMessage(peer: inputPeer, msgId: messageId.id) - case let .slug(slug): - return .inputInvoiceSlug(slug: slug) - } + return _internal_parseInputInvoice(transaction: transaction, source: source) } |> castError(ValidateBotPaymentFormError.self) |> mapToSignal { invoice -> Signal in @@ -440,15 +460,7 @@ public enum SendBotPaymentResult { func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPaymentInvoiceSource, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal { return account.postbox.transaction { transaction -> Api.InputInvoice? in - switch source { - case let .message(messageId): - guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { - return nil - } - return .inputInvoiceMessage(peer: inputPeer, msgId: messageId.id) - case let .slug(slug): - return .inputInvoiceSlug(slug: slug) - } + return _internal_parseInputInvoice(transaction: transaction, source: source) } |> castError(SendBotPaymentFormError.self) |> mapToSignal { invoice -> Signal in @@ -510,6 +522,8 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa } } } + case let .premiumGiftCode(peerIds, boostPeer, quantity, option): + receiptMessageId = nil } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift index 1304e70ce0..82d096254f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift @@ -20,18 +20,23 @@ public struct PremiumGiftCodeOption: Codable, Equatable { case months case storeProductId case storeQuantity + case currency + case amount } public let users: Int32 public let months: Int32 public let storeProductId: String? public let storeQuantity: Int32 - - public init(users: Int32, months: Int32, storeProductId: String?, storeQuantity: Int32) { + public let currency: String + public let amount: Int64 + public init(users: Int32, months: Int32, storeProductId: String?, storeQuantity: Int32, currency: String, amount: Int64) { self.users = users self.months = months self.storeProductId = storeProductId self.storeQuantity = storeQuantity + self.currency = currency + self.amount = amount } public init(from decoder: Decoder) throws { @@ -40,6 +45,9 @@ public struct PremiumGiftCodeOption: Codable, Equatable { self.months = try container.decode(Int32.self, forKey: .months) self.storeProductId = try container.decodeIfPresent(String.self, forKey: .storeProductId) self.storeQuantity = try container.decodeIfPresent(Int32.self, forKey: .storeQuantity) ?? 1 + self.currency = try container.decode(String.self, forKey: .currency) + self.amount = try container.decode(Int64.self, forKey: .amount) + } public func encode(to encoder: Encoder) throws { @@ -48,6 +56,8 @@ public struct PremiumGiftCodeOption: Codable, Equatable { try container.encode(self.months, forKey: .months) try container.encodeIfPresent(self.storeProductId, forKey: .storeProductId) try container.encode(self.storeQuantity, forKey: .storeQuantity) + try container.encode(self.currency, forKey: .currency) + try container.encode(self.amount, forKey: .amount) } } @@ -234,8 +244,8 @@ func _internal_launchPrepaidGiveaway(account: Account, peerId: EnginePeer.Id, id extension PremiumGiftCodeOption { init(apiGiftCodeOption: Api.PremiumGiftCodeOption) { switch apiGiftCodeOption { - case let .premiumGiftCodeOption(_, users, months, storeProduct, storeQuantity, _, _): - self.init(users: users, months: months, storeProductId: storeProduct, storeQuantity: storeQuantity ?? 1) + case let .premiumGiftCodeOption(_, users, months, storeProduct, storeQuantity, curreny, amount): + self.init(users: users, months: months, storeProductId: storeProduct, storeQuantity: storeQuantity ?? 1, currency: curreny, amount: amount) } } } diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index ee9a8dd264..95351ed979 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -52,18 +52,6 @@ public extension Peer { return nil } } - var nameColor: PeerNameColor? { - switch self { - case let user as TelegramUser: - return user.nameColor - case let group as TelegramGroup: - return group.nameColor - case let channel as TelegramChannel: - return channel.nameColor - default: - return nil - } - } var usernames: [TelegramPeerUsername] { switch self { From c48fbdbec7341bae3fac3a3b00994902820389ba Mon Sep 17 00:00:00 2001 From: Mike Renoir <> Date: Tue, 17 Oct 2023 13:43:23 +0400 Subject: [PATCH 5/6] buy giveaway with bot --- .../Payments/BotPaymentForm.swift | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 65ac82ad77..b2c75485a7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -7,7 +7,7 @@ import TelegramApi public enum BotPaymentInvoiceSource { case message(MessageId) case slug(String) - case premiumGiftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, quantity: Int32, option: PremiumGiftCodeOption) + case premiumGiveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, option: PremiumGiftCodeOption) } @@ -214,23 +214,30 @@ private func _internal_parseInputInvoice(transaction: Transaction, source: BotPa return .inputInvoiceMessage(peer: inputPeer, msgId: messageId.id) case let .slug(slug): return .inputInvoiceSlug(slug: slug) - case let .premiumGiftCode(peerIds, boostPeerId, quantity, option): + case let .premiumGiveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, randomId, untilDate, currency, amount, option): + guard let peer = transaction.getPeer(boostPeerId), let apiBoostPeer = apiInputPeer(peer) else { + return nil + } var flags: Int32 = 0 - var apiBoostPeer: Api.InputPeer? var apiInputUsers: [Api.InputUser] = [] - for peerId in peerIds { - if let user = transaction.getPeer(peerId), let apiUser = apiInputUser(user) { - apiInputUsers.append(apiUser) - } - } - - if let boostPeerId = boostPeerId, let boostPeer = transaction.getPeer(boostPeerId), let apiPeer = apiInputPeer(boostPeer) { - apiBoostPeer = apiPeer + if onlyNewSubscribers { flags |= (1 << 0) } - let input: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: option.currency, amount: option.amount) + var additionalPeers: [Api.InputPeer] = [] + if !additionalPeerIds.isEmpty { + flags |= (1 << 1) + for peerId in additionalPeerIds { + if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { + additionalPeers.append(inputPeer) + } + } + } + if !countries.isEmpty { + flags |= (1 << 2) + } + let input: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: countries, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount) flags = 0 @@ -241,7 +248,7 @@ private func _internal_parseInputInvoice(transaction: Transaction, source: BotPa flags |= (1 << 1) } - let option: Api.PremiumGiftCodeOption = .premiumGiftCodeOption(flags: flags, users: quantity, months: option.months, storeProduct: option.storeProductId, storeQuantity: option.storeQuantity, currency: option.currency, amount: option.amount) + let option: Api.PremiumGiftCodeOption = .premiumGiftCodeOption(flags: flags, users: option.users, months: option.months, storeProduct: option.storeProductId, storeQuantity: option.storeQuantity, currency: option.currency, amount: option.amount) return .inputInvoicePremiumGiftCode(purpose: input, option: option) } @@ -522,8 +529,12 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa } } } - case let .premiumGiftCode(peerIds, boostPeer, quantity, option): - receiptMessageId = nil + case let .premiumGiveaway(_, _, _, _, randomId, _, _, _, _): + if message.globallyUniqueId == randomId { + if case let .Id(id) = message.id { + receiptMessageId = id + } + } } } } From fc8b77d946006c281fe0b27ca308ea305fd2856c Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 17 Oct 2023 13:52:47 +0400 Subject: [PATCH 6/6] Update scheme --- .../Telegram-iOS/en.lproj/Localizable.strings | 9 +- submodules/TelegramApi/Sources/Api0.swift | 26 +- submodules/TelegramApi/Sources/Api1.swift | 48 ++- submodules/TelegramApi/Sources/Api14.swift | 54 +++ submodules/TelegramApi/Sources/Api2.swift | 44 ++ submodules/TelegramApi/Sources/Api29.swift | 356 +++++++++------- submodules/TelegramApi/Sources/Api30.swift | 56 --- submodules/TelegramApi/Sources/Api31.swift | 112 +++--- submodules/TelegramApi/Sources/Api6.swift | 44 ++ .../Sources/ApiUtils/ChatContextResult.swift | 7 + .../Sources/State/ChannelBoost.swift | 379 +++++++++--------- .../Payments/BotPaymentForm.swift | 2 +- .../Sources/Utils/PeerUtils.swift | 8 + submodules/TelegramUI/BUILD | 1 + .../CameraScreen/Sources/CameraScreen.swift | 32 +- .../Sources/FlashTintControlComponent.swift | 137 ++++++- .../Sources/ChatMessageReplyInfoNode.swift | 34 +- .../Sources/PeerNameColorScreen.swift | 19 +- .../PeerInfoScreenDisclosureItem.swift | 37 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 45 ++- 20 files changed, 917 insertions(+), 533 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 8cef70f22e..830b5aad11 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10127,15 +10127,18 @@ Sorry for the inconvenience."; "Appearance.NameColor" = "Your Name Color"; "NameColor.Title.Account" = "Your Name Color"; -"NameColor.Title.Channel" = "Channel Title Color"; +"NameColor.Title.Channel" = "Your Channel Color"; "NameColor.ChatPreview.Title" = "COLOR PREVIEW"; -"NameColor.ChatPreview.ReplyText" = "Reply to your message"; -"NameColor.ChatPreview.MessageText" = "Your name and replies to your messages will be shown in the selected color."; +"NameColor.ChatPreview.ReplyText.Account" = "Reply to your message"; +"NameColor.ChatPreview.ReplyText.Channel" = "Reply to your channel message"; +"NameColor.ChatPreview.MessageText.Account" = "Your name and replies to your messages will be shown in the selected color."; +"NameColor.ChatPreview.MessageText.Channel" = "The name of your channek and replies to its messages will be shown in the selected color."; "NameColor.ChatPreview.LinkSite" = "Telegram"; "NameColor.ChatPreview.LinkTitle" = "Link Preview"; "NameColor.ChatPreview.LinkText" = "Your selected color will also tint the link preview."; "NameColor.ChatPreview.Description.Account" = "You can choose an individual color to tint your name, the links you send, and replies to your messages."; +"NameColor.ChatPreview.Description.Channel" = "You can choose an individual color to tint your channel's name, the links it sends, and replies to its messages."; "NameColor.ApplyColor" = "Apply Color"; diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index bcf5b0d117..b9f848ceb6 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -11,6 +11,7 @@ public enum Api { public enum payments {} public enum phone {} public enum photos {} + public enum premium {} public enum stats {} public enum stickers {} public enum storage {} @@ -32,6 +33,7 @@ public enum Api { public enum payments {} public enum phone {} public enum photos {} + public enum premium {} public enum stats {} public enum stickers {} public enum stories {} @@ -73,7 +75,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1834973166] = { return Api.BaseTheme.parse_baseThemeTinted($0) } dict[-1132882121] = { return Api.Bool.parse_boolFalse($0) } dict[-1720552011] = { return Api.Bool.parse_boolTrue($0) } - dict[245261184] = { return Api.Booster.parse_booster($0) } + dict[1405288648] = { return Api.Boost.parse_boost($0) } dict[-1778593322] = { return Api.BotApp.parse_botApp($0) } dict[1571189943] = { return Api.BotApp.parse_botAppNotModified($0) } dict[-1032140601] = { return Api.BotCommand.parse_botCommand($0) } @@ -90,6 +92,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[85477117] = { return Api.BotInlineMessage.parse_botInlineMessageMediaGeo($0) } dict[894081801] = { return Api.BotInlineMessage.parse_botInlineMessageMediaInvoice($0) } dict[-1970903652] = { return Api.BotInlineMessage.parse_botInlineMessageMediaVenue($0) } + dict[-2137335386] = { return Api.BotInlineMessage.parse_botInlineMessageMediaWebPage($0) } dict[-1937807902] = { return Api.BotInlineMessage.parse_botInlineMessageText($0) } dict[400266251] = { return Api.BotInlineResult.parse_botInlineMediaResult($0) } dict[295067450] = { return Api.BotInlineResult.parse_botInlineResult($0) } @@ -281,6 +284,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1768777083] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaGeo($0) } dict[-672693723] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaInvoice($0) } dict[1098628881] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaVenue($0) } + dict[-1109605104] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaWebPage($0) } dict[1036876423] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageText($0) } dict[-1995686519] = { return Api.InputBotInlineMessageID.parse_inputBotInlineMessageID($0) } dict[-1227287081] = { return Api.InputBotInlineMessageID.parse_inputBotInlineMessageID64($0) } @@ -574,6 +578,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[2129714567] = { return Api.MessagesFilter.parse_inputMessagesFilterUrl($0) } dict[-1614803355] = { return Api.MessagesFilter.parse_inputMessagesFilterVideo($0) } dict[1358283666] = { return Api.MessagesFilter.parse_inputMessagesFilterVoice($0) } + dict[1267991078] = { return Api.MyBoost.parse_myBoost($0) } dict[-1910892683] = { return Api.NearestDc.parse_nearestDc($0) } dict[-1746354498] = { return Api.NotificationSound.parse_notificationSoundDefault($0) } dict[-2096391452] = { return Api.NotificationSound.parse_notificationSoundLocal($0) } @@ -1175,6 +1180,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[539045032] = { return Api.photos.Photo.parse_photo($0) } dict[-1916114267] = { return Api.photos.Photos.parse_photos($0) } dict[352657236] = { return Api.photos.Photos.parse_photosSlice($0) } + dict[-2030542532] = { return Api.premium.BoostsList.parse_boostsList($0) } + dict[-1696454430] = { return Api.premium.MyBoosts.parse_myBoosts($0) } dict[-1107852396] = { return Api.stats.BroadcastStats.parse_broadcastStats($0) } dict[-276825834] = { return Api.stats.MegagroupStats.parse_megagroupStats($0) } dict[-1986399595] = { return Api.stats.MessageStats.parse_messageStats($0) } @@ -1191,10 +1198,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[276907596] = { return Api.storage.FileType.parse_fileWebp($0) } dict[1862033025] = { return Api.stories.AllStories.parse_allStories($0) } dict[291044926] = { return Api.stories.AllStories.parse_allStoriesNotModified($0) } - dict[-203604707] = { return Api.stories.BoostersList.parse_boostersList($0) } - dict[1911715597] = { return Api.stories.BoostsStatus.parse_boostsStatus($0) } - dict[-1021889145] = { return Api.stories.CanApplyBoostResult.parse_canApplyBoostOk($0) } - dict[1898726997] = { return Api.stories.CanApplyBoostResult.parse_canApplyBoostReplace($0) } + dict[-869070685] = { return Api.stories.BoostsStatus.parse_boostsStatus($0) } dict[-890861720] = { return Api.stories.PeerStories.parse_peerStories($0) } dict[1574486984] = { return Api.stories.Stories.parse_stories($0) } dict[-560009955] = { return Api.stories.StoryViews.parse_storyViews($0) } @@ -1304,7 +1308,7 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.Bool: _1.serialize(buffer, boxed) - case let _1 as Api.Booster: + case let _1 as Api.Boost: _1.serialize(buffer, boxed) case let _1 as Api.BotApp: _1.serialize(buffer, boxed) @@ -1604,6 +1608,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.MessagesFilter: _1.serialize(buffer, boxed) + case let _1 as Api.MyBoost: + _1.serialize(buffer, boxed) case let _1 as Api.NearestDc: _1.serialize(buffer, boxed) case let _1 as Api.NotificationSound: @@ -2074,6 +2080,10 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.photos.Photos: _1.serialize(buffer, boxed) + case let _1 as Api.premium.BoostsList: + _1.serialize(buffer, boxed) + case let _1 as Api.premium.MyBoosts: + _1.serialize(buffer, boxed) case let _1 as Api.stats.BroadcastStats: _1.serialize(buffer, boxed) case let _1 as Api.stats.MegagroupStats: @@ -2086,12 +2096,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.stories.AllStories: _1.serialize(buffer, boxed) - case let _1 as Api.stories.BoostersList: - _1.serialize(buffer, boxed) case let _1 as Api.stories.BoostsStatus: _1.serialize(buffer, boxed) - case let _1 as Api.stories.CanApplyBoostResult: - _1.serialize(buffer, boxed) case let _1 as Api.stories.PeerStories: _1.serialize(buffer, boxed) case let _1 as Api.stories.Stories: diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index d6409da385..c9f8f98538 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -893,37 +893,57 @@ public extension Api { } } public extension Api { - enum Booster: TypeConstructorDescription { - case booster(userId: Int64, expires: Int32) + enum Boost: TypeConstructorDescription { + case boost(flags: Int32, id: String, userId: Int64?, giveawayMsgId: Int32?, date: Int32, expires: Int32, usedGiftSlug: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .booster(let userId, let expires): + case .boost(let flags, let id, let userId, let giveawayMsgId, let date, let expires, let usedGiftSlug): if boxed { - buffer.appendInt32(245261184) + buffer.appendInt32(1405288648) } - serializeInt64(userId, buffer: buffer, boxed: false) + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(id, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt64(userId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeInt32(giveawayMsgId!, buffer: buffer, boxed: false)} + serializeInt32(date, buffer: buffer, boxed: false) serializeInt32(expires, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 4) != 0 {serializeString(usedGiftSlug!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .booster(let userId, let expires): - return ("booster", [("userId", userId as Any), ("expires", expires as Any)]) + case .boost(let flags, let id, let userId, let giveawayMsgId, let date, let expires, let usedGiftSlug): + return ("boost", [("flags", flags as Any), ("id", id as Any), ("userId", userId as Any), ("giveawayMsgId", giveawayMsgId as Any), ("date", date as Any), ("expires", expires as Any), ("usedGiftSlug", usedGiftSlug as Any)]) } } - public static func parse_booster(_ reader: BufferReader) -> Booster? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() + public static func parse_boost(_ reader: BufferReader) -> Boost? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int64? + if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt64() } + var _4: Int32? + if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() } + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + var _7: String? + if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.Booster.booster(userId: _1!, expires: _2!) + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.Boost.boost(flags: _1!, id: _2!, userId: _3, giveawayMsgId: _4, date: _5!, expires: _6!, usedGiftSlug: _7) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api14.swift b/submodules/TelegramApi/Sources/Api14.swift index 57804e6d00..19781ad8f5 100644 --- a/submodules/TelegramApi/Sources/Api14.swift +++ b/submodules/TelegramApi/Sources/Api14.swift @@ -698,6 +698,60 @@ public extension Api { } } +public extension Api { + enum MyBoost: TypeConstructorDescription { + case myBoost(flags: Int32, slot: Int32, peer: Api.Peer?, expires: Int32, cooldownUntilDate: Int32?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .myBoost(let flags, let slot, let peer, let expires, let cooldownUntilDate): + if boxed { + buffer.appendInt32(1267991078) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(slot, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {peer!.serialize(buffer, true)} + serializeInt32(expires, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(cooldownUntilDate!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .myBoost(let flags, let slot, let peer, let expires, let cooldownUntilDate): + return ("myBoost", [("flags", flags as Any), ("slot", slot as Any), ("peer", peer as Any), ("expires", expires as Any), ("cooldownUntilDate", cooldownUntilDate as Any)]) + } + } + + public static func parse_myBoost(_ reader: BufferReader) -> MyBoost? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.Peer? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.Peer + } } + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.MyBoost.myBoost(flags: _1!, slot: _2!, peer: _3, expires: _4!, cooldownUntilDate: _5) + } + else { + return nil + } + } + + } +} public extension Api { enum NearestDc: TypeConstructorDescription { case nearestDc(country: String, thisDc: Int32, nearestDc: Int32) diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 392ab3910e..70febe0a83 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -77,6 +77,7 @@ public extension Api { case botInlineMessageMediaGeo(flags: Int32, geo: Api.GeoPoint, heading: Int32?, period: Int32?, proximityNotificationRadius: Int32?, replyMarkup: Api.ReplyMarkup?) case botInlineMessageMediaInvoice(flags: Int32, title: String, description: String, photo: Api.WebDocument?, currency: String, totalAmount: Int64, replyMarkup: Api.ReplyMarkup?) case botInlineMessageMediaVenue(flags: Int32, geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String, replyMarkup: Api.ReplyMarkup?) + case botInlineMessageMediaWebPage(flags: Int32, message: String, entities: [Api.MessageEntity]?, url: String, replyMarkup: Api.ReplyMarkup?) case botInlineMessageText(flags: Int32, message: String, entities: [Api.MessageEntity]?, replyMarkup: Api.ReplyMarkup?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -141,6 +142,20 @@ public extension Api { serializeString(venueType, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} break + case .botInlineMessageMediaWebPage(let flags, let message, let entities, let url, let replyMarkup): + if boxed { + buffer.appendInt32(-2137335386) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities!.count)) + for item in entities! { + item.serialize(buffer, true) + }} + serializeString(url, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} + break case .botInlineMessageText(let flags, let message, let entities, let replyMarkup): if boxed { buffer.appendInt32(-1937807902) @@ -169,6 +184,8 @@ public extension Api { return ("botInlineMessageMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("replyMarkup", replyMarkup as Any)]) case .botInlineMessageMediaVenue(let flags, let geo, let title, let address, let provider, let venueId, let venueType, let replyMarkup): return ("botInlineMessageMediaVenue", [("flags", flags as Any), ("geo", geo as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any), ("replyMarkup", replyMarkup as Any)]) + case .botInlineMessageMediaWebPage(let flags, let message, let entities, let url, let replyMarkup): + return ("botInlineMessageMediaWebPage", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("url", url as Any), ("replyMarkup", replyMarkup as Any)]) case .botInlineMessageText(let flags, let message, let entities, let replyMarkup): return ("botInlineMessageText", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("replyMarkup", replyMarkup as Any)]) } @@ -325,6 +342,33 @@ public extension Api { return nil } } + public static func parse_botInlineMessageMediaWebPage(_ reader: BufferReader) -> BotInlineMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } } + var _4: String? + _4 = parseString(reader) + var _5: Api.ReplyMarkup? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.BotInlineMessage.botInlineMessageMediaWebPage(flags: _1!, message: _2!, entities: _3, url: _4!, replyMarkup: _5) + } + else { + return nil + } + } public static func parse_botInlineMessageText(_ reader: BufferReader) -> BotInlineMessage? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index 578458183d..7a173e0b7e 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -1,3 +1,129 @@ +public extension Api.premium { + enum BoostsList: TypeConstructorDescription { + case boostsList(flags: Int32, count: Int32, boosts: [Api.Boost], nextOffset: String?, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .boostsList(let flags, let count, let boosts, let nextOffset, let users): + if boxed { + buffer.appendInt32(-2030542532) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(boosts.count)) + for item in boosts { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .boostsList(let flags, let count, let boosts, let nextOffset, let users): + return ("boostsList", [("flags", flags as Any), ("count", count as Any), ("boosts", boosts as Any), ("nextOffset", nextOffset as Any), ("users", users as Any)]) + } + } + + public static func parse_boostsList(_ reader: BufferReader) -> BoostsList? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.Boost]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Boost.self) + } + var _4: String? + if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.premium.BoostsList.boostsList(flags: _1!, count: _2!, boosts: _3!, nextOffset: _4, users: _5!) + } + else { + return nil + } + } + + } +} +public extension Api.premium { + enum MyBoosts: TypeConstructorDescription { + case myBoosts(myBoosts: [Api.MyBoost], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .myBoosts(let myBoosts, let chats, let users): + if boxed { + buffer.appendInt32(-1696454430) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(myBoosts.count)) + for item in myBoosts { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .myBoosts(let myBoosts, let chats, let users): + return ("myBoosts", [("myBoosts", myBoosts as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_myBoosts(_ reader: BufferReader) -> MyBoosts? { + var _1: [Api.MyBoost]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MyBoost.self) + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.premium.MyBoosts.myBoosts(myBoosts: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + + } +} public extension Api.stats { enum BroadcastStats: TypeConstructorDescription { case broadcastStats(period: Api.StatsDateRangeDays, followers: Api.StatsAbsValueAndPrev, viewsPerPost: Api.StatsAbsValueAndPrev, sharesPerPost: Api.StatsAbsValueAndPrev, enabledNotifications: Api.StatsPercentValue, growthGraph: Api.StatsGraph, followersGraph: Api.StatsGraph, muteGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, interactionsGraph: Api.StatsGraph, ivInteractionsGraph: Api.StatsGraph, viewsBySourceGraph: Api.StatsGraph, newFollowersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, recentMessageInteractions: [Api.MessageInteractionCounters]) @@ -594,91 +720,28 @@ public extension Api.stories { } } -public extension Api.stories { - enum BoostersList: TypeConstructorDescription { - case boostersList(flags: Int32, count: Int32, boosters: [Api.Booster], nextOffset: String?, users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .boostersList(let flags, let count, let boosters, let nextOffset, let users): - if boxed { - buffer.appendInt32(-203604707) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(boosters.count)) - for item in boosters { - item.serialize(buffer, true) - } - if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .boostersList(let flags, let count, let boosters, let nextOffset, let users): - return ("boostersList", [("flags", flags as Any), ("count", count as Any), ("boosters", boosters as Any), ("nextOffset", nextOffset as Any), ("users", users as Any)]) - } - } - - public static func parse_boostersList(_ reader: BufferReader) -> BoostersList? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: [Api.Booster]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Booster.self) - } - var _4: String? - if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.stories.BoostersList.boostersList(flags: _1!, count: _2!, boosters: _3!, nextOffset: _4, users: _5!) - } - else { - return nil - } - } - - } -} public extension Api.stories { enum BoostsStatus: TypeConstructorDescription { - case boostsStatus(flags: Int32, level: Int32, currentLevelBoosts: Int32, boosts: Int32, nextLevelBoosts: Int32?, premiumAudience: Api.StatsPercentValue?, boostUrl: String, prepaidGiveaways: [Api.PrepaidGiveaway]?) + case boostsStatus(flags: Int32, level: Int32, currentLevelBoosts: Int32, boosts: Int32, giftBoosts: Int32?, nextLevelBoosts: Int32?, premiumAudience: Api.StatsPercentValue?, boostUrl: String, myBoostSlots: [Int32]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .boostsStatus(let flags, let level, let currentLevelBoosts, let boosts, let nextLevelBoosts, let premiumAudience, let boostUrl, let prepaidGiveaways): + case .boostsStatus(let flags, let level, let currentLevelBoosts, let boosts, let giftBoosts, let nextLevelBoosts, let premiumAudience, let boostUrl, let myBoostSlots): if boxed { - buffer.appendInt32(1911715597) + buffer.appendInt32(-869070685) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(level, buffer: buffer, boxed: false) serializeInt32(currentLevelBoosts, buffer: buffer, boxed: false) serializeInt32(boosts, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 3) != 0 {serializeInt32(giftBoosts!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeInt32(nextLevelBoosts!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 1) != 0 {premiumAudience!.serialize(buffer, true)} serializeString(boostUrl, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(prepaidGiveaways!.count)) - for item in prepaidGiveaways! { - item.serialize(buffer, true) + if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(myBoostSlots!.count)) + for item in myBoostSlots! { + serializeInt32(item, buffer: buffer, boxed: false) }} break } @@ -686,8 +749,8 @@ public extension Api.stories { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .boostsStatus(let flags, let level, let currentLevelBoosts, let boosts, let nextLevelBoosts, let premiumAudience, let boostUrl, let prepaidGiveaways): - return ("boostsStatus", [("flags", flags as Any), ("level", level as Any), ("currentLevelBoosts", currentLevelBoosts as Any), ("boosts", boosts as Any), ("nextLevelBoosts", nextLevelBoosts as Any), ("premiumAudience", premiumAudience as Any), ("boostUrl", boostUrl as Any), ("prepaidGiveaways", prepaidGiveaways as Any)]) + case .boostsStatus(let flags, let level, let currentLevelBoosts, let boosts, let giftBoosts, let nextLevelBoosts, let premiumAudience, let boostUrl, let myBoostSlots): + return ("boostsStatus", [("flags", flags as Any), ("level", level as Any), ("currentLevelBoosts", currentLevelBoosts as Any), ("boosts", boosts as Any), ("giftBoosts", giftBoosts as Any), ("nextLevelBoosts", nextLevelBoosts as Any), ("premiumAudience", premiumAudience as Any), ("boostUrl", boostUrl as Any), ("myBoostSlots", myBoostSlots as Any)]) } } @@ -701,87 +764,30 @@ public extension Api.stories { var _4: Int32? _4 = reader.readInt32() var _5: Int32? - if Int(_1!) & Int(1 << 0) != 0 {_5 = reader.readInt32() } - var _6: Api.StatsPercentValue? + if Int(_1!) & Int(1 << 3) != 0 {_5 = reader.readInt32() } + var _6: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt32() } + var _7: Api.StatsPercentValue? if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.StatsPercentValue + _7 = Api.parse(reader, signature: signature) as? Api.StatsPercentValue } } - var _7: String? - _7 = parseString(reader) - var _8: [Api.PrepaidGiveaway]? - if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { - _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PrepaidGiveaway.self) + var _8: String? + _8 = parseString(reader) + var _9: [Int32]? + if Int(_1!) & Int(1 << 2) != 0 {if let _ = reader.readInt32() { + _9 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil - let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.stories.BoostsStatus.boostsStatus(flags: _1!, level: _2!, currentLevelBoosts: _3!, boosts: _4!, nextLevelBoosts: _5, premiumAudience: _6, boostUrl: _7!, prepaidGiveaways: _8) - } - else { - return nil - } - } - - } -} -public extension Api.stories { - enum CanApplyBoostResult: TypeConstructorDescription { - case canApplyBoostOk - case canApplyBoostReplace(currentBoost: Api.Peer, chats: [Api.Chat]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .canApplyBoostOk: - if boxed { - buffer.appendInt32(-1021889145) - } - - break - case .canApplyBoostReplace(let currentBoost, let chats): - if boxed { - buffer.appendInt32(1898726997) - } - currentBoost.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .canApplyBoostOk: - return ("canApplyBoostOk", []) - case .canApplyBoostReplace(let currentBoost, let chats): - return ("canApplyBoostReplace", [("currentBoost", currentBoost as Any), ("chats", chats as Any)]) - } - } - - public static func parse_canApplyBoostOk(_ reader: BufferReader) -> CanApplyBoostResult? { - return Api.stories.CanApplyBoostResult.canApplyBoostOk - } - public static func parse_canApplyBoostReplace(_ reader: BufferReader) -> CanApplyBoostResult? { - var _1: Api.Peer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.Peer - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.stories.CanApplyBoostResult.canApplyBoostReplace(currentBoost: _1!, chats: _2!) + let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c8 = _8 != nil + let _c9 = (Int(_1!) & Int(1 << 2) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.stories.BoostsStatus.boostsStatus(flags: _1!, level: _2!, currentLevelBoosts: _3!, boosts: _4!, giftBoosts: _5, nextLevelBoosts: _6, premiumAudience: _7, boostUrl: _8!, myBoostSlots: _9) } else { return nil @@ -1462,3 +1468,59 @@ public extension Api.updates { } } +public extension Api.upload { + enum CdnFile: TypeConstructorDescription { + case cdnFile(bytes: Buffer) + case cdnFileReuploadNeeded(requestToken: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .cdnFile(let bytes): + if boxed { + buffer.appendInt32(-1449145777) + } + serializeBytes(bytes, buffer: buffer, boxed: false) + break + case .cdnFileReuploadNeeded(let requestToken): + if boxed { + buffer.appendInt32(-290921362) + } + serializeBytes(requestToken, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .cdnFile(let bytes): + return ("cdnFile", [("bytes", bytes as Any)]) + case .cdnFileReuploadNeeded(let requestToken): + return ("cdnFileReuploadNeeded", [("requestToken", requestToken as Any)]) + } + } + + public static func parse_cdnFile(_ reader: BufferReader) -> CdnFile? { + var _1: Buffer? + _1 = parseBytes(reader) + let _c1 = _1 != nil + if _c1 { + return Api.upload.CdnFile.cdnFile(bytes: _1!) + } + else { + return nil + } + } + public static func parse_cdnFileReuploadNeeded(_ reader: BufferReader) -> CdnFile? { + var _1: Buffer? + _1 = parseBytes(reader) + let _c1 = _1 != nil + if _c1 { + return Api.upload.CdnFile.cdnFileReuploadNeeded(requestToken: _1!) + } + else { + return nil + } + } + + } +} diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index 81f996c9b3..10815203dd 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -1,59 +1,3 @@ -public extension Api.upload { - enum CdnFile: TypeConstructorDescription { - case cdnFile(bytes: Buffer) - case cdnFileReuploadNeeded(requestToken: Buffer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .cdnFile(let bytes): - if boxed { - buffer.appendInt32(-1449145777) - } - serializeBytes(bytes, buffer: buffer, boxed: false) - break - case .cdnFileReuploadNeeded(let requestToken): - if boxed { - buffer.appendInt32(-290921362) - } - serializeBytes(requestToken, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .cdnFile(let bytes): - return ("cdnFile", [("bytes", bytes as Any)]) - case .cdnFileReuploadNeeded(let requestToken): - return ("cdnFileReuploadNeeded", [("requestToken", requestToken as Any)]) - } - } - - public static func parse_cdnFile(_ reader: BufferReader) -> CdnFile? { - var _1: Buffer? - _1 = parseBytes(reader) - let _c1 = _1 != nil - if _c1 { - return Api.upload.CdnFile.cdnFile(bytes: _1!) - } - else { - return nil - } - } - public static func parse_cdnFileReuploadNeeded(_ reader: BufferReader) -> CdnFile? { - var _1: Buffer? - _1 = parseBytes(reader) - let _c1 = _1 != nil - if _c1 { - return Api.upload.CdnFile.cdnFileReuploadNeeded(requestToken: _1!) - } - else { - return nil - } - } - - } -} public extension Api.upload { enum File: TypeConstructorDescription { case file(type: Api.storage.FileType, mtime: Int32, bytes: Buffer) diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index 5c3fc1d1ad..9bb63bf0a9 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -8361,6 +8361,56 @@ public extension Api.functions.photos { }) } } +public extension Api.functions.premium { + static func applyBoost(flags: Int32, slot: Int32?, peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1186373995) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(slot!, buffer: buffer, boxed: false)} + peer.serialize(buffer, true) + return (FunctionDescription(name: "premium.applyBoost", parameters: [("flags", String(describing: flags)), ("slot", String(describing: slot)), ("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} +public extension Api.functions.premium { + static func getBoostsList(flags: Int32, peer: Api.InputPeer, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1626764896) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeString(offset, buffer: buffer, boxed: false) + serializeInt32(limit, buffer: buffer, boxed: false) + return (FunctionDescription(name: "premium.getBoostsList", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.premium.BoostsList? in + let reader = BufferReader(buffer) + var result: Api.premium.BoostsList? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.premium.BoostsList + } + return result + }) + } +} +public extension Api.functions.premium { + static func getMyBoosts() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(199719754) + + return (FunctionDescription(name: "premium.getMyBoosts", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.premium.MyBoosts? in + let reader = BufferReader(buffer) + var result: Api.premium.MyBoosts? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.premium.MyBoosts + } + return result + }) + } +} public extension Api.functions.stats { static func getBroadcastStats(flags: Int32, channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8632,36 +8682,6 @@ public extension Api.functions.stories { }) } } -public extension Api.functions.stories { - static func applyBoost(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(-224560085) - peer.serialize(buffer, true) - return (FunctionDescription(name: "stories.applyBoost", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in - let reader = BufferReader(buffer) - var result: Api.Bool? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool - } - return result - }) - } -} -public extension Api.functions.stories { - static func canApplyBoost(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(-620379715) - peer.serialize(buffer, true) - return (FunctionDescription(name: "stories.canApplyBoost", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.CanApplyBoostResult? in - let reader = BufferReader(buffer) - var result: Api.stories.CanApplyBoostResult? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.stories.CanApplyBoostResult - } - return result - }) - } -} public extension Api.functions.stories { static func canSendStory(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8778,38 +8798,6 @@ public extension Api.functions.stories { }) } } -public extension Api.functions.stories { - static func getBoostersList(peer: Api.InputPeer, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(863959424) - peer.serialize(buffer, true) - serializeString(offset, buffer: buffer, boxed: false) - serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "stories.getBoostersList", parameters: [("peer", String(describing: peer)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.BoostersList? in - let reader = BufferReader(buffer) - var result: Api.stories.BoostersList? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.stories.BoostersList - } - return result - }) - } -} -public extension Api.functions.stories { - static func getBoostsStatus(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(1279562866) - peer.serialize(buffer, true) - return (FunctionDescription(name: "stories.getBoostsStatus", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.BoostsStatus? in - let reader = BufferReader(buffer) - var result: Api.stories.BoostsStatus? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.stories.BoostsStatus - } - return result - }) - } -} public extension Api.functions.stories { static func getChatsToSend() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramApi/Sources/Api6.swift b/submodules/TelegramApi/Sources/Api6.swift index 7416cdbc5b..60cf7f4290 100644 --- a/submodules/TelegramApi/Sources/Api6.swift +++ b/submodules/TelegramApi/Sources/Api6.swift @@ -1198,6 +1198,7 @@ public extension Api { case inputBotInlineMessageMediaGeo(flags: Int32, geoPoint: Api.InputGeoPoint, heading: Int32?, period: Int32?, proximityNotificationRadius: Int32?, replyMarkup: Api.ReplyMarkup?) case inputBotInlineMessageMediaInvoice(flags: Int32, title: String, description: String, photo: Api.InputWebDocument?, invoice: Api.Invoice, payload: Buffer, provider: String, providerData: Api.DataJSON, replyMarkup: Api.ReplyMarkup?) case inputBotInlineMessageMediaVenue(flags: Int32, geoPoint: Api.InputGeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String, replyMarkup: Api.ReplyMarkup?) + case inputBotInlineMessageMediaWebPage(flags: Int32, message: String, entities: [Api.MessageEntity]?, url: String, replyMarkup: Api.ReplyMarkup?) case inputBotInlineMessageText(flags: Int32, message: String, entities: [Api.MessageEntity]?, replyMarkup: Api.ReplyMarkup?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -1271,6 +1272,20 @@ public extension Api { serializeString(venueType, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} break + case .inputBotInlineMessageMediaWebPage(let flags, let message, let entities, let url, let replyMarkup): + if boxed { + buffer.appendInt32(-1109605104) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities!.count)) + for item in entities! { + item.serialize(buffer, true) + }} + serializeString(url, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} + break case .inputBotInlineMessageText(let flags, let message, let entities, let replyMarkup): if boxed { buffer.appendInt32(1036876423) @@ -1301,6 +1316,8 @@ public extension Api { return ("inputBotInlineMessageMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("payload", payload as Any), ("provider", provider as Any), ("providerData", providerData as Any), ("replyMarkup", replyMarkup as Any)]) case .inputBotInlineMessageMediaVenue(let flags, let geoPoint, let title, let address, let provider, let venueId, let venueType, let replyMarkup): return ("inputBotInlineMessageMediaVenue", [("flags", flags as Any), ("geoPoint", geoPoint as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any), ("replyMarkup", replyMarkup as Any)]) + case .inputBotInlineMessageMediaWebPage(let flags, let message, let entities, let url, let replyMarkup): + return ("inputBotInlineMessageMediaWebPage", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("url", url as Any), ("replyMarkup", replyMarkup as Any)]) case .inputBotInlineMessageText(let flags, let message, let entities, let replyMarkup): return ("inputBotInlineMessageText", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("replyMarkup", replyMarkup as Any)]) } @@ -1483,6 +1500,33 @@ public extension Api { return nil } } + public static func parse_inputBotInlineMessageMediaWebPage(_ reader: BufferReader) -> InputBotInlineMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } } + var _4: String? + _4 = parseString(reader) + var _5: Api.ReplyMarkup? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.InputBotInlineMessage.inputBotInlineMessageMediaWebPage(flags: _1!, message: _2!, entities: _3, url: _4!, replyMarkup: _5) + } + else { + return nil + } + } public static func parse_inputBotInlineMessageText(_ reader: BufferReader) -> InputBotInlineMessage? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift index 46a78ee071..7324205a7c 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift @@ -493,6 +493,13 @@ extension ChatContextResultMessage { parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup) } self = .invoice(media: TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: currency, totalAmount: totalAmount, startParam: "", extendedMedia: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), replyMarkup: parsedReplyMarkup) + case let .botInlineMessageMediaWebPage(flags, message, entities, url, replyMarkup): + let _ = flags + let _ = message + let _ = entities + let _ = url + let _ = replyMarkup + fatalError() } } } diff --git a/submodules/TelegramCore/Sources/State/ChannelBoost.swift b/submodules/TelegramCore/Sources/State/ChannelBoost.swift index 7b7e2375cf..571bbfbda7 100644 --- a/submodules/TelegramCore/Sources/State/ChannelBoost.swift +++ b/submodules/TelegramCore/Sources/State/ChannelBoost.swift @@ -49,29 +49,30 @@ public final class ChannelBoostStatus: Equatable { } func _internal_getChannelBoostStatus(account: Account, peerId: PeerId) -> Signal { - return account.postbox.transaction { transaction -> Api.InputPeer? in - return transaction.getPeer(peerId).flatMap(apiInputPeer) - } - |> mapToSignal { inputPeer -> Signal in - guard let inputPeer = inputPeer else { - return .single(nil) - } - return account.network.request(Api.functions.stories.getBoostsStatus(peer: inputPeer)) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> map { result -> ChannelBoostStatus? in - guard let result = result else { - return nil - } - - switch result { - case let .boostsStatus(_, level, currentLevelBoosts, boosts, nextLevelBoosts, premiumAudience, url, prepaidGiveaways): - return ChannelBoostStatus(level: Int(level), boosts: Int(boosts), currentLevelBoosts: Int(currentLevelBoosts), nextLevelBoosts: nextLevelBoosts.flatMap(Int.init), premiumAudience: premiumAudience.flatMap({ StatsPercentValue(apiPercentValue: $0) }), url: url, prepaidGiveaways: prepaidGiveaways?.map({ PrepaidGiveaway(apiPrepaidGiveaway: $0) }) ?? []) - } - } - } + return .single(nil) +// return account.postbox.transaction { transaction -> Api.InputPeer? in +// return transaction.getPeer(peerId).flatMap(apiInputPeer) +// } +// |> mapToSignal { inputPeer -> Signal in +// guard let inputPeer = inputPeer else { +// return .single(nil) +// } +// return account.network.request(Api.functions.stories.getBoostsStatus(peer: inputPeer)) +// |> map(Optional.init) +// |> `catch` { _ -> Signal in +// return .single(nil) +// } +// |> map { result -> ChannelBoostStatus? in +// guard let result = result else { +// return nil +// } +// +// switch result { +// case let .boostsStatus(_, level, currentLevelBoosts, boosts, nextLevelBoosts, premiumAudience, url, prepaidGiveaways): +// return ChannelBoostStatus(level: Int(level), boosts: Int(boosts), currentLevelBoosts: Int(currentLevelBoosts), nextLevelBoosts: nextLevelBoosts.flatMap(Int.init), premiumAudience: premiumAudience.flatMap({ StatsPercentValue(apiPercentValue: $0) }), url: url, prepaidGiveaways: prepaidGiveaways?.map({ PrepaidGiveaway(apiPrepaidGiveaway: $0) }) ?? []) +// } +// } +// } } public enum CanApplyBoostStatus { @@ -89,85 +90,87 @@ public enum CanApplyBoostStatus { } func _internal_canApplyChannelBoost(account: Account, peerId: PeerId) -> Signal { - return account.postbox.transaction { transaction -> Api.InputPeer? in - return transaction.getPeer(peerId).flatMap(apiInputPeer) - } - |> mapToSignal { inputPeer -> Signal in - guard let inputPeer = inputPeer else { - return .single(.error(.generic)) - } - return account.network.request(Api.functions.stories.canApplyBoost(peer: inputPeer), automaticFloodWait: false) - |> map { result -> (Api.stories.CanApplyBoostResult?, CanApplyBoostStatus.ErrorReason?) in - return (result, nil) - } - |> `catch` { error -> Signal<(Api.stories.CanApplyBoostResult?, CanApplyBoostStatus.ErrorReason?), NoError> in - let reason: CanApplyBoostStatus.ErrorReason - if error.errorDescription == "PREMIUM_ACCOUNT_REQUIRED" { - reason = .premiumRequired - } else if error.errorDescription.hasPrefix("FLOOD_WAIT_") { - let errorText = error.errorDescription ?? "" - if let underscoreIndex = errorText.lastIndex(of: "_") { - let timeoutText = errorText[errorText.index(after: underscoreIndex)...] - if let timeoutValue = Int32(String(timeoutText)) { - reason = .floodWait(timeoutValue) - } else { - reason = .generic - } - } else { - reason = .generic - } - } else if error.errorDescription == "SAME_BOOST_ALREADY_ACTIVE" || error.errorDescription == "BOOST_NOT_MODIFIED" { - reason = .peerBoostAlreadyActive - } else if error.errorDescription == "PREMIUM_GIFTED_NOT_ALLOWED" { - reason = .giftedPremiumNotAllowed - } else { - reason = .generic - } - - return .single((nil, reason)) - } - |> mapToSignal { result, errorReason -> Signal in - guard let result = result else { - return .single(.error(errorReason ?? .generic)) - } - - return account.postbox.transaction { transaction -> CanApplyBoostStatus in - switch result { - case .canApplyBoostOk: - return .ok - case let .canApplyBoostReplace(currentBoost, chats): - updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(transaction: transaction, chats: chats, users: [])) - - if let peer = transaction.getPeer(currentBoost.peerId) { - return .replace(currentBoost: EnginePeer(peer)) - } else { - return .error(.generic) - } - } - } - } - } + return .single(.error(.generic)) +// return account.postbox.transaction { transaction -> Api.InputPeer? in +// return transaction.getPeer(peerId).flatMap(apiInputPeer) +// } +// |> mapToSignal { inputPeer -> Signal in +// guard let inputPeer = inputPeer else { +// return .single(.error(.generic)) +// } +// return account.network.request(Api.functions.stories.canApplyBoost(peer: inputPeer), automaticFloodWait: false) +// |> map { result -> (Api.stories.CanApplyBoostResult?, CanApplyBoostStatus.ErrorReason?) in +// return (result, nil) +// } +// |> `catch` { error -> Signal<(Api.stories.CanApplyBoostResult?, CanApplyBoostStatus.ErrorReason?), NoError> in +// let reason: CanApplyBoostStatus.ErrorReason +// if error.errorDescription == "PREMIUM_ACCOUNT_REQUIRED" { +// reason = .premiumRequired +// } else if error.errorDescription.hasPrefix("FLOOD_WAIT_") { +// let errorText = error.errorDescription ?? "" +// if let underscoreIndex = errorText.lastIndex(of: "_") { +// let timeoutText = errorText[errorText.index(after: underscoreIndex)...] +// if let timeoutValue = Int32(String(timeoutText)) { +// reason = .floodWait(timeoutValue) +// } else { +// reason = .generic +// } +// } else { +// reason = .generic +// } +// } else if error.errorDescription == "SAME_BOOST_ALREADY_ACTIVE" || error.errorDescription == "BOOST_NOT_MODIFIED" { +// reason = .peerBoostAlreadyActive +// } else if error.errorDescription == "PREMIUM_GIFTED_NOT_ALLOWED" { +// reason = .giftedPremiumNotAllowed +// } else { +// reason = .generic +// } +// +// return .single((nil, reason)) +// } +// |> mapToSignal { result, errorReason -> Signal in +// guard let result = result else { +// return .single(.error(errorReason ?? .generic)) +// } +// +// return account.postbox.transaction { transaction -> CanApplyBoostStatus in +// switch result { +// case .canApplyBoostOk: +// return .ok +// case let .canApplyBoostReplace(currentBoost, chats): +// updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(transaction: transaction, chats: chats, users: [])) +// +// if let peer = transaction.getPeer(currentBoost.peerId) { +// return .replace(currentBoost: EnginePeer(peer)) +// } else { +// return .error(.generic) +// } +// } +// } +// } +// } } func _internal_applyChannelBoost(account: Account, peerId: PeerId) -> Signal { - return account.postbox.transaction { transaction -> Api.InputPeer? in - return transaction.getPeer(peerId).flatMap(apiInputPeer) - } - |> mapToSignal { inputPeer -> Signal in - guard let inputPeer = inputPeer else { - return .single(false) - } - return account.network.request(Api.functions.stories.applyBoost(peer: inputPeer)) - |> `catch` { error -> Signal in - return .single(.boolFalse) - } - |> map { result -> Bool in - if case .boolTrue = result { - return true - } - return false - } - } + return .single(false) +// return account.postbox.transaction { transaction -> Api.InputPeer? in +// return transaction.getPeer(peerId).flatMap(apiInputPeer) +// } +// |> mapToSignal { inputPeer -> Signal in +// guard let inputPeer = inputPeer else { +// return .single(false) +// } +// return account.network.request(Api.functions.stories.applyBoost(peer: inputPeer)) +// |> `catch` { error -> Signal in +// return .single(.boolFalse) +// } +// |> map { result -> Bool in +// if case .boolTrue = result { +// return true +// } +// return false +// } +// } } private final class ChannelBoostersContextImpl { @@ -240,95 +243,95 @@ private final class ChannelBoostersContextImpl { } func loadMore() { - if self.isLoadingMore { - return - } - self.isLoadingMore = true - let account = self.account - let accountPeerId = account.peerId - let peerId = self.peerId - let populateCache = self.populateCache - - if self.loadedFromCache { - self.loadedFromCache = false - } - let lastOffset = self.lastOffset - - self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in - return transaction.getPeer(peerId).flatMap(apiInputPeer) - } - |> mapToSignal { inputPeer -> Signal<([ChannelBoostersContext.State.Booster], Int32, String?), NoError> in - if let inputPeer = inputPeer { - let offset = lastOffset ?? "" - let limit: Int32 = lastOffset == nil ? 25 : 50 - - let signal = account.network.request(Api.functions.stories.getBoostersList(peer: inputPeer, offset: offset, limit: limit)) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { result -> Signal<([ChannelBoostersContext.State.Booster], Int32, String?), NoError> in - return account.postbox.transaction { transaction -> ([ChannelBoostersContext.State.Booster], Int32, String?) in - guard let result = result else { - return ([], 0, nil) - } - switch result { - case let .boostersList(_, count, boosters, nextOffset, users): - updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) - var resultBoosters: [ChannelBoostersContext.State.Booster] = [] - for booster in boosters { - let peerId: EnginePeer.Id - let expires: Int32 - switch booster { - case let .booster(userId, expiresValue): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) - expires = expiresValue - } - if let peer = transaction.getPeer(peerId) { - resultBoosters.append(ChannelBoostersContext.State.Booster(peer: EnginePeer(peer), expires: expires)) - } - } - if populateCache { - if let entry = CodableEntry(CachedChannelBoosters(boosters: resultBoosters, count: count)) { - transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosters, key: CachedChannelBoosters.key(peerId: peerId)), entry: entry) - } - } - return (resultBoosters, count, nextOffset) - } - } - } - return signal - } else { - return .single(([], 0, nil)) - } - } - |> deliverOn(self.queue)).start(next: { [weak self] boosters, updatedCount, nextOffset in - guard let strongSelf = self else { - return - } - strongSelf.lastOffset = nextOffset - if strongSelf.populateCache { - strongSelf.populateCache = false - strongSelf.results.removeAll() - } - var existingIds = Set(strongSelf.results.map { $0.peer.id }) - for booster in boosters { - if !existingIds.contains(booster.peer.id) { - strongSelf.results.append(booster) - existingIds.insert(booster.peer.id) - } - } - strongSelf.isLoadingMore = false - strongSelf.hasLoadedOnce = true - strongSelf.canLoadMore = !boosters.isEmpty - if strongSelf.canLoadMore { - strongSelf.count = max(updatedCount, Int32(strongSelf.results.count)) - } else { - strongSelf.count = Int32(strongSelf.results.count) - } - strongSelf.updateState() - })) - self.updateState() +// if self.isLoadingMore { +// return +// } +// self.isLoadingMore = true +// let account = self.account +// let accountPeerId = account.peerId +// let peerId = self.peerId +// let populateCache = self.populateCache +// +// if self.loadedFromCache { +// self.loadedFromCache = false +// } +// let lastOffset = self.lastOffset +// +// self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in +// return transaction.getPeer(peerId).flatMap(apiInputPeer) +// } +// |> mapToSignal { inputPeer -> Signal<([ChannelBoostersContext.State.Booster], Int32, String?), NoError> in +// if let inputPeer = inputPeer { +// let offset = lastOffset ?? "" +// let limit: Int32 = lastOffset == nil ? 25 : 50 +// +// let signal = account.network.request(Api.functions.stories.getBoostersList(peer: inputPeer, offset: offset, limit: limit)) +// |> map(Optional.init) +// |> `catch` { _ -> Signal in +// return .single(nil) +// } +// |> mapToSignal { result -> Signal<([ChannelBoostersContext.State.Booster], Int32, String?), NoError> in +// return account.postbox.transaction { transaction -> ([ChannelBoostersContext.State.Booster], Int32, String?) in +// guard let result = result else { +// return ([], 0, nil) +// } +// switch result { +// case let .boostersList(_, count, boosters, nextOffset, users): +// updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) +// var resultBoosters: [ChannelBoostersContext.State.Booster] = [] +// for booster in boosters { +// let peerId: EnginePeer.Id +// let expires: Int32 +// switch booster { +// case let .booster(userId, expiresValue): +// peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) +// expires = expiresValue +// } +// if let peer = transaction.getPeer(peerId) { +// resultBoosters.append(ChannelBoostersContext.State.Booster(peer: EnginePeer(peer), expires: expires)) +// } +// } +// if populateCache { +// if let entry = CodableEntry(CachedChannelBoosters(boosters: resultBoosters, count: count)) { +// transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosters, key: CachedChannelBoosters.key(peerId: peerId)), entry: entry) +// } +// } +// return (resultBoosters, count, nextOffset) +// } +// } +// } +// return signal +// } else { +// return .single(([], 0, nil)) +// } +// } +// |> deliverOn(self.queue)).start(next: { [weak self] boosters, updatedCount, nextOffset in +// guard let strongSelf = self else { +// return +// } +// strongSelf.lastOffset = nextOffset +// if strongSelf.populateCache { +// strongSelf.populateCache = false +// strongSelf.results.removeAll() +// } +// var existingIds = Set(strongSelf.results.map { $0.peer.id }) +// for booster in boosters { +// if !existingIds.contains(booster.peer.id) { +// strongSelf.results.append(booster) +// existingIds.insert(booster.peer.id) +// } +// } +// strongSelf.isLoadingMore = false +// strongSelf.hasLoadedOnce = true +// strongSelf.canLoadMore = !boosters.isEmpty +// if strongSelf.canLoadMore { +// strongSelf.count = max(updatedCount, Int32(strongSelf.results.count)) +// } else { +// strongSelf.count = Int32(strongSelf.results.count) +// } +// strongSelf.updateState() +// })) +// self.updateState() } private func updateCache() { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 65ac82ad77..19bf4320b8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -522,7 +522,7 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa } } } - case let .premiumGiftCode(peerIds, boostPeer, quantity, option): + case .premiumGiftCode: receiptMessageId = nil } } diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index 95351ed979..e73a756b8d 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -235,6 +235,14 @@ public extension Peer { return nil } } + + var hasCustomNameColor: Bool { + let defaultNameColor = PeerNameColor(rawValue: Int32(self.id.id._internalGetInt64Value() % 7)) ?? .blue + if self.nameColor != defaultNameColor { + return true + } + return false + } } public extension TelegramPeerUsername { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index cb90bdf215..77bb1bb259 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -407,6 +407,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatRecentActionsController", "//submodules/TelegramUI/Components/Chat/ChatNavigationButton", "//submodules/TelegramUI/Components/Chat/ChatLoadingNode", + "//submodules/TelegramUI/Components/Settings/PeerNameColorScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 026b4835dc..eb0145a1d2 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -58,36 +58,41 @@ struct CameraState: Equatable { let flashMode: Camera.FlashMode let flashModeDidChange: Bool let flashTint: FlashTint + let flashTintSize: CGFloat let recording: Recording let duration: Double let isDualCameraEnabled: Bool func updatedMode(_ mode: CameraMode) -> CameraState { - return CameraState(mode: mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) + return CameraState(mode: mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) } func updatedPosition(_ position: Camera.Position) -> CameraState { - return CameraState(mode: self.mode, position: position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) + return CameraState(mode: self.mode, position: position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) } func updatedFlashMode(_ flashMode: Camera.FlashMode) -> CameraState { - return CameraState(mode: self.mode, position: self.position, flashMode: flashMode, flashModeDidChange: self.flashMode != flashMode, flashTint: self.flashTint, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) + return CameraState(mode: self.mode, position: self.position, flashMode: flashMode, flashModeDidChange: self.flashMode != flashMode, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) } func updatedFlashTint(_ flashTint: FlashTint) -> CameraState { - return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: flashTint, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) + return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) + } + + func updatedFlashTintSize(_ size: CGFloat) -> CameraState { + return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: size, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) } func updatedRecording(_ recording: Recording) -> CameraState { - return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) + return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled) } func updatedDuration(_ duration: Double) -> CameraState { - return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, recording: self.recording, duration: duration, isDualCameraEnabled: self.isDualCameraEnabled) + return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: duration, isDualCameraEnabled: self.isDualCameraEnabled) } func updatedIsDualCameraEnabled(_ isDualCameraEnabled: Bool) -> CameraState { - return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, recording: self.recording, duration: self.duration, isDualCameraEnabled: isDualCameraEnabled) + return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: isDualCameraEnabled) } } @@ -439,6 +444,14 @@ private final class CameraScreenComponent: CombinedComponent { } } + func updateFlashTintSize(_ size: CGFloat) { + guard let controller = self.getController() else { + return + } + + controller.updateCameraState({ $0.updatedFlashTintSize(size) }, transition: .immediate) + } + func presentFlashTint() { guard let controller = self.getController(), let camera = controller.camera else { return @@ -1156,9 +1169,13 @@ private final class CameraScreenComponent: CombinedComponent { component: FlashTintControlComponent( position: flashButtonPosition.offsetBy(dx: 0.0, dy: 27.0), tint: component.cameraState.flashTint, + size: component.cameraState.flashTintSize, update: { [weak state] tint in state?.updateFlashTint(tint) }, + updateSize: { [weak state] size in + state?.updateFlashTintSize(size) + }, dismiss: { [weak state] in state?.displayingFlashTint = false state?.updated(transition: .easeInOut(duration: 0.2)) @@ -1461,6 +1478,7 @@ public class CameraScreen: ViewController { flashMode: .off, flashModeDidChange: false, flashTint: .white, + flashTintSize: 1.0, recording: .none, duration: 0.0, isDualCameraEnabled: isDualCameraEnabled diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift index 9f2b676bd0..769817e9bc 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/FlashTintControlComponent.swift @@ -121,7 +121,7 @@ private final class FlashColorComponent: Component { self.circleLayer.fillColor = UIColor(rgb: 0xffffff, alpha: 0.1).cgColor self.ringLayer?.borderColor = UIColor.clear.cgColor } - + return contentSize } } @@ -138,23 +138,29 @@ private final class FlashColorComponent: Component { final class FlashTintControlComponent: Component { let position: CGPoint let tint: CameraState.FlashTint + let size: CGFloat let update: (CameraState.FlashTint?) -> Void + let updateSize: (CGFloat) -> Void let dismiss: () -> Void init( position: CGPoint, tint: CameraState.FlashTint, + size: CGFloat, update: @escaping (CameraState.FlashTint?) -> Void, + updateSize: @escaping (CGFloat) -> Void, dismiss: @escaping () -> Void ) { self.position = position self.tint = tint + self.size = size self.update = update + self.updateSize = updateSize self.dismiss = dismiss } static func == (lhs: FlashTintControlComponent, rhs: FlashTintControlComponent) -> Bool { - return lhs.position == rhs.position && lhs.tint == rhs.tint + return lhs.position == rhs.position && lhs.tint == rhs.tint && lhs.size == rhs.size } final class View: UIButton { @@ -165,10 +171,16 @@ final class FlashTintControlComponent: Component { private let effectView: UIVisualEffectView private let maskLayer = CAShapeLayer() private let swatches = ComponentView() + private let sliderView: SliderView override init(frame: CGRect) { self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + var sizeUpdateImpl: ((CGFloat) -> Void)? + self.sliderView = SliderView(minValue: 0.0, maxValue: 1.0, value: 1.0, valueChanged: { value , _ in + sizeUpdateImpl?(value) + }) + super.init(frame: frame) self.containerView.layer.anchorPoint = CGPoint(x: 0.8, y: 0.0) @@ -176,8 +188,15 @@ final class FlashTintControlComponent: Component { self.addSubview(self.dismissView) self.addSubview(self.containerView) self.containerView.addSubview(self.effectView) + self.containerView.addSubview(self.sliderView) self.dismissView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dismissTapped))) + + sizeUpdateImpl = { [weak self] size in + if let self, let component { + component.updateSize(size) + } + } } required init?(coder: NSCoder) { @@ -193,7 +212,12 @@ final class FlashTintControlComponent: Component { self.component = component let size = CGSize(width: 184.0, height: 92.0) + + self.sliderView.frame = CGRect(origin: CGPoint(x: 8.0, y: size.height - 38.0), size: CGSize(width: size.width - 16.0, height: 30.0)) + if isFirstTime { + self.sliderView.value = component.size + self.maskLayer.path = generateRoundedRectWithTailPath(rectSize: size, cornerRadius: 10.0, tailSize: CGSize(width: 18, height: 7.0), tailRadius: 1.0, tailPosition: 0.8, transformTail: false).cgPath self.maskLayer.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: size.height + 7.0)) self.effectView.layer.mask = self.maskLayer @@ -298,3 +322,112 @@ final class FlashTintControlComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +private final class SliderView: UIView { + private let foregroundView: UIView + private let knobView: UIImageView + + let minValue: CGFloat + let maxValue: CGFloat + var value: CGFloat = 1.0 { + didSet { + self.updateValue() + } + } + + private let valueChanged: (CGFloat, Bool) -> Void + + private let hapticFeedback = HapticFeedback() + + init(minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { + self.minValue = minValue + self.maxValue = maxValue + self.value = value + self.valueChanged = valueChanged + + self.foregroundView = UIView() + self.foregroundView.backgroundColor = UIColor(rgb: 0x8b8b8a) + + self.knobView = UIImageView(image: generateFilledCircleImage(diameter: 30.0, color: .white)) + + super.init(frame: .zero) + + self.backgroundColor = UIColor(rgb: 0x3e3e3e) + self.clipsToBounds = true + self.layer.cornerRadius = 15.0 + + self.foregroundView.isUserInteractionEnabled = false + self.knobView.isUserInteractionEnabled = false + + self.addSubview(self.foregroundView) + self.addSubview(self.knobView) + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + self.addGestureRecognizer(panGestureRecognizer) + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + self.addGestureRecognizer(tapGestureRecognizer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateValue(transition: Transition = .immediate) { + let width = self.frame.width + + let range = self.maxValue - self.minValue + let value = (self.value - self.minValue) / range + + transition.setFrame(view: self.foregroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: 15.0 + value * (width - 30.0), height: 30.0))) + transition.setFrame(view: self.knobView, frame: CGRect(origin: CGPoint(x: (width - 30.0) * value, y: 0.0), size: CGSize(width: 30.0, height: 30.0))) + } + + @objc private func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) { + let range = self.maxValue - self.minValue + switch gestureRecognizer.state { + case .began: + break + case .changed: + let previousValue = self.value + + let translation: CGFloat = gestureRecognizer.translation(in: gestureRecognizer.view).x + let delta = translation / self.bounds.width * range + self.value = max(self.minValue, min(self.maxValue, self.value + delta)) + gestureRecognizer.setTranslation(CGPoint(), in: gestureRecognizer.view) + + if self.value == 1.0 && previousValue != 1.0 { + self.hapticFeedback.impact(.soft) + } else if self.value == 0.0 && previousValue != 0.0 { + self.hapticFeedback.impact(.soft) + } + if abs(previousValue - self.value) >= 0.001 { + self.valueChanged(self.value, false) + } + case .ended: + let translation: CGFloat = gestureRecognizer.translation(in: gestureRecognizer.view).x + let delta = translation / self.bounds.width * range + self.value = max(self.minValue, min(self.maxValue, self.value + delta)) + self.valueChanged(self.value, true) + default: + break + } + } + + @objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { + let range = self.maxValue - self.minValue + let location = gestureRecognizer.location(in: gestureRecognizer.view) + self.value = max(self.minValue, min(self.maxValue, self.minValue + location.x / self.bounds.width * range)) + self.valueChanged(self.value, true) + } + + func canBeHighlighted() -> Bool { + return false + } + + func updateIsHighlighted(isHighlighted: Bool) { + } + + func performAction() { + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift index 92742864f4..48e3d8183e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift @@ -221,25 +221,25 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { let author = arguments.message?.effectiveAuthor - if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(arguments.parentMessage.id.peerId.namespace) && author?.id.namespace == Namespaces.Peer.CloudUser { + if author?.hasCustomNameColor == true || ([Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(arguments.parentMessage.id.peerId.namespace) && author?.id.namespace == Namespaces.Peer.CloudUser) { authorNameColor = author?.nameColor?.color dashSecondaryColor = author?.nameColor?.dashColors.1 - if let rawAuthorNameColor = authorNameColor { - var dimColors = false - switch arguments.presentationData.theme.theme.name { - case .builtin(.nightAccent), .builtin(.night): - dimColors = true - default: - break - } - if dimColors { - var hue: CGFloat = 0.0 - var saturation: CGFloat = 0.0 - var brightness: CGFloat = 0.0 - rawAuthorNameColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil) - authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0) - } - } +// if let rawAuthorNameColor = authorNameColor { +// var dimColors = false +// switch arguments.presentationData.theme.theme.name { +// case .builtin(.nightAccent), .builtin(.night): +// dimColors = true +// default: +// break +// } +// if dimColors { +// var hue: CGFloat = 0.0 +// var saturation: CGFloat = 0.0 +// var brightness: CGFloat = 0.0 +// rawAuthorNameColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil) +// authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0) +// } +// } } let mainColor: UIColor diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift index 5c2edf9f06..c004085599 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift @@ -213,6 +213,16 @@ private func peerNameColorScreenEntries( } else { nameColor = .blue } + + let replyText: String + let messageText: String + if case .channel = peer { + replyText = presentationData.strings.NameColor_ChatPreview_ReplyText_Channel + messageText = presentationData.strings.NameColor_ChatPreview_MessageText_Channel + } else { + replyText = presentationData.strings.NameColor_ChatPreview_ReplyText_Account + messageText = presentationData.strings.NameColor_ChatPreview_MessageText_Account + } let messageItem = PeerNameColorChatPreviewItem.MessageItem( outgoing: false, peerId: peer.id, @@ -220,9 +230,9 @@ private func peerNameColorScreenEntries( photo: peer.profileImageRepresentations, nameColor: nameColor, backgroundEmojiId: nil, - reply: (peer.compactDisplayTitle, presentationData.strings.NameColor_ChatPreview_ReplyText), + reply: (peer.compactDisplayTitle, replyText), linkPreview: (presentationData.strings.NameColor_ChatPreview_LinkSite, presentationData.strings.NameColor_ChatPreview_LinkTitle, presentationData.strings.NameColor_ChatPreview_LinkText), - text: presentationData.strings.NameColor_ChatPreview_MessageText + text: messageText ) entries.append(.colorHeader(presentationData.strings.NameColor_ChatPreview_Title)) @@ -304,17 +314,20 @@ public func PeerNameColorScreen( |> map { presentationData, state, availableReactions, peer -> (ItemListControllerState, (ItemListNodeState, Any)) in let isPremium = peer?.isPremium ?? false let title: String + let isLocked: Bool switch subject { case .account: title = presentationData.strings.NameColor_Title_Account + isLocked = !isPremium case .channel: title = presentationData.strings.NameColor_Title_Channel + isLocked = false } let footerItem = ApplyColorFooterItem( theme: presentationData.theme, title: presentationData.strings.NameColor_ApplyColor, - locked: !isPremium, + locked: isLocked, action: { if isPremium { let state = stateValue.with { $0 } diff --git a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift index 44927d8312..09e9ab45e4 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift @@ -8,12 +8,13 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { case none case text(String) case badge(String, UIColor) + case semitransparentBadge(String, UIColor) var text: String { switch self { case .none: return "" - case let .text(text), let .badge(text, _): + case let .text(text), let .badge(text, _), let .semitransparentBadge(text, _): return text } } @@ -22,7 +23,7 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { switch self { case .none, .text: return nil - case let .badge(_, color): + case let .badge(_, color), let .semitransparentBadge(_, color): return color } } @@ -135,11 +136,14 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { let titleFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize) self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor - + let textColorValue: UIColor = presentationData.theme.list.itemPrimaryTextColor let labelColorValue: UIColor let labelFont: UIFont - if case .badge = item.label { + if case let .semitransparentBadge(_, color) = item.label { + labelColorValue = color + labelFont = Font.semibold(14.0) + } else if case .badge = item.label { labelColorValue = presentationData.theme.list.itemCheckColors.foregroundColor labelFont = Font.regular(15.0) } else { @@ -170,7 +174,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { if previousItem?.text != item.text { self.iconNode.image = nil self.iconDisposable.set((iconSignal - |> deliverOnMainQueue).startStrict(next: { [weak self] icon in + |> deliverOnMainQueue).startStrict(next: { [weak self] icon in if let self { self.iconNode.image = icon } @@ -193,8 +197,16 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { transition.updateFrame(node: self.arrowNode, frame: arrowFrame) } - let badgeDiameter: CGFloat = 20.0 - if case let .badge(text, badgeColor) = item.label, !text.isEmpty { + var badgeDiameter: CGFloat = 20.0 + if case let .semitransparentBadge(text, badgeColor) = item.label, !text.isEmpty { + badgeDiameter = 24.0 + if previousItem?.label.badgeColor != badgeColor { + self.labelBadgeNode.image = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor.withAlphaComponent(0.1)) + } + if self.labelBadgeNode.supernode == nil { + self.insertSubnode(self.labelBadgeNode, belowSubnode: self.labelNode) + } + } else if case let .badge(text, badgeColor) = item.label, !text.isEmpty { if previousItem?.label.badgeColor != badgeColor { self.labelBadgeNode.image = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor) } @@ -205,15 +217,20 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { self.labelBadgeNode.removeFromSupernode() } - let badgeWidth = max(badgeDiameter, labelSize.width + 10.0) + var badgeWidth = max(badgeDiameter, labelSize.width + 10.0) + if case .semitransparentBadge = item.label { + badgeWidth += 2.0 + } let labelFrame: CGRect - if case .badge = item.label { + if case .semitransparentBadge = item.label { + labelFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: floor((height - labelSize.height) / 2.0)), size: labelSize) + } else if case .badge = item.label { labelFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: floor((height - labelSize.height) / 2.0)), size: labelSize) } else { labelFrame = CGRect(origin: CGPoint(x: width - rightInset - labelSize.width, y: 12.0), size: labelSize) } - let labelBadgeNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth, y: labelFrame.minY - 1.0), size: CGSize(width: badgeWidth, height: badgeDiameter)) + let labelBadgeNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth, y: floorToScreenPixels(labelFrame.midY - badgeDiameter / 2.0)), size: CGSize(width: badgeWidth, height: badgeDiameter)) self.activateArea.accessibilityLabel = item.text self.activateArea.accessibilityValue = item.label.text diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 13cad18441..61160180c1 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -94,6 +94,7 @@ import PeerReportScreen import WebUI import ShareWithPeersScreen import ItemListPeerItem +import PeerNameColorScreen enum PeerInfoAvatarEditingMode { case generic @@ -522,6 +523,7 @@ private final class PeerInfoInteraction { let openAddBotToGroup: () -> Void let performBotCommand: (PeerInfoBotCommand) -> Void let editingOpenPublicLinkSetup: () -> Void + let editingOpenNameColorSetup: () -> Void let editingOpenInviteLinksSetup: () -> Void let editingOpenDiscussionGroupSetup: () -> Void let editingToggleMessageSignatures: (Bool) -> Void @@ -574,6 +576,7 @@ private final class PeerInfoInteraction { openAddBotToGroup: @escaping () -> Void, performBotCommand: @escaping (PeerInfoBotCommand) -> Void, editingOpenPublicLinkSetup: @escaping () -> Void, + editingOpenNameColorSetup: @escaping () -> Void, editingOpenInviteLinksSetup: @escaping () -> Void, editingOpenDiscussionGroupSetup: @escaping () -> Void, editingToggleMessageSignatures: @escaping (Bool) -> Void, @@ -625,6 +628,7 @@ private final class PeerInfoInteraction { self.openAddBotToGroup = openAddBotToGroup self.performBotCommand = performBotCommand self.editingOpenPublicLinkSetup = editingOpenPublicLinkSetup + self.editingOpenNameColorSetup = editingOpenNameColorSetup self.editingOpenInviteLinksSetup = editingOpenInviteLinksSetup self.editingOpenDiscussionGroupSetup = editingOpenDiscussionGroupSetup self.editingToggleMessageSignatures = editingToggleMessageSignatures @@ -1537,18 +1541,18 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL switch channel.info { case .broadcast: let ItemUsername = 1 - let ItemInviteLinks = 2 - let ItemDiscussionGroup = 3 - let ItemSignMessages = 4 - let ItemSignMessagesHelp = 5 - let ItemDeleteChannel = 6 - let ItemReactions = 7 - - let ItemAdmins = 8 - let ItemMembers = 9 - let ItemMemberRequests = 10 - let ItemBanned = 11 - let ItemRecentActions = 12 + let ItemNameColor = 2 + let ItemInviteLinks = 3 + let ItemDiscussionGroup = 4 + let ItemSignMessages = 5 + let ItemSignMessagesHelp = 6 + let ItemDeleteChannel = 7 + let ItemReactions = 8 + let ItemAdmins = 9 + let ItemMembers = 10 + let ItemMemberRequests = 11 + let ItemBanned = 12 + let ItemRecentActions = 13 let isCreator = channel.flags.contains(.isCreator) @@ -1563,7 +1567,14 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL interaction.editingOpenPublicLinkSetup() })) } - + + if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { + + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemNameColor, label: .semitransparentBadge(EnginePeer(channel).compactDisplayTitle, (data.peer?.nameColor ?? .blue).color), text: "Channel Color", icon: UIImage(bundleImageName: "Settings/Menu/Appearance"), action: { + interaction.editingOpenNameColorSetup() + })) + } + if (isCreator && (channel.addressName?.isEmpty ?? true)) || (!channel.flags.contains(.isCreator) && channel.adminRights?.rights.contains(.canInviteUsers) == true) { let invitesText: String if let count = data.invitations?.count, count > 0 { @@ -2276,6 +2287,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro editingOpenPublicLinkSetup: { [weak self] in self?.editingOpenPublicLinkSetup() }, + editingOpenNameColorSetup: { [weak self] in + self?.editingOpenNameColorSetup() + }, editingOpenInviteLinksSetup: { [weak self] in self?.editingOpenInviteLinksSetup() }, @@ -7072,6 +7086,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } + private func editingOpenNameColorSetup() { + let controller = PeerNameColorScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, subject: .channel(self.peerId)) + self.controller?.push(controller) + } + private func editingOpenInviteLinksSetup() { self.controller?.push(inviteLinkListController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, admin: nil)) }