From 4f183fa7fe1c166895419d98e0ca34fc53d385ee Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 17 Oct 2023 01:41:53 +0400 Subject: [PATCH 1/2] 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/2] 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