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

This commit is contained in:
Mike Renoir 2023-10-17 09:08:33 +04:00
commit e48319be5c
28 changed files with 2333 additions and 137 deletions

View File

@ -10123,3 +10123,20 @@ Sorry for the inconvenience.";
"Channel.AdminLog.MessageChangedNameColorSet" = "%1$@ set name color to %2$@"; "Channel.AdminLog.MessageChangedNameColorSet" = "%1$@ set name color to %2$@";
"Channel.AdminLog.MessageChangedBackgroundEmojiSet" = "%1$@ set background emoji 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

@ -63,7 +63,7 @@ private class AvatarNodeParameters: NSObject {
} }
} }
private func calculateColors(explicitColorIndex: Int?, peerId: EnginePeer.Id?, icon: AvatarNodeIcon, theme: PresentationTheme?) -> [UIColor] { private func calculateColors(explicitColorIndex: Int?, peerId: EnginePeer.Id?, nameColor: PeerNameColor?, icon: AvatarNodeIcon, theme: PresentationTheme?) -> [UIColor] {
let colorIndex: Int let colorIndex: Int
if let explicitColorIndex = explicitColorIndex { if let explicitColorIndex = explicitColorIndex {
colorIndex = explicitColorIndex colorIndex = explicitColorIndex
@ -109,9 +109,13 @@ private func calculateColors(explicitColorIndex: Int?, peerId: EnginePeer.Id?, i
} else { } else {
colors = AvatarNode.grayscaleColors colors = AvatarNode.grayscaleColors
} }
} else {
if let nameColor {
colors = AvatarNode.gradientColors[Int(nameColor.rawValue) % AvatarNode.gradientColors.count]
} else { } else {
colors = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count] colors = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count]
} }
}
return colors return colors
} }
@ -122,7 +126,7 @@ public enum AvatarNodeExplicitIcon {
private enum AvatarNodeState: Equatable { private enum AvatarNodeState: Equatable {
case empty case empty
case peerAvatar(EnginePeer.Id, [String], TelegramMediaImageRepresentation?, AvatarNodeClipStyle) case peerAvatar(EnginePeer.Id, PeerNameColor?, [String], TelegramMediaImageRepresentation?, AvatarNodeClipStyle)
case custom(letter: [String], explicitColorIndex: Int?, explicitIcon: AvatarNodeExplicitIcon?) case custom(letter: [String], explicitColorIndex: Int?, explicitIcon: AvatarNodeExplicitIcon?)
} }
@ -130,8 +134,8 @@ private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool {
switch (lhs, rhs) { switch (lhs, rhs) {
case (.empty, .empty): case (.empty, .empty):
return true return true
case let (.peerAvatar(lhsPeerId, lhsLetters, lhsPhotoRepresentations, lhsClipStyle), .peerAvatar(rhsPeerId, rhsLetters, rhsPhotoRepresentations, rhsClipStyle)): case let (.peerAvatar(lhsPeerId, lhsPeerNameColor, lhsLetters, lhsPhotoRepresentations, lhsClipStyle), .peerAvatar(rhsPeerId, rhsPeerNameColor, rhsLetters, rhsPhotoRepresentations, rhsClipStyle)):
return lhsPeerId == rhsPeerId && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations && lhsClipStyle == rhsClipStyle return lhsPeerId == rhsPeerId && lhsPeerNameColor == rhsPeerNameColor && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations && lhsClipStyle == rhsClipStyle
case let (.custom(lhsLetters, lhsIndex, lhsIcon), .custom(rhsLetters, rhsIndex, rhsIcon)): case let (.custom(lhsLetters, lhsIndex, lhsIcon), .custom(rhsLetters, rhsIndex, rhsIcon)):
return lhsLetters == rhsLetters && lhsIndex == rhsIndex && lhsIcon == rhsIcon return lhsLetters == rhsLetters && lhsIndex == rhsIndex && lhsIcon == rhsIcon
default: default:
@ -450,7 +454,7 @@ public final class AvatarNode: ASDisplayNode {
} else if peer?.restrictionText(platform: "ios", contentSettings: contentSettings) == nil { } else if peer?.restrictionText(platform: "ios", contentSettings: contentSettings) == nil {
representation = peer?.smallProfileImage representation = peer?.smallProfileImage
} }
let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.displayLetters ?? [], representation, clipStyle) let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.nameColor, peer?.displayLetters ?? [], representation, clipStyle)
if updatedState != self.state || overrideImage != self.overrideImage || theme !== self.theme { if updatedState != self.state || overrideImage != self.overrideImage || theme !== self.theme {
self.state = updatedState self.state = updatedState
self.overrideImage = overrideImage self.overrideImage = overrideImage
@ -485,7 +489,7 @@ public final class AvatarNode: ASDisplayNode {
self.editOverlayNode?.isHidden = true self.editOverlayNode?.isHidden = true
} }
parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer.id, colors: calculateColors(explicitColorIndex: nil, peerId: peer.id, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer.id, colors: calculateColors(explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle)
} else { } else {
self.imageReady.set(.single(true)) self.imageReady.set(.single(true))
self.displaySuspended = false self.displaySuspended = false
@ -494,7 +498,7 @@ public final class AvatarNode: ASDisplayNode {
} }
self.editOverlayNode?.isHidden = true self.editOverlayNode?.isHidden = true
let colors = calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), icon: icon, theme: theme) let colors = calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme)
parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle) parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle)
if let badgeView = self.badgeView { if let badgeView = self.badgeView {
@ -614,7 +618,7 @@ public final class AvatarNode: ASDisplayNode {
} else if peer?.restrictionText(platform: "ios", contentSettings: genericContext.currentContentSettings.with { $0 }) == nil { } else if peer?.restrictionText(platform: "ios", contentSettings: genericContext.currentContentSettings.with { $0 }) == nil {
representation = peer?.smallProfileImage representation = peer?.smallProfileImage
} }
let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.displayLetters ?? [], representation, clipStyle) let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.nameColor, peer?.displayLetters ?? [], representation, clipStyle)
if updatedState != self.state || overrideImage != self.overrideImage || theme !== self.theme { if updatedState != self.state || overrideImage != self.overrideImage || theme !== self.theme {
self.state = updatedState self.state = updatedState
self.overrideImage = overrideImage self.overrideImage = overrideImage
@ -651,7 +655,7 @@ public final class AvatarNode: ASDisplayNode {
self.editOverlayNode?.isHidden = true self.editOverlayNode?.isHidden = true
} }
parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, colors: calculateColors(explicitColorIndex: nil, peerId: peer.id, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, colors: calculateColors(explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle)
} else { } else {
self.imageReady.set(.single(true)) self.imageReady.set(.single(true))
self.displaySuspended = false self.displaySuspended = false
@ -660,7 +664,7 @@ public final class AvatarNode: ASDisplayNode {
} }
self.editOverlayNode?.isHidden = true self.editOverlayNode?.isHidden = true
let colors = calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), icon: icon, theme: theme) let colors = calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme)
parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle) parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle)
if let badgeView = self.badgeView { if let badgeView = self.badgeView {
@ -697,9 +701,9 @@ public final class AvatarNode: ASDisplayNode {
let parameters: AvatarNodeParameters let parameters: AvatarNodeParameters
if let icon = icon, case .phone = icon { if let icon = icon, case .phone = icon {
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(explicitColorIndex: explicitIndex, peerId: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
} else { } else {
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(explicitColorIndex: explicitIndex, peerId: nil, icon: .none, theme: nil), letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round) parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .none, theme: nil), letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
} }
self.displaySuspended = true self.displaySuspended = true

View File

@ -25,7 +25,7 @@ public final class AvatarVideoNode: ASDisplayNode {
private var emojiMarkup: TelegramMediaImage.EmojiMarkup? private var emojiMarkup: TelegramMediaImage.EmojiMarkup?
private var fileDisposable: Disposable? private var fileDisposable = MetaDisposable()
private var animationFile: TelegramMediaFile? private var animationFile: TelegramMediaFile?
private var itemLayer: EmojiPagerContentComponent.View.ItemLayer? private var itemLayer: EmojiPagerContentComponent.View.ItemLayer?
private var useAnimationNode = false private var useAnimationNode = false
@ -55,7 +55,7 @@ public final class AvatarVideoNode: ASDisplayNode {
} }
deinit { deinit {
self.fileDisposable?.dispose() self.fileDisposable.dispose()
self.stickerFetchedDisposable.dispose() self.stickerFetchedDisposable.dispose()
self.playbackStartDisposable.dispose() self.playbackStartDisposable.dispose()
} }
@ -174,15 +174,15 @@ public final class AvatarVideoNode: ASDisplayNode {
switch markup.content { switch markup.content {
case let .emoji(fileId): case let .emoji(fileId):
self.fileDisposable = (self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) self.fileDisposable.set((self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId])
|> deliverOnMainQueue).startStrict(next: { [weak self] files in |> deliverOnMainQueue).startStrict(next: { [weak self] files in
if let strongSelf = self, let file = files.values.first { if let strongSelf = self, let file = files.values.first {
strongSelf.animationFile = file strongSelf.animationFile = file
strongSelf.setupAnimation() strongSelf.setupAnimation()
} }
}).strict() }))
case let .sticker(packReference, fileId): case let .sticker(packReference, fileId):
self.fileDisposable = (self.context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: false) self.fileDisposable.set((self.context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: false)
|> map { pack -> TelegramMediaFile? in |> map { pack -> TelegramMediaFile? in
if case let .result(_, items, _) = pack, let item = items.first(where: { $0.file.fileId.id == fileId }) { if case let .result(_, items, _) = pack, let item = items.first(where: { $0.file.fileId.id == fileId }) {
return item.file return item.file
@ -194,7 +194,7 @@ public final class AvatarVideoNode: ASDisplayNode {
strongSelf.animationFile = file strongSelf.animationFile = file
strongSelf.setupAnimation() strongSelf.setupAnimation()
} }
}).strict() }))
} }
} }

