mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
1230 lines
55 KiB
Swift
1230 lines
55 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import TelegramCore
|
|
import Postbox
|
|
import SwiftSignalKit
|
|
import TelegramPresentationData
|
|
import AnimationUI
|
|
import AppBundle
|
|
import AccountContext
|
|
import Emoji
|
|
import Accelerate
|
|
import ComponentFlow
|
|
import AvatarStoryIndicatorComponent
|
|
import DirectMediaImageCache
|
|
|
|
private let deletedIcon = UIImage(bundleImageName: "Avatar/DeletedIcon")?.precomposed()
|
|
private let phoneIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/PhoneIcon"), color: .white)
|
|
public let savedMessagesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/SavedMessagesIcon"), color: .white)
|
|
private let archivedChatsIcon = UIImage(bundleImageName: "Avatar/ArchiveAvatarIcon")?.precomposed()
|
|
private let repliesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/RepliesMessagesIcon"), color: .white)
|
|
|
|
public func avatarPlaceholderFont(size: CGFloat) -> UIFont {
|
|
return Font.with(size: size, design: .round, weight: .bold)
|
|
}
|
|
|
|
public enum AvatarNodeClipStyle {
|
|
case none
|
|
case round
|
|
case roundedRect
|
|
}
|
|
|
|
private class AvatarNodeParameters: NSObject {
|
|
let theme: PresentationTheme?
|
|
let accountPeerId: EnginePeer.Id?
|
|
let peerId: EnginePeer.Id?
|
|
let colors: [UIColor]
|
|
let letters: [String]
|
|
let font: UIFont
|
|
let icon: AvatarNodeIcon
|
|
let explicitColorIndex: Int?
|
|
let hasImage: Bool
|
|
let clipStyle: AvatarNodeClipStyle
|
|
|
|
init(theme: PresentationTheme?, accountPeerId: EnginePeer.Id?, peerId: EnginePeer.Id?, colors: [UIColor], letters: [String], font: UIFont, icon: AvatarNodeIcon, explicitColorIndex: Int?, hasImage: Bool, clipStyle: AvatarNodeClipStyle) {
|
|
self.theme = theme
|
|
self.accountPeerId = accountPeerId
|
|
self.peerId = peerId
|
|
self.colors = colors
|
|
self.letters = letters
|
|
self.font = font
|
|
self.icon = icon
|
|
self.explicitColorIndex = explicitColorIndex
|
|
self.hasImage = hasImage
|
|
self.clipStyle = clipStyle
|
|
|
|
super.init()
|
|
}
|
|
|
|
func withUpdatedHasImage(_ hasImage: Bool) -> AvatarNodeParameters {
|
|
return AvatarNodeParameters(theme: self.theme, accountPeerId: self.accountPeerId, peerId: self.peerId, colors: self.colors, letters: self.letters, font: self.font, icon: self.icon, explicitColorIndex: self.explicitColorIndex, hasImage: hasImage, clipStyle: self.clipStyle)
|
|
}
|
|
}
|
|
|
|
private func calculateColors(explicitColorIndex: Int?, peerId: EnginePeer.Id?, icon: AvatarNodeIcon, theme: PresentationTheme?) -> [UIColor] {
|
|
let colorIndex: Int
|
|
if let explicitColorIndex = explicitColorIndex {
|
|
colorIndex = explicitColorIndex
|
|
} else {
|
|
if let peerId {
|
|
if peerId.namespace == .max {
|
|
colorIndex = -1
|
|
} else {
|
|
colorIndex = abs(Int(clamping: peerId.id._internalGetInt64Value()))
|
|
}
|
|
} else {
|
|
colorIndex = -1
|
|
}
|
|
}
|
|
|
|
let colors: [UIColor]
|
|
if icon != .none {
|
|
if case .deletedIcon = icon {
|
|
colors = AvatarNode.grayscaleColors
|
|
} else if case .phoneIcon = icon {
|
|
colors = AvatarNode.grayscaleColors
|
|
} else if case .savedMessagesIcon = icon {
|
|
colors = AvatarNode.savedMessagesColors
|
|
} else if case .repliesIcon = icon {
|
|
colors = AvatarNode.savedMessagesColors
|
|
} else if case .editAvatarIcon = icon, let theme {
|
|
colors = [theme.list.itemAccentColor.withAlphaComponent(0.1), theme.list.itemAccentColor.withAlphaComponent(0.1)]
|
|
} else if case let .archivedChatsIcon(hiddenByDefault) = icon, let theme = theme {
|
|
let backgroundColors: (UIColor, UIColor)
|
|
if hiddenByDefault {
|
|
backgroundColors = theme.chatList.unpinnedArchiveAvatarColor.backgroundColors.colors
|
|
} else {
|
|
backgroundColors = theme.chatList.pinnedArchiveAvatarColor.backgroundColors.colors
|
|
}
|
|
colors = [backgroundColors.1, backgroundColors.0]
|
|
} else {
|
|
colors = AvatarNode.grayscaleColors
|
|
}
|
|
} else if colorIndex == -1 {
|
|
if let theme {
|
|
let backgroundColors = theme.chatList.unpinnedArchiveAvatarColor.backgroundColors.colors
|
|
colors = [backgroundColors.1, backgroundColors.0]
|
|
} else {
|
|
colors = AvatarNode.grayscaleColors
|
|
}
|
|
} else {
|
|
colors = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count]
|
|
}
|
|
|
|
return colors
|
|
}
|
|
|
|
public enum AvatarNodeExplicitIcon {
|
|
case phone
|
|
}
|
|
|
|
private enum AvatarNodeState: Equatable {
|
|
case empty
|
|
case peerAvatar(EnginePeer.Id, [String], TelegramMediaImageRepresentation?, AvatarNodeClipStyle)
|
|
case custom(letter: [String], explicitColorIndex: Int?, explicitIcon: AvatarNodeExplicitIcon?)
|
|
}
|
|
|
|
private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool {
|
|
switch (lhs, rhs) {
|
|
case (.empty, .empty):
|
|
return true
|
|
case let (.peerAvatar(lhsPeerId, lhsLetters, lhsPhotoRepresentations, lhsClipStyle), .peerAvatar(rhsPeerId, rhsLetters, rhsPhotoRepresentations, rhsClipStyle)):
|
|
return lhsPeerId == rhsPeerId && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations && lhsClipStyle == rhsClipStyle
|
|
case let (.custom(lhsLetters, lhsIndex, lhsIcon), .custom(rhsLetters, rhsIndex, rhsIcon)):
|
|
return lhsLetters == rhsLetters && lhsIndex == rhsIndex && lhsIcon == rhsIcon
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
private enum AvatarNodeIcon: Equatable {
|
|
case none
|
|
case savedMessagesIcon
|
|
case repliesIcon
|
|
case archivedChatsIcon(hiddenByDefault: Bool)
|
|
case editAvatarIcon
|
|
case deletedIcon
|
|
case phoneIcon
|
|
}
|
|
|
|
public enum AvatarNodeImageOverride: Equatable {
|
|
case none
|
|
case image(TelegramMediaImageRepresentation)
|
|
case savedMessagesIcon
|
|
case repliesIcon
|
|
case archivedChatsIcon(hiddenByDefault: Bool)
|
|
case editAvatarIcon(forceNone: Bool)
|
|
case deletedIcon
|
|
case phoneIcon
|
|
}
|
|
|
|
public enum AvatarNodeColorOverride {
|
|
case blue
|
|
}
|
|
|
|
public final class AvatarEditOverlayNode: ASDisplayNode {
|
|
override public init() {
|
|
super.init()
|
|
|
|
self.isOpaque = false
|
|
self.displaysAsynchronously = true
|
|
}
|
|
|
|
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
|
assertNotOnMainThread()
|
|
|
|
let context = UIGraphicsGetCurrentContext()!
|
|
|
|
if !isRasterizing {
|
|
context.setBlendMode(.copy)
|
|
context.setFillColor(UIColor.clear.cgColor)
|
|
context.fill(bounds)
|
|
}
|
|
|
|
context.beginPath()
|
|
context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: bounds.size.width, height:
|
|
bounds.size.height))
|
|
context.clip()
|
|
|
|
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.4).cgColor)
|
|
context.fill(bounds)
|
|
|
|
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
|
context.scaleBy(x: 1.0, y: -1.0)
|
|
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
|
|
|
|
context.setBlendMode(.normal)
|
|
|
|
if bounds.width > 90.0 {
|
|
if let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIconLarge"), color: .white) {
|
|
context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0) + 0.5, y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0) + 1.0), size: editAvatarIcon.size))
|
|
}
|
|
} else {
|
|
if let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIcon"), color: .white) {
|
|
context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0) + 0.5, y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0) + 1.0), size: editAvatarIcon.size))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public final class AvatarNode: ASDisplayNode {
|
|
public static let gradientColors: [[UIColor]] = [
|
|
[UIColor(rgb: 0xff516a), UIColor(rgb: 0xff885e)],
|
|
[UIColor(rgb: 0xffa85c), UIColor(rgb: 0xffcd6a)],
|
|
[UIColor(rgb: 0x665fff), UIColor(rgb: 0x82b1ff)],
|
|
[UIColor(rgb: 0x54cb68), UIColor(rgb: 0xa0de7e)],
|
|
[UIColor(rgb: 0x4acccd), UIColor(rgb: 0x00fcfd)],
|
|
[UIColor(rgb: 0x2a9ef1), UIColor(rgb: 0x72d5fd)],
|
|
[UIColor(rgb: 0xd669ed), UIColor(rgb: 0xe0a2f3)],
|
|
]
|
|
|
|
static let grayscaleColors: [UIColor] = [
|
|
UIColor(rgb: 0xb1b1b1), UIColor(rgb: 0xcdcdcd)
|
|
]
|
|
|
|
static let savedMessagesColors: [UIColor] = [
|
|
UIColor(rgb: 0x2a9ef1), UIColor(rgb: 0x72d5fd)
|
|
]
|
|
|
|
public final class ContentNode: ASDisplayNode {
|
|
private struct Params: Equatable {
|
|
let peerId: EnginePeer.Id?
|
|
let resourceId: String?
|
|
|
|
init(
|
|
peerId: EnginePeer.Id?,
|
|
resourceId: String?
|
|
) {
|
|
self.peerId = peerId
|
|
self.resourceId = resourceId
|
|
}
|
|
}
|
|
|
|
public var font: UIFont {
|
|
didSet {
|
|
if oldValue.pointSize != font.pointSize {
|
|
if let parameters = self.parameters {
|
|
self.parameters = AvatarNodeParameters(theme: parameters.theme, accountPeerId: parameters.accountPeerId, peerId: parameters.peerId, colors: parameters.colors, letters: parameters.letters, font: self.font, icon: parameters.icon, explicitColorIndex: parameters.explicitColorIndex, hasImage: parameters.hasImage, clipStyle: parameters.clipStyle)
|
|
}
|
|
|
|
if !self.displaySuspended {
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
private var parameters: AvatarNodeParameters?
|
|
private var theme: PresentationTheme?
|
|
private var overrideImage: AvatarNodeImageOverride?
|
|
public let imageNode: ImageNode
|
|
private var animationBackgroundNode: ImageNode?
|
|
private var animationNode: AnimationNode?
|
|
public var editOverlayNode: AvatarEditOverlayNode?
|
|
|
|
private let imageReadyDisposable = MetaDisposable()
|
|
fileprivate var state: AvatarNodeState = .empty
|
|
|
|
public var unroundedImage: UIImage?
|
|
private var currentImage: UIImage?
|
|
|
|
private var params: Params?
|
|
private var loadDisposable: Disposable?
|
|
|
|
public var badgeView: AvatarBadgeView? {
|
|
didSet {
|
|
if self.badgeView !== oldValue {
|
|
if let badgeView = self.badgeView, let parameters = self.parameters {
|
|
if parameters.hasImage {
|
|
if let currentImage = self.currentImage {
|
|
badgeView.update(content: .image(currentImage))
|
|
}
|
|
} else {
|
|
let badgeColor: UIColor
|
|
if parameters.colors.isEmpty {
|
|
badgeColor = .white
|
|
} else {
|
|
badgeColor = parameters.colors[parameters.colors.count - 1]
|
|
}
|
|
badgeView.update(content: .color(badgeColor))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private let imageReady = Promise<Bool>(false)
|
|
public var ready: Signal<Void, NoError> {
|
|
let imageReady = self.imageReady
|
|
return Signal { subscriber in
|
|
return imageReady.get().start(next: { next in
|
|
if next {
|
|
subscriber.putCompletion()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
public init(font: UIFont) {
|
|
self.font = font
|
|
self.imageNode = ImageNode(enableHasImage: true, enableAnimatedTransition: true)
|
|
|
|
super.init()
|
|
|
|
self.isOpaque = false
|
|
self.displaysAsynchronously = true
|
|
self.disableClearContentsOnHide = true
|
|
|
|
self.imageNode.isLayerBacked = true
|
|
self.addSubnode(self.imageNode)
|
|
|
|
self.imageNode.contentUpdated = { [weak self] image in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
self.currentImage = image
|
|
|
|
guard let badgeView = self.badgeView, let parameters = self.parameters else {
|
|
return
|
|
}
|
|
|
|
if parameters.hasImage, let image {
|
|
badgeView.update(content: .image(image))
|
|
}
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
self.loadDisposable?.dispose()
|
|
}
|
|
|
|
override public func didLoad() {
|
|
super.didLoad()
|
|
|
|
if #available(iOSApplicationExtension 11.0, iOS 11.0, *), !self.isLayerBacked {
|
|
self.view.accessibilityIgnoresInvertColors = true
|
|
}
|
|
}
|
|
|
|
public func updateSize(size: CGSize) {
|
|
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
self.editOverlayNode?.frame = self.imageNode.frame
|
|
if !self.displaySuspended {
|
|
self.setNeedsDisplay()
|
|
self.editOverlayNode?.setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
public func playArchiveAnimation() {
|
|
guard let theme = self.theme else {
|
|
return
|
|
}
|
|
|
|
var iconColor = theme.chatList.unpinnedArchiveAvatarColor.foregroundColor
|
|
var backgroundColor = theme.chatList.unpinnedArchiveAvatarColor.backgroundColors.topColor
|
|
let animationBackgroundNode = ASImageNode()
|
|
animationBackgroundNode.isUserInteractionEnabled = false
|
|
animationBackgroundNode.frame = self.imageNode.frame
|
|
if let overrideImage = self.overrideImage, case let .archivedChatsIcon(hiddenByDefault) = overrideImage {
|
|
let backgroundColors: (UIColor, UIColor)
|
|
if hiddenByDefault {
|
|
backgroundColors = theme.chatList.unpinnedArchiveAvatarColor.backgroundColors.colors
|
|
iconColor = theme.chatList.unpinnedArchiveAvatarColor.foregroundColor
|
|
} else {
|
|
backgroundColors = theme.chatList.pinnedArchiveAvatarColor.backgroundColors.colors
|
|
iconColor = theme.chatList.pinnedArchiveAvatarColor.foregroundColor
|
|
}
|
|
let colors: NSArray = [backgroundColors.1.cgColor, backgroundColors.0.cgColor]
|
|
backgroundColor = backgroundColors.1.mixedWith(backgroundColors.0, alpha: 0.5)
|
|
animationBackgroundNode.image = generateGradientFilledCircleImage(diameter: self.imageNode.frame.width, colors: colors)
|
|
}
|
|
|
|
self.addSubnode(animationBackgroundNode)
|
|
|
|
let animationNode = AnimationNode(animation: "anim_archiveAvatar", colors: ["box1.box1.Fill 1": iconColor, "box3.box3.Fill 1": iconColor, "box2.box2.Fill 1": backgroundColor], scale: 0.1653828)
|
|
animationNode.isUserInteractionEnabled = false
|
|
animationNode.completion = { [weak animationBackgroundNode, weak self] in
|
|
self?.imageNode.isHidden = false
|
|
animationBackgroundNode?.removeFromSupernode()
|
|
}
|
|
animationBackgroundNode.addSubnode(animationNode)
|
|
|
|
animationBackgroundNode.layer.animateScale(from: 1.0, to: 1.07, duration: 0.12, removeOnCompletion: false, completion: { [weak animationBackgroundNode] finished in
|
|
animationBackgroundNode?.layer.animateScale(from: 1.07, to: 1.0, duration: 0.12, removeOnCompletion: false)
|
|
})
|
|
|
|
if var size = animationNode.preferredSize() {
|
|
size = CGSize(width: ceil(size.width), height: ceil(size.height))
|
|
animationNode.frame = CGRect(x: floor((self.bounds.width - size.width) / 2.0), y: floor((self.bounds.height - size.height) / 2.0) + 1.0, width: size.width, height: size.height)
|
|
animationNode.play()
|
|
}
|
|
self.imageNode.isHidden = true
|
|
}
|
|
|
|
public func setPeer(
|
|
accountPeerId: EnginePeer.Id,
|
|
postbox: Postbox,
|
|
network: Network,
|
|
contentSettings: ContentSettings,
|
|
theme: PresentationTheme,
|
|
peer: EnginePeer?,
|
|
authorOfMessage: MessageReference? = nil,
|
|
overrideImage: AvatarNodeImageOverride? = nil,
|
|
emptyColor: UIColor? = nil,
|
|
clipStyle: AvatarNodeClipStyle = .round,
|
|
synchronousLoad: Bool = false,
|
|
displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0),
|
|
storeUnrounded: Bool = false
|
|
) {
|
|
var synchronousLoad = synchronousLoad
|
|
var representation: TelegramMediaImageRepresentation?
|
|
var icon = AvatarNodeIcon.none
|
|
if let overrideImage = overrideImage {
|
|
switch overrideImage {
|
|
case .none:
|
|
representation = nil
|
|
case let .image(image):
|
|
representation = image
|
|
synchronousLoad = false
|
|
case .savedMessagesIcon:
|
|
representation = nil
|
|
icon = .savedMessagesIcon
|
|
case .repliesIcon:
|
|
representation = nil
|
|
icon = .repliesIcon
|
|
case let .archivedChatsIcon(hiddenByDefault):
|
|
representation = nil
|
|
icon = .archivedChatsIcon(hiddenByDefault: hiddenByDefault)
|
|
case let .editAvatarIcon(forceNone):
|
|
representation = forceNone ? nil : peer?.smallProfileImage
|
|
icon = .editAvatarIcon
|
|
case .deletedIcon:
|
|
representation = nil
|
|
icon = .deletedIcon
|
|
case .phoneIcon:
|
|
representation = nil
|
|
icon = .phoneIcon
|
|
}
|
|
} else if peer?.restrictionText(platform: "ios", contentSettings: contentSettings) == nil {
|
|
representation = peer?.smallProfileImage
|
|
}
|
|
let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.displayLetters ?? [], representation, clipStyle)
|
|
if updatedState != self.state || overrideImage != self.overrideImage || theme !== self.theme {
|
|
self.state = updatedState
|
|
self.overrideImage = overrideImage
|
|
self.theme = theme
|
|
|
|
let parameters: AvatarNodeParameters
|
|
|
|
if let peer = peer, let signal = peerAvatarImage(postbox: postbox, network: network, peerReference: PeerReference(peer._asPeer()), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, clipStyle: clipStyle, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded) {
|
|
self.contents = nil
|
|
self.displaySuspended = true
|
|
self.imageReady.set(self.imageNode.contentReady)
|
|
self.imageNode.setSignal(signal |> beforeNext { [weak self] next in
|
|
Queue.mainQueue().async {
|
|
self?.unroundedImage = next?.1
|
|
}
|
|
}
|
|
|> map { next -> UIImage? in
|
|
return next?.0
|
|
})
|
|
|
|
if case .editAvatarIcon = icon {
|
|
if self.editOverlayNode == nil {
|
|
let editOverlayNode = AvatarEditOverlayNode()
|
|
editOverlayNode.frame = self.imageNode.frame
|
|
editOverlayNode.isUserInteractionEnabled = false
|
|
self.addSubnode(editOverlayNode)
|
|
|
|
self.editOverlayNode = editOverlayNode
|
|
}
|
|
self.editOverlayNode?.isHidden = false
|
|
} else {
|
|
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)
|
|
} else {
|
|
self.imageReady.set(.single(true))
|
|
self.displaySuspended = false
|
|
if self.isNodeLoaded {
|
|
self.imageNode.contents = nil
|
|
}
|
|
|
|
self.editOverlayNode?.isHidden = true
|
|
let colors = calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), icon: icon, theme: theme)
|
|
parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle)
|
|
|
|
if let badgeView = self.badgeView {
|
|
let badgeColor: UIColor
|
|
if colors.isEmpty {
|
|
badgeColor = .white
|
|
} else {
|
|
badgeColor = colors[colors.count - 1]
|
|
}
|
|
badgeView.update(content: .color(badgeColor))
|
|
}
|
|
}
|
|
if self.parameters == nil || self.parameters != parameters {
|
|
self.parameters = parameters
|
|
self.setNeedsDisplay()
|
|
if synchronousLoad {
|
|
self.recursivelyEnsureDisplaySynchronously(true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func setPeerV2(
|
|
context genericContext: AccountContext,
|
|
account: Account? = nil,
|
|
theme: PresentationTheme,
|
|
peer: EnginePeer?,
|
|
authorOfMessage: MessageReference? = nil,
|
|
overrideImage: AvatarNodeImageOverride? = nil,
|
|
emptyColor: UIColor? = nil,
|
|
clipStyle: AvatarNodeClipStyle = .round,
|
|
synchronousLoad: Bool = false,
|
|
displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0),
|
|
storeUnrounded: Bool = false
|
|
) {
|
|
let smallProfileImage = peer?.smallProfileImage
|
|
let params = Params(
|
|
peerId: peer?.id,
|
|
resourceId: smallProfileImage?.resource.id.stringRepresentation
|
|
)
|
|
if self.params == params {
|
|
return
|
|
}
|
|
self.params = params
|
|
|
|
switch clipStyle {
|
|
case .none:
|
|
self.imageNode.clipsToBounds = false
|
|
self.imageNode.cornerRadius = 0.0
|
|
case .round:
|
|
self.imageNode.clipsToBounds = true
|
|
self.imageNode.cornerRadius = displayDimensions.height * 0.5
|
|
case .roundedRect:
|
|
self.imageNode.clipsToBounds = true
|
|
self.imageNode.cornerRadius = displayDimensions.height * 0.25
|
|
}
|
|
|
|
if let imageCache = genericContext.imageCache as? DirectMediaImageCache, let peer, let smallProfileImage = peer.smallProfileImage, let peerReference = PeerReference(peer._asPeer()) {
|
|
if let result = imageCache.getAvatarImage(peer: peerReference, resource: MediaResourceReference.avatar(peer: peerReference, resource: smallProfileImage.resource), immediateThumbnail: peer.profileImageRepresentations.first?.immediateThumbnailData, size: Int(displayDimensions.width * UIScreenScale), synchronous: synchronousLoad) {
|
|
if let image = result.image {
|
|
self.imageNode.contents = image.cgImage
|
|
}
|
|
if let loadSignal = result.loadSignal {
|
|
self.loadDisposable = (loadSignal |> deliverOnMainQueue).start(next: { [weak self] image in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.imageNode.contents = image?.cgImage
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public func setPeer(
|
|
context genericContext: AccountContext,
|
|
account: Account? = nil,
|
|
theme: PresentationTheme,
|
|
peer: EnginePeer?,
|
|
authorOfMessage: MessageReference? = nil,
|
|
overrideImage: AvatarNodeImageOverride? = nil,
|
|
emptyColor: UIColor? = nil,
|
|
clipStyle: AvatarNodeClipStyle = .round,
|
|
synchronousLoad: Bool = false,
|
|
displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0),
|
|
storeUnrounded: Bool = false
|
|
) {
|
|
var synchronousLoad = synchronousLoad
|
|
var representation: TelegramMediaImageRepresentation?
|
|
var icon = AvatarNodeIcon.none
|
|
if let overrideImage = overrideImage {
|
|
switch overrideImage {
|
|
case .none:
|
|
representation = nil
|
|
case let .image(image):
|
|
representation = image
|
|
synchronousLoad = false
|
|
case .savedMessagesIcon:
|
|
representation = nil
|
|
icon = .savedMessagesIcon
|
|
case .repliesIcon:
|
|
representation = nil
|
|
icon = .repliesIcon
|
|
case let .archivedChatsIcon(hiddenByDefault):
|
|
representation = nil
|
|
icon = .archivedChatsIcon(hiddenByDefault: hiddenByDefault)
|
|
case let .editAvatarIcon(forceNone):
|
|
representation = forceNone ? nil : peer?.smallProfileImage
|
|
icon = .editAvatarIcon
|
|
case .deletedIcon:
|
|
representation = nil
|
|
icon = .deletedIcon
|
|
case .phoneIcon:
|
|
representation = nil
|
|
icon = .phoneIcon
|
|
}
|
|
} else if peer?.restrictionText(platform: "ios", contentSettings: genericContext.currentContentSettings.with { $0 }) == nil {
|
|
representation = peer?.smallProfileImage
|
|
}
|
|
let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.displayLetters ?? [], representation, clipStyle)
|
|
if updatedState != self.state || overrideImage != self.overrideImage || theme !== self.theme {
|
|
self.state = updatedState
|
|
self.overrideImage = overrideImage
|
|
self.theme = theme
|
|
|
|
let parameters: AvatarNodeParameters
|
|
|
|
let account = account ?? genericContext.account
|
|
|
|
if let peer = peer, let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, clipStyle: clipStyle, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded) {
|
|
self.contents = nil
|
|
self.displaySuspended = true
|
|
self.imageReady.set(self.imageNode.contentReady)
|
|
self.imageNode.setSignal(signal |> beforeNext { [weak self] next in
|
|
Queue.mainQueue().async {
|
|
self?.unroundedImage = next?.1
|
|
}
|
|
}
|
|
|> map { next -> UIImage? in
|
|
return next?.0
|
|
})
|
|
|
|
if case .editAvatarIcon = icon {
|
|
if self.editOverlayNode == nil {
|
|
let editOverlayNode = AvatarEditOverlayNode()
|
|
editOverlayNode.frame = self.imageNode.frame
|
|
editOverlayNode.isUserInteractionEnabled = false
|
|
self.addSubnode(editOverlayNode)
|
|
|
|
self.editOverlayNode = editOverlayNode
|
|
}
|
|
self.editOverlayNode?.isHidden = false
|
|
} else {
|
|
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)
|
|
} else {
|
|
self.imageReady.set(.single(true))
|
|
self.displaySuspended = false
|
|
if self.isNodeLoaded {
|
|
self.imageNode.contents = nil
|
|
}
|
|
|
|
self.editOverlayNode?.isHidden = true
|
|
let colors = calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), icon: icon, theme: theme)
|
|
parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle)
|
|
|
|
if let badgeView = self.badgeView {
|
|
let badgeColor: UIColor
|
|
if colors.isEmpty {
|
|
badgeColor = .white
|
|
} else {
|
|
badgeColor = colors[colors.count - 1]
|
|
}
|
|
badgeView.update(content: .color(badgeColor))
|
|
}
|
|
}
|
|
if self.parameters == nil || self.parameters != parameters {
|
|
self.parameters = parameters
|
|
self.setNeedsDisplay()
|
|
if synchronousLoad {
|
|
self.recursivelyEnsureDisplaySynchronously(true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public func setCustomLetters(_ letters: [String], explicitColor: AvatarNodeColorOverride? = nil, icon: AvatarNodeExplicitIcon? = nil) {
|
|
var explicitIndex: Int?
|
|
if let explicitColor = explicitColor {
|
|
switch explicitColor {
|
|
case .blue:
|
|
explicitIndex = 5
|
|
}
|
|
}
|
|
let updatedState: AvatarNodeState = .custom(letter: letters, explicitColorIndex: explicitIndex, explicitIcon: icon)
|
|
if updatedState != self.state {
|
|
self.state = updatedState
|
|
|
|
let parameters: AvatarNodeParameters
|
|
if let icon = icon, case .phone = icon {
|
|
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(explicitColorIndex: explicitIndex, peerId: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
|
|
} 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)
|
|
}
|
|
|
|
self.displaySuspended = true
|
|
self.contents = nil
|
|
|
|
self.imageReady.set(.single(true))
|
|
self.displaySuspended = false
|
|
|
|
if self.parameters == nil || self.parameters != parameters {
|
|
self.parameters = parameters
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
}
|
|
|
|
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol {
|
|
return parameters ?? NSObject()
|
|
}
|
|
|
|
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
|
assertNotOnMainThread()
|
|
|
|
let context = UIGraphicsGetCurrentContext()!
|
|
|
|
if !isRasterizing {
|
|
context.setBlendMode(.copy)
|
|
context.setFillColor(UIColor.clear.cgColor)
|
|
context.fill(bounds)
|
|
}
|
|
|
|
if !(parameters is AvatarNodeParameters) {
|
|
return
|
|
}
|
|
|
|
let colors: [UIColor]
|
|
if let parameters = parameters as? AvatarNodeParameters {
|
|
colors = parameters.colors
|
|
|
|
if case .round = parameters.clipStyle {
|
|
context.beginPath()
|
|
context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: bounds.size.width, height:
|
|
bounds.size.height))
|
|
context.clip()
|
|
} else if case .roundedRect = parameters.clipStyle {
|
|
context.beginPath()
|
|
context.addPath(UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: bounds.size.width, height: bounds.size.height), cornerRadius: floor(bounds.size.width * 0.25)).cgPath)
|
|
context.clip()
|
|
}
|
|
} else {
|
|
colors = grayscaleColors
|
|
}
|
|
|
|
let colorsArray: NSArray = colors.map(\.cgColor) as NSArray
|
|
|
|
var iconColor = UIColor.white
|
|
if let parameters = parameters as? AvatarNodeParameters, parameters.icon != .none {
|
|
if case let .archivedChatsIcon(hiddenByDefault) = parameters.icon, let theme = parameters.theme {
|
|
if hiddenByDefault {
|
|
iconColor = theme.chatList.unpinnedArchiveAvatarColor.foregroundColor
|
|
} else {
|
|
iconColor = theme.chatList.pinnedArchiveAvatarColor.foregroundColor
|
|
}
|
|
}
|
|
}
|
|
|
|
var locations: [CGFloat] = [1.0, 0.0]
|
|
|
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
|
let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)!
|
|
|
|
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.size.height), options: CGGradientDrawingOptions())
|
|
|
|
context.setBlendMode(.normal)
|
|
|
|
if let parameters = parameters as? AvatarNodeParameters {
|
|
if case .deletedIcon = parameters.icon {
|
|
let factor = bounds.size.width / 60.0
|
|
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
|
context.scaleBy(x: factor, y: -factor)
|
|
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
|
|
|
|
if let deletedIcon = deletedIcon {
|
|
context.draw(deletedIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - deletedIcon.size.width) / 2.0), y: floor((bounds.size.height - deletedIcon.size.height) / 2.0)), size: deletedIcon.size))
|
|
}
|
|
} else if case .phoneIcon = parameters.icon {
|
|
let factor: CGFloat = 1.0
|
|
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
|
context.scaleBy(x: factor, y: -factor)
|
|
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
|
|
|
|
if let phoneIcon = phoneIcon {
|
|
context.draw(phoneIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - phoneIcon.size.width) / 2.0), y: floor((bounds.size.height - phoneIcon.size.height) / 2.0)), size: phoneIcon.size))
|
|
}
|
|
} else if case .savedMessagesIcon = parameters.icon {
|
|
let factor = bounds.size.width / 60.0
|
|
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
|
context.scaleBy(x: factor, y: -factor)
|
|
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
|
|
|
|
if let savedMessagesIcon = savedMessagesIcon {
|
|
context.draw(savedMessagesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - savedMessagesIcon.size.width) / 2.0), y: floor((bounds.size.height - savedMessagesIcon.size.height) / 2.0)), size: savedMessagesIcon.size))
|
|
}
|
|
} else if case .repliesIcon = parameters.icon {
|
|
let factor = bounds.size.width / 60.0
|
|
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
|
context.scaleBy(x: factor, y: -factor)
|
|
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
|
|
|
|
if let repliesIcon = repliesIcon {
|
|
context.draw(repliesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - repliesIcon.size.width) / 2.0), y: floor((bounds.size.height - repliesIcon.size.height) / 2.0)), size: repliesIcon.size))
|
|
}
|
|
} else if case .editAvatarIcon = parameters.icon, let theme = parameters.theme, !parameters.hasImage {
|
|
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
|
context.scaleBy(x: 1.0, y: -1.0)
|
|
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
|
|
|
|
if bounds.width > 90.0, let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIconLarge"), color: theme.list.itemAccentColor) {
|
|
context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0) + 0.5, y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0) + 1.0), size: editAvatarIcon.size))
|
|
} else if let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIcon"), color: theme.list.itemAccentColor) {
|
|
context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0) + 0.5, y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0) + 1.0), size: editAvatarIcon.size))
|
|
}
|
|
} else if case .archivedChatsIcon = parameters.icon {
|
|
let factor = bounds.size.width / 60.0
|
|
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
|
context.scaleBy(x: factor, y: -factor)
|
|
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
|
|
|
|
if let archivedChatsIcon = generateTintedImage(image: archivedChatsIcon, color: iconColor) {
|
|
context.draw(archivedChatsIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - archivedChatsIcon.size.width) / 2.0), y: floor((bounds.size.height - archivedChatsIcon.size.height) / 2.0)), size: archivedChatsIcon.size))
|
|
}
|
|
} else {
|
|
var letters = parameters.letters
|
|
if letters.count == 2 && letters[0].isSingleEmoji && letters[1].isSingleEmoji {
|
|
letters = [letters[0]]
|
|
}
|
|
|
|
let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1]))
|
|
let attributedString = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: parameters.font, NSAttributedString.Key.foregroundColor: UIColor.white])
|
|
|
|
let line = CTLineCreateWithAttributedString(attributedString)
|
|
let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
|
|
|
|
let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0)
|
|
let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (bounds.size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (bounds.size.height - lineBounds.size.height) / 2.0))
|
|
|
|
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
|
context.scaleBy(x: 1.0, y: -1.0)
|
|
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
|
|
|
|
context.translateBy(x: lineOrigin.x, y: lineOrigin.y)
|
|
CTLineDraw(line, context)
|
|
context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public let contentNode: ContentNode
|
|
private var storyIndicator: ComponentView<Empty>?
|
|
public private(set) var storyPresentationParams: StoryPresentationParams?
|
|
|
|
private var loadingStatuses = Bag<Disposable>()
|
|
|
|
public struct StoryStats: Equatable {
|
|
public var totalCount: Int
|
|
public var unseenCount: Int
|
|
public var hasUnseenCloseFriendsItems: Bool
|
|
|
|
public init(
|
|
totalCount: Int,
|
|
unseenCount: Int,
|
|
hasUnseenCloseFriendsItems: Bool
|
|
) {
|
|
self.totalCount = totalCount
|
|
self.unseenCount = unseenCount
|
|
self.hasUnseenCloseFriendsItems = hasUnseenCloseFriendsItems
|
|
}
|
|
}
|
|
|
|
public private(set) var storyStats: StoryStats?
|
|
|
|
public var font: UIFont {
|
|
get {
|
|
return self.contentNode.font
|
|
} set(value) {
|
|
self.contentNode.font = value
|
|
}
|
|
}
|
|
|
|
public var editOverlayNode: AvatarEditOverlayNode? {
|
|
get {
|
|
return self.contentNode.editOverlayNode
|
|
} set(value) {
|
|
self.contentNode.editOverlayNode = value
|
|
}
|
|
}
|
|
|
|
public var unroundedImage: UIImage? {
|
|
get {
|
|
return self.contentNode.unroundedImage
|
|
} set(value) {
|
|
self.contentNode.unroundedImage = value
|
|
}
|
|
}
|
|
|
|
public var badgeView: AvatarBadgeView? {
|
|
get {
|
|
return self.contentNode.badgeView
|
|
} set(value) {
|
|
self.contentNode.badgeView = value
|
|
}
|
|
}
|
|
|
|
public var ready: Signal<Void, NoError> {
|
|
return self.contentNode.ready
|
|
}
|
|
|
|
public var imageNode: ImageNode {
|
|
return self.contentNode.imageNode
|
|
}
|
|
|
|
public init(font: UIFont) {
|
|
self.contentNode = ContentNode(font: font)
|
|
|
|
super.init()
|
|
|
|
self.onDidLoad { [weak self] _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.updateStoryIndicator(transition: .immediate)
|
|
}
|
|
|
|
self.addSubnode(self.contentNode)
|
|
}
|
|
|
|
deinit {
|
|
self.cancelLoading()
|
|
}
|
|
|
|
override public var frame: CGRect {
|
|
get {
|
|
return super.frame
|
|
} set(value) {
|
|
let updateImage = !value.size.equalTo(super.frame.size)
|
|
super.frame = value
|
|
|
|
if updateImage {
|
|
self.updateSize(size: value.size)
|
|
}
|
|
}
|
|
}
|
|
|
|
override public func nodeDidLoad() {
|
|
super.nodeDidLoad()
|
|
}
|
|
|
|
public func updateSize(size: CGSize) {
|
|
self.contentNode.position = CGRect(origin: CGPoint(), size: size).center
|
|
self.contentNode.bounds = CGRect(origin: CGPoint(), size: size)
|
|
|
|
self.contentNode.updateSize(size: size)
|
|
|
|
self.updateStoryIndicator(transition: .immediate)
|
|
}
|
|
|
|
public func playArchiveAnimation() {
|
|
self.contentNode.playArchiveAnimation()
|
|
}
|
|
|
|
public func setPeer(
|
|
accountPeerId: EnginePeer.Id,
|
|
postbox: Postbox,
|
|
network: Network,
|
|
contentSettings: ContentSettings,
|
|
theme: PresentationTheme,
|
|
peer: EnginePeer?,
|
|
authorOfMessage: MessageReference? = nil,
|
|
overrideImage: AvatarNodeImageOverride? = nil,
|
|
emptyColor: UIColor? = nil,
|
|
clipStyle: AvatarNodeClipStyle = .round,
|
|
synchronousLoad: Bool = false,
|
|
displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0),
|
|
storeUnrounded: Bool = false
|
|
) {
|
|
self.contentNode.setPeer(
|
|
accountPeerId: accountPeerId,
|
|
postbox: postbox,
|
|
network: network,
|
|
contentSettings: contentSettings,
|
|
theme: theme,
|
|
peer: peer,
|
|
authorOfMessage: authorOfMessage,
|
|
overrideImage: overrideImage,
|
|
emptyColor: emptyColor,
|
|
clipStyle: clipStyle,
|
|
synchronousLoad: synchronousLoad,
|
|
displayDimensions: displayDimensions,
|
|
storeUnrounded: storeUnrounded
|
|
)
|
|
}
|
|
|
|
public func setPeerV2(
|
|
context genericContext: AccountContext,
|
|
theme: PresentationTheme,
|
|
peer: EnginePeer?,
|
|
authorOfMessage: MessageReference? = nil,
|
|
overrideImage: AvatarNodeImageOverride? = nil,
|
|
emptyColor: UIColor? = nil,
|
|
clipStyle: AvatarNodeClipStyle = .round,
|
|
synchronousLoad: Bool = false,
|
|
displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0),
|
|
storeUnrounded: Bool = false
|
|
) {
|
|
self.contentNode.setPeerV2(
|
|
context: genericContext,
|
|
theme: theme,
|
|
peer: peer,
|
|
authorOfMessage: authorOfMessage,
|
|
overrideImage: overrideImage,
|
|
emptyColor: emptyColor,
|
|
clipStyle: clipStyle,
|
|
synchronousLoad: synchronousLoad,
|
|
displayDimensions: displayDimensions,
|
|
storeUnrounded: storeUnrounded
|
|
)
|
|
}
|
|
|
|
public func setPeer(
|
|
context: AccountContext,
|
|
account: Account? = nil,
|
|
theme: PresentationTheme,
|
|
peer: EnginePeer?,
|
|
authorOfMessage: MessageReference? = nil,
|
|
overrideImage: AvatarNodeImageOverride? = nil,
|
|
emptyColor: UIColor? = nil,
|
|
clipStyle: AvatarNodeClipStyle = .round,
|
|
synchronousLoad: Bool = false,
|
|
displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0),
|
|
storeUnrounded: Bool = false
|
|
) {
|
|
self.contentNode.setPeer(
|
|
context: context,
|
|
account: account,
|
|
theme: theme,
|
|
peer: peer,
|
|
authorOfMessage: authorOfMessage,
|
|
overrideImage: overrideImage,
|
|
emptyColor: emptyColor,
|
|
clipStyle: clipStyle,
|
|
synchronousLoad: synchronousLoad,
|
|
displayDimensions: displayDimensions,
|
|
storeUnrounded: storeUnrounded
|
|
)
|
|
}
|
|
|
|
public func setCustomLetters(_ letters: [String], explicitColor: AvatarNodeColorOverride? = nil, icon: AvatarNodeExplicitIcon? = nil) {
|
|
self.contentNode.setCustomLetters(letters, explicitColor: explicitColor, icon: icon)
|
|
}
|
|
|
|
public func setStoryStats(storyStats: StoryStats?, presentationParams: StoryPresentationParams, transition: Transition) {
|
|
if self.storyStats != storyStats || self.storyPresentationParams != presentationParams {
|
|
self.storyStats = storyStats
|
|
self.storyPresentationParams = presentationParams
|
|
|
|
self.updateStoryIndicator(transition: transition)
|
|
}
|
|
}
|
|
|
|
public struct Colors: Equatable {
|
|
public var unseenColors: [UIColor]
|
|
public var unseenCloseFriendsColors: [UIColor]
|
|
public var seenColors: [UIColor]
|
|
|
|
public init(
|
|
unseenColors: [UIColor],
|
|
unseenCloseFriendsColors: [UIColor],
|
|
seenColors: [UIColor]
|
|
) {
|
|
self.unseenColors = unseenColors
|
|
self.unseenCloseFriendsColors = unseenCloseFriendsColors
|
|
self.seenColors = seenColors
|
|
}
|
|
|
|
public init(theme: PresentationTheme) {
|
|
self.unseenColors = [theme.chatList.storyUnseenColors.topColor, theme.chatList.storyUnseenColors.bottomColor]
|
|
self.unseenCloseFriendsColors = [theme.chatList.storyUnseenPrivateColors.topColor, theme.chatList.storyUnseenPrivateColors.bottomColor]
|
|
self.seenColors = [theme.chatList.storySeenColors.topColor, theme.chatList.storySeenColors.bottomColor]
|
|
}
|
|
}
|
|
|
|
public struct StoryPresentationParams: Equatable {
|
|
public var colors: Colors
|
|
public var lineWidth: CGFloat
|
|
public var inactiveLineWidth: CGFloat
|
|
|
|
public init(
|
|
colors: Colors,
|
|
lineWidth: CGFloat,
|
|
inactiveLineWidth: CGFloat
|
|
) {
|
|
self.colors = colors
|
|
self.lineWidth = lineWidth
|
|
self.inactiveLineWidth = inactiveLineWidth
|
|
}
|
|
}
|
|
|
|
private func updateStoryIndicator(transition: Transition) {
|
|
if !self.isNodeLoaded {
|
|
return
|
|
}
|
|
if self.bounds.isEmpty {
|
|
return
|
|
}
|
|
guard let storyPresentationParams = self.storyPresentationParams else {
|
|
return
|
|
}
|
|
|
|
let size = self.bounds.size
|
|
|
|
if let storyStats = self.storyStats {
|
|
let activeLineWidth = storyPresentationParams.lineWidth
|
|
let inactiveLineWidth = storyPresentationParams.inactiveLineWidth
|
|
let indicatorSize = CGSize(width: size.width - activeLineWidth * 4.0, height: size.height - activeLineWidth * 4.0)
|
|
let avatarScale = (size.width - activeLineWidth * 4.0) / size.width
|
|
|
|
let storyIndicator: ComponentView<Empty>
|
|
var indicatorTransition = transition
|
|
if let current = self.storyIndicator {
|
|
storyIndicator = current
|
|
} else {
|
|
indicatorTransition = transition.withAnimation(.none)
|
|
storyIndicator = ComponentView()
|
|
self.storyIndicator = storyIndicator
|
|
}
|
|
let _ = storyIndicator.update(
|
|
transition: indicatorTransition,
|
|
component: AnyComponent(AvatarStoryIndicatorComponent(
|
|
hasUnseen: storyStats.unseenCount != 0,
|
|
hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriendsItems,
|
|
colors: AvatarStoryIndicatorComponent.Colors(
|
|
unseenColors: storyPresentationParams.colors.unseenColors,
|
|
unseenCloseFriendsColors: storyPresentationParams.colors.unseenCloseFriendsColors,
|
|
seenColors: storyPresentationParams.colors.seenColors
|
|
),
|
|
activeLineWidth: activeLineWidth,
|
|
inactiveLineWidth: inactiveLineWidth,
|
|
counters: AvatarStoryIndicatorComponent.Counters(
|
|
totalCount: storyStats.totalCount,
|
|
unseenCount: storyStats.unseenCount
|
|
),
|
|
displayProgress: !self.loadingStatuses.isEmpty
|
|
)),
|
|
environment: {},
|
|
containerSize: indicatorSize
|
|
)
|
|
if let storyIndicatorView = storyIndicator.view {
|
|
if storyIndicatorView.superview == nil {
|
|
self.view.insertSubview(storyIndicatorView, aboveSubview: self.contentNode.view)
|
|
}
|
|
indicatorTransition.setFrame(view: storyIndicatorView, frame: CGRect(origin: CGPoint(x: (size.width - indicatorSize.width) * 0.5, y: (size.height - indicatorSize.height) * 0.5), size: indicatorSize))
|
|
}
|
|
transition.setScale(view: self.contentNode.view, scale: avatarScale)
|
|
} else {
|
|
transition.setScale(view: self.contentNode.view, scale: 1.0)
|
|
if let storyIndicator = self.storyIndicator {
|
|
self.storyIndicator = nil
|
|
if let storyIndicatorView = storyIndicator.view {
|
|
transition.setAlpha(view: storyIndicatorView, alpha: 0.0, completion: { [weak storyIndicatorView] _ in
|
|
storyIndicatorView?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public func cancelLoading() {
|
|
for disposable in self.loadingStatuses.copyItems() {
|
|
disposable.dispose()
|
|
}
|
|
self.loadingStatuses.removeAll()
|
|
self.updateStoryIndicator(transition: .immediate)
|
|
}
|
|
|
|
public func pushLoadingStatus(signal: Signal<Never, NoError>) -> Disposable {
|
|
let disposable = MetaDisposable()
|
|
|
|
for d in self.loadingStatuses.copyItems() {
|
|
d.dispose()
|
|
}
|
|
self.loadingStatuses.removeAll()
|
|
|
|
let index = self.loadingStatuses.add(disposable)
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { [weak self] in
|
|
self?.updateStoryIndicator(transition: .immediate)
|
|
})
|
|
|
|
disposable.set(signal.start(completed: { [weak self] in
|
|
Queue.mainQueue().async {
|
|
guard let self else {
|
|
return
|
|
}
|
|
if let previousDisposable = self.loadingStatuses.copyItemsWithIndices().first(where: { $0.0 == index })?.1 {
|
|
previousDisposable.dispose()
|
|
}
|
|
self.loadingStatuses.remove(index)
|
|
if self.loadingStatuses.isEmpty {
|
|
self.updateStoryIndicator(transition: .immediate)
|
|
}
|
|
}
|
|
}))
|
|
|
|
return ActionDisposable { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if let previousDisposable = self.loadingStatuses.copyItemsWithIndices().first(where: { $0.0 == index })?.1 {
|
|
previousDisposable.dispose()
|
|
}
|
|
self.loadingStatuses.remove(index)
|
|
if self.loadingStatuses.isEmpty {
|
|
self.updateStoryIndicator(transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
}
|