Name colors improvements

This commit is contained in:
Ilya Laktyushin 2023-10-17 01:41:53 +04:00
parent 137c3d9101
commit 4f183fa7fe
25 changed files with 2299 additions and 117 deletions

View File

@ -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.";

View File

@ -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 {

View File

@ -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

View File

@ -233,6 +233,8 @@ public final class ItemListControllerNodeView: UITracingLayerView {
}
open class ItemListControllerNode: ASDisplayNode {
private weak var controller: ItemListController?
private var _ready = ValuePromise<Bool>()
open var ready: Signal<Bool, NoError> {
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 = []

View File

@ -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:

View File

@ -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",

View File

@ -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))
}

View File

@ -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

View File

@ -53,23 +53,24 @@ public enum UpdateNameColorAndEmojiError {
func _internal_updateNameColorAndEmoji(account: Account, nameColor: PeerNameColor, backgroundEmojiId: Int64?) -> Signal<Void, UpdateNameColorAndEmojiError> {
let flags: Int32 = (1 << 0)
return account.postbox.loadedPeerWithId(account.peerId)
|> castError(UpdateNameColorAndEmojiError.self)
|> mapToSignal { accountPeer -> Signal<Void, UpdateNameColorAndEmojiError> in
guard let accountPeer = accountPeer as? TelegramUser else {
return .fail(.generic)
return account.postbox.transaction { transaction -> Signal<Peer, NoError> 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<Void, UpdateNameColorAndEmojiError> 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<Void, UpdateNameColorAndEmojiError> 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<Void, UpdateNameColorAndEmojiError> in
return .complete()
}
}
}

View File

@ -505,6 +505,10 @@ public extension EnginePeer {
}
return false
}
var nameColor: PeerNameColor? {
return self._asPeer().nameColor
}
}
public extension EnginePeer {

View File

@ -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 {

View File

@ -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)
}
}
}

View File

@ -300,6 +300,7 @@ public enum PresentationResourceKey: Int32 {
case chatReplyBackgroundTemplateImage
case chatReplyServiceBackgroundTemplateImage
case chatReplyLineDashTemplateImage
}
public enum ChatExpiredStoryIndicatorType: Hashable {

View File

@ -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
}

View File

@ -81,7 +81,7 @@ private final class FlashColorComponent: Component {
func update(component: FlashColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, 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)

View File

@ -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

View File

@ -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
}

View File

@ -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 {

View File

@ -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",
],
)

View File

@ -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
}
}
}

View File

@ -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<Void, NoError>?, (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<Empty>
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<Empty>()
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)
}
}

View File

@ -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<Void, NoError>?, (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<PeerId, Peer>()
var messages = SimpleDictionary<MessageId, Message>()
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)
}
}

View File

@ -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<Void, NoError>?, (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<Void, NoError>?, (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)
}
}

View File

@ -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<PresentationData, NoError>)? = 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<MediaId>()
// 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
}

View File

@ -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