View File

@ -29,7 +29,7 @@ open class ListViewItemHeaderNode: ASDisplayNode {
private var isFlashingOnScrolling = false private var isFlashingOnScrolling = false
weak var attachedToItemNode: ListViewItemNode? weak var attachedToItemNode: ListViewItemNode?
var item: ListViewItemHeader? public var item: ListViewItemHeader?
func updateInternalStickLocationDistanceFactor(_ factor: CGFloat, animated: Bool) { func updateInternalStickLocationDistanceFactor(_ factor: CGFloat, animated: Bool) {
self.internalStickLocationDistanceFactor = factor self.internalStickLocationDistanceFactor = factor
@ -124,7 +124,7 @@ open class ListViewItemHeaderNode: ASDisplayNode {
private var cachedLayout: (CGSize, CGFloat, CGFloat)? 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 var update = false
if let cachedLayout = self.cachedLayout { if let cachedLayout = self.cachedLayout {
if cachedLayout.0 != size || cachedLayout.1 != leftInset || cachedLayout.2 != rightInset { 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.isOpaqueWhenInOverlay = true
self.blocksBackgroundWhenInOverlay = true self.blocksBackgroundWhenInOverlay = true
self.automaticallyControlPresentationContextLayout = false
self.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style self.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style

View File

@ -233,6 +233,8 @@ public final class ItemListControllerNodeView: UITracingLayerView {
} }
open class ItemListControllerNode: ASDisplayNode { open class ItemListControllerNode: ASDisplayNode {
private weak var controller: ItemListController?
private var _ready = ValuePromise<Bool>() private var _ready = ValuePromise<Bool>()
open var ready: Signal<Bool, NoError> { open var ready: Signal<Bool, NoError> {
return self._ready.get() return self._ready.get()
@ -295,6 +297,7 @@ open class ItemListControllerNode: ASDisplayNode {
private var previousContentOffset: ListViewVisibleContentOffset? private var previousContentOffset: ListViewVisibleContentOffset?
public init(controller: ItemListController?, navigationBar: NavigationBar, state: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError>) { public init(controller: ItemListController?, navigationBar: NavigationBar, state: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError>) {
self.controller = controller
self.navigationBar = navigationBar self.navigationBar = navigationBar
self.listNode = ListView() self.listNode = ListView()
@ -585,9 +588,10 @@ open class ItemListControllerNode: ASDisplayNode {
insets.top += headerHeight insets.top += headerHeight
} }
var footerHeight: CGFloat = 0.0
if let footerItemNode = self.footerItemNode { if let footerItemNode = self.footerItemNode {
let footerHeight = footerItemNode.updateLayout(layout: layout, transition: transition) footerHeight = footerItemNode.updateLayout(layout: layout, transition: transition)
insets.bottom += footerHeight insets.bottom = footerHeight
} }
self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) 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() 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 { if !self.afterLayoutActions.isEmpty {
let afterLayoutActions = self.afterLayoutActions let afterLayoutActions = self.afterLayoutActions
self.afterLayoutActions = [] self.afterLayoutActions = []

View File

@ -35,6 +35,7 @@ public enum ItemListDisclosureLabelStyle {
case multilineDetailText case multilineDetailText
case badge(UIColor) case badge(UIColor)
case color(UIColor) case color(UIColor)
case semitransparentBadge(UIColor)
case image(image: UIImage, size: CGSize) case image(image: UIImage, size: CGSize)
} }
@ -125,6 +126,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem {
} }
private let badgeFont = Font.regular(15.0) private let badgeFont = Font.regular(15.0)
private let boldBadgeFont = Font.semibold(14.0)
public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
@ -256,11 +258,21 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
var updatedLabelBadgeImage: UIImage? var updatedLabelBadgeImage: UIImage?
var updatedLabelImage: UIImage? var updatedLabelImage: UIImage?
var badgeDiameter: CGFloat = 20.0
var badgeColor: UIColor? var badgeColor: UIColor?
var badgeColorUpdated = false
if case let .badge(color) = item.labelStyle { if case let .badge(color) = item.labelStyle {
if item.label.count > 0 { if item.label.count > 0 {
badgeColor = color 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 { if case let .color(color) = item.labelStyle {
var updatedColor = true var updatedColor = true
@ -277,7 +289,6 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
updatedLabelImage = image updatedLabelImage = image
} }
let badgeDiameter: CGFloat = 20.0
if currentItem?.presentationData.theme !== item.presentationData.theme { if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme updatedTheme = item.presentationData.theme
switch item.disclosureStyle { switch item.disclosureStyle {
@ -289,7 +300,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
if let badgeColor = badgeColor { if let badgeColor = badgeColor {
updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: 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) updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor)
} }
@ -313,7 +324,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
var additionalTextRightInset: CGFloat = 0.0 var additionalTextRightInset: CGFloat = 0.0
switch item.labelStyle { switch item.labelStyle {
case .badge: case .badge, .semitransparentBadge:
additionalTextRightInset += 44.0 additionalTextRightInset += 44.0
default: default:
break break
@ -355,6 +366,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
case .badge: case .badge:
labelBadgeColor = item.presentationData.theme.list.itemCheckColors.foregroundColor labelBadgeColor = item.presentationData.theme.list.itemCheckColors.foregroundColor
labelFont = badgeFont labelFont = badgeFont
case let .semitransparentBadge(color):
labelBadgeColor = color
labelFont = boldBadgeFont
case .detailText, .multilineDetailText: case .detailText, .multilineDetailText:
labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor
labelFont = detailFont labelFont = detailFont
@ -578,14 +592,19 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.labelBadgeNode.removeFromSupernode() 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)) 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 strongSelf.labelBadgeNode.frame = badgeFrame
let labelFrame: CGRect let labelFrame: CGRect
switch item.labelStyle { switch item.labelStyle {
case .badge: 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: case .detailText, .multilineDetailText:
labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size) labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size)
default: default:

View File

@ -114,6 +114,7 @@ swift_library(
"//submodules/ImageBlur:ImageBlur", "//submodules/ImageBlur:ImageBlur",
"//submodules/AttachmentUI:AttachmentUI", "//submodules/AttachmentUI:AttachmentUI",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen",
"//submodules/TelegramUI/Components/Settings/PeerNameColorScreen",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -18,6 +18,7 @@ import AccountContext
import ContextUI import ContextUI
import UndoUI import UndoUI
import PremiumUI import PremiumUI
import PeerNameColorScreen
func themeDisplayName(strings: PresentationStrings, reference: PresentationThemeReference) -> String { func themeDisplayName(strings: PresentationStrings, reference: PresentationThemeReference) -> String {
let name: String let name: String
@ -50,6 +51,7 @@ private final class ThemeSettingsControllerArguments {
let selectTheme: (PresentationThemeReference) -> Void let selectTheme: (PresentationThemeReference) -> Void
let openThemeSettings: () -> Void let openThemeSettings: () -> Void
let openWallpaperSettings: () -> Void let openWallpaperSettings: () -> Void
let openNameColorSettings: () -> Void
let selectAccentColor: (PresentationThemeAccentColor?) -> Void let selectAccentColor: (PresentationThemeAccentColor?) -> Void
let openAccentColorPicker: (PresentationThemeReference, Bool) -> Void let openAccentColorPicker: (PresentationThemeReference, Bool) -> Void
let toggleNightTheme: (Bool) -> Void let toggleNightTheme: (Bool) -> Void
@ -64,11 +66,12 @@ private final class ThemeSettingsControllerArguments {
let themeContextAction: (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void let themeContextAction: (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void
let colorContextAction: (Bool, PresentationThemeReference, ThemeSettingsColorOption?, 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.context = context
self.selectTheme = selectTheme self.selectTheme = selectTheme
self.openThemeSettings = openThemeSettings self.openThemeSettings = openThemeSettings
self.openWallpaperSettings = openWallpaperSettings self.openWallpaperSettings = openWallpaperSettings
self.openNameColorSettings = openNameColorSettings
self.selectAccentColor = selectAccentColor self.selectAccentColor = selectAccentColor
self.openAccentColorPicker = openAccentColorPicker self.openAccentColorPicker = openAccentColorPicker
self.toggleNightTheme = toggleNightTheme 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 themes(PresentationTheme, PresentationStrings, [PresentationThemeReference], PresentationThemeReference, Bool, [String: [StickerPackItem]], [Int64: PresentationThemeAccentColor], [Int64: TelegramWallpaper])
case chatTheme(PresentationTheme, String) case chatTheme(PresentationTheme, String)
case wallpaper(PresentationTheme, String) case wallpaper(PresentationTheme, String)
case nameColor(PresentationTheme, String, String, UIColor)
case autoNight(PresentationTheme, String, Bool, Bool) case autoNight(PresentationTheme, String, Bool, Bool)
case autoNightTheme(PresentationTheme, String, String) case autoNightTheme(PresentationTheme, String, String)
case textSize(PresentationTheme, String, String) case textSize(PresentationTheme, String, String)
@ -133,7 +137,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .themeListHeader, .chatPreview, .themes, .chatTheme, .wallpaper: case .themeListHeader, .chatPreview, .themes, .chatTheme, .wallpaper, .nameColor:
return ThemeSettingsControllerSection.chatPreview.rawValue return ThemeSettingsControllerSection.chatPreview.rawValue
case .autoNight, .autoNightTheme: case .autoNight, .autoNightTheme:
return ThemeSettingsControllerSection.nightMode.rawValue return ThemeSettingsControllerSection.nightMode.rawValue
@ -160,28 +164,30 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
return 3 return 3
case .wallpaper: case .wallpaper:
return 4 return 4
case .autoNight: case .nameColor:
return 5 return 5
case .autoNightTheme: case .autoNight:
return 6 return 6
case .textSize: case .autoNightTheme:
return 7 return 7
case .bubbleSettings: case .textSize:
return 8 return 8
case .powerSaving: case .bubbleSettings:
return 9 return 9
case .stickersAndEmoji: case .powerSaving:
return 10 return 10
case .iconHeader: case .stickersAndEmoji:
return 11 return 11
case .iconItem: case .iconHeader:
return 12 return 12
case .otherHeader: case .iconItem:
return 13 return 13
case .showNextMediaOnTap: case .otherHeader:
return 14 return 14
case .showNextMediaOnTapInfo: case .showNextMediaOnTap:
return 15 return 15
case .showNextMediaOnTapInfo:
return 16
} }
} }
@ -211,6 +217,12 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
} else { } else {
return false 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): case let .autoNight(lhsTheme, lhsText, lhsValue, lhsEnabled):
if case let .autoNight(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { if case let .autoNight(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled {
return true return true
@ -309,6 +321,10 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: {
arguments.openWallpaperSettings() 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): case let .autoNight(_, title, value, enabled):
return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleNightTheme(value) 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] = [] var entries: [ThemeSettingsControllerEntry] = []
let strings = presentationData.strings let strings = presentationData.strings
@ -365,6 +381,10 @@ private func themeSettingsControllerEntries(presentationData: PresentationData,
entries.append(.chatTheme(presentationData.theme, strings.Settings_ChatThemes)) entries.append(.chatTheme(presentationData.theme, strings.Settings_ChatThemes))
entries.append(.wallpaper(presentationData.theme, strings.Settings_ChatBackground)) 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)) entries.append(.autoNight(presentationData.theme, strings.Appearance_NightTheme, presentationThemeSettings.automaticThemeSwitchSetting.force, !presentationData.autoNightModeTriggered || presentationThemeSettings.automaticThemeSwitchSetting.force))
let autoNightMode: String let autoNightMode: String
switch presentationThemeSettings.automaticThemeSwitchSetting.trigger { switch presentationThemeSettings.automaticThemeSwitchSetting.trigger {
@ -488,6 +508,8 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
pushControllerImpl?(themePickerController(context: context)) pushControllerImpl?(themePickerController(context: context))
}, openWallpaperSettings: { }, openWallpaperSettings: {
pushControllerImpl?(ThemeGridController(context: context)) pushControllerImpl?(ThemeGridController(context: context))
}, openNameColorSettings: {
pushControllerImpl?(PeerNameColorScreen(context: context, subject: .account))
}, selectAccentColor: { accentColor in }, selectAccentColor: { accentColor in
selectAccentColorImpl?(accentColor) selectAccentColorImpl?(accentColor)
}, openAccentColorPicker: { themeReference, create in }, 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)) 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 -> (ItemListControllerState, (ItemListNodeState, Any)) in |> 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 settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings
let mediaSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaDisplaySettings]?.get(MediaDisplaySettings.self) ?? MediaDisplaySettings.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) 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 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)) 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 red
case orange case orange
case violet case violet
@ -122,12 +122,12 @@ public enum PeerNameColor: Int32 {
case cyan case cyan
case blue case blue
case pink case pink
case other7 case redDash
case other8 case orangeDash
case other9 case violetDash
case other10 case greenDash
case other11 case cyanDash
case other12 case blueDash
case other13 case other13
case other14 case other14
case other15 case other15

View File

@ -53,23 +53,24 @@ public enum UpdateNameColorAndEmojiError {
func _internal_updateNameColorAndEmoji(account: Account, nameColor: PeerNameColor, backgroundEmojiId: Int64?) -> Signal<Void, UpdateNameColorAndEmojiError> { func _internal_updateNameColorAndEmoji(account: Account, nameColor: PeerNameColor, backgroundEmojiId: Int64?) -> Signal<Void, UpdateNameColorAndEmojiError> {
let flags: Int32 = (1 << 0) let flags: Int32 = (1 << 0)
return account.postbox.loadedPeerWithId(account.peerId) return account.postbox.transaction { transaction -> Signal<Peer, NoError> in
|> castError(UpdateNameColorAndEmojiError.self) guard let peer = transaction.getPeer(account.peerId) as? TelegramUser else {
|> mapToSignal { accountPeer -> Signal<Void, UpdateNameColorAndEmojiError> in return .complete()
guard let accountPeer = accountPeer as? TelegramUser else {
return .fail(.generic)
} }
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)) return account.network.request(Api.functions.account.updateColor(flags: flags, color: nameColor.rawValue, backgroundEmojiId: backgroundEmojiId ?? 0))
|> mapError { _ -> UpdateNameColorAndEmojiError in |> mapError { _ -> UpdateNameColorAndEmojiError in
return .generic return .generic
} }
|> mapToSignal { apiUser -> Signal<Void, UpdateNameColorAndEmojiError> in |> mapToSignal { _ -> Signal<Void, UpdateNameColorAndEmojiError> in
return account.postbox.transaction { transaction -> Void in return .complete()
updatePeersCustom(transaction: transaction, peers: [accountPeer.withUpdatedNameColor(nameColor).withUpdatedBackgroundEmojiId(backgroundEmojiId)], update: { _, updated in
return updated
})
}
|> castError(UpdateNameColorAndEmojiError.self)
} }
} }
} }

View File

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

View File

@ -228,6 +228,25 @@ public extension Peer {
return false 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 { 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 chatReplyBackgroundTemplateImage
case chatReplyServiceBackgroundTemplateImage case chatReplyServiceBackgroundTemplateImage
case chatReplyLineDashTemplateImage
} }
public enum ChatExpiredStoryIndicatorType: Hashable { public enum ChatExpiredStoryIndicatorType: Hashable {

View File

@ -1316,4 +1316,26 @@ public struct PresentationResourcesChat {
})?.stretchableImage(withLeftCapWidth: Int(radius), topCapHeight: Int(radius)).withRenderingMode(.alwaysTemplate) })?.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 { func update(component: FlashColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component 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) self.contentView.frame = CGRect(origin: .zero, size: contentSize)
let bounds = 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 let isFirstTime = self.component == nil
self.component = component self.component = component
let size = CGSize(width: 160.0, height: 40.0) let size = CGSize(width: 184.0, height: 92.0)
if isFirstTime { 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.path = generateRoundedRectWithTailPath(rectSize: size, cornerRadius: 10.0, tailSize: CGSize(width: 18, height: 7.0), tailRadius: 1.0, tailPosition: 0.8, transformTail: false).cgPath
self.maskLayer.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: size.height + 7.0)) self.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 { if view.superview == nil {
self.containerView.addSubview(view) 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) self.dismissView.frame = CGRect(origin: .zero, size: availableSize)

View File

@ -53,6 +53,7 @@ public struct ChatMessageAttachedContentNodeMediaFlags: OptionSet {
public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode { public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode {
private var backgroundView: UIImageView? private var backgroundView: UIImageView?
private var lineDashView: UIImageView?
private let topTitleNode: TextNode private let topTitleNode: TextNode
private let textNode: TextNodeWithEntities private let textNode: TextNodeWithEntities
private let inlineImageNode: TransformImageNode private let inlineImageNode: TransformImageNode
@ -251,13 +252,47 @@ public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode {
let string = NSMutableAttributedString() let string = NSMutableAttributedString()
var notEmpty = false 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 let title = title, !title.isEmpty {
if titleBeforeMedia { 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 { } 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 notEmpty = true
} }
} }
@ -565,7 +600,6 @@ public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode {
let upatedTextCutout = textCutout 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 (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())) let (textLayout, textApply) = textAsyncLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: upatedTextCutout, insets: UIEdgeInsets()))
@ -604,39 +638,6 @@ public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode {
textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top) 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 var boundingSize = textFrame.size
if titleBeforeMedia { if titleBeforeMedia {
boundingSize.height += topTitleLayout.size.height + 4.0 boundingSize.height += topTitleLayout.size.height + 4.0
@ -855,9 +856,28 @@ public final class ChatMessageAttachedContentNode: HighlightTrackingButtonNode {
} }
backgroundView.tintColor = mainColor 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 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.borderColor = UIColor.red.cgColor
//strongSelf.borderWidth = 2.0 //strongSelf.borderWidth = 2.0

View File

@ -1836,13 +1836,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if initialDisplayHeader && displayAuthorInfo { if initialDisplayHeader && displayAuthorInfo {
if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info, item.content.firstMessage.adAttribute == nil { 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) 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 { } else if let effectiveAuthor = effectiveAuthor {
authorNameString = EnginePeer(effectiveAuthor).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) authorNameString = EnginePeer(effectiveAuthor).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
let nameColor: UIColor let nameColor: UIColor
if incoming { if incoming {
nameColor = chatMessagePeerIdColors[Int(clamping: effectiveAuthor.id.id._internalGetInt64Value() % 7)] nameColor = (effectiveAuthor.nameColor ?? .blue).color
} else { } else {
nameColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor nameColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor
} }

View File

@ -425,6 +425,9 @@ public final class ChatMessageAvatarHeader: ListViewItemHeader {
return return
} }
node.updatePresentationData(self.presentationData, context: self.context) node.updatePresentationData(self.presentationData, context: self.context)
if let peer = self.peer {
node.updatePeer(peer: peer)
}
node.updateStoryStats(storyStats: self.storyStats, theme: self.presentationData.theme.theme, force: false) node.updateStoryStats(storyStats: self.storyStats, theme: self.presentationData.theme.theme, force: false)
} }
} }
@ -441,7 +444,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode {
private let peerId: PeerId private let peerId: PeerId
private let messageReference: MessageReference? private let messageReference: MessageReference?
private let peer: Peer? private var peer: Peer?
private let adMessageId: EngineMessage.Id? private let adMessageId: EngineMessage.Id?
private let containerNode: ContextControllerSourceNode private let containerNode: ContextControllerSourceNode
@ -519,6 +522,13 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode {
self.avatarNode.setCustomLetters(letters, icon: !letters.isEmpty ? nil : .phone) self.avatarNode.setCustomLetters(letters, icon: !letters.isEmpty ? nil : .phone)
} }
public func updatePeer(peer: Peer) {
if let previousPeer = self.peer, previousPeer.nameColor != peer.nameColor {
self.peer = peer
self.avatarNode.setPeer(context: self.context, theme: self.presentationData.theme.theme, peer: EnginePeer(peer), authorOfMessage: self.messageReference, overrideImage: nil, emptyColor: .black, synchronousLoad: false, displayDimensions: CGSize(width: 38.0, height: 38.0))
}
}
public func setPeer(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, peer: Peer, authorOfMessage: MessageReference?, emptyColor: UIColor) { public func setPeer(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, peer: Peer, authorOfMessage: MessageReference?, emptyColor: UIColor) {
self.containerNode.isGestureEnabled = peer.smallProfileImage != nil self.containerNode.isGestureEnabled = peer.smallProfileImage != nil

View File

@ -100,6 +100,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
} }
private let backgroundView: UIImageView private let backgroundView: UIImageView
private var lineDashView: UIImageView?
private var quoteIconView: UIImageView? private var quoteIconView: UIImageView?
private let contentNode: ASDisplayNode private let contentNode: ASDisplayNode
private var titleNode: TextNode? private var titleNode: TextNode?
@ -216,11 +217,13 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
let dustColor: UIColor let dustColor: UIColor
var authorNameColor: UIColor? var authorNameColor: UIColor?
var dashSecondaryColor: UIColor?
let author = arguments.message?.effectiveAuthor let author = arguments.message?.effectiveAuthor
if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(arguments.parentMessage.id.peerId.namespace) && author?.id.namespace == Namespaces.Peer.CloudUser { if [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 { if let rawAuthorNameColor = authorNameColor {
var dimColors = false var dimColors = false
switch arguments.presentationData.theme.theme.name { switch arguments.presentationData.theme.theme.name {
@ -240,6 +243,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
} }
let mainColor: UIColor let mainColor: UIColor
var secondaryColor: UIColor?
switch arguments.type { switch arguments.type {
case let .bubble(incoming): case let .bubble(incoming):
@ -247,6 +251,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
if incoming { if incoming {
if let authorNameColor { if let authorNameColor {
mainColor = authorNameColor mainColor = authorNameColor
secondaryColor = dashSecondaryColor
} else { } else {
mainColor = arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor mainColor = arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor
} }
@ -608,6 +613,24 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
node.backgroundView.tintColor = mainColor node.backgroundView.tintColor = mainColor
node.backgroundView.frame = backgroundFrame 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 { if arguments.quote != nil || arguments.replyForward?.quote != nil {
let quoteIconView: UIImageView let quoteIconView: UIImageView
if let current = node.quoteIconView { 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 var contentHeight: CGFloat = 20.0
let margin: CGFloat = 12.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 buttonTextSize = self.undoButtonTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
let buttonMinX: CGFloat let buttonMinX: CGFloat