Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,133 @@
import Foundation
import UIKit
import AsyncDisplayKit
import TelegramPresentationData
import AnimationUI
import Display
final class DynamicIslandMaskNode: ASDisplayNode {
var animationNode: AnimationNode?
var isForum = false {
didSet {
if self.isForum != oldValue {
self.animationNode?.removeFromSupernode()
let animationNode = AnimationNode(animation: "ForumAvatarMask")
self.addSubnode(animationNode)
self.animationNode = animationNode
}
}
}
override init() {
let animationNode = AnimationNode(animation: "UserAvatarMask")
self.animationNode = animationNode
super.init()
self.addSubnode(animationNode)
}
func update(_ value: CGFloat) {
self.animationNode?.setProgress(value)
}
var animating = false
override func layout() {
self.animationNode?.frame = self.bounds
}
}
final class DynamicIslandBlurNode: ASDisplayNode {
private var effectView: UIVisualEffectView?
private let fadeNode = ASDisplayNode()
let gradientNode = ASImageNode()
private var hierarchyTrackingNode: HierarchyTrackingNode?
deinit {
self.animator?.stopAnimation(true)
}
override func didLoad() {
super.didLoad()
let hierarchyTrackingNode = HierarchyTrackingNode({ [weak self] value in
if !value {
self?.animator?.stopAnimation(true)
self?.animator = nil
}
})
self.hierarchyTrackingNode = hierarchyTrackingNode
self.addSubnode(hierarchyTrackingNode)
self.fadeNode.backgroundColor = .black
self.fadeNode.alpha = 0.0
self.gradientNode.displaysAsynchronously = false
let gradientImage = generateImage(CGSize(width: 100.0, height: 100.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds)
var locations: [CGFloat] = [0.0, 0.87, 1.0]
let colors: [CGColor] = [UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 1.0).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
let endRadius: CGFloat = 90.0
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0 + 38.0)
context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: endRadius, options: .drawsAfterEndLocation)
})
self.gradientNode.image = gradientImage
let effectView = UIVisualEffectView(effect: nil)
self.effectView = effectView
self.view.insertSubview(effectView, at: 0)
self.addSubnode(self.gradientNode)
self.addSubnode(self.fadeNode)
}
private var animator: UIViewPropertyAnimator?
func prepare() -> Bool {
guard self.animator == nil else {
return false
}
let animator = UIViewPropertyAnimator(duration: 1.0, curve: .linear)
self.animator = animator
self.effectView?.effect = nil
animator.addAnimations { [weak self] in
self?.effectView?.effect = UIBlurEffect(style: .dark)
}
return true
}
func update(_ value: CGFloat) {
let fadeAlpha = min(1.0, max(0.0, -0.25 + value * 1.55))
if value > 0.0 {
var value = value
let updated = self.prepare()
if value > 0.99 && updated {
value = 0.99
}
self.animator?.fractionComplete = max(0.0, -0.1 + value * 1.1)
} else {
self.animator?.stopAnimation(true)
self.animator = nil
self.effectView?.effect = nil
}
self.fadeNode.alpha = fadeAlpha
}
override func layout() {
super.layout()
self.effectView?.frame = self.bounds
self.fadeNode.frame = self.bounds
let gradientSize = CGSize(width: 100.0, height: 100.0)
self.gradientNode.frame = CGRect(origin: CGPoint(x: (self.bounds.width - gradientSize.width) / 2.0, y: 0.0), size: gradientSize)
}
}
@@ -0,0 +1,185 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import AvatarNode
import AccountContext
enum PeerInfoScreenActionColor {
case accent
case destructive
}
enum PeerInfoScreenActionAligmnent {
case natural
case center
case peerList
}
final class PeerInfoScreenActionItem: PeerInfoScreenItem {
let id: AnyHashable
let text: String
let color: PeerInfoScreenActionColor
let icon: UIImage?
let iconSignal: Signal<UIImage?, NoError>?
let alignment: PeerInfoScreenActionAligmnent
let action: (() -> Void)?
init(id: AnyHashable, text: String, color: PeerInfoScreenActionColor = .accent, icon: UIImage? = nil, iconSignal: Signal<UIImage?, NoError>? = nil, alignment: PeerInfoScreenActionAligmnent = .natural, action: (() -> Void)?) {
self.id = id
self.text = text
self.color = color
self.icon = icon
self.iconSignal = iconSignal
self.alignment = alignment
self.action = action
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenActionItemNode()
}
}
private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode {
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
private let maskNode: ASImageNode
private let iconNode: ASImageNode
private let textNode: ImmediateTextNode
private let bottomSeparatorNode: ASDisplayNode
private let activateArea: AccessibilityAreaNode
private var item: PeerInfoScreenActionItem?
private let iconDisposable = MetaDisposable()
override init() {
var bringToFrontForHighlightImpl: (() -> Void)?
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init()
bringToFrontForHighlightImpl = { [weak self] in
self?.bringToFrontForHighlight?()
}
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.selectionNode)
self.addSubnode(self.maskNode)
self.addSubnode(self.textNode)
self.addSubnode(self.activateArea)
}
deinit {
self.iconDisposable.dispose()
}
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenActionItem else {
return 10.0
}
self.item = item
self.selectionNode.pressed = item.action
let sideInset: CGFloat = 16.0 + safeInsets.left
var leftInset = (item.icon == nil && item.iconSignal == nil ? sideInset : sideInset + 29.0 + 16.0)
var iconInset = sideInset
if case .peerList = item.alignment {
leftInset += 5.0
iconInset += 5.0
}
let rightInset = sideInset
let separatorInset = item.icon == nil ? sideInset : leftInset - 1.0
let titleFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize)
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let textColorValue: UIColor
switch item.color {
case .accent:
textColorValue = presentationData.theme.list.itemAccentColor
case .destructive:
textColorValue = presentationData.theme.list.itemDestructiveColor
}
self.textNode.maximumNumberOfLines = 1
self.textNode.attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: textColorValue)
self.activateArea.accessibilityLabel = item.text
let textSize = self.textNode.updateLayout(CGSize(width: width - (leftInset + rightInset), height: .greatestFiniteMagnitude))
let height = textSize.height + 32.0
let textFrame = CGRect(origin: CGPoint(x: item.alignment == .center ? floorToScreenPixels((width - textSize.width) / 2.0) : leftInset, y: floorToScreenPixels((height - textSize.height) / 2.0)), size: textSize)
if let icon = item.icon {
if self.iconNode.supernode == nil {
self.addSubnode(self.iconNode)
}
self.iconNode.image = generateTintedImage(image: icon, color: textColorValue)
let iconFrame = CGRect(origin: CGPoint(x: iconInset, y: floorToScreenPixels((height - icon.size.height) / 2.0)), size: icon.size)
transition.updateFrame(node: self.iconNode, frame: iconFrame)
} else if let iconSignal = item.iconSignal {
self.iconDisposable.set((iconSignal
|> deliverOnMainQueue).startStrict(next: { [weak self] image in
if let strongSelf = self, let image {
strongSelf.iconNode.image = image
let iconFrame = CGRect(origin: CGPoint(x: iconInset, y: floorToScreenPixels((height - image.size.height) / 2.0)), size: image.size)
transition.updateFrame(node: strongSelf.iconNode, frame: iconFrame)
}
}))
if self.iconNode.supernode == nil {
self.addSubnode(self.iconNode)
}
} else if self.iconNode.supernode != nil {
self.iconNode.image = nil
self.iconNode.removeFromSupernode()
}
if self.textNode.frame.width != textFrame.width {
self.textNode.frame = textFrame
} else {
transition.updateFrame(node: self.textNode, frame: textFrame)
}
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
self.bottomSeparatorNode.isHidden = hasBottomCorners
let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
let separatorRightInset: CGFloat = 16.0
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, y: height - UIScreenPixel), size: CGSize(width: width - separatorInset - separatorRightInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
self.activateArea.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
return height
}
}
@@ -0,0 +1,257 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListAddressItem
import SwiftSignalKit
import AccountContext
final class PeerInfoScreenAddressItem: PeerInfoScreenItem {
let id: AnyHashable
let label: String
let text: String
let imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
let action: (() -> Void)?
let longTapAction: ((ASDisplayNode, String) -> Void)?
let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)?
let contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
init(
id: AnyHashable,
label: String,
text: String,
imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?,
action: (() -> Void)?,
longTapAction: ((ASDisplayNode, String) -> Void)? = nil,
linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil,
contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil
) {
self.id = id
self.label = label
self.text = text
self.imageSignal = imageSignal
self.action = action
self.longTapAction = longTapAction
self.linkItemAction = linkItemAction
self.contextAction = contextAction
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenAddressItemNode()
}
}
private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode {
private let containerNode: ContextControllerSourceNode
private let contextSourceNode: ContextExtractedContentContainingNode
private let extractedBackgroundImageNode: ASImageNode
private var extractedRect: CGRect?
private var nonExtractedRect: CGRect?
private let maskNode: ASImageNode
private let bottomSeparatorNode: ASDisplayNode
private let activateArea: AccessibilityAreaNode
private var item: PeerInfoScreenAddressItem?
private var itemNode: ItemListAddressItemNode?
private var presentationData: PresentationData?
override init() {
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.extractedBackgroundImageNode = ASImageNode()
self.extractedBackgroundImageNode.displaysAsynchronously = false
self.extractedBackgroundImageNode.alpha = 0.0
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init()
self.addSubnode(self.bottomSeparatorNode)
self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.addSubnode(self.containerNode)
self.addSubnode(self.maskNode)
self.contextSourceNode.contentNode.clipsToBounds = true
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
self.addSubnode(self.activateArea)
self.containerNode.isGestureEnabled = false
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.item, let contextAction = item.contextAction else {
gesture.cancel()
return
}
contextAction(strongSelf.contextSourceNode, gesture, nil)
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let strongSelf = self, let presentationData = strongSelf.presentationData else {
return
}
let theme = presentationData.theme
if isExtracted {
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 52.0, color: theme.list.plainBackgroundColor)
}
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
let rect = isExtracted ? extractedRect : nonExtractedRect
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
}
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
if !isExtracted {
self?.extractedBackgroundImageNode.image = nil
}
})
}
}
override func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { [weak self] _ in
guard let self, let item = self.item else {
return .keepWithSingleTap
}
if item.longTapAction != nil {
return .waitForSingleTap
}
return .waitForSingleTap
}
recognizer.highlight = { [weak self] point in
guard let strongSelf = self else {
return
}
strongSelf.updateTouchesAtPoint(point)
}
self.view.addGestureRecognizer(recognizer)
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
if let item = self.item {
item.action?()
}
case .longTap:
if let item = self.item {
item.longTapAction?(self, item.text)
}
default:
break
}
}
default:
break
}
}
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenAddressItem else {
return 10.0
}
self.item = item
self.presentationData = presentationData
self.containerNode.isGestureEnabled = item.contextAction != nil
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let addressItem = ItemListAddressItem(theme: presentationData.theme, systemStyle: .glass, label: item.label, text: item.text, imageSignal: item.imageSignal, sectionId: 0, style: .blocks, displayDecorations: false, action: nil, longTapAction: nil, linkItemAction: item.linkItemAction)
let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0)
let itemNode: ItemListAddressItemNode
if let current = self.itemNode {
itemNode = current
addressItem.updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: addressItem, nextItem: addressItem, animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
apply(ListViewItemApply(isOnScreen: true))
})
} else {
var itemNodeValue: ListViewItemNode?
addressItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in
itemNodeValue = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode = itemNodeValue as! ItemListAddressItemNode
itemNode.isUserInteractionEnabled = false
self.itemNode = itemNode
self.contextSourceNode.contentNode.addSubnode(itemNode)
}
let height = itemNode.contentSize.height
let separatorRightInset: CGFloat = 16.0
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset - separatorRightInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height)))
self.bottomSeparatorNode.isHidden = hasBottomCorners
self.activateArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: height))
self.activateArea.accessibilityLabel = item.label
let contentSize = CGSize(width: width, height: height)
self.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: contentSize)
transition.updateFrame(node: self.contextSourceNode.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize))
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: contentSize.width, height: contentSize.height))
let extractedRect = nonExtractedRect
self.extractedRect = extractedRect
self.nonExtractedRect = nonExtractedRect
if self.contextSourceNode.isExtractedToContextPreview {
self.extractedBackgroundImageNode.frame = extractedRect
} else {
self.extractedBackgroundImageNode.frame = nonExtractedRect
}
self.contextSourceNode.contentRect = extractedRect
return height
}
private func updateTouchesAtPoint(_ point: CGPoint?) {
}
}
@@ -0,0 +1,101 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import TelegramCore
import AccountContext
import ComponentFlow
import BirthdayPickerScreen
final class PeerInfoScreenBirthdatePickerItem: PeerInfoScreenItem {
let id: AnyHashable
let value: TelegramBirthday
let valueUpdated: (TelegramBirthday) -> Void
init(
id: AnyHashable,
value: TelegramBirthday,
valueUpdated: @escaping (TelegramBirthday) -> Void
) {
self.id = id
self.value = value
self.valueUpdated = valueUpdated
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenBirthdatePickerItemNode()
}
}
private final class PeerInfoScreenBirthdatePickerItemNode: PeerInfoScreenItemNode {
private let maskNode: ASImageNode
private let picker = ComponentView<Empty>()
private let bottomSeparatorNode: ASDisplayNode
private var item: PeerInfoScreenBirthdatePickerItem?
private var presentationData: PresentationData?
private var theme: PresentationTheme?
override init() {
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
super.init()
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.maskNode)
}
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenBirthdatePickerItem else {
return 10.0
}
self.item = item
self.presentationData = presentationData
self.theme = presentationData.theme
let sideInset: CGFloat = 16.0 + safeInsets.left
let height: CGFloat = 226.0
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let pickerSize = self.picker.update(
transition: .immediate,
component: AnyComponent(BirthdayPickerComponent(
theme: BirthdayPickerComponent.Theme(presentationTheme: presentationData.theme),
strings: presentationData.strings,
value: item.value,
valueUpdated: item.valueUpdated
)),
environment: {},
containerSize: CGSize(width: width - sideInset * 2.0, height: height)
)
let pickerFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: pickerSize)
if let pickerView = self.picker.view {
if pickerView.superview == nil {
self.view.addSubview(pickerView)
}
transition.updateFrame(view: pickerView, frame: pickerFrame)
}
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
self.bottomSeparatorNode.isHidden = hasBottomCorners
return height
}
}
@@ -0,0 +1,677 @@
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AccountContext
import TextFormat
import UIKit
import AppBundle
import TelegramStringFormatting
import ContextUI
import TelegramCore
import ComponentFlow
import MultilineTextComponent
import BundleIconComponent
import PlainButtonComponent
import AccountContext
func businessHoursTextToCopy(businessHours: TelegramBusinessHours, presentationData: PresentationData, displayLocalTimezone: Bool) -> String {
var text = ""
let cachedDays = businessHours.splitIntoWeekDays()
var timezoneOffsetMinutes: Int = 0
if displayLocalTimezone {
var currentCalendar = Calendar(identifier: .gregorian)
currentCalendar.timeZone = TimeZone(identifier: businessHours.timezoneId) ?? TimeZone.current
let currentTimezone = TimeZone.current
timezoneOffsetMinutes = (currentTimezone.secondsFromGMT() - currentCalendar.timeZone.secondsFromGMT()) / 60
}
let businessDays: [TelegramBusinessHours.WeekDay] = cachedDays
for i in 0 ..< businessDays.count {
let dayTitleValue: String
switch i {
case 0:
dayTitleValue = presentationData.strings.Weekday_Monday
case 1:
dayTitleValue = presentationData.strings.Weekday_Tuesday
case 2:
dayTitleValue = presentationData.strings.Weekday_Wednesday
case 3:
dayTitleValue = presentationData.strings.Weekday_Thursday
case 4:
dayTitleValue = presentationData.strings.Weekday_Friday
case 5:
dayTitleValue = presentationData.strings.Weekday_Saturday
case 6:
dayTitleValue = presentationData.strings.Weekday_Sunday
default:
dayTitleValue = " "
}
let businessHoursText = dayBusinessHoursText(presentationData: presentationData, day: businessDays[i], offsetMinutes: timezoneOffsetMinutes, formatAsPlainText: true)
if !text.isEmpty {
text.append("\n")
}
text.append("\(dayTitleValue): \(businessHoursText)")
}
return text
}
private func dayBusinessHoursText(presentationData: PresentationData, day: TelegramBusinessHours.WeekDay, offsetMinutes: Int, formatAsPlainText: Bool = false) -> String {
var businessHoursText: String = ""
switch day {
case .open:
businessHoursText += presentationData.strings.PeerInfo_BusinessHours_DayOpen24h
case .closed:
businessHoursText += presentationData.strings.PeerInfo_BusinessHours_DayClosed
case let .intervals(intervals):
func clipMinutes(_ value: Int) -> Int {
var value = value
if value < 0 {
value = 24 * 60 + value
}
return value % (24 * 60)
}
var resultText: String = ""
for range in intervals {
let range = TelegramBusinessHours.WorkingTimeInterval(startMinute: range.startMinute + offsetMinutes, endMinute: range.endMinute + offsetMinutes)
if !resultText.isEmpty {
if formatAsPlainText {
resultText.append(", ")
} else {
resultText.append("\n")
}
}
let startHours = clipMinutes(range.startMinute) / 60
let startMinutes = clipMinutes(range.startMinute) % 60
let startText = stringForShortTimestamp(hours: Int32(startHours), minutes: Int32(startMinutes), dateTimeFormat: presentationData.dateTimeFormat, formatAsPlainText: formatAsPlainText)
let endHours = clipMinutes(range.endMinute) / 60
let endMinutes = clipMinutes(range.endMinute) % 60
let endText = stringForShortTimestamp(hours: Int32(endHours), minutes: Int32(endMinutes), dateTimeFormat: presentationData.dateTimeFormat, formatAsPlainText: formatAsPlainText)
resultText.append("\(startText) - \(endText)")
}
businessHoursText += resultText
}
return businessHoursText
}
final class PeerInfoScreenBusinessHoursItem: PeerInfoScreenItem {
let id: AnyHashable
let label: String
let businessHours: TelegramBusinessHours
let requestLayout: (Bool) -> Void
let longTapAction: ((ASDisplayNode, String) -> Void)?
let contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
init(
id: AnyHashable,
label: String,
businessHours: TelegramBusinessHours,
requestLayout: @escaping (Bool) -> Void,
longTapAction: ((ASDisplayNode, String) -> Void)? = nil,
contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil
) {
self.id = id
self.label = label
self.businessHours = businessHours
self.requestLayout = requestLayout
self.longTapAction = longTapAction
self.contextAction = contextAction
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenBusinessHoursItemNode()
}
}
private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode {
private let containerNode: ContextControllerSourceNode
private let contextSourceNode: ContextExtractedContentContainingNode
private let extractedBackgroundImageNode: ASImageNode
private var extractedRect: CGRect?
private var nonExtractedRect: CGRect?
private let maskNode: ASImageNode
private let labelNode: ImmediateTextNode
private let currentStatusText = ComponentView<Empty>()
private let currentDayText = ComponentView<Empty>()
private var timezoneSwitchButton: ComponentView<Empty>?
private var dayTitles: [ComponentView<Empty>] = []
private var dayValues: [ComponentView<Empty>] = []
private let arrowIcon = ComponentView<Empty>()
private let bottomSeparatorNode: ASDisplayNode
private let activateArea: AccessibilityAreaNode
private var item: PeerInfoScreenBusinessHoursItem?
private var presentationData: PresentationData?
private var theme: PresentationTheme?
private var currentTimezone: TimeZone
private var displayLocalTimezone: Bool = false
private var cachedDays: [TelegramBusinessHours.WeekDay] = []
private var cachedWeekMinuteSet = IndexSet()
private var isExpanded: Bool = false
override init() {
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.extractedBackgroundImageNode = ASImageNode()
self.extractedBackgroundImageNode.displaysAsynchronously = false
self.extractedBackgroundImageNode.alpha = 0.0
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.labelNode = ImmediateTextNode()
self.labelNode.displaysAsynchronously = false
self.labelNode.isUserInteractionEnabled = false
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
self.currentTimezone = TimeZone.current
super.init()
self.addSubnode(self.bottomSeparatorNode)
self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.addSubnode(self.containerNode)
self.addSubnode(self.maskNode)
self.contextSourceNode.contentNode.clipsToBounds = true
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
self.contextSourceNode.contentNode.addSubnode(self.labelNode)
self.addSubnode(self.activateArea)
self.containerNode.isGestureEnabled = false
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.item, let contextAction = item.contextAction else {
gesture.cancel()
return
}
contextAction(strongSelf.contextSourceNode, gesture, nil)
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let strongSelf = self, let theme = strongSelf.theme else {
return
}
if isExtracted {
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 52.0, color: theme.list.plainBackgroundColor)
}
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
let rect = isExtracted ? extractedRect : nonExtractedRect
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
}
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
if !isExtracted {
self?.extractedBackgroundImageNode.image = nil
}
})
}
}
override func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { [weak self] _ in
guard let self, let item = self.item else {
return .keepWithSingleTap
}
if item.longTapAction != nil {
return .waitForSingleTap
}
return .waitForSingleTap
}
recognizer.highlight = { [weak self] point in
guard let strongSelf = self else {
return
}
strongSelf.updateTouchesAtPoint(point)
}
self.view.addGestureRecognizer(recognizer)
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
self.isExpanded = !self.isExpanded
self.item?.requestLayout(true)
case .longTap:
if let item = self.item, let presentationData = self.presentationData {
item.longTapAction?(self, businessHoursTextToCopy(businessHours: item.businessHours, presentationData: presentationData, displayLocalTimezone: self.displayLocalTimezone))
}
default:
break
}
}
default:
break
}
}
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenBusinessHoursItem else {
return 10.0
}
let businessDays: [TelegramBusinessHours.WeekDay]
if self.item?.businessHours != item.businessHours {
businessDays = item.businessHours.splitIntoWeekDays()
self.cachedDays = businessDays
self.cachedWeekMinuteSet = item.businessHours.weekMinuteSet()
} else {
businessDays = self.cachedDays
}
self.item = item
self.presentationData = presentationData
self.theme = presentationData.theme
self.containerNode.isGestureEnabled = item.contextAction != nil
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(14.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
let labelSize = self.labelNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
var topOffset = 10.0
let labelFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset), size: labelSize)
if labelSize.height > 0.0 {
topOffset += labelSize.height
topOffset += 3.0
}
let arrowIconSize = self.arrowIcon.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(
name: "Item List/DownArrow",
tintColor: presentationData.theme.list.disclosureArrowColor
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let arrowIconFrame = CGRect(origin: CGPoint(x: width - sideInset + 1.0 - arrowIconSize.width, y: topOffset + 5.0), size: arrowIconSize)
if let arrowIconView = self.arrowIcon.view {
if arrowIconView.superview == nil {
self.contextSourceNode.contentNode.view.addSubview(arrowIconView)
arrowIconView.frame = arrowIconFrame
}
transition.updatePosition(layer: arrowIconView.layer, position: arrowIconFrame.center)
transition.updateBounds(layer: arrowIconView.layer, bounds: CGRect(origin: CGPoint(), size: arrowIconFrame.size))
transition.updateTransformRotation(view: arrowIconView, angle: self.isExpanded ? CGFloat.pi * 1.0 : CGFloat.pi * 0.0)
}
var currentCalendar = Calendar(identifier: .gregorian)
currentCalendar.timeZone = TimeZone(identifier: item.businessHours.timezoneId) ?? TimeZone.current
let currentDate = Date()
var currentDayIndex = currentCalendar.component(.weekday, from: currentDate)
if currentDayIndex == 1 {
currentDayIndex = 6
} else {
currentDayIndex -= 2
}
let currentMinute = currentCalendar.component(.minute, from: currentDate)
let currentHour = currentCalendar.component(.hour, from: currentDate)
let currentWeekMinute = currentDayIndex * 24 * 60 + currentHour * 60 + currentMinute
var timezoneOffsetMinutes: Int = 0
if self.displayLocalTimezone {
timezoneOffsetMinutes = (self.currentTimezone.secondsFromGMT() - currentCalendar.timeZone.secondsFromGMT()) / 60
}
let isOpen = self.cachedWeekMinuteSet.contains(currentWeekMinute)
let openStatusText = isOpen ? presentationData.strings.PeerInfo_BusinessHours_StatusOpen : presentationData.strings.PeerInfo_BusinessHours_StatusClosed
var currentDayStatusText = currentDayIndex >= 0 && currentDayIndex < businessDays.count ? dayBusinessHoursText(presentationData: presentationData, day: businessDays[currentDayIndex], offsetMinutes: timezoneOffsetMinutes) : " "
if !isOpen {
for range in self.cachedWeekMinuteSet.rangeView {
if range.lowerBound > currentWeekMinute {
let openInMinutes = range.lowerBound - currentWeekMinute
if openInMinutes < 60 {
currentDayStatusText = presentationData.strings.PeerInfo_BusinessHours_StatusOpensInMinutes(Int32(openInMinutes))
} else if openInMinutes < 6 * 60 {
currentDayStatusText = presentationData.strings.PeerInfo_BusinessHours_StatusOpensInHours(Int32(openInMinutes / 60))
} else {
let openDate = currentDate.addingTimeInterval(Double(openInMinutes * 60))
let openTimestamp = Int32(openDate.timeIntervalSince1970) + Int32(currentCalendar.timeZone.secondsFromGMT() - TimeZone.current.secondsFromGMT())
let dateText = humanReadableStringForTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: openTimestamp, alwaysShowTime: true, allowYesterday: false, format: HumanReadableStringFormat(
dateFormatString: { value in
let text = PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageSeenTimestamp_Date(value).string, ranges: [])
return presentationData.strings.PeerInfo_BusinessHours_StatusOpensOnDate(text.string)
},
tomorrowFormatString: { value in
return PresentationStrings.FormattedString(string: presentationData.strings.PeerInfo_BusinessHours_StatusOpensTomorrowAt(value).string, ranges: [])
},
todayFormatString: { value in
return PresentationStrings.FormattedString(string: presentationData.strings.PeerInfo_BusinessHours_StatusOpensTodayAt(value).string, ranges: [])
},
yesterdayFormatString: { value in
return PresentationStrings.FormattedString(string: presentationData.strings.PeerInfo_BusinessHours_StatusOpensTodayAt(value).string, ranges: [])
},
daysFormatString: { value in
return PresentationStrings.FormattedString(string: presentationData.strings.PeerInfo_BusinessHours_StatusOpensInDays(Int32(value)), ranges: [])
}
)).string
currentDayStatusText = dateText
}
break
}
}
}
let currentStatusTextSize = self.currentStatusText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: openStatusText, font: Font.regular(15.0), textColor: isOpen ? presentationData.theme.list.freeTextSuccessColor : presentationData.theme.list.itemDestructiveColor))
)),
environment: {},
containerSize: CGSize(width: width - sideInset * 2.0, height: 100.0)
)
let currentStatusTextFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset), size: currentStatusTextSize)
if let currentStatusTextView = self.currentStatusText.view {
if currentStatusTextView.superview == nil {
currentStatusTextView.layer.anchorPoint = CGPoint()
self.contextSourceNode.contentNode.view.addSubview(currentStatusTextView)
}
transition.updatePosition(layer: currentStatusTextView.layer, position: currentStatusTextFrame.origin)
currentStatusTextView.bounds = CGRect(origin: CGPoint(), size: currentStatusTextFrame.size)
}
let dayRightInset = sideInset + 17.0
let currentDayTextSize = self.currentDayText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: currentDayStatusText, font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)),
horizontalAlignment: .right,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: width - sideInset - dayRightInset, height: 100.0)
)
var hasTimezoneDependentEntries = false
if item.businessHours.timezoneId != self.currentTimezone.identifier {
var currentCalendar = Calendar(identifier: .gregorian)
currentCalendar.timeZone = TimeZone(identifier: item.businessHours.timezoneId) ?? TimeZone.current
let timezoneOffsetMinutes = (self.currentTimezone.secondsFromGMT() - currentCalendar.timeZone.secondsFromGMT()) / 60
for i in 0 ..< businessDays.count {
let businessHoursTextLocal = dayBusinessHoursText(presentationData: presentationData, day: businessDays[i], offsetMinutes: 0)
let businessHoursTextOffset = dayBusinessHoursText(presentationData: presentationData, day: businessDays[i], offsetMinutes: timezoneOffsetMinutes)
if businessHoursTextOffset != businessHoursTextLocal {
hasTimezoneDependentEntries = true
break
}
}
}
var timezoneSwitchButtonSize: CGSize?
if hasTimezoneDependentEntries {
let timezoneSwitchButton: ComponentView<Empty>
if let current = self.timezoneSwitchButton {
timezoneSwitchButton = current
} else {
timezoneSwitchButton = ComponentView()
self.timezoneSwitchButton = timezoneSwitchButton
}
let timezoneSwitchTitle: String
if self.displayLocalTimezone {
timezoneSwitchTitle = presentationData.strings.PeerInfo_BusinessHours_TimezoneSwitchMy
} else {
timezoneSwitchTitle = presentationData.strings.PeerInfo_BusinessHours_TimezoneSwitchBusiness
}
timezoneSwitchButtonSize = timezoneSwitchButton.update(
transition: .immediate,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: timezoneSwitchTitle, font: Font.regular(12.0), textColor: presentationData.theme.list.itemAccentColor))
)),
background: AnyComponent(RoundedRectangle(
color: presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.1),
cornerRadius: nil
)),
effectAlignment: .center,
contentInsets: UIEdgeInsets(top: 1.0, left: 7.0, bottom: 2.0, right: 7.0),
action: { [weak self] in
guard let self else {
return
}
self.displayLocalTimezone = !self.displayLocalTimezone
self.item?.requestLayout(false)
if !self.isExpanded {
self.isExpanded = true
self.item?.requestLayout(true)
}
},
animateAlpha: true,
animateScale: false,
animateContents: false
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 100.0)
)
} else {
if let timezoneSwitchButton = self.timezoneSwitchButton {
self.timezoneSwitchButton = nil
timezoneSwitchButton.view?.removeFromSuperview()
}
}
let timezoneSwitchButtonSpacing: CGFloat = 3.0
if timezoneSwitchButtonSize != nil {
topOffset -= 20.0
}
let currentDayTextFrame = CGRect(origin: CGPoint(x: width - dayRightInset - currentDayTextSize.width, y: topOffset), size: currentDayTextSize)
if let currentDayTextView = self.currentDayText.view {
if currentDayTextView.superview == nil {
currentDayTextView.layer.anchorPoint = CGPoint()
self.contextSourceNode.contentNode.view.addSubview(currentDayTextView)
}
transition.updatePosition(layer: currentDayTextView.layer, position: currentDayTextFrame.origin)
currentDayTextView.bounds = CGRect(origin: CGPoint(), size: currentDayTextFrame.size)
}
topOffset += max(currentStatusTextSize.height, currentDayTextSize.height)
if let timezoneSwitchButtonView = self.timezoneSwitchButton?.view, let timezoneSwitchButtonSize {
topOffset += timezoneSwitchButtonSpacing
var timezoneSwitchButtonTransition = transition
if timezoneSwitchButtonView.superview == nil {
timezoneSwitchButtonTransition = .immediate
self.contextSourceNode.contentNode.view.addSubview(timezoneSwitchButtonView)
}
let timezoneSwitchButtonFrame = CGRect(origin: CGPoint(x: width - dayRightInset - timezoneSwitchButtonSize.width, y: topOffset), size: timezoneSwitchButtonSize)
timezoneSwitchButtonTransition.updateFrame(view: timezoneSwitchButtonView, frame: timezoneSwitchButtonFrame)
topOffset += timezoneSwitchButtonSize.height
}
let daySpacing: CGFloat = 15.0
var dayHeights: CGFloat = 0.0
for rawI in 0 ..< businessDays.count {
if rawI == 0 {
//skip current day
continue
}
let i = (rawI + currentDayIndex) % businessDays.count
dayHeights += daySpacing
var dayTransition = transition
let dayTitle: ComponentView<Empty>
if self.dayTitles.count > i {
dayTitle = self.dayTitles[i]
} else {
dayTransition = .immediate
dayTitle = ComponentView()
self.dayTitles.append(dayTitle)
}
let dayValue: ComponentView<Empty>
if self.dayValues.count > i {
dayValue = self.dayValues[i]
} else {
dayValue = ComponentView()
self.dayValues.append(dayValue)
}
let dayTitleValue: String
switch i {
case 0:
dayTitleValue = presentationData.strings.Weekday_Monday
case 1:
dayTitleValue = presentationData.strings.Weekday_Tuesday
case 2:
dayTitleValue = presentationData.strings.Weekday_Wednesday
case 3:
dayTitleValue = presentationData.strings.Weekday_Thursday
case 4:
dayTitleValue = presentationData.strings.Weekday_Friday
case 5:
dayTitleValue = presentationData.strings.Weekday_Saturday
case 6:
dayTitleValue = presentationData.strings.Weekday_Sunday
default:
dayTitleValue = " "
}
let businessHoursText = dayBusinessHoursText(presentationData: presentationData, day: businessDays[i], offsetMinutes: timezoneOffsetMinutes)
let dayTitleSize = dayTitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: dayTitleValue, font: Font.regular(15.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: width - sideInset * 2.0, height: 100.0)
)
let dayTitleFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset + dayHeights), size: dayTitleSize)
if let dayTitleView = dayTitle.view {
if dayTitleView.superview == nil {
dayTitleView.layer.anchorPoint = CGPoint()
self.contextSourceNode.contentNode.view.addSubview(dayTitleView)
dayTitleView.alpha = 0.0
}
dayTransition.updatePosition(layer: dayTitleView.layer, position: dayTitleFrame.origin)
dayTitleView.bounds = CGRect(origin: CGPoint(), size: dayTitleFrame.size)
transition.updateAlpha(layer: dayTitleView.layer, alpha: self.isExpanded ? 1.0 : 0.0)
}
let dayValueSize = dayValue.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: businessHoursText, font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor, paragraphAlignment: .right)),
horizontalAlignment: .right,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: width - sideInset - dayRightInset, height: 100.0)
)
let dayValueFrame = CGRect(origin: CGPoint(x: width - dayRightInset - dayValueSize.width, y: topOffset + dayHeights), size: dayValueSize)
if let dayValueView = dayValue.view {
if dayValueView.superview == nil {
dayValueView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0)
self.contextSourceNode.contentNode.view.addSubview(dayValueView)
dayValueView.alpha = 0.0
}
dayTransition.updatePosition(layer: dayValueView.layer, position: CGPoint(x: dayValueFrame.maxX, y: dayValueFrame.minY))
dayValueView.bounds = CGRect(origin: CGPoint(), size: dayValueFrame.size)
transition.updateAlpha(layer: dayValueView.layer, alpha: self.isExpanded ? 1.0 : 0.0)
}
dayHeights += max(dayTitleSize.height, dayValueSize.height)
}
if self.isExpanded {
topOffset += dayHeights
}
topOffset += 11.0
transition.updateFrame(node: self.labelNode, frame: labelFrame)
let height = topOffset
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height)))
self.bottomSeparatorNode.isHidden = hasBottomCorners
self.activateArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: height))
self.activateArea.accessibilityLabel = item.label
let contentSize = CGSize(width: width, height: height)
self.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: contentSize)
transition.updateFrame(node: self.contextSourceNode.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize))
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: contentSize.width, height: contentSize.height))
let extractedRect = nonExtractedRect
self.extractedRect = extractedRect
self.nonExtractedRect = nonExtractedRect
if self.contextSourceNode.isExtractedToContextPreview {
self.extractedBackgroundImageNode.frame = extractedRect
} else {
self.extractedBackgroundImageNode.frame = nonExtractedRect
}
self.contextSourceNode.contentRect = extractedRect
return height
}
private func updateTouchesAtPoint(_ point: CGPoint?) {
}
}
@@ -0,0 +1,125 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListAddressItem
import SwiftSignalKit
import AccountContext
import Postbox
import PeerInfoUI
import ItemListUI
final class PeerInfoScreenCallListItem: PeerInfoScreenItem {
let id: AnyHashable
let messages: [Message]
init(
id: AnyHashable,
messages: [Message]
) {
self.id = id
self.messages = messages
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenCallListItemNode()
}
}
private final class PeerInfoScreenCallListItemNode: PeerInfoScreenItemNode {
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
private let bottomSeparatorNode: ASDisplayNode
private let maskNode: ASImageNode
private var item: PeerInfoScreenCallListItem?
private var itemNode: ItemListCallListItemNode?
override init() {
var bringToFrontForHighlightImpl: (() -> Void)?
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
self.selectionNode.isUserInteractionEnabled = false
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
super.init()
bringToFrontForHighlightImpl = { [weak self] in
self?.bringToFrontForHighlight?()
}
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.selectionNode)
self.addSubnode(self.maskNode)
}
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenCallListItem else {
return 10.0
}
self.item = item
self.selectionNode.pressed = nil
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let addressItem = ItemListCallListItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, messages: item.messages, sectionId: 0, style: .blocks, displayDecorations: false)
let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0)
let itemNode: ItemListCallListItemNode
if let current = self.itemNode {
itemNode = current
addressItem.updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
apply(ListViewItemApply(isOnScreen: true))
})
} else {
var itemNodeValue: ListViewItemNode?
addressItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in
itemNodeValue = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode = itemNodeValue as! ItemListCallListItemNode
itemNode.isUserInteractionEnabled = false
self.itemNode = itemNode
self.addSubnode(itemNode)
}
let verticalInset: CGFloat = 8.0
let height = itemNode.contentSize.height + verticalInset * 2.0
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalInset), size: itemNode.bounds.size))
let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
self.bottomSeparatorNode.isHidden = hasBottomCorners
return height
}
}
@@ -0,0 +1,203 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import TextFormat
import Markdown
import AccountContext
import TextNodeWithEntities
import TextFormat
final class PeerInfoScreenCommentItem: PeerInfoScreenItem {
enum LinkAction {
case tap(String)
}
let id: AnyHashable
let text: String
let attributedPrefix: NSAttributedString?
let useAccentLinkColor: Bool
let linkAction: ((LinkAction) -> Void)?
init(id: AnyHashable, text: String, attributedPrefix: NSAttributedString? = nil, useAccentLinkColor: Bool = true, linkAction: ((LinkAction) -> Void)? = nil) {
self.id = id
self.text = text
self.attributedPrefix = attributedPrefix
self.useAccentLinkColor = useAccentLinkColor
self.linkAction = linkAction
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenCommentItemNode()
}
}
private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode {
private let textNode: ImmediateTextNodeWithEntities
private var linkHighlightingNode: LinkHighlightingNode?
private let activateArea: AccessibilityAreaNode
private var item: PeerInfoScreenCommentItem?
private var presentationData: PresentationData?
private var chevronImage: UIImage?
override init() {
self.textNode = ImmediateTextNodeWithEntities()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
self.activateArea = AccessibilityAreaNode()
self.activateArea.accessibilityTraits = .staticText
super.init()
self.addSubnode(self.textNode)
self.addSubnode(self.activateArea)
}
override func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
recognizer.highlight = { [weak self] point in
if let strongSelf = self {
strongSelf.updateTouchesAtPoint(point)
}
}
self.view.addGestureRecognizer(recognizer)
}
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenCommentItem else {
return 10.0
}
let themeUpdated = self.presentationData?.theme !== presentationData.theme
self.item = item
self.presentationData = presentationData
let sideInset: CGFloat = 16.0 + safeInsets.left
let verticalInset: CGFloat = 7.0
self.textNode.maximumNumberOfLines = 0
self.textNode.arguments = TextNodeWithEntities.Arguments(
context: context,
cache: context.animationCache,
renderer: context.animationRenderer,
placeholderColor: presentationData.theme.list.mediaPlaceholderColor,
attemptSynchronous: false
)
let textFont = Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize)
let textColor = presentationData.theme.list.freeTextColor
var text = item.text
text = text.replacingOccurrences(of: " >]", with: "\u{00A0}>]")
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: item.useAccentLinkColor ? presentationData.theme.list.itemAccentColor : textColor, additionalAttributes: item.useAccentLinkColor ? [:] : [NSAttributedString.Key.underlineStyle.rawValue: NSUnderlineStyle.single.rawValue as NSNumber]), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})).mutableCopy() as! NSMutableAttributedString
if let attributedPrefix = item.attributedPrefix {
attributedText.insert(attributedPrefix, at: 0)
attributedText.addAttribute(NSAttributedString.Key.font, value: textFont, range: NSRange(location: 0, length: attributedPrefix.length))
attributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: textColor, range: NSRange(location: 0, length: attributedPrefix.length))
}
if let _ = item.text.range(of: ">]"), let range = attributedText.string.range(of: ">") {
if themeUpdated || self.chevronImage == nil {
self.chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: presentationData.theme.list.itemAccentColor)
}
if let chevronImage = self.chevronImage {
attributedText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedText.string))
}
}
self.textNode.attributedText = attributedText
self.textNode.visibility = true
self.textNode.lineSpacing = 0.12
self.activateArea.accessibilityLabel = attributedText.string
let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: textSize)
let height = textSize.height + verticalInset * 2.0
transition.updateFrame(node: self.textNode, frame: textFrame)
self.activateArea.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
return height
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
let titleFrame = self.textNode.frame
if let item = self.item, titleFrame.contains(location) {
if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
item.linkAction?(.tap(url))
}
}
}
default:
break
}
}
default:
break
}
}
private func updateTouchesAtPoint(_ point: CGPoint?) {
if let item = self.item, let presentationData = self.presentationData {
var rects: [CGRect]?
if let point = point {
let textNodeFrame = self.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
let possibleNames: [String] = [
TelegramTextAttributes.URL,
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
rects = self.textNode.attributeRects(name: name, at: index)
break
}
}
}
}
if let rects = rects {
let linkHighlightingNode: LinkHighlightingNode
if let current = self.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: item.useAccentLinkColor ? presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2) : presentationData.theme.list.freeTextColor.withAlphaComponent(0.2))
self.linkHighlightingNode = linkHighlightingNode
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode)
}
linkHighlightingNode.frame = self.textNode.frame
linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = self.linkHighlightingNode {
self.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
}
@@ -0,0 +1,402 @@
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AccountContext
import TextFormat
import UIKit
import AppBundle
import TelegramStringFormatting
import ContextUI
final class PeerInfoScreenContactInfoItem: PeerInfoScreenItem {
let id: AnyHashable
let username: String
let phoneNumber: String
let additionalText: String?
let usernameAction: ((ASDisplayNode) -> Void)?
let usernameLongTapAction: ((ASDisplayNode) -> Void)?
let phoneAction: ((ASDisplayNode) -> Void)?
let phoneLongTapAction: ((ASDisplayNode) -> Void)?
let linkItemAction: ((TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?) -> Void)?
let contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
let requestLayout: () -> Void
init(
id: AnyHashable,
username: String,
phoneNumber: String,
additionalText: String? = nil,
usernameAction: ((ASDisplayNode) -> Void)?,
usernameLongTapAction: ((ASDisplayNode) -> Void)?,
phoneAction: ((ASDisplayNode) -> Void)?,
phoneLongTapAction: ((ASDisplayNode) -> Void)?,
linkItemAction: ((TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?) -> Void)? = nil,
contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil,
requestLayout: @escaping () -> Void
) {
self.id = id
self.username = username
self.phoneNumber = phoneNumber
self.additionalText = additionalText
self.usernameAction = usernameAction
self.usernameLongTapAction = usernameLongTapAction
self.phoneAction = phoneAction
self.phoneLongTapAction = phoneLongTapAction
self.linkItemAction = linkItemAction
self.contextAction = contextAction
self.requestLayout = requestLayout
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenContactInfoItemNode()
}
}
private final class PeerInfoScreenContactInfoItemNode: PeerInfoScreenItemNode {
private let containerNode: ContextControllerSourceNode
private let contextSourceNode: ContextExtractedContentContainingNode
private let extractedBackgroundImageNode: ASImageNode
private var extractedRect: CGRect?
private var nonExtractedRect: CGRect?
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
private let maskNode: ASImageNode
private let usernameNode: ImmediateTextNode
private let phoneNumberNode: ImmediateTextNode
private let additionalTextNode: ImmediateTextNode
private let measureTextNode: ImmediateTextNode
private let bottomSeparatorNode: ASDisplayNode
private var linkHighlightingNode: LinkHighlightingNode?
private let activateArea: AccessibilityAreaNode
private var item: PeerInfoScreenContactInfoItem?
private var theme: PresentationTheme?
override init() {
var bringToFrontForHighlightImpl: (() -> Void)?
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.extractedBackgroundImageNode = ASImageNode()
self.extractedBackgroundImageNode.displaysAsynchronously = false
self.extractedBackgroundImageNode.alpha = 0.0
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
self.selectionNode.isUserInteractionEnabled = false
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.usernameNode = ImmediateTextNode()
self.usernameNode.displaysAsynchronously = false
self.usernameNode.isUserInteractionEnabled = false
self.phoneNumberNode = ImmediateTextNode()
self.phoneNumberNode.displaysAsynchronously = false
self.phoneNumberNode.isUserInteractionEnabled = false
self.additionalTextNode = ImmediateTextNode()
self.additionalTextNode.displaysAsynchronously = false
self.additionalTextNode.isUserInteractionEnabled = false
self.measureTextNode = ImmediateTextNode()
self.measureTextNode.displaysAsynchronously = false
self.measureTextNode.isUserInteractionEnabled = false
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init()
bringToFrontForHighlightImpl = { [weak self] in
self?.bringToFrontForHighlight?()
}
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.selectionNode)
self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.addSubnode(self.containerNode)
self.addSubnode(self.maskNode)
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
self.contextSourceNode.contentNode.addSubnode(self.usernameNode)
self.contextSourceNode.contentNode.addSubnode(self.phoneNumberNode)
self.contextSourceNode.contentNode.addSubnode(self.additionalTextNode)
self.addSubnode(self.activateArea)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.item, let contextAction = item.contextAction else {
gesture.cancel()
return
}
contextAction(strongSelf.contextSourceNode, gesture, nil)
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let strongSelf = self, let theme = strongSelf.theme else {
return
}
if isExtracted {
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 52.0, color: theme.list.plainBackgroundColor)
}
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
let rect = isExtracted ? extractedRect : nonExtractedRect
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
}
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
if !isExtracted {
self?.extractedBackgroundImageNode.image = nil
}
})
}
}
override func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { [weak self] point in
guard let strongSelf = self else {
return .keepWithSingleTap
}
if let _ = strongSelf.linkItemAtPoint(point) {
return .waitForSingleTap
}
return .waitForSingleTap
}
recognizer.highlight = { [weak self] point in
guard let strongSelf = self else {
return
}
strongSelf.updateTouchesAtPoint(point)
}
self.view.addGestureRecognizer(recognizer)
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap, .longTap:
if let item = self.item {
if let linkItem = self.linkItemAtPoint(location) {
item.linkItemAction?(gesture == .tap ? .tap : .longTap, linkItem, self.linkHighlightingNode ?? self, self.linkHighlightingNode?.rects.first)
} else if case .longTap = gesture {
if self.usernameNode.frame.insetBy(dx: -10.0, dy: -10.0).contains(location) {
item.usernameLongTapAction?(self.usernameNode)
} else if self.phoneNumberNode.frame.insetBy(dx: -10.0, dy: -10.0).contains(location) {
item.phoneLongTapAction?(self.phoneNumberNode)
}
} else if case .tap = gesture {
if self.usernameNode.frame.insetBy(dx: -10.0, dy: -10.0).contains(location) {
item.usernameAction?(self.contextSourceNode)
} else if self.phoneNumberNode.frame.insetBy(dx: -10.0, dy: -10.0).contains(location) {
item.phoneAction?(self.contextSourceNode)
}
}
}
default:
break
}
}
default:
break
}
}
private func linkItemAtPoint(_ point: CGPoint) -> TextLinkItem? {
let additionalTextNodeFrame = self.additionalTextNode.frame
if let (_, attributes) = self.additionalTextNode.attributesAtPoint(CGPoint(x: point.x - additionalTextNodeFrame.minX, y: point.y - additionalTextNodeFrame.minY)) {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
return .url(url: url, concealed: false)
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .mention(peerName)
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return .hashtag(hashtag.peerName, hashtag.hashtag)
} else {
return nil
}
}
return nil
}
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenContactInfoItem else {
return 10.0
}
self.item = item
self.theme = presentationData.theme
// if let action = item.action {
// self.selectionNode.pressed = { [weak self] in
// if let strongSelf = self {
// action(strongSelf.contextSourceNode)
// }
// }
// } else {
// self.selectionNode.pressed = nil
// }
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
self.usernameNode.attributedText = NSAttributedString(string: item.username, font: Font.regular(15.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
self.phoneNumberNode.maximumNumberOfLines = 1
self.phoneNumberNode.cutout = nil
self.phoneNumberNode.attributedText = NSAttributedString(string: item.phoneNumber, font: Font.regular(15.0), textColor: presentationData.theme.list.itemAccentColor)
let fontSize: CGFloat = 15.0
let baseFont = Font.regular(fontSize)
let linkFont = baseFont
let boldFont = Font.medium(fontSize)
let italicFont = Font.italic(fontSize)
let boldItalicFont = Font.semiboldItalic(fontSize)
let titleFixedFont = Font.monospace(fontSize)
if let additionalText = item.additionalText {
let entities = generateTextEntities(additionalText, enabledTypes: [.mention])
let attributedAdditionalText = stringWithAppliedEntities(additionalText, entities: entities, baseColor: presentationData.theme.list.itemPrimaryTextColor, linkColor: presentationData.theme.list.itemAccentColor, baseFont: baseFont, linkFont: linkFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: titleFixedFont, blockQuoteFont: baseFont, underlineLinks: false, message: nil)
self.additionalTextNode.maximumNumberOfLines = 10
self.additionalTextNode.attributedText = attributedAdditionalText
} else {
self.additionalTextNode.attributedText = nil
}
let usernameSize = self.usernameNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let phoneSize = self.phoneNumberNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let additionalTextSize = self.additionalTextNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let topOffset = 12.0
var height = topOffset * 2.0
let usernameFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset), size: usernameSize)
let phoneFrame = CGRect(origin: CGPoint(x: usernameSize.width > 0.0 ? width - sideInset - phoneSize.width : sideInset, y: topOffset), size: phoneSize)
let textHeight = max(usernameSize.height, phoneSize.height)
height += textHeight
let additionalTextFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset + textHeight + 3.0), size: additionalTextSize)
transition.updateFrame(node: self.usernameNode, frame: usernameFrame)
transition.updateFrame(node: self.phoneNumberNode, frame: phoneFrame)
transition.updateFrame(node: self.additionalTextNode, frame: additionalTextFrame)
if additionalTextSize.height > 0.0 {
height += additionalTextSize.height + 3.0
}
let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
self.bottomSeparatorNode.isHidden = hasBottomCorners
self.activateArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: height))
self.activateArea.accessibilityLabel = item.username
self.activateArea.accessibilityValue = item.phoneNumber
let contentSize = CGSize(width: width, height: height)
self.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: contentSize)
self.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: contentSize)
self.containerNode.isGestureEnabled = item.contextAction != nil
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: contentSize.width, height: contentSize.height))
let extractedRect = nonExtractedRect
self.extractedRect = extractedRect
self.nonExtractedRect = nonExtractedRect
if self.contextSourceNode.isExtractedToContextPreview {
self.extractedBackgroundImageNode.frame = extractedRect
} else {
self.extractedBackgroundImageNode.frame = nonExtractedRect
}
self.contextSourceNode.contentRect = extractedRect
return height
}
private func updateTouchesAtPoint(_ point: CGPoint?) {
guard let _ = self.item, let theme = self.theme else {
return
}
var rects: [CGRect]?
var textNode: ASDisplayNode?
if let point = point {
if self.usernameNode.frame.insetBy(dx: -10.0, dy: -10.0).contains(point) {
textNode = self.usernameNode
rects = [self.usernameNode.bounds]
} else if self.phoneNumberNode.frame.insetBy(dx: -10.0, dy: -10.0).contains(point) {
textNode = self.phoneNumberNode
rects = [self.phoneNumberNode.bounds]
} else if self.additionalTextNode.frame.contains(point) {
let mappedPoint = CGPoint(x: point.x - self.additionalTextNode.frame.minX, y: point.y - self.additionalTextNode.frame.minY)
if mappedPoint.y > 0.0, let (index, attributes) = self.additionalTextNode.attributesAtPoint(mappedPoint) {
let possibleNames: [String] = [
TelegramTextAttributes.URL,
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
rects = self.additionalTextNode.attributeRects(name: name, at: index)
textNode = self.additionalTextNode
break
}
}
}
}
}
if let rects = rects, let textNode = textNode {
let linkHighlightingNode: LinkHighlightingNode
if let current = self.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: theme.list.itemAccentColor.withAlphaComponent(0.2))
self.linkHighlightingNode = linkHighlightingNode
self.contextSourceNode.contentNode.insertSubnode(linkHighlightingNode, belowSubnode: textNode)
}
linkHighlightingNode.frame = textNode.frame
linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = self.linkHighlightingNode {
self.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
@@ -0,0 +1,134 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import EncryptionKeyVisualization
import TelegramCore
import AccountContext
final class PeerInfoScreenDisclosureEncryptionKeyItem: PeerInfoScreenItem {
let id: AnyHashable
let text: String
let fingerprint: SecretChatKeyFingerprint
let action: (() -> Void)?
init(id: AnyHashable, text: String, fingerprint: SecretChatKeyFingerprint, action: (() -> Void)?) {
self.id = id
self.text = text
self.fingerprint = fingerprint
self.action = action
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenDisclosureEncryptionKeyItemNode()
}
}
private final class PeerInfoScreenDisclosureEncryptionKeyItemNode: PeerInfoScreenItemNode {
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
private let textNode: ImmediateTextNode
private let keyNode: ASImageNode
private let arrowNode: ASImageNode
private let bottomSeparatorNode: ASDisplayNode
private let maskNode: ASImageNode
private var item: PeerInfoScreenDisclosureEncryptionKeyItem?
override init() {
var bringToFrontForHighlightImpl: (() -> Void)?
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
self.keyNode = ASImageNode()
self.keyNode.displaysAsynchronously = false
self.keyNode.displayWithoutProcessing = true
self.keyNode.isUserInteractionEnabled = false
self.arrowNode = ASImageNode()
self.arrowNode.isLayerBacked = true
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.arrowNode.isUserInteractionEnabled = false
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
super.init()
bringToFrontForHighlightImpl = { [weak self] in
self?.bringToFrontForHighlight?()
}
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.selectionNode)
self.addSubnode(self.textNode)
self.addSubnode(self.keyNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.maskNode)
}
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenDisclosureEncryptionKeyItem else {
return 10.0
}
if self.item?.fingerprint != item.fingerprint {
self.keyNode.image = secretChatKeyImage(item.fingerprint, size: CGSize(width: 24.0, height: 24.0))
}
self.item = item
self.selectionNode.pressed = item.action
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
self.textNode.maximumNumberOfLines = 1
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0 - 44.0, height: .greatestFiniteMagnitude))
let arrowInset: CGFloat = 18.0
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 12.0), size: textSize)
let height = textSize.height + 24.0
if let arrowImage = PresentationResourcesItemList.disclosureArrowImage(presentationData.theme) {
self.arrowNode.image = arrowImage
let arrowFrame = CGRect(origin: CGPoint(x: width - 7.0 - arrowImage.size.width, y: floorToScreenPixels((height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
}
if let image = self.keyNode.image {
self.keyNode.frame = CGRect(origin: CGPoint(x: width - sideInset - arrowInset - image.size.width, y: floor((height - image.size.height) / 2.0)), size: image.size)
}
transition.updateFrame(node: self.textNode, frame: textFrame)
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
self.bottomSeparatorNode.isHidden = hasBottomCorners
let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
return height
}
}
@@ -0,0 +1,417 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import TextNodeWithEntities
import AccountContext
final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
enum Label {
enum LabelColor {
case generic
case accent
}
case none
case text(String)
case attributedText(NSAttributedString)
case coloredText(String, LabelColor)
case badge(String, UIColor)
case semitransparentBadge(String, UIColor)
case titleBadge(String, UIColor)
case image(UIImage, CGSize)
case labelBadge(String)
var text: String {
switch self {
case .none, .image:
return ""
case let .attributedText(text):
return text.string
case let .text(text), let .coloredText(text, _), let .badge(text, _), let .semitransparentBadge(text, _), let .titleBadge(text, _), let .labelBadge(text):
return text
}
}
var badgeColor: UIColor? {
switch self {
case .none, .text, .coloredText, .image, .attributedText, .labelBadge:
return nil
case let .badge(_, color), let .semitransparentBadge(_, color), let .titleBadge(_, color):
return color
}
}
}
let id: AnyHashable
let label: Label
let additionalBadgeLabel: String?
let additionalBadgeIcon: UIImage?
let text: String
let icon: UIImage?
let iconSignal: Signal<UIImage?, NoError>?
let hasArrow: Bool
let action: (() -> Void)?
init(id: AnyHashable, label: Label = .none, additionalBadgeLabel: String? = nil, additionalBadgeIcon: UIImage? = nil, text: String, icon: UIImage? = nil, iconSignal: Signal<UIImage?, NoError>? = nil, hasArrow: Bool = true, action: (() -> Void)?) {
self.id = id
self.label = label
self.additionalBadgeLabel = additionalBadgeLabel
self.additionalBadgeIcon = additionalBadgeIcon
self.text = text
self.icon = icon
self.iconSignal = iconSignal
self.hasArrow = hasArrow
self.action = action
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenDisclosureItemNode()
}
}
private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
private let maskNode: ASImageNode
private let iconNode: ASImageNode
private let labelBadgeNode: ASImageNode
private let labelNode: ImmediateTextNodeWithEntities
private var additionalLabelNode: ImmediateTextNode?
private var additionalLabelBadgeNode: ASImageNode?
private let textNode: ImmediateTextNode
private let arrowNode: ASImageNode
private let bottomSeparatorNode: ASDisplayNode
private let activateArea: AccessibilityAreaNode
private var iconDisposable = MetaDisposable()
private var item: PeerInfoScreenDisclosureItem?
override init() {
var bringToFrontForHighlightImpl: (() -> Void)?
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
self.labelBadgeNode = ASImageNode()
self.labelBadgeNode.displayWithoutProcessing = true
self.labelBadgeNode.displaysAsynchronously = false
self.labelBadgeNode.isLayerBacked = true
self.labelNode = ImmediateTextNodeWithEntities()
self.labelNode.displaysAsynchronously = false
self.labelNode.isUserInteractionEnabled = false
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
self.arrowNode = ASImageNode()
self.arrowNode.isLayerBacked = true
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.arrowNode.isUserInteractionEnabled = false
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init()
bringToFrontForHighlightImpl = { [weak self] in
self?.bringToFrontForHighlight?()
}
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.selectionNode)
self.addSubnode(self.maskNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.textNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.activateArea)
}
deinit {
self.iconDisposable.dispose()
}
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenDisclosureItem else {
return 10.0
}
let previousItem = self.item
self.item = item
self.selectionNode.pressed = item.action
let sideInset: CGFloat = 16.0 + safeInsets.left
let leftInset = (item.icon == nil && item.iconSignal == nil ? sideInset : sideInset + 29.0 + 16.0)
let rightInset = sideInset + (item.hasArrow ? 18.0 : 0.0)
let separatorInset = item.icon == nil && item.iconSignal == nil ? sideInset : leftInset - 1.0
let titleFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize)
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let textColorValue: UIColor = presentationData.theme.list.itemPrimaryTextColor
let labelColorValue: UIColor
let labelFont: UIFont
if case let .semitransparentBadge(_, color) = item.label {
labelColorValue = color
labelFont = Font.semibold(14.0)
} else if case .badge = item.label {
labelColorValue = presentationData.theme.list.itemCheckColors.foregroundColor
labelFont = Font.regular(15.0)
} else if case .titleBadge = item.label {
labelColorValue = presentationData.theme.list.itemCheckColors.foregroundColor
labelFont = Font.medium(11.0)
} else if case .labelBadge = item.label {
labelColorValue = presentationData.theme.list.itemCheckColors.foregroundColor
labelFont = Font.medium(12.0)
} else if case let .coloredText(_, color) = item.label {
switch color {
case .generic:
labelColorValue = presentationData.theme.list.itemSecondaryTextColor
case .accent:
labelColorValue = presentationData.theme.list.itemAccentColor
}
labelFont = titleFont
} else {
labelColorValue = presentationData.theme.list.itemSecondaryTextColor
labelFont = titleFont
}
self.labelNode.arguments = TextNodeWithEntities.Arguments(
context: context,
cache: context.animationCache,
renderer: context.animationRenderer,
placeholderColor: .clear,
attemptSynchronous: true
)
if case let .attributedText(text) = item.label {
self.labelNode.attributedText = text
} else {
self.labelNode.attributedText = NSAttributedString(string: item.label.text, font: labelFont, textColor: labelColorValue)
}
self.textNode.maximumNumberOfLines = 1
self.textNode.attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: textColorValue)
let textSize = self.textNode.updateLayout(CGSize(width: width - (leftInset + rightInset), height: .greatestFiniteMagnitude))
var labelConstrainWidth = width - textSize.width - (leftInset + rightInset)
if case .semitransparentBadge = item.label {
labelConstrainWidth -= 16.0
}
let labelSize = self.labelNode.updateLayout(CGSize(width: labelConstrainWidth, height: .greatestFiniteMagnitude))
let height = textSize.height + 32.0
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((height - textSize.height) / 2.0)), size: textSize)
if item.icon != nil || item.iconSignal != nil {
if self.iconNode.supernode == nil {
self.addSubnode(self.iconNode)
}
let iconSize: CGSize
if let icon = item.icon {
self.iconNode.image = icon
iconSize = icon.size
} else if let iconSignal = item.iconSignal {
if previousItem?.text != item.text {
self.iconNode.image = nil
self.iconDisposable.set((iconSignal
|> deliverOnMainQueue).startStrict(next: { [weak self] icon in
if let self {
self.iconNode.image = icon
}
}))
}
iconSize = CGSize(width: 29.0, height: 29.0)
} else {
iconSize = CGSize(width: 29.0, height: 29.0)
}
let iconFrame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize)
transition.updateFrame(node: self.iconNode, frame: iconFrame)
} else if self.iconNode.supernode != nil {
self.iconNode.image = nil
self.iconNode.removeFromSupernode()
}
if item.hasArrow, let arrowImage = PresentationResourcesItemList.disclosureArrowImage(presentationData.theme) {
self.arrowNode.image = arrowImage
let arrowFrame = CGRect(origin: CGPoint(x: width - 7.0 - arrowImage.size.width - safeInsets.right, y: floorToScreenPixels((height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
}
var badgeDiameter: CGFloat = 20.0
if case let .image(image, imageSize) = item.label {
self.labelBadgeNode.image = image
badgeDiameter = imageSize.height
if self.labelBadgeNode.supernode == nil {
self.insertSubnode(self.labelBadgeNode, belowSubnode: self.labelNode)
}
} else if case let .semitransparentBadge(text, badgeColor) = item.label, !text.isEmpty {
badgeDiameter = 24.0
if previousItem?.label.badgeColor != badgeColor {
self.labelBadgeNode.image = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor.withAlphaComponent(0.1))
}
if self.labelBadgeNode.supernode == nil {
self.insertSubnode(self.labelBadgeNode, belowSubnode: self.labelNode)
}
} else if case let .badge(text, badgeColor) = item.label, !text.isEmpty {
if previousItem?.label.badgeColor != badgeColor {
self.labelBadgeNode.image = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor)
}
if self.labelBadgeNode.supernode == nil {
self.insertSubnode(self.labelBadgeNode, belowSubnode: self.labelNode)
}
} else if case let .titleBadge(text, badgeColor) = item.label, !text.isEmpty {
if previousItem?.label.badgeColor != badgeColor {
self.labelBadgeNode.image = generateFilledRoundedRectImage(size: CGSize(width: 16.0, height: 16.0), cornerRadius: 5.0, color: badgeColor)?.stretchableImage(withLeftCapWidth: 6, topCapHeight: 6)
}
if self.labelBadgeNode.supernode == nil {
self.insertSubnode(self.labelBadgeNode, belowSubnode: self.labelNode)
}
} else if case let .labelBadge(text) = item.label, !text.isEmpty {
let badgeColor = presentationData.theme.list.itemCheckColors.fillColor
if previousItem?.label.badgeColor != badgeColor {
self.labelBadgeNode.image = generateFilledRoundedRectImage(size: CGSize(width: 16.0, height: 16.0), cornerRadius: 5.0, color: badgeColor)?.stretchableImage(withLeftCapWidth: 6, topCapHeight: 6)
}
if self.labelBadgeNode.supernode == nil {
self.insertSubnode(self.labelBadgeNode, belowSubnode: self.labelNode)
}
} else {
self.labelBadgeNode.removeFromSupernode()
}
if item.additionalBadgeLabel != nil {
if previousItem?.additionalBadgeLabel == nil {
let additionalLabelBadgeNode: ASImageNode
if let current = self.additionalLabelBadgeNode {
additionalLabelBadgeNode = current
} else {
additionalLabelBadgeNode = ASImageNode()
additionalLabelBadgeNode.isUserInteractionEnabled = false
self.additionalLabelBadgeNode = additionalLabelBadgeNode
self.insertSubnode(additionalLabelBadgeNode, belowSubnode: self.labelNode)
}
additionalLabelBadgeNode.image = generateFilledRoundedRectImage(size: CGSize(width: 16.0, height: 16.0), cornerRadius: 5.0, color: presentationData.theme.list.itemCheckColors.fillColor)?.stretchableImage(withLeftCapWidth: 6, topCapHeight: 6)
}
}
if let additionalBadgeIcon = item.additionalBadgeIcon {
let additionalLabelBadgeNode: ASImageNode
if let current = self.additionalLabelBadgeNode {
additionalLabelBadgeNode = current
} else {
additionalLabelBadgeNode = ASImageNode()
additionalLabelBadgeNode.isUserInteractionEnabled = false
self.additionalLabelBadgeNode = additionalLabelBadgeNode
self.insertSubnode(additionalLabelBadgeNode, belowSubnode: self.labelNode)
}
additionalLabelBadgeNode.image = additionalBadgeIcon
} else if item.additionalBadgeLabel == nil {
if let additionalLabelBadgeNode = self.additionalLabelBadgeNode {
self.additionalLabelBadgeNode = nil
additionalLabelBadgeNode.removeFromSupernode()
}
}
var badgeWidth = max(badgeDiameter, labelSize.width + 10.0)
if case .semitransparentBadge = item.label {
badgeWidth += 2.0
}
let labelFrame: CGRect
if case .semitransparentBadge = item.label {
labelFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: floor((height - labelSize.height) / 2.0)), size: labelSize)
} else if case .badge = item.label {
labelFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: floor((height - labelSize.height) / 2.0)), size: labelSize)
} else if case .titleBadge = item.label {
labelFrame = CGRect(origin: CGPoint(x: textFrame.maxX + 10.0, y: floor((height - labelSize.height) / 2.0) + 1.0), size: labelSize)
} else if case .labelBadge = item.label {
labelFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: floor((height - labelSize.height) / 2.0)), size: labelSize)
} else {
labelFrame = CGRect(origin: CGPoint(x: width - rightInset - labelSize.width, y: floorToScreenPixels((height - labelSize.height) / 2.0)), size: labelSize)
}
if let additionalBadgeLabel = item.additionalBadgeLabel {
let additionalLabelNode: ImmediateTextNode
if let current = self.additionalLabelNode {
additionalLabelNode = current
} else {
additionalLabelNode = ImmediateTextNode()
additionalLabelNode.isUserInteractionEnabled = false
self.additionalLabelNode = additionalLabelNode
self.addSubnode(additionalLabelNode)
}
additionalLabelNode.attributedText = NSAttributedString(string: additionalBadgeLabel, font: Font.medium(11.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor)
let additionalLabelSize = additionalLabelNode.updateLayout(CGSize(width: labelConstrainWidth, height: .greatestFiniteMagnitude))
additionalLabelNode.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 10.0, y: floor((height - additionalLabelSize.height) / 2.0) + 1.0), size: additionalLabelSize)
} else if let additionalLabelNode = self.additionalLabelNode {
self.additionalLabelNode = nil
additionalLabelNode.removeFromSupernode()
}
if let additionalLabelBadgeNode = self.additionalLabelBadgeNode, let image = additionalLabelBadgeNode.image {
if item.additionalBadgeLabel != nil, let additionalLabelNode = self.additionalLabelNode {
additionalLabelBadgeNode.frame = additionalLabelNode.frame.insetBy(dx: -4.0, dy: -2.0 + UIScreenPixel)
} else {
let additionalLabelSize = image.size
additionalLabelBadgeNode.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 6.0, y: floor((height - additionalLabelSize.height) / 2.0) + 1.0), size: additionalLabelSize)
}
}
let labelBadgeNodeFrame: CGRect
if case let .image(_, imageSize) = item.label {
labelBadgeNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - imageSize.width, y: floorToScreenPixels(textFrame.midY - imageSize.height / 2.0)), size: imageSize)
} else if case .titleBadge = item.label {
labelBadgeNodeFrame = labelFrame.insetBy(dx: -4.0, dy: -2.0 + UIScreenPixel)
} else if case .labelBadge = item.label {
labelBadgeNodeFrame = labelFrame.insetBy(dx: -4.0, dy: -2.0 + UIScreenPixel)
} else if let additionalLabelNode = self.additionalLabelNode {
labelBadgeNodeFrame = additionalLabelNode.frame.insetBy(dx: -4.0, dy: -2.0 + UIScreenPixel)
} else {
labelBadgeNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - badgeWidth, y: floorToScreenPixels(labelFrame.midY - badgeDiameter / 2.0)), size: CGSize(width: badgeWidth, height: badgeDiameter))
}
self.activateArea.accessibilityLabel = item.text
self.activateArea.accessibilityValue = item.label.text
transition.updateFrame(node: self.labelBadgeNode, frame: labelBadgeNodeFrame)
if self.labelNode.bounds.size != labelFrame.size {
self.labelNode.frame = labelFrame
} else {
transition.updateFrame(node: self.labelNode, frame: labelFrame)
}
transition.updateFrame(node: self.textNode, frame: textFrame)
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
self.bottomSeparatorNode.isHidden = hasBottomCorners
let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
let separatorRightInset: CGFloat = 16.0
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, y: height - UIScreenPixel), size: CGSize(width: width - separatorInset - separatorRightInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
self.activateArea.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
return height
}
}
@@ -0,0 +1,84 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AccountContext
final class PeerInfoScreenHeaderItem: PeerInfoScreenItem {
let id: AnyHashable
let text: String
let label: String?
init(id: AnyHashable, text: String, label: String? = nil) {
self.id = id
self.text = text
self.label = label
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenHeaderItemNode()
}
}
private final class PeerInfoScreenHeaderItemNode: PeerInfoScreenItemNode {
private let textNode: ImmediateTextNode
private let labelNode: ImmediateTextNode
private let activateArea: AccessibilityAreaNode
private var item: PeerInfoScreenHeaderItem?
override init() {
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
self.labelNode = ImmediateTextNode()
self.labelNode.displaysAsynchronously = false
self.labelNode.isUserInteractionEnabled = false
self.activateArea = AccessibilityAreaNode()
self.activateArea.accessibilityTraits = [.staticText, .header]
super.init()
self.addSubnode(self.textNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.activateArea)
}
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenHeaderItem else {
return 10.0
}
self.item = item
let sideInset: CGFloat = 16.0 + safeInsets.left
let verticalInset: CGFloat = 7.0
self.textNode.maximumNumberOfLines = 0
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(13.0), textColor: presentationData.theme.list.freeTextColor)
self.activateArea.accessibilityLabel = item.text
let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: textSize)
self.labelNode.maximumNumberOfLines = 0
self.labelNode.attributedText = NSAttributedString(string: item.label ?? "", font: Font.regular(13.0), textColor: presentationData.theme.list.freeTextColor)
let labelSize = self.labelNode.updateLayout(CGSize(width: max(0.0, width - sideInset * 2.0 - textSize.width - 4.0), height: .greatestFiniteMagnitude))
let labelFrame = CGRect(origin: CGPoint(x: width - sideInset - labelSize.width, y: verticalInset), size: labelSize)
let height = textSize.height + verticalInset * 2.0
transition.updateFrame(node: self.textNode, frame: textFrame)
transition.updateFrame(node: self.labelNode, frame: labelFrame)
self.activateArea.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
return height
}
}
@@ -0,0 +1,126 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListPeerItem
import SwiftSignalKit
import AccountContext
import Postbox
import TelegramCore
import ItemListUI
final class PeerInfoScreenInfoItem: PeerInfoScreenItem {
let id: AnyHashable
let title: String
let text: InfoListItemText
let isWarning: Bool
let linkAction: ((InfoListItemLinkAction) -> Void)?
init(
id: AnyHashable,
title: String,
text: InfoListItemText,
isWarning: Bool = false,
linkAction: ((InfoListItemLinkAction) -> Void)?
) {
self.id = id
self.title = title
self.text = text
self.isWarning = isWarning
self.linkAction = linkAction
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenInfoItemNode()
}
}
private final class PeerInfoScreenInfoItemNode: PeerInfoScreenItemNode {
private let bottomSeparatorNode: ASDisplayNode
private let maskNode: ASImageNode
private var item: PeerInfoScreenInfoItem?
private var itemNode: InfoItemNode?
override init() {
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.bottomSeparatorNode)
}
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenInfoItem else {
return 10.0
}
self.item = item
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let infoItem = InfoListItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, title: item.title, text: item.text, style: .blocks, hasDecorations: false, isWarning: item.isWarning, linkAction: { link in
item.linkAction?(link)
}, closeAction: nil)
let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0)
let itemNode: InfoItemNode
if let current = self.itemNode {
itemNode = current
infoItem.updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
apply(ListViewItemApply(isOnScreen: true))
})
} else {
var itemNodeValue: ListViewItemNode?
infoItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in
itemNodeValue = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode = itemNodeValue as! InfoItemNode
self.itemNode = itemNode
self.addSubnode(itemNode)
}
let height = itemNode.contentSize.height
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size))
var separatorInset: CGFloat = sideInset
if bottomItem != nil {
separatorInset += 49.0
}
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
self.bottomSeparatorNode.isHidden = hasBottomCorners
if self.maskNode.supernode == nil {
self.addSubnode(self.maskNode)
}
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
return height
}
}
@@ -0,0 +1,289 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListPeerItem
import SwiftSignalKit
import AccountContext
import Postbox
import TelegramCore
import ItemListUI
enum PeerInfoScreenMemberItemAction {
case open
case promote
case restrict
case remove
}
final class PeerInfoScreenMemberItem: PeerInfoScreenItem {
let id: AnyHashable
let context: ItemListPeerItem.Context
let enclosingPeer: Peer?
let member: PeerInfoMember
let badge: String?
let isAccount: Bool
let action: ((PeerInfoScreenMemberItemAction) -> Void)?
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
let openStories: ((UIView) -> Void)?
init(
id: AnyHashable,
context: ItemListPeerItem.Context,
enclosingPeer: Peer?,
member: PeerInfoMember,
badge: String? = nil,
isAccount: Bool,
action: ((PeerInfoScreenMemberItemAction) -> Void)?,
contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil,
openStories: ((UIView) -> Void)? = nil
) {
self.id = id
self.context = context
self.enclosingPeer = enclosingPeer
self.member = member
self.badge = badge
self.isAccount = isAccount
self.action = action
self.contextAction = contextAction
self.openStories = openStories
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenMemberItemNode()
}
}
private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode {
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
private let maskNode: ASImageNode
private let bottomSeparatorNode: ASDisplayNode
private var item: PeerInfoScreenMemberItem?
private var itemNode: ItemListPeerItemNode?
override init() {
var bringToFrontForHighlightImpl: (() -> Void)?
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
self.selectionNode.isUserInteractionEnabled = false
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
super.init()
bringToFrontForHighlightImpl = { [weak self] in
self?.bringToFrontForHighlight?()
}
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.selectionNode)
}
override func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { point in
return .keepWithSingleTap
}
recognizer.highlight = { [weak self] point in
guard let strongSelf = self else {
return
}
strongSelf.updateTouchesAtPoint(point)
}
self.view.addGestureRecognizer(recognizer)
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
if let item = self.item {
item.action?(.open)
}
default:
break
}
}
default:
break
}
}
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenMemberItem else {
return 10.0
}
self.item = item
self.selectionNode.pressed = item.action.flatMap { action in
return {
action(.open)
}
}
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let label: String?
if let rank = item.member.rank {
label = rank
} else {
switch item.member.role {
case .creator:
label = presentationData.strings.GroupInfo_LabelOwner
case .admin:
label = presentationData.strings.GroupInfo_LabelAdmin
case .member:
label = nil
}
}
let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.accountPeerId, peer: item.enclosingPeer, member: item.member)
var options: [ItemListPeerItemRevealOption] = []
if actions.contains(.promote) && item.enclosingPeer is TelegramChannel {
options.append(ItemListPeerItemRevealOption(type: .neutral, title: presentationData.strings.GroupInfo_ActionPromote, action: {
item.action?(.promote)
}))
}
if actions.contains(.restrict) {
if item.enclosingPeer is TelegramChannel {
options.append(ItemListPeerItemRevealOption(type: .warning, title: presentationData.strings.GroupInfo_ActionRestrict, action: {
item.action?(.restrict)
}))
}
options.append(ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Common_Delete, action: {
item.action?(.remove)
}))
}
if actions.contains(.logout) {
options.append(ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Settings_Context_Logout, action: {
item.action?(.remove)
}))
}
let itemLabel: ItemListPeerItemLabel
if let label = label {
itemLabel = .text(label, .standard)
} else if let badge = item.badge {
itemLabel = .badge(badge)
} else {
itemLabel = .none
}
let itemHeight: ItemListPeerItemHeight
let itemText: ItemListPeerItemText
var synchronousLoads = false
if case .account = item.member {
itemHeight = .generic
itemText = .none
synchronousLoads = true
} else {
itemHeight = .peerList
itemText = .presence
}
let peerItem = ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: item.context, peer: EnginePeer(item.member.peer), height: itemHeight, presence: item.member.presence.flatMap(EnginePeer.Presence.init), text: itemText, label: itemLabel, editing: ItemListPeerItemEditing(editable: !options.isEmpty, editing: false, revealed: nil), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: false, animateFirstAvatarTransition: !item.isAccount, sectionId: 0, action: nil, setPeerIdWithRevealedOptions: { lhs, rhs in
}, removePeer: { _ in
}, contextAction: item.contextAction, hasTopStripe: false, hasTopGroupInset: false, noInsets: true, noCorners: true, displayDecorations: false, storyStats: item.member.storyStats, openStories: { [weak self] sourceView in
guard let self, let item = self.item else {
return
}
item.openStories?(sourceView)
})
let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0)
let itemNode: ItemListPeerItemNode
if let current = self.itemNode {
itemNode = current
peerItem.updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
apply(ListViewItemApply(isOnScreen: true))
})
} else {
var itemNodeValue: ListViewItemNode?
peerItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: synchronousLoads, previousItem: nil, nextItem: nil, completion: { node, apply in
itemNodeValue = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode = itemNodeValue as! ItemListPeerItemNode
self.itemNode = itemNode
self.addSubnode(itemNode)
}
itemNode.visibility = .visible(1.0, .infinite)
let height = itemNode.contentSize.height
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size))
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
self.bottomSeparatorNode.isHidden = hasBottomCorners
if self.maskNode.supernode == nil {
self.addSubnode(self.maskNode)
}
let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
var separatorInset: CGFloat = sideInset
if bottomItem != nil {
separatorInset += 49.0
}
let separatorRightInset: CGFloat = 16.0
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, y: height - UIScreenPixel), size: CGSize(width: width - separatorInset - separatorRightInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
return height
}
private func updateTouchesAtPoint(_ point: CGPoint?) {
guard let item = self.item else {
return
}
var highlight = point != nil
if case .account = item.member {
} else if item.context.accountPeerId == item.member.id {
highlight = false
}
if let point, let itemNode = self.itemNode, let value = itemNode.view.hitTest(self.view.convert(point, to: itemNode.view), with: nil), value is UIControl {
highlight = false
}
if highlight {
self.selectionNode.updateIsHighlighted(true)
} else {
self.selectionNode.updateIsHighlighted(false)
}
}
}
@@ -0,0 +1,127 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import TelegramCore
import AccountContext
import ComponentFlow
import TextFieldComponent
final class PeerInfoScreenNoteListItem: PeerInfoScreenItem {
let id: AnyHashable
let initialValue: NSAttributedString
let valueUpdated: (NSAttributedString) -> Void
let requestLayout: (Bool) -> Void
init(
id: AnyHashable,
initialValue: NSAttributedString,
valueUpdated: @escaping (NSAttributedString) -> Void,
requestLayout: @escaping (Bool) -> Void
) {
self.id = id
self.initialValue = initialValue
self.valueUpdated = valueUpdated
self.requestLayout = requestLayout
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenNoteListItemNode()
}
}
final class PeerInfoScreenNoteListItemNode: PeerInfoScreenItemNode {
private let maskNode: ASImageNode
private let textField = ComponentView<Empty>()
private let textFieldExternalState = TextFieldComponent.ExternalState()
private let state = EmptyComponentState()
private let bottomSeparatorNode: ASDisplayNode
private var item: PeerInfoScreenNoteListItem?
private var presentationData: PresentationData?
private var theme: PresentationTheme?
override init() {
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
super.init()
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.maskNode)
}
func focus() {
if let textView = self.textField.view as? TextFieldComponent.View {
textView.activateInput()
}
}
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenNoteListItem else {
return 10.0
}
var resetText: NSAttributedString?
if self.item?.initialValue != item.initialValue {
resetText = item.initialValue
}
self.item = item
self.presentationData = presentationData
self.theme = presentationData.theme
let sideInset: CGFloat = 1.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
self.state._updated = { [weak self] transition, _ in
guard let self else {
return
}
self.item?.requestLayout(!transition.animation.isImmediate)
}
var characterLimit: Int = 128
if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["contact_note_length_limit"] as? Double {
characterLimit = Int(value)
}
self.textField.parentState = self.state
let textFieldSize = self.textField.update(
transition: .immediate,
component: AnyComponent(TextFieldComponent(context: context, theme: presentationData.theme, strings: presentationData.strings, externalState: self.textFieldExternalState, fontSize: 17.0, textColor: presentationData.theme.list.itemPrimaryTextColor, accentColor: presentationData.theme.list.itemAccentColor, insets: UIEdgeInsets(top: 9.0, left: 8.0, bottom: 10.0, right: 8.0), hideKeyboard: false, customInputView: nil, placeholder: NSAttributedString(string: presentationData.strings.PeerInfo_AddNotesPlaceholder, font: Font.regular(17.0), textColor: presentationData.theme.list.itemPlaceholderTextColor), resetText: resetText, isOneLineWhenUnfocused: false, characterLimit: characterLimit, formatMenuAvailability: .available([.bold, .italic, .underline, .strikethrough, .spoiler]), lockedFormatAction: {}, present: { c in }, paste: { _ in })),
environment: {},
containerSize: CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)
)
let textFieldFrame = CGRect(origin: CGPoint(x: sideInset, y: 7.0), size: textFieldSize)
let height: CGFloat = 12.0 + textFieldSize.height
if let textFieldView = self.textField.view {
if textFieldView.superview == nil {
self.view.addSubview(textFieldView)
}
transition.updateFrame(view: textFieldView, frame: textFieldFrame)
}
item.valueUpdated(self.textFieldExternalState.text)
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
self.bottomSeparatorNode.isHidden = hasBottomCorners
return height
}
}
@@ -0,0 +1,755 @@
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AccountContext
import TextFormat
import UIKit
import AppBundle
import TelegramStringFormatting
import ContextUI
import TelegramCore
import ChatListUI
import Postbox
import StoryContainerScreen
import AvatarNode
final class PeerInfoScreenPersonalChannelItem: PeerInfoScreenItem {
let id: AnyHashable
let context: AccountContext
let data: PeerInfoPersonalChannelData
let controller: () -> ViewController?
let action: () -> Void
init(
id: AnyHashable,
context: AccountContext,
data: PeerInfoPersonalChannelData,
controller: @escaping () -> ViewController?,
action: @escaping () -> Void
) {
self.id = id
self.context = context
self.data = data
self.controller = controller
self.action = action
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenPersonalChannelItemNode()
}
}
private final class LoadingOverlayShimmerNode: ASDisplayNode {
private var currentBackgroundColor: UIColor?
private var currentForegroundColor: UIColor?
private let imageNodeContainer: ASDisplayNode
private let imageNode: ASImageNode
private var absoluteLocation: (CGRect, CGSize)?
private var isCurrentlyInHierarchy = false
private var shouldBeAnimating = false
override init() {
self.imageNodeContainer = ASDisplayNode()
self.imageNodeContainer.isLayerBacked = true
self.imageNode = ASImageNode()
self.imageNode.isLayerBacked = true
self.imageNode.displaysAsynchronously = false
self.imageNode.displayWithoutProcessing = true
self.imageNode.contentMode = .scaleToFill
super.init()
self.imageNodeContainer.addSubnode(self.imageNode)
self.addSubnode(self.imageNodeContainer)
}
override func didEnterHierarchy() {
super.didEnterHierarchy()
self.isCurrentlyInHierarchy = true
self.updateAnimation()
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.isCurrentlyInHierarchy = false
self.updateAnimation()
}
func update(backgroundColor: UIColor, foregroundColor: UIColor) {
if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) {
return
}
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
self.backgroundColor = backgroundColor
self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.clip(to: CGRect(origin: CGPoint(), size: size))
let transparentColor = backgroundColor.cgColor
let peakColor = foregroundColor.cgColor
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
}
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize {
return
}
let sizeUpdated = self.absoluteLocation?.1 != containerSize
let frameUpdated = self.absoluteLocation?.0 != rect
self.absoluteLocation = (rect, containerSize)
if sizeUpdated {
if self.shouldBeAnimating {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
self.addImageAnimation()
}
}
if frameUpdated {
self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize)
}
self.updateAnimation()
}
private func updateAnimation() {
let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil
if shouldBeAnimating != self.shouldBeAnimating {
self.shouldBeAnimating = shouldBeAnimating
if shouldBeAnimating {
self.addImageAnimation()
} else {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
}
}
}
private func addImageAnimation() {
guard let containerSize = self.absoluteLocation?.1 else {
return
}
let gradientHeight: CGFloat = 250.0
self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight))
let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
animation.repeatCount = Float.infinity
animation.beginTime = 1.0
self.imageNode.layer.add(animation, forKey: "shimmer")
}
}
public final class LoadingOverlayNode: ASDisplayNode {
private let effectNode: LoadingOverlayShimmerNode
private let maskNode: ASImageNode
private var currentParams: (size: CGSize, presentationData: PresentationData)?
override public init() {
self.effectNode = LoadingOverlayShimmerNode()
self.maskNode = ASImageNode()
super.init()
self.isUserInteractionEnabled = false
self.addSubnode(self.effectNode)
self.effectNode.view.mask = self.maskNode.view
}
public func update(context: AccountContext, size: CGSize, isInlineMode: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData {
self.currentParams = (size, presentationData)
let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let timestamp1: Int32 = 100000
let peers: [EnginePeer.Id: EnginePeer] = [:]
let interaction = ChatListNodeInteraction(context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
}, openPhotoSetup: {
}, openAdInfo: { _, _ in
}, openAccountFreezeInfo: {
}, openUrl: { _ in
})
let items = (0 ..< 1).map { _ -> ChatListItem in
let message = EngineMessage(
stableId: 0,
stableVersion: 0,
id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: timestamp1,
flags: [],
tags: [],
globalTags: [],
localTags: [],
customTags: [],
forwardInfo: nil,
author: peer1,
text: "Text",
attributes: [],
media: [],
peers: peers,
associatedMessages: [:],
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
let readState = EnginePeerReadCounters()
return ChatListItem(presentationData: chatListPresentationData, context: context, chatListLocation: .chatList(groupId: .root), filterData: nil, index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1))), content: .peer(ChatListItemContent.PeerData(
messages: [message],
peer: EngineRenderedPeer(peer: peer1),
threadInfo: nil,
combinedReadState: readState,
isRemovedFromTotalUnreadCount: false,
presence: nil,
hasUnseenMentions: false,
hasUnseenReactions: false,
draftState: nil,
mediaDraftContentType: nil,
inputActivities: nil,
promoInfo: nil,
ignoreUnreadBadge: false,
displayAsMessage: false,
hasFailedMessages: false,
forumTopicData: nil,
topForumTopicItems: [],
autoremoveTimeout: nil,
storyState: nil,
requiresPremiumForMessaging: false,
displayAsTopicList: false,
tags: []
)), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enabledContextActions: nil, hiddenOffset: false, interaction: interaction)
}
var itemNodes: [ChatListItemNode] = []
for i in 0 ..< items.count {
items[i].nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 100.0), synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: (i == items.count - 1) ? nil : items[i + 1], completion: { node, apply in
if let itemNode = node as? ChatListItemNode {
itemNodes.append(itemNode)
}
apply().1(ListViewItemApply(isOnScreen: true))
})
}
self.maskNode.image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
var currentY: CGFloat = 0.0
let fakeLabelPlaceholderHeight: CGFloat = 8.0
func fillLabelPlaceholderRect(origin: CGPoint, width: CGFloat) {
let startPoint = origin
let diameter = fakeLabelPlaceholderHeight
context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter)))
context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter)))
}
while currentY < size.height {
let sampleIndex = 0
let itemHeight: CGFloat = itemNodes[sampleIndex].contentSize.height
context.setFillColor(UIColor.black.cgColor)
let textFrame = itemNodes[sampleIndex].textNode.textNode.frame.offsetBy(dx: 0.0, dy: currentY)
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + itemHeight - floor(itemNodes[sampleIndex].titleNode.frame.midY - fakeLabelPlaceholderHeight / 2.0) - fakeLabelPlaceholderHeight), width: 60.0)
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 120.0)
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX + 120.0 + 10.0, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 60.0)
let dateFrame = itemNodes[sampleIndex].dateNode.frame.offsetBy(dx: 0.0, dy: currentY)
fillLabelPlaceholderRect(origin: CGPoint(x: dateFrame.maxX - 30.0, y: dateFrame.minY + 4.0), width: 30.0)
currentY += itemHeight
}
})
self.effectNode.update(backgroundColor: presentationData.theme.list.mediaPlaceholderColor, foregroundColor: presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4))
self.effectNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size)
}
transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
}
}
private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNode {
private let containerNode: ContextControllerSourceNode
private let contextSourceNode: ContextExtractedContentContainingNode
private let extractedBackgroundImageNode: ASImageNode
private var extractedRect: CGRect?
private var nonExtractedRect: CGRect?
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
private let maskNode: ASImageNode
private let bottomSeparatorNode: ASDisplayNode
private let activateArea: AccessibilityAreaNode
private var item: PeerInfoScreenPersonalChannelItem?
private var presentationData: PresentationData?
private var theme: PresentationTheme?
private var itemNode: ListViewItemNode?
private var loadingOverlayNode: LoadingOverlayNode?
override init() {
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.extractedBackgroundImageNode = ASImageNode()
self.extractedBackgroundImageNode.displaysAsynchronously = false
self.extractedBackgroundImageNode.alpha = 0.0
var bringToFrontForHighlightImpl: (() -> Void)?
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
self.selectionNode.isUserInteractionEnabled = false
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init()
bringToFrontForHighlightImpl = { [weak self] in
self?.bringToFrontForHighlight?()
}
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.selectionNode)
self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.addSubnode(self.containerNode)
self.addSubnode(self.maskNode)
self.contextSourceNode.contentNode.clipsToBounds = true
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
self.addSubnode(self.activateArea)
self.containerNode.isGestureEnabled = false
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let strongSelf = self, let theme = strongSelf.theme else {
return
}
if isExtracted {
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 52.0, color: theme.list.plainBackgroundColor)
}
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
let rect = isExtracted ? extractedRect : nonExtractedRect
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
}
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
if !isExtracted {
self?.extractedBackgroundImageNode.image = nil
}
})
}
}
override func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
recognizer.highlight = { [weak self] point in
guard let strongSelf = self else {
return
}
strongSelf.updateTouchesAtPoint(point)
}
self.view.addGestureRecognizer(recognizer)
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
self.item?.action()
case .longTap:
break
default:
break
}
}
default:
break
}
}
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenPersonalChannelItem else {
return 50.0
}
self.item = item
self.presentationData = presentationData
self.theme = presentationData.theme
self.selectionNode.pressed = { [weak self] in
if let strongSelf = self {
strongSelf.item?.action()
}
}
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let chatListPresentationData = ChatListPresentationData(
theme: presentationData.theme,
fontSize: presentationData.listsFontSize,
strings: presentationData.strings,
dateTimeFormat: presentationData.dateTimeFormat,
nameSortOrder: presentationData.nameSortOrder,
nameDisplayOrder: presentationData.nameDisplayOrder,
disableAnimations: false
)
let chatListNodeInteraction = ChatListNodeInteraction(
context: item.context,
animationCache: item.context.animationCache,
animationRenderer: item.context.animationRenderer,
activateSearch: {
},
peerSelected: { _, _, _, _, _ in
},
disabledPeerSelected: { _, _, _ in
},
togglePeerSelected: { _, _ in
},
togglePeersSelection: { _, _ in
},
additionalCategorySelected: { _ in
},
messageSelected: { _, _, _, _ in
},
groupSelected: { _ in
},
addContact: { _ in
},
setPeerIdWithRevealedOptions: { _, _ in
},
setItemPinned: { _, _ in
},
setPeerMuted: { _, _ in
},
setPeerThreadMuted: { _, _, _ in
},
deletePeer: { _, _ in
},
deletePeerThread: { _, _ in
},
setPeerThreadStopped: { _, _, _ in
},
setPeerThreadPinned: { _, _, _ in
},
setPeerThreadHidden: { _, _, _ in
},
updatePeerGrouping: { _, _ in
},
togglePeerMarkedUnread: { _, _ in
},
toggleArchivedFolderHiddenByDefault: {
},
toggleThreadsSelection: { _, _ in
},
hidePsa: { _ in
},
activateChatPreview: nil,
present: { _ in
},
openForumThread: { _, _ in
},
openStorageManagement: {
},
openPasswordSetup: {
},
openPremiumIntro: {
},
openPremiumGift: { _, _ in
},
openPremiumManagement: {
},
openActiveSessions: {
},
openBirthdaySetup: {
},
performActiveSessionAction: { _, _ in
},
openChatFolderUpdates: {
},
hideChatFolderUpdates: {
},
openStories: { [weak self] _, sourceNode in
guard let self, let item = self.item else {
return
}
guard let itemNode = self.itemNode as? ChatListItemNode else {
return
}
guard let controller = item.controller() else {
return
}
StoryContainerScreen.openPeerStories(context: item.context, peerId: item.data.peer.peerId, parentController: controller, avatarNode: itemNode.avatarNode)
},
openStarsTopup: { _ in
},
dismissNotice: { _ in
},
editPeer: { _ in
},
openWebApp: { _ in
},
openPhotoSetup: {
},
openAdInfo: { _, _ in
},
openAccountFreezeInfo: {
},
openUrl: { _ in
}
)
let index: EngineChatList.Item.Index
let messages: [EngineMessage]
let isLoading = item.data.isLoading
if !isLoading, !item.data.topMessages.isEmpty {
index = EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: item.data.topMessages[0].index))
messages = item.data.topMessages
} else {
index = EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: item.data.peer.peerId, namespace: Namespaces.Message.Cloud, id: 1), timestamp: 0)))
messages = []
}
let chatListItem = ChatListItem(
presentationData: chatListPresentationData,
context: item.context,
chatListLocation: .chatList(groupId: .root),
filterData: nil,
index: index,
content: .peer(ChatListItemContent.PeerData(
messages: messages,
peer: item.data.peer,
threadInfo: nil,
combinedReadState: nil,
isRemovedFromTotalUnreadCount: false,
presence: nil,
hasUnseenMentions: false,
hasUnseenReactions: false,
draftState: nil,
mediaDraftContentType: nil,
inputActivities: nil,
promoInfo: nil,
ignoreUnreadBadge: false,
displayAsMessage: false,
hasFailedMessages: false,
forumTopicData: nil,
topForumTopicItems: [],
autoremoveTimeout: nil,
storyState: item.data.storyStats.flatMap { storyStats in
return ChatListItemContent.StoryState(
stats: storyStats,
hasUnseenCloseFriends: false
)
},
requiresPremiumForMessaging: false,
displayAsTopicList: false,
tags: [],
customMessageListData: ChatListItemContent.CustomMessageListData(
commandPrefix: nil,
searchQuery: nil,
messageCount: nil,
hideSeparator: true,
hideDate: isLoading,
hidePeerStatus: false
)
)),
editing: false,
hasActiveRevealControls: false,
selected: false,
header: nil,
enabledContextActions: nil,
hiddenOffset: false,
interaction: chatListNodeInteraction
)
var itemNode: ListViewItemNode?
let params = ListViewItemLayoutParams(width: width - safeInsets.left - safeInsets.right - 4.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 1000.0)
if let current = self.itemNode {
itemNode = current
chatListItem.updateNode(
async: { f in f () },
node: {
return current
},
params: params,
previousItem: nil,
nextItem: nil, animation: .None,
completion: { layout, apply in
let nodeFrame = CGRect(origin: current.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height))
current.contentSize = layout.contentSize
current.insets = layout.insets
current.frame = nodeFrame
apply(ListViewItemApply(isOnScreen: true))
})
} else {
var outItemNode: ListViewItemNode?
chatListItem.nodeConfiguredForParams(
async: { f in f() },
params: params,
synchronousLoads: true,
previousItem: nil,
nextItem: nil,
completion: { node, apply in
outItemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
}
)
itemNode = outItemNode
}
var height = itemNode?.contentSize.height ?? 50.0
if self.itemNode !== itemNode {
self.itemNode?.removeFromSupernode()
self.itemNode = itemNode
if let itemNode {
itemNode.isUserInteractionEnabled = false
self.contextSourceNode.contentNode.addSubnode(itemNode)
}
}
let itemFrame = CGRect(origin: CGPoint(x: safeInsets.left, y: 4.0), size: CGSize(width: width - safeInsets.left - safeInsets.right - 4.0, height: height))
if let itemNode = self.itemNode {
itemNode.frame = itemFrame
}
if let itemNode = self.itemNode, item.data.isLoading {
let loadingOverlayNode: LoadingOverlayNode
if let current = self.loadingOverlayNode {
loadingOverlayNode = current
} else {
loadingOverlayNode = LoadingOverlayNode()
self.loadingOverlayNode = loadingOverlayNode
itemNode.supernode?.insertSubnode(loadingOverlayNode, aboveSubnode: itemNode)
}
loadingOverlayNode.frame = itemFrame
loadingOverlayNode.update(
context: item.context,
size: itemFrame.size,
isInlineMode: false,
presentationData: presentationData,
transition: .immediate
)
} else {
if let loadingOverlayNode = self.loadingOverlayNode {
self.loadingOverlayNode = nil
loadingOverlayNode.removeFromSupernode()
}
}
height += 8.0
let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
self.bottomSeparatorNode.isHidden = hasBottomCorners
self.activateArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: height))
let contentSize = CGSize(width: width, height: height)
self.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: contentSize)
transition.updateFrame(node: self.contextSourceNode.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize))
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: contentSize.width, height: contentSize.height))
let extractedRect = nonExtractedRect
self.extractedRect = extractedRect
self.nonExtractedRect = nonExtractedRect
if self.contextSourceNode.isExtractedToContextPreview {
self.extractedBackgroundImageNode.frame = extractedRect
} else {
self.extractedBackgroundImageNode.frame = nonExtractedRect
}
self.contextSourceNode.contentRect = extractedRect
return height
}
private func updateTouchesAtPoint(_ point: CGPoint?) {
var isHighlighted = false
if let point, let itemNode = self.itemNode as? ChatListItemNode {
if !itemNode.avatarNode.view.convert(itemNode.avatarNode.view.bounds, to: self.view).contains(point) {
isHighlighted = true
} else if let item = self.item, item.data.storyStats == nil {
isHighlighted = true
}
}
if isHighlighted {
self.selectionNode.updateIsHighlighted(true)
} else {
self.selectionNode.updateIsHighlighted(false)
}
}
}
@@ -0,0 +1,65 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
final class PeerInfoScreenSelectableBackgroundNode: ASDisplayNode {
private let backgroundNode: ASDisplayNode
private let button: HighlightTrackingButton
let bringToFrontForHighlight: () -> Void
private var isHighlighted: Bool = false
var pressed: (() -> Void)? {
didSet {
self.button.isUserInteractionEnabled = self.pressed != nil
}
}
init(bringToFrontForHighlight: @escaping () -> Void) {
self.bringToFrontForHighlight = bringToFrontForHighlight
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.alpha = 0.0
self.button = HighlightTrackingButton()
self.button.isAccessibilityElement = false
super.init()
self.addSubnode(self.backgroundNode)
self.view.addSubview(self.button)
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
self.button.highligthedChanged = { [weak self] highlighted in
self?.updateIsHighlighted(highlighted)
}
}
@objc private func buttonPressed() {
self.pressed?()
}
func updateIsHighlighted(_ isHighlighted: Bool) {
if self.isHighlighted != isHighlighted {
self.isHighlighted = isHighlighted
if isHighlighted {
self.bringToFrontForHighlight()
self.backgroundNode.layer.removeAnimation(forKey: "opacity")
self.backgroundNode.alpha = 1.0
} else {
self.backgroundNode.alpha = 0.0
self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
}
}
}
func update(size: CGSize, theme: PresentationTheme, transition: ContainedViewLayoutTransition) {
self.backgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrame(view: self.button, frame: CGRect(origin: CGPoint(), size: size))
}
}
@@ -0,0 +1,231 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AppBundle
import AccountContext
final class PeerInfoScreenSwitchItem: PeerInfoScreenItem {
let id: AnyHashable
let text: String
let value: Bool
let icon: UIImage?
let isLocked: Bool
let toggled: ((Bool) -> Void)?
init(id: AnyHashable, text: String, value: Bool, icon: UIImage? = nil, isLocked: Bool = false, toggled: ((Bool) -> Void)?) {
self.id = id
self.text = text
self.value = value
self.icon = icon
self.isLocked = isLocked
self.toggled = toggled
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenSwitchItemNode()
}
}
private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode {
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
private let maskNode: ASImageNode
private let iconNode: ASImageNode
private let textNode: ImmediateTextNode
private let switchNode: SwitchNode
private var lockedIconNode: ASImageNode?
private let bottomSeparatorNode: ASDisplayNode
private var lockedButtonNode: HighlightableButtonNode?
private let activateArea: AccessibilityAreaNode
private var item: PeerInfoScreenSwitchItem?
private var theme: PresentationTheme?
override init() {
var bringToFrontForHighlightImpl: (() -> Void)?
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
self.switchNode = SwitchNode()
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init()
bringToFrontForHighlightImpl = { [weak self] in
self?.bringToFrontForHighlight?()
}
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.selectionNode)
self.addSubnode(self.maskNode)
self.addSubnode(self.textNode)
self.addSubnode(self.switchNode)
self.addSubnode(self.activateArea)
self.switchNode.valueUpdated = { [weak self] value in
self?.item?.toggled?(value)
}
self.activateArea.activate = { [weak self] in
guard let strongSelf = self, let item = strongSelf.item else {
return false
}
let value = !strongSelf.switchNode.isOn
item.toggled?(value)
return true
}
}
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenSwitchItem else {
return 10.0
}
let firstTime = self.item == nil
var updateLockedIconImage = false
if item.isLocked {
let lockedIconNode: ASImageNode
if let current = self.lockedIconNode {
lockedIconNode = current
} else {
updateLockedIconImage = true
lockedIconNode = ASImageNode()
self.lockedIconNode = lockedIconNode
self.insertSubnode(lockedIconNode, aboveSubnode: self.switchNode)
}
} else if let lockedIconNode = self.lockedIconNode {
self.lockedIconNode = nil
lockedIconNode.removeFromSupernode()
}
if item.isLocked {
self.switchNode.isUserInteractionEnabled = false
if self.lockedButtonNode == nil {
let lockedButtonNode = HighlightableButtonNode()
self.lockedButtonNode = lockedButtonNode
self.insertSubnode(lockedButtonNode, aboveSubnode: self.switchNode)
lockedButtonNode.addTarget(self, action: #selector(self.lockedButtonPressed), forControlEvents: .touchUpInside)
}
} else {
if let lockedButtonNode = self.lockedButtonNode {
self.lockedButtonNode = nil
lockedButtonNode.removeFromSupernode()
}
self.switchNode.isUserInteractionEnabled = true
}
if self.theme !== presentationData.theme {
self.theme = presentationData.theme
self.switchNode.frameColor = presentationData.theme.list.itemSwitchColors.frameColor
self.switchNode.contentColor = presentationData.theme.list.itemSwitchColors.contentColor
self.switchNode.handleColor = presentationData.theme.list.itemSwitchColors.handleColor
updateLockedIconImage = true
}
if updateLockedIconImage, let lockedIconNode = self.lockedIconNode, let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: presentationData.theme.list.itemSecondaryTextColor) {
lockedIconNode.image = image
}
self.item = item
self.selectionNode.pressed = nil
let sideInset: CGFloat = 16.0 + safeInsets.left
let leftInset = (item.icon == nil ? sideInset : sideInset + 29.0 + 16.0)
let rightInset: CGFloat = 56.0 + safeAreaInsets.right
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let textColorValue: UIColor = presentationData.theme.list.itemPrimaryTextColor
self.textNode.maximumNumberOfLines = 1
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue)
self.activateArea.accessibilityLabel = item.text
self.activateArea.accessibilityValue = item.value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off
self.activateArea.accessibilityHint = presentationData.strings.VoiceOver_Common_SwitchHint
let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - rightInset, height: .greatestFiniteMagnitude))
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: 16.0), size: textSize)
let height = textSize.height + 32.0
if let icon = item.icon {
if self.iconNode.supernode == nil {
self.addSubnode(self.iconNode)
}
self.iconNode.image = icon
let iconFrame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((height - icon.size.height) / 2.0)), size: icon.size)
transition.updateFrame(node: self.iconNode, frame: iconFrame)
} else if self.iconNode.supernode != nil {
self.iconNode.image = nil
self.iconNode.removeFromSupernode()
}
transition.updateFrame(node: self.textNode, frame: textFrame)
if let switchView = self.switchNode.view as? UISwitch {
if self.switchNode.bounds.size.width.isZero {
switchView.sizeToFit()
}
let switchSize = switchView.bounds.size
let switchFrame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0 - safeInsets.right, y: floor((height - switchSize.height) / 2.0)), size: switchSize)
self.switchNode.frame = switchFrame
if switchView.isOn != item.value {
switchView.setOn(item.value, animated: !firstTime)
}
self.lockedButtonNode?.frame = switchFrame
if let lockedIconNode = self.lockedIconNode, let icon = lockedIconNode.image {
lockedIconNode.frame = CGRect(origin: CGPoint(x: switchFrame.minX + 16.0 - UIScreenPixel, y: switchFrame.minY + 8.0), size: icon.size)
}
}
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
self.bottomSeparatorNode.isHidden = hasBottomCorners
let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
let separatorRightInset: CGFloat = 16.0
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset - separatorRightInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
self.activateArea.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
return height
}
@objc private func lockedButtonPressed() {
self.item?.toggled?(self.switchNode.isOn)
}
}
@@ -0,0 +1,249 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import Postbox
import TelegramPresentationData
import AccountContext
import ContextUI
import PhotoResources
import TelegramUIPreferences
import ItemListPeerItem
import MergeLists
import ItemListUI
import ChatControllerInteraction
import PeerInfoVisualMediaPaneNode
import PeerInfoPaneNode
private struct GroupsInCommonListTransaction {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
}
private struct GroupsInCommonListEntry: Comparable, Identifiable {
var index: Int
var peer: Peer
var stableId: PeerId {
return self.peer.id
}
static func ==(lhs: GroupsInCommonListEntry, rhs: GroupsInCommonListEntry) -> Bool {
return lhs.peer.isEqual(rhs.peer)
}
static func <(lhs: GroupsInCommonListEntry, rhs: GroupsInCommonListEntry) -> Bool {
return lhs.index < rhs.index
}
func item(context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem {
let peer = self.peer
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: EnginePeer(self.peer), presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: {
openPeer(peer)
}, setPeerIdWithRevealedOptions: { _, _ in
}, removePeer: { _ in
}, contextAction: { node, gesture in
openPeerContextAction(peer, node, gesture)
}, hasTopStripe: false, noInsets: true, noCorners: true, style: .plain)
}
}
private func preparedTransition(from fromEntries: [GroupsInCommonListEntry], to toEntries: [GroupsInCommonListEntry], context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> GroupsInCommonListTransaction {
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(context: context, presentationData: presentationData, openPeer: openPeer, openPeerContextAction: openPeerContextAction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, openPeer: openPeer, openPeerContextAction: openPeerContextAction), directionHint: nil) }
return GroupsInCommonListTransaction(deletions: deletions, insertions: insertions, updates: updates)
}
final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
private let context: AccountContext
private let peerId: PeerId
private let chatControllerInteraction: ChatControllerInteraction
private let openPeerContextAction: (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void
private let groupsInCommonContext: GroupsInCommonContext
weak var parentController: ViewController?
private let listNode: ListView
private var state: GroupsInCommonState?
private var currentEntries: [GroupsInCommonListEntry] = []
private var enqueuedTransactions: [GroupsInCommonListTransaction] = []
private var currentParams: (size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData)?
private let ready = Promise<Bool>()
private var didSetReady: Bool = false
var isReady: Signal<Bool, NoError> {
return self.ready.get()
}
var status: Signal<PeerInfoStatusData?, NoError> {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
return self.groupsInCommonContext.state
|> map { state in
if let count = state.count {
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_CommonGroupCount(Int32(count)), isActivity: false, key: .groupsInCommon)
} else {
return nil
}
}
}
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
var tabBarOffset: CGFloat {
return 0.0
}
private var disposable: Disposable?
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void, groupsInCommonContext: GroupsInCommonContext) {
self.context = context
self.peerId = peerId
self.chatControllerInteraction = chatControllerInteraction
self.openPeerContextAction = openPeerContextAction
self.groupsInCommonContext = groupsInCommonContext
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.listNode = ListView()
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
super.init()
self.listNode.preloadPages = true
self.addSubnode(self.listNode)
self.disposable = (groupsInCommonContext.state
|> deliverOnMainQueue).startStrict(next: { [weak self] state in
guard let strongSelf = self else {
return
}
strongSelf.state = state
if let (_, _, presentationData) = strongSelf.currentParams {
strongSelf.updatePeers(state: state, presentationData: presentationData)
}
})
self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in
guard let strongSelf = self, let state = strongSelf.state, case .ready(true) = state.dataState else {
return
}
if case let .known(value) = offset, value < 100.0, case .ready(true) = state.dataState {
strongSelf.groupsInCommonContext.loadMore()
}
}
}
deinit {
self.disposable?.dispose()
}
func ensureMessageIsVisible(id: MessageId) {
}
func scrollToTop() -> Bool {
if !self.listNode.scrollToOffsetFromTop(0.0, animated: true) {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
return true
} else {
return false
}
}
func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
let isFirstLayout = self.currentParams == nil
self.currentParams = (size, isScrollingLockedAtTop, presentationData)
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
var scrollToItem: ListViewScrollToItem?
if isScrollingLockedAtTop {
switch self.listNode.visibleContentOffset() {
case let .known(value) where value <= CGFloat.ulpOfOne:
break
default:
scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: duration), directionHint: .Up)
}
}
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
self.listNode.scrollEnabled = !isScrollingLockedAtTop
if isFirstLayout, let state = self.state {
self.updatePeers(state: state, presentationData: presentationData)
}
}
private func updatePeers(state: GroupsInCommonState, presentationData: PresentationData) {
var entries: [GroupsInCommonListEntry] = []
for peer in state.peers {
if let peer = peer.peer {
entries.append(GroupsInCommonListEntry(index: entries.count, peer: peer))
}
}
let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, openPeer: { [weak self] peer in
self?.chatControllerInteraction.openPeer(EnginePeer(peer), .default, nil, .default)
}, openPeerContextAction: { [weak self] peer, node, gesture in
self?.openPeerContextAction(false, peer, node, gesture)
})
self.currentEntries = entries
self.enqueuedTransactions.append(transaction)
self.dequeueTransaction()
}
private func dequeueTransaction() {
guard let _ = self.currentParams, let transaction = self.enqueuedTransactions.first else {
return
}
self.enqueuedTransactions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.Synchronous)
self.listNode.transaction(deleteIndices: transaction.deletions, insertIndicesAndItems: transaction.insertions, updateIndicesAndItems: transaction.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf.ready.set(.single(true))
}
})
}
func findLoadedMessage(id: MessageId) -> Message? {
return nil
}
func updateHiddenMedia() {
}
func transferVelocity(_ velocity: CGFloat) {
if velocity > 0.0 {
self.listNode.transferVelocity(velocity)
}
}
func cancelPreviewGestures() {
}
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return nil
}
func addToTransitionSurface(view: UIView) {
}
func updateSelectedMessages(animated: Bool) {
}
}
@@ -0,0 +1,528 @@
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import Postbox
import TelegramPresentationData
import PresentationDataUtils
import AccountContext
import ContextUI
import PhotoResources
import TelegramUIPreferences
import UniversalMediaPlayer
import TelegramBaseController
import OverlayStatusController
import ListMessageItem
import UndoUI
import ChatPresentationInterfaceState
import ChatControllerInteraction
import PeerInfoVisualMediaPaneNode
import ChatMessageItemView
import PeerInfoPaneNode
final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
private let context: AccountContext
private let peerId: PeerId
private let chatLocation: ChatLocation
private let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>
private let chatControllerInteraction: ChatControllerInteraction
weak var parentController: ViewController?
private let listNode: ChatHistoryListNode
private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData)?
private let ready = Promise<Bool>()
private var didSetReady: Bool = false
var isReady: Signal<Bool, NoError> {
return self.ready.get()
}
private let selectedMessagesPromise = Promise<Set<MessageId>?>(nil)
private var selectedMessages: Set<MessageId>? {
didSet {
if self.selectedMessages != oldValue {
self.selectedMessagesPromise.set(.single(self.selectedMessages))
}
}
}
private var hiddenMediaDisposable: Disposable?
private var mediaStatusDisposable: Disposable?
private var playlistPreloadDisposable: Disposable?
private var playlistStateAndType: (SharedMediaPlaylistItem, SharedMediaPlaylistItem?, SharedMediaPlaylistItem?, MusicPlaybackSettingsOrder, MediaManagerPlayerType, Account)?
private var playlistLocation: SharedMediaPlaylistLocation?
private var mediaAccessoryPanelContainer: PassthroughContainerNode
private var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)?
private var dismissingPanel: ASDisplayNode?
private let statusPromise = Promise<PeerInfoStatusData?>(nil)
var status: Signal<PeerInfoStatusData?, NoError> {
self.statusPromise.get()
}
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
var tabBarOffset: CGFloat {
return 0.0
}
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, tagMask: MessageTags) {
self.context = context
self.peerId = peerId
self.chatLocation = chatLocation
self.chatLocationContextHolder = chatLocationContextHolder
self.chatControllerInteraction = chatControllerInteraction
self.selectedMessages = chatControllerInteraction.selectionState.flatMap { $0.selectedIds }
self.selectedMessagesPromise.set(.single(self.selectedMessages))
self.listNode = context.sharedContext.makeChatHistoryListNode(context: context, updatedPresentationData: updatedPresentationData ?? (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tag: .tag(tagMask), source: .default, subject: nil, controllerInteraction: chatControllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), mode: .list(search: false, reversed: false, reverseGroups: false, displayHeaders: .allButLast, hintLinks: tagMask == .webPage, isGlobalSearch: false))
self.listNode.clipsToBounds = true
self.listNode.defaultToSynchronousTransactionWhileScrolling = true
self.listNode.scroller.bounces = false
self.mediaAccessoryPanelContainer = PassthroughContainerNode()
self.mediaAccessoryPanelContainer.clipsToBounds = true
super.init()
self.listNode.preloadPages = true
self.addSubnode(self.listNode)
self.addSubnode(self.mediaAccessoryPanelContainer)
self.ready.set(self.listNode.historyState.get()
|> take(1)
|> map { _ -> Bool in true })
if [.file, .music, .voiceOrInstantVideo].contains(tagMask) {
self.mediaStatusDisposable = (context.sharedContext.mediaManager.globalMediaPlayerState
|> mapToSignal { playlistStateAndType -> Signal<(Account, SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)?, NoError> in
if let (account, state, type) = playlistStateAndType {
switch state {
case let .state(state):
if let playlistId = state.playlistId as? PeerMessagesMediaPlaylistId, case .peer(peerId) = playlistId {
switch type {
case .voice:
if tagMask != .voiceOrInstantVideo {
return .single(nil) |> delay(0.2, queue: .mainQueue())
}
case .music:
if tagMask != .music {
return .single(nil) |> delay(0.2, queue: .mainQueue())
}
case .file:
if tagMask != .file {
return .single(nil) |> delay(0.2, queue: .mainQueue())
}
}
return .single((account, state, type))
} else {
return .single(nil) |> delay(0.2, queue: .mainQueue())
}
case .loading:
return .single(nil) |> delay(0.2, queue: .mainQueue())
}
} else {
return .single(nil)
}
}
|> deliverOnMainQueue).startStrict(next: { [weak self] playlistStateAndType in
guard let strongSelf = self else {
return
}
if !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.0, playlistStateAndType?.1.item) ||
!arePlaylistItemsEqual(strongSelf.playlistStateAndType?.1, playlistStateAndType?.1.previousItem) ||
!arePlaylistItemsEqual(strongSelf.playlistStateAndType?.2, playlistStateAndType?.1.nextItem) ||
strongSelf.playlistStateAndType?.3 != playlistStateAndType?.1.order || strongSelf.playlistStateAndType?.4 != playlistStateAndType?.2 {
if let playlistStateAndType = playlistStateAndType {
strongSelf.playlistStateAndType = (playlistStateAndType.1.item, playlistStateAndType.1.previousItem, playlistStateAndType.1.nextItem, playlistStateAndType.1.order, playlistStateAndType.2, playlistStateAndType.0)
strongSelf.playlistLocation = playlistStateAndType.1.playlistLocation
} else {
strongSelf.playlistStateAndType = nil
strongSelf.playlistLocation = nil
}
if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = strongSelf.currentParams {
strongSelf.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: true, transition: .animated(duration: 0.4, curve: .spring))
}
}
})
}
self.statusPromise.set(context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: peerId, threadId: chatLocation.threadId, tag: tagMask)
)
|> map { count -> PeerInfoStatusData? in
let count: Int = count ?? 0
if count == 0 {
return nil
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
switch tagMask {
case MessageTags.file:
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_FileCount(Int32(count)), isActivity: false, key: .files)
case MessageTags.music:
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_MusicCount(Int32(count)), isActivity: false, key: .music)
case MessageTags.voiceOrInstantVideo:
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_VoiceMessageCount(Int32(count)), isActivity: false, key: .voice)
case MessageTags.webPage:
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_LinkCount(Int32(count)), isActivity: false, key: .links)
default:
return nil
}
})
}
deinit {
self.hiddenMediaDisposable?.dispose()
self.mediaStatusDisposable?.dispose()
self.playlistPreloadDisposable?.dispose()
}
func ensureMessageIsVisible(id: MessageId) {
}
func scrollToTop() -> Bool {
let offset = self.listNode.visibleContentOffset()
switch offset {
case let .known(value) where value <= CGFloat.ulpOfOne:
return false
default:
self.listNode.scrollToEndOfHistory()
return true
}
}
func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData)
var topPanelHeight: CGFloat = 0.0
if let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType {
let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight
topPanelHeight = floor(panelHeight * expandProgress)
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - panelHeight), size: CGSize(width: size.width, height: panelHeight))
if let (mediaAccessoryPanel, mediaType) = self.mediaAccessoryPanel, mediaType == type {
transition.updateFrame(layer: mediaAccessoryPanel.layer, frame: panelFrame)
mediaAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: sideInset, rightInset: sideInset, isHidden: false, transition: transition)
switch order {
case .regular:
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, previousItem, nextItem)
case .reversed:
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nextItem, previousItem)
case .random:
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nil, nil)
}
let delayedStatus = self.context.sharedContext.mediaManager.globalMediaPlayerState
|> mapToSignal { value -> Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> in
guard let value = value else {
return .single(nil)
}
switch value.1 {
case .state:
return .single(value)
case .loading:
return .single(value) |> delay(0.1, queue: .mainQueue())
}
}
mediaAccessoryPanel.containerNode.headerNode.playbackStatus = delayedStatus
|> map { state -> MediaPlayerStatus in
if let stateOrLoading = state?.1, case let .state(state) = stateOrLoading {
return state.status
} else {
return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true)
}
}
} else {
if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel {
self.mediaAccessoryPanel = nil
self.dismissingPanel = mediaAccessoryPanel
mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak self, weak mediaAccessoryPanel] in
mediaAccessoryPanel?.removeFromSupernode()
if let strongSelf = self, strongSelf.dismissingPanel === mediaAccessoryPanel {
strongSelf.dismissingPanel = nil
}
})
}
let mediaAccessoryPanel = MediaNavigationAccessoryPanel(context: self.context, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, displayBackground: true)
mediaAccessoryPanel.containerNode.headerNode.displayScrubber = item.playbackData?.type != .instantVideo
mediaAccessoryPanel.getController = { [weak self] in
return self?.parentController
}
mediaAccessoryPanel.presentInGlobalOverlay = { [weak self] c in
self?.parentController?.presentInGlobalOverlay(c)
}
mediaAccessoryPanel.close = { [weak self] in
if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType {
strongSelf.context.sharedContext.mediaManager.setPlaylist(nil, type: type, control: SharedMediaPlayerControlAction.playback(.pause))
}
}
mediaAccessoryPanel.setRate = { [weak self] rate, changeType in
guard let strongSelf = self else {
return
}
let _ = (strongSelf.context.sharedContext.accountManager.transaction { transaction -> AudioPlaybackRate in
let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings)?.get(MusicPlaybackSettings.self) ?? MusicPlaybackSettings.defaultSettings
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings, { _ in
return PreferencesEntry(settings.withUpdatedVoicePlaybackRate(rate))
})
return rate
}
|> deliverOnMainQueue).startStandalone(next: { baseRate in
guard let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType else {
return
}
strongSelf.context.sharedContext.mediaManager.playlistControl(.setBaseRate(baseRate), type: type)
if let controller = strongSelf.parentController {
var hasTooltip = false
controller.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
hasTooltip = true
controller.dismissWithCommitAction()
}
return true
})
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let text: String?
let rate: CGFloat?
if case let .sliderCommit(previousValue, newValue) = changeType {
let value = String(format: "%0.1f", baseRate.doubleValue)
if baseRate == .x1 {
text = presentationData.strings.Conversation_AudioRateTooltipNormal
} else {
text = presentationData.strings.Conversation_AudioRateTooltipCustom(value).string
}
if newValue > previousValue {
rate = .infinity
} else if newValue < previousValue {
rate = -.infinity
} else {
rate = nil
}
} else if baseRate == .x1 {
text = presentationData.strings.Conversation_AudioRateTooltipNormal
rate = 1.0
} else if baseRate == .x1_5 {
text = presentationData.strings.Conversation_AudioRateTooltip15X
rate = 1.5
} else if baseRate == .x2 {
text = presentationData.strings.Conversation_AudioRateTooltipSpeedUp
rate = 2.0
} else {
text = nil
rate = nil
}
var showTooltip = true
if case .sliderChange = changeType {
showTooltip = false
}
if let rate, let text, showTooltip {
controller.present(
UndoOverlayController(
presentationData: presentationData,
content: .audioRate(
rate: rate,
text: text
),
elevatedLayout: false,
animateInAsReplacement: hasTooltip,
action: { action in
return true
}
),
in: .current
)
}
}
})
}
mediaAccessoryPanel.togglePlayPause = { [weak self] in
if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType {
strongSelf.context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: type)
}
}
mediaAccessoryPanel.playPrevious = { [weak self] in
if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType {
strongSelf.context.sharedContext.mediaManager.playlistControl(.next, type: type)
}
}
mediaAccessoryPanel.playNext = { [weak self] in
if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType {
strongSelf.context.sharedContext.mediaManager.playlistControl(.previous, type: type)
}
}
mediaAccessoryPanel.tapAction = { [weak self] in
guard let strongSelf = self, let _ = strongSelf.chatControllerInteraction.navigationController(), let (state, _, _, order, type, account) = strongSelf.playlistStateAndType else {
return
}
if let id = state.id as? PeerMessagesMediaPlaylistItemId, let playlistLocation = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation, case let .messages(chatLocation, _, _) = playlistLocation {
if type == .music {
let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId)), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: .peer(id: id.messageId.peerId), subject: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), tag: .tag(MessageTags.music))
var cancelImpl: (() -> Void)?
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
self?.chatControllerInteraction.presentController(controller, nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = MetaDisposable()
var progressStarted = false
strongSelf.playlistPreloadDisposable?.dispose()
strongSelf.playlistPreloadDisposable = (signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
|> deliverOnMainQueue).startStandalone(next: { index in
guard let strongSelf = self else {
return
}
if let _ = index.0 {
let controllerContext: AccountContext
if account.id == strongSelf.context.account.id {
controllerContext = strongSelf.context
} else {
controllerContext = strongSelf.context.sharedContext.makeTempAccountContext(account: account)
}
let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: chatLocation, type: type, initialMessageId: id.messageId, initialOrder: order, playlistLocation: nil, parentNavigationController: strongSelf.chatControllerInteraction.navigationController())
strongSelf.view.window?.endEditing(true)
strongSelf.chatControllerInteraction.presentController(controller, nil)
} else if index.1 {
if !progressStarted {
progressStarted = true
progressDisposable.set(progressSignal.startStandalone())
}
}
}, completed: {
})
cancelImpl = {
self?.playlistPreloadDisposable?.dispose()
}
} else {
strongSelf.context.sharedContext.navigateToChat(accountId: strongSelf.context.account.id, peerId: id.messageId.peerId, messageId: id.messageId)
}
}
}
mediaAccessoryPanel.frame = panelFrame
if let dismissingPanel = self.dismissingPanel {
self.mediaAccessoryPanelContainer.insertSubnode(mediaAccessoryPanel, aboveSubnode: dismissingPanel)
} else {
self.mediaAccessoryPanelContainer.addSubnode(mediaAccessoryPanel)
}
self.mediaAccessoryPanel = (mediaAccessoryPanel, type)
mediaAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: sideInset, rightInset: sideInset, isHidden: false, transition: .immediate)
switch order {
case .regular:
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, previousItem, nextItem)
case .reversed:
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nextItem, previousItem)
case .random:
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nil, nil)
}
mediaAccessoryPanel.containerNode.headerNode.playbackStatus = self.context.sharedContext.mediaManager.globalMediaPlayerState
|> map { state -> MediaPlayerStatus in
if let stateOrLoading = state?.1, case let .state(state) = stateOrLoading {
return state.status
} else {
return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true)
}
}
mediaAccessoryPanel.animateIn(transition: transition)
}
} else if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel {
self.mediaAccessoryPanel = nil
self.dismissingPanel = mediaAccessoryPanel
mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak self, weak mediaAccessoryPanel] in
mediaAccessoryPanel?.removeFromSupernode()
if let strongSelf = self, strongSelf.dismissingPanel === mediaAccessoryPanel {
strongSelf.dismissingPanel = nil
}
})
}
transition.updateFrame(node: self.mediaAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: MediaNavigationAccessoryHeaderNode.minimizedHeight)))
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
self.listNode.updateLayout(transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topPanelHeight + topInset, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve))
if isScrollingLockedAtTop {
switch self.listNode.visibleContentOffset() {
case let .known(value) where value <= CGFloat.ulpOfOne:
break
case .none:
break
default:
self.listNode.scrollToEndOfHistory()
}
}
self.listNode.scrollEnabled = !isScrollingLockedAtTop
}
func findLoadedMessage(id: MessageId) -> Message? {
self.listNode.messageInCurrentHistoryView(id)
}
func updateHiddenMedia() {
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListMessageNode {
itemNode.updateHiddenMedia()
}
}
}
func transferVelocity(_ velocity: CGFloat) {
if velocity > 0.0 {
self.listNode.transferVelocity(velocity)
}
}
func cancelPreviewGestures() {
}
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListMessageNode {
if let result = itemNode.transitionNode(id: messageId, media: media, adjustRect: false) {
transitionNode = result
}
}
}
return transitionNode
}
func addToTransitionSurface(view: UIView) {
self.view.addSubview(view)
}
func updateSelectedMessages(animated: Bool) {
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
itemNode.updateSelectionState(animated: animated)
}
}
self.selectedMessages = self.chatControllerInteraction.selectionState.flatMap { $0.selectedIds }
}
}
@@ -0,0 +1,531 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import Postbox
import TelegramPresentationData
import AccountContext
import ContextUI
import PhotoResources
import TelegramUIPreferences
import ItemListPeerItem
import ItemListPeerActionItem
import MergeLists
import ItemListUI
import PeerInfoVisualMediaPaneNode
import PeerInfoPaneNode
import ContactsPeerItem
private struct PeerMembersListTransaction {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let animated: Bool
}
enum PeerMembersListAction {
case open
case promote
case restrict
case remove
case openStories(sourceView: UIView)
}
private enum PeerMembersListEntryStableId: Hashable {
case addMember
case peer(PeerId)
}
private enum PeerMembersListEntry: Comparable, Identifiable {
case addMember(PresentationTheme, String)
case member(theme: PresentationTheme, index: Int, member: PeerInfoMember)
var stableId: PeerMembersListEntryStableId {
switch self {
case .addMember:
return .addMember
case let .member(_, _, member):
return .peer(member.id)
}
}
static func ==(lhs: PeerMembersListEntry, rhs: PeerMembersListEntry) -> Bool {
switch lhs {
case let .addMember(lhsTheme, lhsText):
if case let .addMember(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .member(lhsTheme, lhsIndex, lhsMember):
if case let .member(rhsTheme, rhsIndex, rhsMember) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsMember == rhsMember {
return true
} else {
return false
}
}
}
static func <(lhs: PeerMembersListEntry, rhs: PeerMembersListEntry) -> Bool {
switch lhs {
case .addMember:
switch rhs {
case .addMember:
return false
case .member:
return true
}
case let .member(_, lhsIndex, _):
switch rhs {
case .addMember:
return false
case let .member(_, rhsIndex, _):
return lhsIndex < rhsIndex
}
}
}
func item(context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem {
switch self {
case let .addMember(_, text):
return ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: PresentationResourcesItemList.addPersonIcon(presentationData.theme), title: text, alwaysPlain: true, sectionId: 0, height: .compactPeerList, color: .accent, editing: false, action: {
addMemberAction()
})
case let .member(_, _, member):
let label: String?
if let rank = member.rank {
label = rank
} else {
switch member.role {
case .creator:
label = presentationData.strings.GroupInfo_LabelOwner
case .admin:
label = presentationData.strings.GroupInfo_LabelAdmin
case .member:
label = nil
}
}
let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: enclosingPeer, member: member)
var options: [ItemListPeerItemRevealOption] = []
if actions.contains(.promote) && enclosingPeer is TelegramChannel{
options.append(ItemListPeerItemRevealOption(type: .neutral, title: presentationData.strings.GroupInfo_ActionPromote, action: {
action(member, .promote)
}))
}
if actions.contains(.restrict) {
if enclosingPeer is TelegramChannel {
options.append(ItemListPeerItemRevealOption(type: .warning, title: presentationData.strings.GroupInfo_ActionRestrict, action: {
action(member, .restrict)
}))
}
options.append(ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Common_Delete, action: {
action(member, .remove)
}))
}
var presence: EnginePeer.Presence
if member.peer.id == context.account.peerId {
presence = EnginePeer.Presence(status: .present(until: Int32.max), lastActivity: 0)
} else if let value = member.presence {
presence = EnginePeer.Presence(value)
} else {
presence = EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0)
}
var status: ContactsPeerItemStatus = .presence(presence, presentationData.dateTimeFormat)
if let user = member.peer as? TelegramUser, let botInfo = user.botInfo {
let botStatus: String
if botInfo.flags.contains(.hasAccessToChatHistory) {
botStatus = presentationData.strings.Bot_GroupStatusReadsHistory
} else {
botStatus = presentationData.strings.Bot_GroupStatusDoesNotReadHistory
}
status = .custom(string: NSAttributedString(string: botStatus, font: Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize * 14.0 / 17.0)), textColor: presentationData.theme.list.itemSecondaryTextColor), multiline: false, isActive: false, icon: nil)
}
return ContactsPeerItem(
presentationData: ItemListPresentationData(presentationData),
style: .plain,
sectionId: 0,
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
peerMode: .memberList,
peer: .peer(peer: EnginePeer(member.peer), chatPeer: EnginePeer(member.peer)),
status: status,
rightLabelText: label,
enabled: true,
selection: .none,
editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false),
options: options,
additionalActions: [],
actionIcon: .none,
index: nil,
header: nil,
action: member.peer.id == context.account.peerId ? nil : { _ in
action(member, .open)
},
disabledAction: nil,
setPeerIdWithRevealedOptions: { _, _ in
},
deletePeer: nil,
itemHighlighting: nil,
contextAction: contextAction == nil ? nil : { node, gesture, _ in
contextAction?(member, node, gesture)
},
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
storyStats: member.storyStats.flatMap { storyStats in
return (
total: storyStats.totalCount,
unseen: storyStats.unseenCount,
hasUnseenCloseFriends: storyStats.hasUnseenCloseFriends,
hasLiveItems: storyStats.hasLiveItems
)
},
openStories: { _, sourceNode in
action(member, .openStories(sourceView: sourceNode.view))
}
)
/*return ItemListPeerItem(
presentationData: ItemListPresentationData(presentationData),
dateTimeFormat: presentationData.dateTimeFormat,
nameDisplayOrder: presentationData.nameDisplayOrder,
context: context,
peer: EnginePeer(member.peer),
presence: member.presence.flatMap(EnginePeer.Presence.init),
text: .presence,
label: label == nil ? .none : .text(label!, .standard),
editing: ItemListPeerItemEditing(editable: !options.isEmpty, editing: false, revealed: false),
revealOptions: ItemListPeerItemRevealOptions(options: options),
switchValue: nil,
enabled: true,
selectable: member.id != context.account.peerId,
sectionId: 0,
action: {
action(member, .open)
},
setPeerIdWithRevealedOptions: { _, _ in
},
removePeer: { _ in
},
contextAction: contextAction == nil ? nil : { node, gesture in
contextAction?(member, node, gesture)
},
hasTopStripe: false,
noInsets: true,
noCorners: true,
disableInteractiveTransitionIfNecessary: true,
storyStats: member.storyStats,
openStories: { sourceView in
action(member, .openStories(sourceView: sourceView))
}
)*/
}
}
}
private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toEntries: [PeerMembersListEntry], context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> PeerMembersListTransaction {
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(context: context, presentationData: presentationData, enclosingPeer: enclosingPeer, addMemberAction: addMemberAction, action: action, contextAction: contextAction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enclosingPeer: enclosingPeer, addMemberAction: addMemberAction, action: action, contextAction: contextAction), directionHint: nil) }
return PeerMembersListTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: toEntries.count < fromEntries.count)
}
final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
private let context: AccountContext
private let membersContext: PeerInfoMembersContext
private let addMemberAction: () -> Void
private let action: (PeerInfoMember, PeerMembersListAction) -> Void
weak var parentController: ViewController?
private let listNode: ListView
private var currentEntries: [PeerMembersListEntry] = []
private var enclosingPeer: Peer?
private var currentState: PeerInfoMembersState?
private var canLoadMore: Bool = false
private var enqueuedTransactions: [PeerMembersListTransaction] = []
private var currentParams: (size: CGSize, isScrollingLockedAtTop: Bool)?
private let presentationDataPromise = Promise<PresentationData>()
private let ready = Promise<Bool>()
private var didSetReady: Bool = false
var isReady: Signal<Bool, NoError> {
return self.ready.get()
}
var status: Signal<PeerInfoStatusData?, NoError> {
return .single(nil)
}
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
var tabBarOffset: CGFloat {
return 0.0
}
private var disposable: Disposable?
init(context: AccountContext, peerId: PeerId, membersContext: PeerInfoMembersContext, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) {
self.context = context
self.membersContext = membersContext
self.addMemberAction = addMemberAction
self.action = action
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.listNode = ListView()
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
super.init()
self.listNode.preloadPages = true
self.addSubnode(self.listNode)
self.disposable = (combineLatest(queue: .mainQueue(),
membersContext.state,
self.presentationDataPromise.get(),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
)
|> deliverOnMainQueue).startStrict(next: { [weak self] state, presentationData, enclosingPeer in
guard let strongSelf = self, let enclosingPeer = enclosingPeer else {
return
}
strongSelf.enclosingPeer = enclosingPeer._asPeer()
strongSelf.currentState = state
strongSelf.updateState(enclosingPeer: enclosingPeer._asPeer(), state: state, presentationData: presentationData)
})
self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in
guard let strongSelf = self, let state = strongSelf.currentState, case .ready(true) = state.dataState else {
return
}
if case let .known(value) = offset, value < 100.0 {
strongSelf.membersContext.loadMore()
}
}
}
deinit {
self.disposable?.dispose()
}
func ensureMessageIsVisible(id: MessageId) {
}
func scrollToTop() -> Bool {
if !self.listNode.scrollToOffsetFromTop(0.0, animated: true) {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
return true
} else {
return false
}
}
func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
let isFirstLayout = self.currentParams == nil
self.currentParams = (size, isScrollingLockedAtTop)
self.presentationDataPromise.set(.single(presentationData))
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
var scrollToItem: ListViewScrollToItem?
if isScrollingLockedAtTop {
switch self.listNode.visibleContentOffset() {
case let .known(value) where value <= CGFloat.ulpOfOne:
break
default:
scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: duration), directionHint: .Up)
}
}
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
self.listNode.scrollEnabled = !isScrollingLockedAtTop
if isFirstLayout, let enclosingPeer = self.enclosingPeer, let state = self.currentState {
self.updateState(enclosingPeer: enclosingPeer, state: state, presentationData: presentationData)
}
}
private func updateState(enclosingPeer: Peer, state: PeerInfoMembersState, presentationData: PresentationData) {
var entries: [PeerMembersListEntry] = []
if state.canAddMembers {
entries.append(.addMember(presentationData.theme, presentationData.strings.GroupInfo_AddParticipant))
}
for member in state.members {
entries.append(.member(theme: presentationData.theme, index: entries.count, member: member))
}
let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, enclosingPeer: enclosingPeer, addMemberAction: { [weak self] in
self?.addMemberAction()
}, action: { [weak self] member, action in
guard let self else {
return
}
if case .open = action {
self.listNode.clearHighlightAnimated(true)
}
self.action(member, action)
}, contextAction: { [weak self] member, sourceNode, gesture in
guard let self else {
return
}
var node: ContextExtractedContentContainingNode?
if let sourceNode = sourceNode as? ContextExtractedContentContainingNode {
node = sourceNode
} else {
for subnode in sourceNode.subnodes ?? [] {
if let subnode = subnode as? ContextExtractedContentContainingNode {
node = subnode
break
}
}
}
guard let node else {
gesture?.cancel()
return
}
let actions = availableActionsForMemberOfPeer(accountPeerId: self.context.account.peerId, peer: enclosingPeer, member: member)
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = []
let action = self.action
if actions.contains(.promote) && enclosingPeer is TelegramChannel {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.GroupInfo_ActionPromote, icon: { _ in
return nil
}, action: { c, _ in
c?.dismiss(completion: {
action(member, .promote)
})
})))
}
if actions.contains(.restrict) {
if enclosingPeer is TelegramChannel {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.GroupInfo_ActionRestrict, icon: { _ in
return nil
}, action: { c, _ in
c?.dismiss(completion: {
action(member, .restrict)
})
})))
}
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Delete, textColor: .destructive, icon: { _ in
return nil
}, action: { c, _ in
c?.dismiss(completion: {
action(member, .remove)
})
})))
}
if items.isEmpty {
gesture?.cancel()
return
}
let dismissPromise = ValuePromise<Bool>(false)
let source = PeerInfoMemberExtractedContentSource(sourceNode: node, keepInPlace: false, blurBackground: true, centerVertically: false, shouldBeDismissed: dismissPromise.get())
let contextController = ContextController(presentationData: presentationData, source: .extracted(source), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
self.parentController?.presentInGlobalOverlay(contextController)
})
self.enclosingPeer = enclosingPeer
self.currentEntries = entries
self.enqueuedTransactions.append(transaction)
self.dequeueTransaction()
}
private func dequeueTransaction() {
guard let _ = self.currentParams, let transaction = self.enqueuedTransactions.first else {
return
}
self.enqueuedTransactions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
if transaction.animated {
options.insert(.AnimateInsertion)
} else {
options.insert(.Synchronous)
}
self.listNode.transaction(deleteIndices: transaction.deletions, insertIndicesAndItems: transaction.insertions, updateIndicesAndItems: transaction.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf.ready.set(.single(true))
}
})
}
func findLoadedMessage(id: MessageId) -> Message? {
return nil
}
func updateHiddenMedia() {
}
func transferVelocity(_ velocity: CGFloat) {
if velocity > 0.0 {
self.listNode.transferVelocity(velocity)
}
}
func cancelPreviewGestures() {
}
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return nil
}
func addToTransitionSurface(view: UIView) {
}
func updateSelectedMessages(animated: Bool) {
}
}
final class PeerInfoMemberExtractedContentSource: ContextExtractedContentSource {
var keepInPlace: Bool
let ignoreContentTouches: Bool = false
let blurBackground: Bool
private let sourceNode: ContextExtractedContentContainingNode
var centerVertically: Bool
var shouldBeDismissed: Signal<Bool, NoError>
init(sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool, blurBackground: Bool, centerVertically: Bool, shouldBeDismissed: Signal<Bool, NoError>) {
self.sourceNode = sourceNode
self.keepInPlace = keepInPlace
self.blurBackground = blurBackground
self.centerVertically = centerVertically
self.shouldBeDismissed = shouldBeDismissed
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
@@ -0,0 +1,483 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import TelegramCore
import SwiftSignalKit
import Postbox
import TelegramPresentationData
import AccountContext
import ContextUI
import PhotoResources
import TelegramUIPreferences
import ItemListPeerItem
import ItemListPeerActionItem
import MergeLists
import ItemListUI
import PeerInfoVisualMediaPaneNode
import ChatControllerInteraction
import MultilineTextComponent
import Markdown
import SolidRoundedButtonNode
import PeerInfoPaneNode
private struct RecommendedPeersListTransaction {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let animated: Bool
}
private enum RecommendedPeersListEntryStableId: Hashable {
case addMember
case peer(PeerId)
}
private enum RecommendedPeersListEntry: Comparable, Identifiable {
case peer(theme: PresentationTheme, index: Int, peer: EnginePeer, subscribers: Int32)
var stableId: RecommendedPeersListEntryStableId {
switch self {
case let .peer(_, _, peer, _):
return .peer(peer.id)
}
}
static func ==(lhs: RecommendedPeersListEntry, rhs: RecommendedPeersListEntry) -> Bool {
switch lhs {
case let .peer(lhsTheme, lhsIndex, lhsPeer, lhsSubscribers):
if case let .peer(rhsTheme, rhsIndex, rhsPeer, rhsSubscribers) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsPeer == rhsPeer, lhsSubscribers == rhsSubscribers {
return true
} else {
return false
}
}
}
static func <(lhs: RecommendedPeersListEntry, rhs: RecommendedPeersListEntry) -> Bool {
switch lhs {
case let .peer(_, lhsIndex, _, _):
switch rhs {
case let .peer(_, rhsIndex, _, _):
return lhsIndex < rhsIndex
}
}
}
func item(context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem {
switch self {
case let .peer(_, _, peer, subscribers):
let text: ItemListPeerItemText
if subscribers > 0 {
if peer.id.namespace == Namespaces.Peer.CloudChannel {
text = .text(presentationData.strings.SharedMedia_SimilarChannel_Subscribers(subscribers), .secondary)
} else {
text = .text(presentationData.strings.SharedMedia_SimilarBot_Users(subscribers), .secondary)
}
} else if let addressName = peer.addressName {
text = .text("@\(addressName)", .secondary)
} else {
text = .none
}
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, presence: nil, text: text, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: {
action(peer)
}, setPeerIdWithRevealedOptions: { _, _ in
}, removePeer: { _ in
}, contextAction: { node, gesture in
openPeerContextAction(peer._asPeer(), node, gesture)
}, hasTopStripe: false, noInsets: true, noCorners: true, style: .plain, disableInteractiveTransitionIfNecessary: true)
}
}
}
private func preparedTransition(from fromEntries: [RecommendedPeersListEntry], to toEntries: [RecommendedPeersListEntry], context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> RecommendedPeersListTransaction {
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(context: context, presentationData: presentationData, action: action, openPeerContextAction: openPeerContextAction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, action: action, openPeerContextAction: openPeerContextAction), directionHint: nil) }
return RecommendedPeersListTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: toEntries.count < fromEntries.count)
}
private protocol RecommendedPeers {
}
extension RecommendedChannels: RecommendedPeers {
}
extension RecommendedBots: RecommendedPeers {
}
final class PeerInfoRecommendedPeersPaneNode: ASDisplayNode, PeerInfoPaneNode {
private let context: AccountContext
private let chatControllerInteraction: ChatControllerInteraction
private let openPeerContextAction: (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void
weak var parentController: ViewController?
private let listNode: ListView
private var currentEntries: [RecommendedPeersListEntry] = []
private var enqueuedTransactions: [RecommendedPeersListTransaction] = []
private var currentState: (RecommendedPeers?, Bool)?
private var unlockBackground: UIImageView?
private var unlockText: ComponentView<Empty>?
private var unlockButton: SolidRoundedButtonNode?
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)?
private var theme: PresentationTheme?
private let presentationDataPromise = Promise<PresentationData>()
private let ready = Promise<Bool>()
private var didSetReady: Bool = false
var isReady: Signal<Bool, NoError> {
return self.ready.get()
}
private let statusPromise = Promise<PeerInfoStatusData?>(nil)
var status: Signal<PeerInfoStatusData?, NoError> {
self.statusPromise.get()
}
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
var tabBarOffset: CGFloat {
return 0.0
}
private var disposable: Disposable?
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void) {
self.context = context
self.chatControllerInteraction = chatControllerInteraction
self.openPeerContextAction = openPeerContextAction
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.listNode = ListView()
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
super.init()
self.listNode.preloadPages = true
self.addSubnode(self.listNode)
let signal: Signal<RecommendedPeers?, NoError>
if peerId.namespace == Namespaces.Peer.CloudUser {
signal = context.engine.peers.recommendedBots(peerId: peerId)
|> map {
$0 as RecommendedPeers?
}
} else {
signal = context.engine.peers.recommendedChannels(peerId: peerId)
|> map {
$0 as RecommendedPeers?
}
}
self.disposable = (combineLatest(queue: .mainQueue(),
self.presentationDataPromise.get(),
signal,
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in
return peer?.isPremium ?? false
}
)
|> deliverOnMainQueue).startStrict(next: { [weak self] presentationData, recommendedPeers, isPremium in
guard let self else {
return
}
self.currentState = (recommendedPeers, isPremium)
self.updateState(recommendedPeers: recommendedPeers, isPremium: isPremium, presentationData: presentationData)
})
self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in
if let self {
self.layoutUnlockPanel(transition: .animated(duration: 0.4, curve: .spring))
}
}
}
deinit {
self.disposable?.dispose()
}
func ensureMessageIsVisible(id: MessageId) {
}
func scrollToTop() -> Bool {
if !self.listNode.scrollToOffsetFromTop(0.0, animated: true) {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
return true
} else {
return false
}
}
func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
let isFirstLayout = self.currentParams == nil
self.currentParams = (size, sideInset, bottomInset, isScrollingLockedAtTop, presentationData)
self.presentationDataPromise.set(.single(presentationData))
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
var scrollToItem: ListViewScrollToItem?
if isScrollingLockedAtTop {
switch self.listNode.visibleContentOffset() {
case let .known(value) where value <= CGFloat.ulpOfOne:
break
default:
scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: duration), directionHint: .Up)
}
}
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
self.listNode.scrollEnabled = !isScrollingLockedAtTop
if isFirstLayout, let (recommendedPeers, isPremium) = self.currentState {
self.updateState(recommendedPeers: recommendedPeers, isPremium: isPremium, presentationData: presentationData)
}
}
@objc private func unlockPressed() {
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads, forceDark: false, dismissed: nil)
self.chatControllerInteraction.navigationController()?.pushViewController(controller)
}
private func updateState(recommendedPeers: RecommendedPeers?, isPremium: Bool, presentationData: PresentationData) {
if let recommendedChannels = recommendedPeers as? RecommendedChannels {
self.updateState(recommendedChannels: recommendedChannels, isPremium: isPremium, presentationData: presentationData)
} else if let recommendedBots = recommendedPeers as? RecommendedBots {
self.updateState(recommendedBots: recommendedBots, isPremium: isPremium, presentationData: presentationData)
}
}
private func updateState(recommendedChannels: RecommendedChannels?, isPremium: Bool, presentationData: PresentationData) {
var entries: [RecommendedPeersListEntry] = []
if let channels = recommendedChannels?.channels {
for channel in channels {
entries.append(.peer(theme: presentationData.theme, index: entries.count, peer: channel.peer, subscribers: channel.subscribers))
}
}
let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, action: { [weak self] peer in
self?.chatControllerInteraction.openPeer(peer, .default, nil, .default)
}, openPeerContextAction: { [weak self] peer, node, gesture in
self?.openPeerContextAction(true, peer, node, gesture)
})
self.currentEntries = entries
self.enqueuedTransactions.append(transaction)
self.dequeueTransaction()
if let recommendedChannels {
self.statusPromise.set(.single(
PeerInfoStatusData(text: presentationData.strings.SharedMedia_SimilarChannelCount(recommendedChannels.count), isActivity: true, key: .similarChannels)
))
}
}
private func updateState(recommendedBots: RecommendedBots?, isPremium: Bool, presentationData: PresentationData) {
var entries: [RecommendedPeersListEntry] = []
if let bots = recommendedBots?.bots {
for bot in bots {
var subscriberCount: Int32 = 0
if case let .user(user) = bot {
subscriberCount = user.subscriberCount ?? 0
}
entries.append(.peer(theme: presentationData.theme, index: entries.count, peer: bot, subscribers: subscriberCount))
}
}
let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, action: { [weak self] peer in
self?.chatControllerInteraction.openPeer(peer, .info(nil), nil, .default)
}, openPeerContextAction: { [weak self] peer, node, gesture in
self?.openPeerContextAction(true, peer, node, gesture)
})
self.currentEntries = entries
self.enqueuedTransactions.append(transaction)
self.dequeueTransaction()
if let recommendedBots {
self.statusPromise.set(.single(
PeerInfoStatusData(text: presentationData.strings.SharedMedia_SimilarBotCount(recommendedBots.count), isActivity: true, key: .similarBots)
))
}
}
private func layoutUnlockPanel(transition: ContainedViewLayoutTransition) {
guard let (_, isPremium) = self.currentState, let currentParams = self.currentParams else {
return
}
if !isPremium {
var transition = transition
let size = currentParams.size
let sideInset = currentParams.sideInset
let bottomInset = currentParams.bottomInset
let presentationData = currentParams.presentationData
let themeUpdated = self.theme !== presentationData.theme
self.theme = presentationData.theme
let unlockText: ComponentView<Empty>
let unlockBackground: UIImageView
let unlockButton: SolidRoundedButtonNode
if let current = self.unlockText {
unlockText = current
} else {
unlockText = ComponentView<Empty>()
self.unlockText = unlockText
transition = .immediate
}
if let current = self.unlockBackground {
unlockBackground = current
} else {
unlockBackground = UIImageView()
unlockBackground.contentMode = .scaleToFill
self.view.addSubview(unlockBackground)
self.unlockBackground = unlockBackground
}
var isBots = false
if let (state, _) = self.currentState, state is RecommendedBots {
isBots = true
}
if let current = self.unlockButton {
unlockButton = current
} else {
unlockButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: presentationData.theme), height: 50.0, cornerRadius: 10.0)
self.view.addSubview(unlockButton.view)
self.unlockButton = unlockButton
unlockButton.animationLoopTime = 2.5
unlockButton.animation = "premium_unlock"
unlockButton.iconPosition = .right
unlockButton.title = isBots ? presentationData.strings.PeerInfo_SimilarBots_ShowMore : presentationData.strings.Channel_SimilarChannels_ShowMore
unlockButton.pressed = { [weak self] in
self?.unlockPressed()
}
}
if themeUpdated {
let topColor = presentationData.theme.list.plainBackgroundColor.withAlphaComponent(0.0)
let bottomColor = presentationData.theme.list.plainBackgroundColor
unlockBackground.image = generateGradientImage(size: CGSize(width: 1.0, height: 170.0), colors: [topColor, bottomColor, bottomColor], locations: [0.0, 0.3, 1.0])
unlockButton.updateTheme(SolidRoundedButtonTheme(theme: presentationData.theme))
}
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = presentationData.theme.list.itemSecondaryTextColor
let linkColor = presentationData.theme.list.itemAccentColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: boldTextFont, textColor: linkColor), linkAttribute: { _ in
return nil
})
var scrollOffset: CGFloat = 0.0
if case let .known(offset) = self.listNode.visibleBottomContentOffset() {
scrollOffset = min(0.0, offset + bottomInset + 80.0)
}
let unlockSize = unlockText.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .markdown(text: isBots ? presentationData.strings.PeerInfo_SimilarBots_ShowMoreInfo : presentationData.strings.Channel_SimilarChannels_ShowMoreInfo, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
)
),
environment: {},
containerSize: CGSize(width: size.width - 32.0, height: 200.0)
)
if let view = unlockText.view {
if view.superview == nil {
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.unlockPressed)))
self.view.addSubview(view)
}
transition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: floor((size.width - unlockSize.width) / 2.0), y: size.height - bottomInset - unlockSize.height - 13.0 + scrollOffset), size: unlockSize))
}
transition.updateFrame(view: unlockBackground, frame: CGRect(x: 0.0, y: size.height - bottomInset - 170.0 + scrollOffset, width: size.width, height: bottomInset + 170.0))
let buttonSideInset = sideInset + 16.0
let buttonSize = CGSize(width: size.width - buttonSideInset * 2.0, height: 50.0)
transition.updateFrame(node: unlockButton, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - unlockSize.height - buttonSize.height - 26.0 + scrollOffset), size: buttonSize))
let _ = unlockButton.updateLayout(width: buttonSize.width, transition: .immediate)
} else {
self.unlockBackground?.removeFromSuperview()
self.unlockBackground = nil
self.unlockButton?.view.removeFromSuperview()
self.unlockButton = nil
self.unlockText?.view?.removeFromSuperview()
self.unlockText = nil
}
}
private func dequeueTransaction() {
guard let _ = self.currentParams, let transaction = self.enqueuedTransactions.first else {
return
}
self.enqueuedTransactions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
if transaction.animated {
options.insert(.AnimateInsertion)
} else {
options.insert(.Synchronous)
}
self.listNode.transaction(deleteIndices: transaction.deletions, insertIndicesAndItems: transaction.insertions, updateIndicesAndItems: transaction.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf.ready.set(.single(true))
}
})
}
func findLoadedMessage(id: MessageId) -> Message? {
return nil
}
func updateHiddenMedia() {
}
func transferVelocity(_ velocity: CGFloat) {
if velocity > 0.0 {
self.listNode.transferVelocity(velocity)
}
}
func cancelPreviewGestures() {
}
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return nil
}
func addToTransitionSurface(view: UIView) {
}
func updateSelectedMessages(animated: Bool) {
}
}
@@ -0,0 +1,179 @@
import Foundation
import UIKit
import AsyncDisplayKit
import TelegramPresentationData
import PeerInfoAvatarListNode
import SwiftSignalKit
import Postbox
import TelegramCore
import ContextUI
import AccountContext
import Display
final class PeerInfoAvatarListNode: ASDisplayNode {
private let isSettings: Bool
let containerNode: ASDisplayNode
let pinchSourceNode: PinchSourceContainerNode
let bottomCoverNode: ASDisplayNode
let maskNode: DynamicIslandMaskNode
let topCoverNode: DynamicIslandBlurNode
let avatarContainerNode: PeerInfoAvatarTransformContainerNode
let listContainerTransformNode: ASDisplayNode
let listContainerNode: PeerInfoAvatarListContainerNode
let isReady = Promise<Bool>()
var arguments: (Peer?, Int64?, EngineMessageHistoryThread.Info?, PresentationTheme, CGFloat, Bool)?
var item: PeerInfoAvatarListItem?
var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)?
var animateOverlaysFadeIn: (() -> Void)?
var openStories: (() -> Void)?
init(context: AccountContext, readyWhenGalleryLoads: Bool, isSettings: Bool) {
self.isSettings = isSettings
self.containerNode = ASDisplayNode()
self.bottomCoverNode = ASDisplayNode()
self.maskNode = DynamicIslandMaskNode()
self.pinchSourceNode = PinchSourceContainerNode()
self.avatarContainerNode = PeerInfoAvatarTransformContainerNode(context: context)
self.listContainerTransformNode = ASDisplayNode()
self.listContainerNode = PeerInfoAvatarListContainerNode(context: context, isSettings: isSettings)
self.listContainerNode.clipsToBounds = true
self.listContainerNode.isHidden = true
self.topCoverNode = DynamicIslandBlurNode()
super.init()
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.bottomCoverNode)
self.containerNode.addSubnode(self.pinchSourceNode)
self.pinchSourceNode.contentNode.addSubnode(self.avatarContainerNode)
self.listContainerTransformNode.addSubnode(self.listContainerNode)
self.pinchSourceNode.contentNode.addSubnode(self.listContainerTransformNode)
self.containerNode.addSubnode(self.topCoverNode)
let avatarReady = (self.avatarContainerNode.avatarNode.ready
|> mapToSignal { _ -> Signal<Bool, NoError> in
return .complete()
}
|> then(.single(true)))
let galleryReady = self.listContainerNode.isReady.get()
|> filter { value in
return value
}
|> take(1)
let combinedSignal: Signal<Bool, NoError>
if readyWhenGalleryLoads {
combinedSignal = combineLatest(queue: .mainQueue(),
avatarReady,
galleryReady
)
|> map { lhs, rhs in
return lhs && rhs
}
} else {
combinedSignal = avatarReady
}
self.isReady.set(combinedSignal
|> filter { value in
return value
}
|> take(1))
self.listContainerNode.itemsUpdated = { [weak self] items in
if let strongSelf = self {
strongSelf.item = items.first
strongSelf.itemsUpdated?(items)
if let (peer, threadId, threadInfo, theme, avatarSize, isExpanded) = strongSelf.arguments {
strongSelf.avatarContainerNode.update(peer: peer, threadId: threadId, threadInfo: threadInfo, item: strongSelf.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: strongSelf.isSettings)
}
}
}
self.pinchSourceNode.activate = { [weak self] sourceNode in
guard let strongSelf = self, let (_, _, _, _, _, isExpanded) = strongSelf.arguments, isExpanded else {
return
}
let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: {
return UIScreen.main.bounds
})
context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController)
strongSelf.listContainerNode.bottomShadowNode.alpha = 0.0
strongSelf.listContainerNode.contentNode.updateIsInPinchMode(true)
}
self.pinchSourceNode.animatedOut = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.animateOverlaysFadeIn?()
strongSelf.listContainerNode.contentNode.updateIsInPinchMode(false)
}
self.listContainerNode.openStories = { [weak self] in
guard let self else {
return
}
self.openStories?()
}
}
func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, isForum: Bool, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) {
self.arguments = (peer, threadId, threadInfo, theme, avatarSize, isExpanded)
self.maskNode.isForum = isForum
self.pinchSourceNode.update(size: size, transition: transition)
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
self.pinchSourceNode.frame = CGRect(origin: CGPoint(), size: size)
self.avatarContainerNode.update(peer: peer, threadId: threadId, threadInfo: threadInfo, item: self.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: self.isSettings)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.listContainerNode.isHidden {
if let result = self.listContainerNode.view.hitTest(self.view.convert(point, to: self.listContainerNode.view), with: event) {
return result
}
} else {
if let result = self.avatarContainerNode.avatarNode.view.hitTest(self.view.convert(point, to: self.avatarContainerNode.avatarNode.view), with: event) {
return result
} else if let result = self.avatarContainerNode.iconView?.view?.hitTest(self.view.convert(point, to: self.avatarContainerNode.iconView?.view), with: event) {
return result
}
}
return super.hitTest(point, with: event)
}
func animateAvatarCollapse(transition: ContainedViewLayoutTransition) {
if let currentItemNode = self.listContainerNode.currentItemNode, case .animated = transition {
if let _ = self.avatarContainerNode.videoNode {
} else if let _ = self.avatarContainerNode.markupNode {
} else if let unroundedImage = self.avatarContainerNode.avatarNode.unroundedImage {
let avatarCopyView = UIImageView()
avatarCopyView.image = unroundedImage
avatarCopyView.frame = self.avatarContainerNode.avatarNode.frame
avatarCopyView.center = currentItemNode.imageNode.position
currentItemNode.view.addSubview(avatarCopyView)
let scale = currentItemNode.imageNode.bounds.height / avatarCopyView.bounds.height
avatarCopyView.layer.transform = CATransform3DMakeScale(scale, scale, scale)
avatarCopyView.alpha = 0.0
transition.updateAlpha(layer: avatarCopyView.layer, alpha: 1.0, completion: { [weak avatarCopyView] _ in
Queue.mainQueue().after(0.1, {
avatarCopyView?.removeFromSuperview()
})
})
}
}
}
}
@@ -0,0 +1,519 @@
import Foundation
import UIKit
import AsyncDisplayKit
import ContextUI
import TelegramPresentationData
import AccountContext
import AvatarNode
import UniversalMediaPlayer
import Display
import ComponentFlow
import UniversalMediaPlayer
import AvatarVideoNode
import SwiftSignalKit
import TelegramUniversalVideoContent
import PeerInfoAvatarListNode
import Postbox
import TelegramCore
import EmojiStatusComponent
import GalleryUI
import HierarchyTrackingLayer
final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
let context: AccountContext
let containerNode: ContextControllerSourceNode
let avatarNode: AvatarNode
private(set) var avatarStoryView: ComponentView<Empty>?
var videoNode: UniversalVideoNode?
var markupNode: AvatarVideoNode?
var iconView: ComponentView<Empty>?
private var videoContent: NativeVideoContent?
private var videoStartTimestamp: Double?
private let hierarchyTrackingLayer = HierarchyTrackingLayer()
var isExpanded: Bool = false
var canAttachVideo: Bool = true {
didSet {
if oldValue != self.canAttachVideo {
self.videoNode?.canAttachContent = !self.isExpanded && self.canAttachVideo
}
}
}
var tapped: (() -> Void)?
var emojiTapped: (() -> Void)?
var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
private var isFirstAvatarLoading = true
var item: PeerInfoAvatarListItem?
private let playbackStartDisposable = MetaDisposable()
var storyData: (totalCount: Int, unseenCount: Int, hasUnseenCloseFriends: Bool, hasLiveItems: Bool)?
var storyProgress: Float?
init(context: AccountContext) {
self.context = context
self.containerNode = ContextControllerSourceNode()
let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0))
self.avatarNode = AvatarNode(font: avatarFont)
super.init()
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.avatarNode)
self.containerNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0))
self.avatarNode.frame = self.containerNode.bounds
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
self.avatarNode.view.addGestureRecognizer(tapGestureRecognizer)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
return
}
tapGestureRecognizer.isEnabled = false
tapGestureRecognizer.isEnabled = true
strongSelf.contextAction?(strongSelf.containerNode, gesture)
}
self.hierarchyTrackingLayer.isInHierarchyUpdated = { [weak self] value in
guard let self else {
return
}
if value {
self.updateFromParams()
} else {
self.videoNode?.removeFromSupernode()
self.videoNode = nil
self.videoContent = nil
}
}
self.layer.addSublayer(self.hierarchyTrackingLayer)
}
deinit {
self.playbackStartDisposable.dispose()
}
func updateStoryView(transition: ContainedViewLayoutTransition, theme: PresentationTheme, peer: Peer?) {
var colors = AvatarNode.Colors(theme: theme)
let regularNavigationContentsSecondaryColor: UIColor
if case let .starGift(_, _, _, _, _, innerColorValue, outerColorValue, _, _) = peer?.emojiStatus?.content {
let innerColor = UIColor(rgb: UInt32(bitPattern: innerColorValue))
let outerColor = UIColor(rgb: UInt32(bitPattern: outerColorValue))
regularNavigationContentsSecondaryColor = UIColor(white: 1.0, alpha: 0.6).blitOver(innerColor.withMultiplied(hue: 1.0, saturation: 2.2, brightness: 1.5), alpha: 1.0)
let baseBackgroundColor = UIColor(white: 1.0, alpha: 0.75)
let topColor = baseBackgroundColor.blendOver(background: innerColor.mixedWith(outerColor, alpha: 0.1)).withMultiplied(hue: 1.0, saturation: 1.2, brightness: 1.5)
let bottomColor = baseBackgroundColor.blendOver(background: outerColor).withMultiplied(hue: 1.0, saturation: 1.2, brightness: 1.5)
colors.unseenColors = [topColor, bottomColor]
colors.unseenCloseFriendsColors = colors.unseenColors
colors.seenColors = colors.unseenColors
} else if let profileColor = peer?.profileColor {
let backgroundColors = self.context.peerNameColors.getProfile(profileColor, dark: theme.overallDarkAppearance)
regularNavigationContentsSecondaryColor = UIColor(white: 1.0, alpha: 0.6).blitOver(backgroundColors.main.withMultiplied(hue: 1.0, saturation: 2.2, brightness: 1.5), alpha: 1.0)
let storyColors = self.context.peerNameColors.getProfile(profileColor, dark: theme.overallDarkAppearance, subject: .stories)
var unseenColors: [UIColor] = [storyColors.main]
if let secondary = storyColors.secondary {
unseenColors.insert(secondary, at: 0)
}
colors.unseenColors = unseenColors
colors.unseenCloseFriendsColors = colors.unseenColors
colors.seenColors = colors.unseenColors
} else {
regularNavigationContentsSecondaryColor = theme.list.controlSecondaryColor
}
colors.seenColors = [
regularNavigationContentsSecondaryColor,
regularNavigationContentsSecondaryColor
]
var storyStats: AvatarNode.StoryStats?
if let storyData = self.storyData {
storyStats = AvatarNode.StoryStats(
totalCount: storyData.totalCount,
unseenCount: storyData.unseenCount,
hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends,
hasLiveItems: storyData.hasLiveItems,
progress: self.storyProgress
)
} else if let storyProgress = self.storyProgress {
storyStats = AvatarNode.StoryStats(
totalCount: 1,
unseenCount: 1,
hasUnseenCloseFriendsItems: false,
hasLiveItems: false,
progress: storyProgress
)
}
var isForum = false
if let peer, let channel = peer as? TelegramChannel, channel.isForumOrMonoForum {
isForum = true
}
self.avatarNode.setStoryStats(storyStats: storyStats, presentationParams: AvatarNode.StoryPresentationParams(
colors: colors,
lineWidth: 3.0,
inactiveLineWidth: 1.5,
forceRoundedRect: isForum
), transition: ComponentTransition(transition))
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.tapped?()
}
}
@objc private func emojiTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.emojiTapped?()
}
}
func updateTransitionFraction(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) {
if let videoNode = self.videoNode {
if case .immediate = transition, fraction == 1.0 {
return
}
if fraction > 0.0 {
videoNode.pause()
} else {
videoNode.play()
}
transition.updateAlpha(node: videoNode, alpha: 1.0 - fraction)
}
if let markupNode = self.markupNode {
if case .immediate = transition, fraction == 1.0 {
return
}
if fraction > 0.0 {
markupNode.updateVisibility(false)
} else {
markupNode.updateVisibility(true)
}
transition.updateAlpha(node: markupNode, alpha: 1.0 - fraction)
}
}
private struct Params {
let peer: Peer?
let threadId: Int64?
let threadInfo: EngineMessageHistoryThread.Info?
let item: PeerInfoAvatarListItem?
let theme: PresentationTheme
let avatarSize: CGFloat
let isExpanded: Bool
let isSettings: Bool
init(peer: Peer?, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) {
self.peer = peer
self.threadId = threadId
self.threadInfo = threadInfo
self.item = item
self.theme = theme
self.avatarSize = avatarSize
self.isExpanded = isExpanded
self.isSettings = isSettings
}
}
var removedPhotoResourceIds = Set<String>()
private var params: Params?
private func updateFromParams() {
guard let params = self.params else {
return
}
self.update(
peer: params.peer,
threadId: params.threadId,
threadInfo: params.threadInfo,
item: params.item,
theme: params.theme,
avatarSize: params.avatarSize,
isExpanded: params.isExpanded,
isSettings: params.isSettings
)
}
func update(peer: Peer?, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) {
self.params = Params(peer: peer, threadId: threadId, threadInfo: threadInfo, item: item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: isSettings)
if let peer = peer {
let previousItem = self.item
var item = item
self.item = item
var overrideImage: AvatarNodeImageOverride?
if peer.isDeleted {
overrideImage = .deletedIcon
} else if let previousItem = previousItem, item == nil {
if case let .image(_, representations, _, _, _, _) = previousItem, let rep = representations.last {
self.removedPhotoResourceIds.insert(rep.representation.resource.id.stringRepresentation)
}
overrideImage = AvatarNodeImageOverride.none
item = nil
} else if let rep = peer.profileImageRepresentations.last, self.removedPhotoResourceIds.contains(rep.resource.id.stringRepresentation) {
overrideImage = AvatarNodeImageOverride.none
item = nil
}
if let _ = overrideImage {
self.containerNode.isGestureEnabled = false
} else if peer.profileImageRepresentations.isEmpty {
self.containerNode.isGestureEnabled = false
} else {
self.containerNode.isGestureEnabled = false
}
self.avatarNode.imageNode.animateFirstTransition = !isSettings
self.avatarNode.setPeer(context: self.context, theme: theme, peer: EnginePeer(peer), overrideImage: overrideImage, clipStyle: .none, synchronousLoad: self.isFirstAvatarLoading, displayDimensions: CGSize(width: avatarSize, height: avatarSize), storeUnrounded: true)
if let threadInfo = threadInfo {
self.avatarNode.isHidden = true
let iconView: ComponentView<Empty>
if let current = self.iconView {
iconView = current
} else {
iconView = ComponentView()
self.iconView = iconView
}
let content: EmojiStatusComponent.Content
if threadId == 1 {
content = .image(image: PresentationResourcesChat.chatGeneralThreadIcon(theme), tintColor: nil)
} else if let iconFileId = threadInfo.icon {
content = .animation(content: .customEmoji(fileId: iconFileId), size: CGSize(width: avatarSize, height: avatarSize), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .forever)
} else {
content = .topic(title: String(threadInfo.title.prefix(1)), color: threadInfo.iconColor, size: CGSize(width: avatarSize, height: avatarSize))
}
let _ = iconView.update(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: self.context,
animationCache: self.context.animationCache,
animationRenderer: self.context.animationRenderer,
content: content,
isVisibleForAnimations: true,
action: nil
)),
environment: {},
containerSize: CGSize(width: avatarSize, height: avatarSize)
)
if let iconComponentView = iconView.view {
iconComponentView.isUserInteractionEnabled = true
if iconComponentView.superview == nil {
iconComponentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.emojiTapGesture(_:))))
self.avatarNode.view.superview?.addSubview(iconComponentView)
}
iconComponentView.frame = CGRect(origin: CGPoint(), size: CGSize(width: avatarSize, height: avatarSize))
}
}
var isForum = false
let avatarCornerRadius: CGFloat
if let channel = peer as? TelegramChannel, channel.isForumOrMonoForum {
avatarCornerRadius = floor(avatarSize * 0.25)
isForum = true
} else {
avatarCornerRadius = avatarSize / 2.0
}
if self.avatarNode.layer.cornerRadius != 0.0 {
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut).updateCornerRadius(layer: self.avatarNode.contentNode.layer, cornerRadius: avatarCornerRadius)
} else {
self.avatarNode.contentNode.layer.cornerRadius = avatarCornerRadius
}
self.avatarNode.contentNode.layer.masksToBounds = true
self.isFirstAvatarLoading = false
self.containerNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize))
self.avatarNode.frame = self.containerNode.bounds
self.avatarNode.font = avatarPlaceholderFont(size: floor(avatarSize * 16.0 / 37.0))
if let item = item {
let representations: [ImageRepresentationWithReference]
let videoRepresentations: [VideoRepresentationWithReference]
let immediateThumbnailData: Data?
var videoId: Int64
let markup: TelegramMediaImage.EmojiMarkup?
switch item {
case .custom:
representations = []
videoRepresentations = []
immediateThumbnailData = nil
videoId = 0
markup = nil
case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail):
representations = topRepresentations
videoRepresentations = videoRepresentationsValue
immediateThumbnailData = immediateThumbnail
videoId = peer.id.id._internalGetInt64Value()
if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource {
videoId = videoId &+ resource.photoId
}
markup = nil
case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, markupValue):
representations = imageRepresentations
videoRepresentations = videoRepresentationsValue
immediateThumbnailData = immediateThumbnail
if case let .cloud(imageId, _, _) = reference {
videoId = imageId
} else {
videoId = peer.id.id._internalGetInt64Value()
}
markup = markupValue
}
self.containerNode.isGestureEnabled = !isSettings
if let markup {
if let videoNode = self.videoNode {
self.videoContent = nil
self.videoStartTimestamp = nil
self.videoNode = nil
videoNode.removeFromSupernode()
}
let markupNode: AvatarVideoNode
if let current = self.markupNode {
markupNode = current
} else {
markupNode = AvatarVideoNode(context: self.context)
self.avatarNode.contentNode.addSubnode(markupNode)
self.markupNode = markupNode
}
markupNode.update(markup: markup, size: CGSize(width: 320.0, height: 320.0))
markupNode.updateVisibility(true)
} else if threadInfo == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) {
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []))
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil)
if videoContent.id != self.videoContent?.id {
self.videoNode?.removeFromSupernode()
if self.hierarchyTrackingLayer.isInHierarchy {
let mediaManager = self.context.sharedContext.mediaManager
let videoNode = UniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .embedded)
videoNode.isUserInteractionEnabled = false
videoNode.isHidden = true
if let startTimestamp = video.representation.startTimestamp {
self.videoStartTimestamp = startTimestamp
self.playbackStartDisposable.set((videoNode.status
|> map { status -> Bool in
if let status = status, case .playing = status.status {
return true
} else {
return false
}
}
|> filter { playing in
return playing
}
|> take(1)
|> deliverOnMainQueue).start(completed: { [weak self] in
if let strongSelf = self {
Queue.mainQueue().after(0.15) {
strongSelf.videoNode?.isHidden = false
}
}
}))
} else {
self.videoStartTimestamp = nil
self.playbackStartDisposable.set(nil)
videoNode.isHidden = false
}
self.videoContent = videoContent
self.videoNode = videoNode
let maskPath: UIBezierPath
if isForum {
maskPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size), cornerRadius: avatarCornerRadius)
} else {
maskPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size))
}
let shape = CAShapeLayer()
shape.path = maskPath.cgPath
videoNode.layer.mask = shape
self.avatarNode.contentNode.addSubnode(videoNode)
}
}
} else {
if let markupNode = self.markupNode {
self.markupNode = nil
markupNode.removeFromSupernode()
}
if let videoNode = self.videoNode {
self.videoStartTimestamp = nil
self.videoContent = nil
self.videoNode = nil
videoNode.removeFromSupernode()
}
}
} else {
if let markupNode = self.markupNode {
self.markupNode = nil
markupNode.removeFromSupernode()
}
if let videoNode = self.videoNode {
self.videoStartTimestamp = nil
self.videoContent = nil
self.videoNode = nil
videoNode.removeFromSupernode()
}
self.containerNode.isGestureEnabled = false
}
if let markupNode = self.markupNode {
markupNode.frame = self.avatarNode.bounds
markupNode.updateLayout(size: self.avatarNode.bounds.size, cornerRadius: avatarCornerRadius, transition: .immediate)
}
if let videoNode = self.videoNode {
if self.canAttachVideo {
videoNode.updateLayout(size: self.avatarNode.frame.size, transition: .immediate)
}
videoNode.frame = self.avatarNode.contentNode.bounds
if isExpanded == videoNode.canAttachContent {
self.isExpanded = isExpanded
let update = {
videoNode.canAttachContent = !self.isExpanded && self.canAttachVideo
if videoNode.canAttachContent {
videoNode.play()
}
}
if isExpanded {
DispatchQueue.main.async {
update()
}
} else {
update()
}
}
}
}
self.updateStoryView(transition: .immediate, theme: theme, peer: peer)
}
}
@@ -0,0 +1,245 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import ConfettiEffect
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import TelegramStringFormatting
final class PeerInfoBirthdayOverlay: ASDisplayNode {
private let context: AccountContext
private var disposable: Disposable?
init(context: AccountContext) {
self.context = context
super.init()
self.isUserInteractionEnabled = false
}
deinit {
self.disposable?.dispose()
}
func setup(size: CGSize, birthday: TelegramBirthday, sourceRect: CGRect?) {
self.setupAnimations(size: size, birthday: birthday, sourceRect: sourceRect)
Queue.mainQueue().after(0.1) {
self.view.addSubview(ConfettiView(frame: CGRect(origin: .zero, size: size)))
}
}
private func setupAnimations(size: CGSize, birthday: TelegramBirthday, sourceRect: CGRect?) {
self.disposable = (combineLatest(
self.context.engine.stickers.loadedStickerPack(reference: .animatedEmojiAnimations, forceActualized: false),
self.context.engine.stickers.loadedStickerPack(reference: .name("FestiveFontEmoji"), forceActualized: false)
)
|> map { animatedEmoji, numbers -> (FileMediaReference?, [FileMediaReference]) in
var effectFile: FileMediaReference?
if case let .result(_, items, _) = animatedEmoji {
let randomKey = ["🎉", "🎈", "🎆"].randomElement()!
outer: for item in items {
let indexKeys = item.getStringRepresentationsOfIndexKeys()
for key in indexKeys {
if key == randomKey {
effectFile = .stickerPack(stickerPack: .animatedEmojiAnimations, media: item.file._parse())
break outer
}
}
}
}
var numberFiles: [FileMediaReference] = []
if let age = ageForBirthday(birthday), case let .result(info, items, _) = numbers {
let ageKeys = ageToKeys(age)
for ageKey in ageKeys {
for item in items {
let indexKeys = item.getStringRepresentationsOfIndexKeys()
for key in indexKeys {
if key == ageKey {
numberFiles.append(.stickerPack(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), media: item.file._parse()))
}
}
}
}
}
return (effectFile, numberFiles)
}
|> deliverOnMainQueue).start(next: { [weak self] effectAndNumberFiles in
guard let self else {
return
}
let (effectFile, numberFiles) = effectAndNumberFiles
if let effectFile {
let _ = freeMediaFileInteractiveFetched(account: self.context.account, userLocation: .peer(self.context.account.peerId), fileReference: effectFile).startStandalone()
self.setupEffectAnimation(size: size, file: effectFile, sourceRect: sourceRect)
}
for file in numberFiles {
let _ = freeMediaFileInteractiveFetched(account: self.context.account, userLocation: .peer(self.context.account.peerId), fileReference: file).startStandalone()
}
self.setupNumberAnimations(size: size, files: numberFiles, sourceRect: sourceRect)
})
}
private func setupEffectAnimation(size: CGSize, file: FileMediaReference, sourceRect: CGRect?) {
guard let dimensions = file.media.dimensions else {
return
}
let minSide = min(size.width, size.height)
let pixelSize = dimensions.cgSize.aspectFitted(CGSize(width: 512.0, height: 512.0))
let animationSize = dimensions.cgSize.aspectFitted(CGSize(width: minSide, height: minSide))
let animationNode = DefaultAnimatedStickerNodeImpl()
let source = AnimatedStickerResourceSource(account: self.context.account, resource: file.media.resource, fitzModifier: nil)
let pathPrefix = self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.media.resource.id)
animationNode.setup(source: source, width: Int(pixelSize.width), height: Int(pixelSize.height), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix))
self.addSubnode(animationNode)
let startY = sourceRect?.midY ?? size.height / 2.0
animationNode.updateLayout(size: animationSize)
animationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: startY - animationSize.height / 2.0), size: animationSize)
animationNode.visibility = true
}
private func setupNumberAnimations(size: CGSize, files: [FileMediaReference], sourceRect: CGRect?) {
guard !files.isEmpty else {
return
}
let startY = sourceRect?.midY ?? 475.0
var offset: CGFloat = 0.0
var scaleDelay: Double = 0.0
if files.count > 1 {
offset -= 45.0
}
for file in files {
guard let dimensions = file.media.dimensions else {
continue
}
let animationSize = dimensions.cgSize.aspectFitted(CGSize(width: 144.0, height: 144.0))
let pixelSize = dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0))
let animationNode = DefaultAnimatedStickerNodeImpl()
let source = AnimatedStickerResourceSource(account: self.context.account, resource: file.media.resource, fitzModifier: nil)
let pathPrefix: String? = self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.media.resource.id)
animationNode.setup(source: source, width: Int(pixelSize.width), height: Int(pixelSize.height), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix))
self.addSubnode(animationNode)
animationNode.updateLayout(size: animationSize)
animationNode.bounds = CGRect(origin: .zero, size: animationSize)
animationNode.transform = CATransform3DMakeScale(0.001, 0.001, 1.0)
animationNode.visibility = true
let path = UIBezierPath()
let startPoint = CGPoint(x: 90.0 + offset * 0.7, y: startY + CGFloat.random(in: -20.0 ..< 20.0))
animationNode.position = startPoint
path.move(to: startPoint)
path.addCurve(to: CGPoint(x: 205.0 + offset, y: -90.0), controlPoint1: CGPoint(x: 213.0 + offset * 0.8, y: 380.0 + CGFloat.random(in: -20.0 ..< 20.0)), controlPoint2: CGPoint(x: 206.0 + offset * 0.8, y: 134.0 + CGFloat.random(in: -20.0 ..< 20.0)))
let riseAnimation = CAKeyframeAnimation(keyPath: "position")
riseAnimation.path = path.cgPath
riseAnimation.duration = 2.2
riseAnimation.calculationMode = .cubic
riseAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn)
riseAnimation.beginTime = CACurrentMediaTime() + 0.5
riseAnimation.isRemovedOnCompletion = false
riseAnimation.fillMode = .forwards
riseAnimation.completion = { [weak self] _ in
self?.removeFromSupernode()
}
animationNode.layer.add(riseAnimation, forKey: "position")
offset += 132.0
let scaleAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
scaleAnimation.duration = 2.0
scaleAnimation.values = [0.0, 0.45, 1.0]
scaleAnimation.keyTimes = [0.0, 0.3, 1.0]
scaleAnimation.beginTime = CACurrentMediaTime() + scaleDelay
scaleAnimation.isRemovedOnCompletion = false
scaleAnimation.fillMode = .forwards
animationNode.layer.add(scaleAnimation, forKey: "scale")
scaleDelay += 0.3
}
}
static func preloadBirthdayAnimations(context: AccountContext, birthday: TelegramBirthday) {
let preload = combineLatest(
context.engine.stickers.loadedStickerPack(reference: .animatedEmojiAnimations, forceActualized: false),
context.engine.stickers.loadedStickerPack(reference: .name("FestiveFontEmoji"), forceActualized: false)
)
|> mapToSignal { animatedEmoji, numbers -> Signal<Never, NoError> in
var signals: [Signal<FetchResourceSourceType, FetchResourceError>] = []
if case let .result(_, items, _) = animatedEmoji {
for item in items {
let indexKeys = item.getStringRepresentationsOfIndexKeys()
for key in indexKeys {
if ["🎉", "🎈", "🎆"].contains(key) {
signals.append(freeMediaFileInteractiveFetched(account: context.account, userLocation: .peer(context.account.peerId), fileReference: .stickerPack(stickerPack: .animatedEmojiAnimations, media: item.file._parse())))
}
}
}
}
if let age = ageForBirthday(birthday), case let .result(info, items, _) = numbers {
let ageKeys = ageToKeys(age)
for item in items {
let indexKeys = item.getStringRepresentationsOfIndexKeys()
for key in indexKeys {
if ageKeys.contains(key) {
signals.append(freeMediaFileInteractiveFetched(account: context.account, userLocation: .peer(context.account.peerId), fileReference: .stickerPack(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), media: item.file._parse())))
}
}
}
}
if !signals.isEmpty {
return combineLatest(signals)
|> `catch` { _ in
return .complete()
}
|> ignoreValues
} else {
return .complete()
}
}
let _ = preload.startStandalone()
}
}
private func ageToKeys(_ age: Int) -> [String] {
return "\(age)".map { String($0) }.map {
switch $0 {
case "0":
return "0️⃣"
case "1":
return "1️⃣"
case "2":
return "2️⃣"
case "3":
return "3️⃣"
case "4":
return "4️⃣"
case "5":
return "5️⃣"
case "6":
return "6️⃣"
case "7":
return "7️⃣"
case "8":
return "8️⃣"
case "9":
return "9️⃣"
default:
return $0
}
}
}
@@ -0,0 +1,235 @@
import Foundation
import UIKit
import AsyncDisplayKit
import TelegramPresentationData
import AccountContext
import AvatarNode
import UniversalMediaPlayer
import PeerInfoAvatarListNode
import AvatarVideoNode
import TelegramUniversalVideoContent
import SwiftSignalKit
import Postbox
import TelegramCore
import Display
import GalleryUI
final class PeerInfoEditingAvatarNode: ASDisplayNode {
private let context: AccountContext
let avatarNode: AvatarNode
fileprivate var videoNode: UniversalVideoNode?
fileprivate var markupNode: AvatarVideoNode?
private var videoContent: NativeVideoContent?
private var videoStartTimestamp: Double?
var item: PeerInfoAvatarListItem?
var tapped: ((Bool) -> Void)?
var canAttachVideo: Bool = true
init(context: AccountContext) {
self.context = context
let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0))
self.avatarNode = AvatarNode(font: avatarFont)
super.init()
self.addSubnode(self.avatarNode)
self.avatarNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0))
self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.tapped?(false)
}
}
func reset() {
guard let videoNode = self.videoNode else {
return
}
videoNode.isHidden = true
videoNode.seek(self.videoStartTimestamp ?? 0.0)
Queue.mainQueue().after(0.15) {
videoNode.isHidden = false
}
}
var removedPhotoResourceIds = Set<String>()
func update(peer: Peer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, item: PeerInfoAvatarListItem?, updatingAvatar: PeerInfoUpdatingAvatar?, uploadProgress: AvatarUploadProgress?, theme: PresentationTheme, avatarSize: CGFloat, isEditing: Bool) {
guard let peer = peer else {
return
}
let canEdit = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData)
let previousItem = self.item
var item = item
self.item = item
let overrideImage: AvatarNodeImageOverride?
if canEdit, peer.profileImageRepresentations.isEmpty {
overrideImage = .editAvatarIcon(forceNone: true)
} else if let previousItem = previousItem, item == nil {
if case let .image(_, representations, _, _, _, _) = previousItem, let rep = representations.last {
self.removedPhotoResourceIds.insert(rep.representation.resource.id.stringRepresentation)
}
overrideImage = canEdit ? .editAvatarIcon(forceNone: true) : AvatarNodeImageOverride.none
item = nil
} else if let representation = peer.profileImageRepresentations.last, self.removedPhotoResourceIds.contains(representation.resource.id.stringRepresentation) {
overrideImage = canEdit ? .editAvatarIcon(forceNone: true) : AvatarNodeImageOverride.none
item = nil
} else {
overrideImage = item == nil && canEdit ? .editAvatarIcon(forceNone: true) : nil
}
self.avatarNode.font = avatarPlaceholderFont(size: floor(avatarSize * 16.0 / 37.0))
self.avatarNode.setPeer(context: self.context, theme: theme, peer: EnginePeer(peer), overrideImage: overrideImage, clipStyle: .none, synchronousLoad: false, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
self.avatarNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize))
var isForum = false
let avatarCornerRadius: CGFloat
if let channel = peer as? TelegramChannel, channel.isForumOrMonoForum {
isForum = true
avatarCornerRadius = floor(avatarSize * 0.25)
} else {
avatarCornerRadius = avatarSize / 2.0
}
if self.avatarNode.layer.cornerRadius != 0.0 {
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut).updateCornerRadius(layer: self.avatarNode.layer, cornerRadius: avatarCornerRadius)
} else {
self.avatarNode.layer.cornerRadius = avatarCornerRadius
}
self.avatarNode.layer.masksToBounds = true
if let item = item {
let representations: [ImageRepresentationWithReference]
let videoRepresentations: [VideoRepresentationWithReference]
let immediateThumbnailData: Data?
var videoId: Int64
let markup: TelegramMediaImage.EmojiMarkup?
switch item {
case .custom:
representations = []
videoRepresentations = []
immediateThumbnailData = nil
videoId = 0
markup = nil
case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail):
representations = topRepresentations
videoRepresentations = videoRepresentationsValue
immediateThumbnailData = immediateThumbnail
videoId = peer.id.id._internalGetInt64Value()
if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource {
videoId = videoId &+ resource.photoId
}
markup = nil
case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, markupValue):
representations = imageRepresentations
videoRepresentations = videoRepresentationsValue
immediateThumbnailData = immediateThumbnail
if case let .cloud(imageId, _, _) = reference {
videoId = imageId
} else {
videoId = peer.id.id._internalGetInt64Value()
}
markup = markupValue
}
if let markup {
if let videoNode = self.videoNode {
self.videoContent = nil
self.videoStartTimestamp = nil
self.videoNode = nil
videoNode.removeFromSupernode()
}
let markupNode: AvatarVideoNode
if let current = self.markupNode {
markupNode = current
} else {
markupNode = AvatarVideoNode(context: self.context)
self.avatarNode.contentNode.addSubnode(markupNode)
self.markupNode = markupNode
}
markupNode.update(markup: markup, size: CGSize(width: 320.0, height: 320.0))
markupNode.updateVisibility(true)
} else if threadData == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) {
if let markupNode = self.markupNode {
self.markupNode = nil
markupNode.removeFromSupernode()
}
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []))
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil)
if videoContent.id != self.videoContent?.id {
self.videoNode?.removeFromSupernode()
let mediaManager = self.context.sharedContext.mediaManager
let videoNode = UniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .gallery)
videoNode.isUserInteractionEnabled = false
self.videoStartTimestamp = video.representation.startTimestamp
self.videoContent = videoContent
self.videoNode = videoNode
let maskPath: UIBezierPath
if isForum {
maskPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size), cornerRadius: avatarCornerRadius)
} else {
maskPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size))
}
let shape = CAShapeLayer()
shape.path = maskPath.cgPath
videoNode.layer.mask = shape
self.avatarNode.contentNode.addSubnode(videoNode)
}
} else {
if let markupNode = self.markupNode {
self.markupNode = nil
markupNode.removeFromSupernode()
}
if let videoNode = self.videoNode {
self.videoStartTimestamp = nil
self.videoContent = nil
self.videoNode = nil
DispatchQueue.main.async {
videoNode.removeFromSupernode()
}
}
}
} else if let videoNode = self.videoNode {
self.videoStartTimestamp = nil
self.videoContent = nil
self.videoNode = nil
videoNode.removeFromSupernode()
}
if let markupNode = self.markupNode {
markupNode.frame = self.avatarNode.bounds
markupNode.updateLayout(size: self.avatarNode.bounds.size, cornerRadius: avatarCornerRadius, transition: .immediate)
}
if let videoNode = self.videoNode {
if self.canAttachVideo {
videoNode.updateLayout(size: self.avatarNode.bounds.size, transition: .immediate)
}
videoNode.frame = self.avatarNode.bounds
if isEditing != videoNode.canAttachContent {
videoNode.canAttachContent = isEditing && self.canAttachVideo
}
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.avatarNode.frame.contains(point) {
return self.avatarNode.view
}
return super.hitTest(point, with: event)
}
}
@@ -0,0 +1,149 @@
import Foundation
import UIKit
import AsyncDisplayKit
import TelegramPresentationData
import AccountContext
import Display
import RadialStatusNode
import Postbox
import TelegramCore
import PeerInfoAvatarListNode
import AvatarNode
import SwiftSignalKit
final class PeerInfoEditingAvatarOverlayNode: ASDisplayNode {
private let context: AccountContext
private let imageNode: ImageNode
private let updatingAvatarOverlay: ASImageNode
private let iconNode: ASImageNode
private var statusNode: RadialStatusNode
private var currentRepresentation: TelegramMediaImageRepresentation?
init(context: AccountContext) {
self.context = context
self.imageNode = ImageNode(enableEmpty: true)
self.updatingAvatarOverlay = ASImageNode()
self.updatingAvatarOverlay.displayWithoutProcessing = true
self.updatingAvatarOverlay.displaysAsynchronously = false
self.updatingAvatarOverlay.alpha = 0.0
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.6))
self.statusNode.isUserInteractionEnabled = false
self.iconNode = ASImageNode()
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIconLarge"), color: .white)
self.iconNode.alpha = 0.0
super.init()
self.imageNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0))
self.updatingAvatarOverlay.frame = self.imageNode.frame
let radialStatusSize: CGFloat = 50.0
let imagePosition = self.imageNode.position
self.statusNode.frame = CGRect(origin: CGPoint(x: floor(imagePosition.x - radialStatusSize / 2.0), y: floor(imagePosition.y - radialStatusSize / 2.0)), size: CGSize(width: radialStatusSize, height: radialStatusSize))
if let image = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor(imagePosition.x - image.size.width / 2.0), y: floor(imagePosition.y - image.size.height / 2.0)), size: image.size)
}
self.addSubnode(self.imageNode)
self.addSubnode(self.updatingAvatarOverlay)
self.addSubnode(self.statusNode)
}
func updateTransitionFraction(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self, alpha: 1.0 - fraction)
}
func update(peer: Peer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, item: PeerInfoAvatarListItem?, updatingAvatar: PeerInfoUpdatingAvatar?, uploadProgress: AvatarUploadProgress?, theme: PresentationTheme, avatarSize: CGFloat, isEditing: Bool) {
guard let peer = peer else {
return
}
self.imageNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize))
self.updatingAvatarOverlay.frame = self.imageNode.frame
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear)
let clipStyle: AvatarNodeClipStyle
if let channel = peer as? TelegramChannel, channel.isForumOrMonoForum {
clipStyle = .roundedRect
} else {
clipStyle = .round
}
var isPersonal = false
if let updatingAvatar, case let .image(image) = updatingAvatar, image.isPersonal {
isPersonal = true
}
if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData)
|| isPersonal
|| self.currentRepresentation != nil && updatingAvatar == nil {
var overlayHidden = true
if let updatingAvatar = updatingAvatar {
overlayHidden = false
var cancelEnabled = true
let progressValue: CGFloat?
if let uploadProgress {
switch uploadProgress {
case let .value(value):
progressValue = max(0.027, value)
case .indefinite:
progressValue = nil
cancelEnabled = false
}
} else {
progressValue = 0.027
}
self.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: progressValue, cancelEnabled: cancelEnabled, animateRotation: true))
if case let .image(representation) = updatingAvatar {
if representation != self.currentRepresentation {
self.currentRepresentation = representation
if let signal = peerAvatarImage(account: context.account, peerReference: nil, authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: avatarSize, height: avatarSize), clipStyle: clipStyle, emptyColor: nil, synchronousLoad: false, provideUnrounded: false) {
self.imageNode.setSignal(signal |> map { $0?.0 })
}
}
}
transition.updateAlpha(node: self.updatingAvatarOverlay, alpha: 1.0)
} else {
let targetOverlayAlpha: CGFloat = 0.0
if self.updatingAvatarOverlay.alpha != targetOverlayAlpha {
let update = {
self.statusNode.transitionToState(.none)
self.currentRepresentation = nil
self.imageNode.setSignal(.single(nil))
transition.updateAlpha(node: self.updatingAvatarOverlay, alpha: overlayHidden ? 0.0 : 1.0)
}
Queue.mainQueue().after(0.3) {
update()
}
}
}
if !overlayHidden && self.updatingAvatarOverlay.image == nil {
switch clipStyle {
case .round:
self.updatingAvatarOverlay.image = generateFilledCircleImage(diameter: avatarSize, color: UIColor(white: 0.0, alpha: 0.4), backgroundColor: nil)
case .roundedRect:
self.updatingAvatarOverlay.image = generateFilledRoundedRectImage(size: CGSize(width: avatarSize, height: avatarSize), cornerRadius: avatarSize * 0.25, color: UIColor(white: 0.0, alpha: 0.4), backgroundColor: nil)
default:
break
}
}
} else {
self.statusNode.transitionToState(.none)
self.currentRepresentation = nil
transition.updateAlpha(node: self.iconNode, alpha: 0.0)
transition.updateAlpha(node: self.updatingAvatarOverlay, alpha: 0.0)
}
}
}
@@ -0,0 +1,88 @@
import Foundation
import UIKit
import AsyncDisplayKit
import ContextUI
import TelegramPresentationData
import Display
final class PeerInfoHeaderActionButtonNode: HighlightableButtonNode {
let key: PeerInfoHeaderButtonKey
private let action: (PeerInfoHeaderActionButtonNode, ContextGesture?) -> Void
let referenceNode: ContextReferenceContentNode
let containerNode: ContextControllerSourceNode
private let backgroundNode: ASDisplayNode
private let textNode: ImmediateTextNode
private var theme: PresentationTheme?
init(key: PeerInfoHeaderButtonKey, action: @escaping (PeerInfoHeaderActionButtonNode, ContextGesture?) -> Void) {
self.key = key
self.action = action
self.referenceNode = ContextReferenceContentNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.animateScale = false
self.backgroundNode = ASDisplayNode()
self.backgroundNode.cornerRadius = 11.0
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
super.init()
self.accessibilityTraits = .button
self.containerNode.addSubnode(self.referenceNode)
self.referenceNode.addSubnode(self.backgroundNode)
self.addSubnode(self.containerNode)
self.addSubnode(self.textNode)
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.layer.removeAnimation(forKey: "opacity")
strongSelf.alpha = 0.4
} else {
strongSelf.alpha = 1.0
strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.containerNode.activated = { [weak self] gesture, _ in
if let strongSelf = self {
strongSelf.action(strongSelf, gesture)
}
}
self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
@objc private func buttonPressed() {
self.action(self, nil)
}
func update(size: CGSize, text: String, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
let themeUpdated = self.theme != presentationData.theme
if themeUpdated {
self.theme = presentationData.theme
self.containerNode.isGestureEnabled = false
self.backgroundNode.backgroundColor = presentationData.theme.list.itemAccentColor
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
}
self.textNode.attributedText = NSAttributedString(string: text, font: Font.semibold(16.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor)
self.accessibilityLabel = text
let titleSize = self.textNode.updateLayout(CGSize(width: 120.0, height: .greatestFiniteMagnitude))
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrameAdditiveToCenter(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize))
self.referenceNode.frame = self.containerNode.bounds
}
}
@@ -0,0 +1,286 @@
import Foundation
import UIKit
import AsyncDisplayKit
import ContextUI
import AnimationUI
import Display
import TelegramPresentationData
import ComponentFlow
import LottieComponent
enum PeerInfoHeaderButtonKey: Hashable {
case message
case discussion
case call
case videoCall
case voiceChat
case mute
case more
case addMember
case search
case leave
case stop
case addContact
}
enum PeerInfoHeaderButtonIcon {
case message
case call
case videoCall
case voiceChat
case mute
case unmute
case more
case addMember
case search
case leave
case stop
}
final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
let key: PeerInfoHeaderButtonKey
private let action: (PeerInfoHeaderButtonNode, ContextGesture?) -> Void
let referenceNode: ContextReferenceContentNode
let containerNode: ContextControllerSourceNode
//private let backgroundNode: NavigationBackgroundNode
private let contentNode: ASDisplayNode
private let iconNode: ASImageNode
private let textNode: ImmediateTextNode
private var animatedIcon: ComponentView<Empty>?
private var theme: PresentationTheme?
private var icon: PeerInfoHeaderButtonIcon?
private var isActive: Bool?
let backgroundContainerView: UIView
let backgroundView: UIView
init(key: PeerInfoHeaderButtonKey, action: @escaping (PeerInfoHeaderButtonNode, ContextGesture?) -> Void) {
self.key = key
self.action = action
self.referenceNode = ContextReferenceContentNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.animateScale = false
self.backgroundContainerView = UIView()
self.backgroundView = UIView()
self.backgroundView.backgroundColor = .white
self.backgroundContainerView.addSubview(self.backgroundView)
/*self.backgroundNode = NavigationBackgroundNode(color: UIColor(white: 1.0, alpha: 0.2), enableBlur: true, enableSaturation: false)
self.backgroundNode.isUserInteractionEnabled = false*/
self.contentNode = ASDisplayNode()
self.contentNode.isUserInteractionEnabled = false
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.iconNode.isUserInteractionEnabled = false
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
super.init()
self.accessibilityTraits = .button
self.containerNode.addSubnode(self.referenceNode)
//self.referenceNode.addSubnode(self.backgroundNode)
self.referenceNode.addSubnode(self.contentNode)
self.contentNode.addSubnode(self.iconNode)
self.addSubnode(self.containerNode)
self.contentNode.addSubnode(self.textNode)
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.layer.removeAnimation(forKey: "opacity")
strongSelf.alpha = 0.4
} else {
strongSelf.alpha = 1.0
strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.containerNode.activated = { [weak self] gesture, _ in
if let strongSelf = self {
strongSelf.action(strongSelf, gesture)
}
}
self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
@objc private func buttonPressed() {
switch self.icon {
case .voiceChat, .more, .leave:
if let animatedIconView = self.animatedIcon?.view as? LottieComponent.View {
animatedIconView.playOnce()
}
default:
break
}
self.action(self, nil)
}
func update(size: CGSize, text: String, icon: PeerInfoHeaderButtonIcon, isActive: Bool, presentationData: PresentationData, backgroundColor: UIColor, foregroundColor: UIColor, fraction: CGFloat, transition: ContainedViewLayoutTransition) {
let previousIcon = self.icon
let themeUpdated = self.theme != presentationData.theme
let iconUpdated = self.icon != icon
let isActiveUpdated = self.isActive != isActive
self.isActive = isActive
let iconSize = CGSize(width: 40.0, height: 40.0)
if themeUpdated || iconUpdated {
self.theme = presentationData.theme
self.icon = icon
var isGestureEnabled = false
if [.mute, .voiceChat, .more].contains(icon) {
isGestureEnabled = true
}
self.containerNode.isGestureEnabled = isGestureEnabled
let iconColor = UIColor.white
self.iconNode.image = generateImage(iconSize, contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.normal)
context.setFillColor(iconColor.cgColor)
let imageName: String?
switch icon {
case .message:
imageName = "Peer Info/ButtonMessage"
case .call:
imageName = "Peer Info/ButtonCall"
case .videoCall:
imageName = "Peer Info/ButtonVideo"
case .voiceChat:
imageName = nil
case .mute:
imageName = nil
case .unmute:
imageName = nil
case .more:
imageName = nil
case .addMember:
imageName = "Peer Info/ButtonAddMember"
case .search:
imageName = "Peer Info/ButtonSearch"
case .leave:
imageName = nil
case .stop:
imageName = "Peer Info/ButtonStop"
}
if let imageName = imageName, let image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: .white) {
let imageRect = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
context.clip(to: imageRect, mask: image.cgImage!)
context.fill(imageRect)
}
})
}
let animationName: String?
var playOnce = false
var seekToEnd = false
switch icon {
case .voiceChat:
animationName = "anim_profilevc"
case .mute:
animationName = "anim_profileunmute"
if previousIcon == .unmute {
playOnce = true
} else {
seekToEnd = true
}
case .unmute:
animationName = "anim_profilemute"
if previousIcon == .mute {
playOnce = true
} else {
seekToEnd = true
}
case .more:
animationName = "anim_profilemore"
case .leave:
animationName = "anim_profileleave"
default:
animationName = nil
}
if let animationName = animationName {
let animatedIcon: ComponentView<Empty>
if let current = self.animatedIcon {
animatedIcon = current
} else {
animatedIcon = ComponentView()
self.animatedIcon = animatedIcon
}
let _ = animatedIcon.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: animationName),
color: foregroundColor,
startingPosition: seekToEnd ? .end : .begin
)),
environment: {},
containerSize: iconSize
)
} else if let animatedIcon = self.animatedIcon {
self.animatedIcon = nil
animatedIcon.view?.removeFromSuperview()
}
if let animatedIconView = self.animatedIcon?.view as? LottieComponent.View {
if animatedIconView.superview == nil {
self.contentNode.view.addSubview(animatedIconView)
}
if playOnce {
animatedIconView.playOnce()
}
}
transition.updateTintColor(layer: self.iconNode.layer, color: foregroundColor)
transition.updateTintColor(layer: self.titleNode.layer, color: foregroundColor)
transition.updateTintColor(layer: self.textNode.layer, color: foregroundColor)
if isActiveUpdated {
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
alphaTransition.updateAlpha(node: self.iconNode, alpha: isActive ? 1.0 : 0.3)
if let animatedIconView = self.animatedIcon?.view {
alphaTransition.updateAlpha(layer: animatedIconView.layer, alpha: isActive ? 1.0 : 0.3)
}
alphaTransition.updateAlpha(node: self.textNode, alpha: isActive ? 1.0 : 0.3)
}
self.textNode.attributedText = NSAttributedString(string: text.lowercased(), font: Font.regular(11.0), textColor: .white)
self.accessibilityLabel = text
let titleSize = self.textNode.updateLayout(CGSize(width: 120.0, height: .greatestFiniteMagnitude))
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height * 0.5 * (1.0 - fraction)), size: size))
transition.updateAlpha(node: self.contentNode, alpha: fraction)
let backgroundY: CGFloat = size.height * (1.0 - fraction)
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: backgroundY), size: CGSize(width: size.width, height: max(0.0, size.height - backgroundY)))
//transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
transition.updateFrame(view: self.backgroundView, frame: backgroundFrame)
transition.updateSublayerTransformScale(node: self.contentNode, scale: 1.0 * fraction + 0.001 * (1.0 - fraction))
transition.updateCornerRadius(layer: self.backgroundView.layer, cornerRadius: min(16.0, backgroundFrame.height * 0.5))
//self.backgroundNode.update(size: backgroundFrame.size, cornerRadius: min(11.0, backgroundFrame.height * 0.5), transition: transition)
//self.backgroundNode.updateColor(color: backgroundColor, transition: transition)
transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: 1.0), size: iconSize))
if let animatedIconView = self.animatedIcon?.view {
transition.updateFrame(view: animatedIconView, frame: CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: 1.0), size: iconSize))
}
transition.updateFrameAdditiveToCenter(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: size.height - titleSize.height - 9.0), size: titleSize))
self.referenceNode.frame = self.containerNode.bounds
}
}
@@ -0,0 +1,183 @@
import Foundation
import UIKit
import AsyncDisplayKit
import TelegramPresentationData
import AccountContext
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
final class PeerInfoHeaderEditingContentNode: ASDisplayNode {
private let context: AccountContext
private let requestUpdateLayout: () -> Void
var requestEditing: (() -> Void)?
let avatarNode: PeerInfoEditingAvatarNode
let avatarTextNode: ImmediateTextNode
let avatarButtonNode: HighlightableButtonNode
var itemNodes: [PeerInfoHeaderTextFieldNodeKey: PeerInfoHeaderTextFieldNode] = [:]
init(context: AccountContext, requestUpdateLayout: @escaping () -> Void) {
self.context = context
self.requestUpdateLayout = requestUpdateLayout
self.avatarNode = PeerInfoEditingAvatarNode(context: context)
self.avatarTextNode = ImmediateTextNode()
self.avatarButtonNode = HighlightableButtonNode()
super.init()
self.addSubnode(self.avatarNode)
self.avatarButtonNode.addSubnode(self.avatarTextNode)
self.avatarButtonNode.addTarget(self, action: #selector(textPressed), forControlEvents: .touchUpInside)
}
@objc private func textPressed() {
self.requestEditing?()
}
func editingTextForKey(_ key: PeerInfoHeaderTextFieldNodeKey) -> String? {
return self.itemNodes[key]?.text
}
func shakeTextForKey(_ key: PeerInfoHeaderTextFieldNodeKey) {
self.itemNodes[key]?.layer.addShakeAnimation()
}
func update(width: CGFloat, safeInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, peer: Peer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, cachedData: CachedPeerData?, isContact: Bool, isSettings: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat {
let avatarSize: CGFloat = isModalOverlay ? 200.0 : 100.0
let avatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize) / 2.0), y: statusBarHeight + 22.0), size: CGSize(width: avatarSize, height: avatarSize))
transition.updateFrameAdditiveToCenter(node: self.avatarNode, frame: CGRect(origin: avatarFrame.center, size: CGSize()))
var contentHeight: CGFloat = statusBarHeight + 10.0 + avatarSize + 28.0
if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) {
if self.avatarButtonNode.supernode == nil {
self.addSubnode(self.avatarButtonNode)
}
self.avatarTextNode.attributedText = NSAttributedString(string: presentationData.strings.Settings_SetNewProfilePhotoOrVideo, font: Font.regular(17.0), textColor: presentationData.theme.list.itemAccentColor)
self.avatarButtonNode.accessibilityLabel = self.avatarTextNode.attributedText?.string
let avatarTextSize = self.avatarTextNode.updateLayout(CGSize(width: width, height: 32.0))
transition.updateFrame(node: self.avatarTextNode, frame: CGRect(origin: CGPoint(), size: avatarTextSize))
transition.updateFrame(node: self.avatarButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - avatarTextSize.width) / 2.0), y: contentHeight - 5.0), size: avatarTextSize))
contentHeight += 32.0
}
var isEditableBot = false
if let user = peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) {
isEditableBot = true
}
var fieldKeys: [PeerInfoHeaderTextFieldNodeKey] = []
if let user = peer as? TelegramUser {
if !user.isDeleted {
fieldKeys.append(.firstName)
if isEditableBot {
fieldKeys.append(.description)
} else if user.botInfo == nil {
fieldKeys.append(.lastName)
}
}
} else if let _ = peer as? TelegramGroup {
fieldKeys.append(.title)
if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) {
fieldKeys.append(.description)
}
} else if let _ = peer as? TelegramChannel {
fieldKeys.append(.title)
if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) {
fieldKeys.append(.description)
}
}
var hasPrevious = false
for key in fieldKeys {
let itemNode: PeerInfoHeaderTextFieldNode
var updateText: String?
if let current = self.itemNodes[key] {
itemNode = current
} else {
var isMultiline = false
switch key {
case .firstName:
if let peer = peer as? TelegramUser {
if let editableBotInfo = (cachedData as? CachedUserData)?.editableBotInfo {
updateText = editableBotInfo.name
} else {
updateText = peer.firstName ?? ""
}
}
case .lastName:
updateText = (peer as? TelegramUser)?.lastName ?? ""
case .title:
updateText = peer?.debugDisplayTitle ?? ""
case .description:
isMultiline = true
if let cachedData = cachedData as? CachedChannelData {
updateText = cachedData.about ?? ""
} else if let cachedData = cachedData as? CachedGroupData {
updateText = cachedData.about ?? ""
} else if let cachedData = cachedData as? CachedUserData {
if let editableBotInfo = cachedData.editableBotInfo {
updateText = editableBotInfo.about
} else {
updateText = cachedData.about ?? ""
}
} else {
updateText = ""
}
}
if isMultiline {
itemNode = PeerInfoHeaderMultiLineTextFieldNode(requestUpdateHeight: { [weak self] in
self?.requestUpdateLayout()
})
} else {
itemNode = PeerInfoHeaderSingleLineTextFieldNode()
}
self.itemNodes[key] = itemNode
self.addSubnode(itemNode)
}
let placeholder: String
var isEnabled = true
switch key {
case .firstName:
placeholder = isEditableBot ? presentationData.strings.UserInfo_BotNamePlaceholder : presentationData.strings.UserInfo_FirstNamePlaceholder
isEnabled = isContact || isSettings || isEditableBot
case .lastName:
placeholder = presentationData.strings.UserInfo_LastNamePlaceholder
isEnabled = isContact || isSettings
case .title:
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
placeholder = presentationData.strings.GroupInfo_ChannelListNamePlaceholder
} else {
placeholder = presentationData.strings.GroupInfo_GroupNamePlaceholder
}
isEnabled = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData)
case .description:
placeholder = presentationData.strings.Channel_Edit_AboutItem
isEnabled = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) || isEditableBot
}
let itemHeight = itemNode.update(width: width, safeInset: safeInset, isSettings: isSettings, hasPrevious: hasPrevious, hasNext: key != fieldKeys.last, placeholder: placeholder, isEnabled: isEnabled, presentationData: presentationData, updateText: updateText)
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight)))
contentHeight += itemHeight
hasPrevious = true
}
var removeKeys: [PeerInfoHeaderTextFieldNodeKey] = []
for (key, _) in self.itemNodes {
if !fieldKeys.contains(key) {
removeKeys.append(key)
}
}
for key in removeKeys {
if let itemNode = self.itemNodes.removeValue(forKey: key) {
itemNode.removeFromSupernode()
}
}
return contentHeight
}
}
@@ -0,0 +1,229 @@
import Foundation
import UIKit
import AsyncDisplayKit
import ContextUI
import TelegramPresentationData
import Display
import TelegramUIPreferences
final class PeerInfoHeaderMultiLineTextFieldNode: ASDisplayNode, PeerInfoHeaderTextFieldNode, ASEditableTextNodeDelegate {
private let backgroundNode: ASDisplayNode
private let textNode: EditableTextNode
private let textNodeContainer: ASDisplayNode
private let measureTextNode: ImmediateTextNode
private let clearIconNode: ASImageNode
private let clearButtonNode: HighlightableButtonNode
private let topSeparator: ASDisplayNode
private let maskNode: ASImageNode
private let requestUpdateHeight: () -> Void
private var fontSize: PresentationFontSize?
private var theme: PresentationTheme?
private var currentParams: (width: CGFloat, safeInset: CGFloat)?
private var currentMeasuredHeight: CGFloat?
var text: String {
return self.textNode.attributedText?.string ?? ""
}
init(requestUpdateHeight: @escaping () -> Void) {
self.requestUpdateHeight = requestUpdateHeight
self.backgroundNode = ASDisplayNode()
self.textNode = EditableTextNode()
self.textNode.clipsToBounds = false
self.textNode.textView.clipsToBounds = false
self.textNode.textContainerInset = UIEdgeInsets()
self.textNodeContainer = ASDisplayNode()
self.measureTextNode = ImmediateTextNode()
self.measureTextNode.maximumNumberOfLines = 0
self.measureTextNode.isUserInteractionEnabled = false
self.measureTextNode.lineSpacing = 0.1
self.topSeparator = ASDisplayNode()
self.clearIconNode = ASImageNode()
self.clearIconNode.isLayerBacked = true
self.clearIconNode.displayWithoutProcessing = true
self.clearIconNode.displaysAsynchronously = false
self.clearIconNode.isHidden = true
self.clearButtonNode = HighlightableButtonNode()
self.clearButtonNode.isHidden = true
self.clearButtonNode.isAccessibilityElement = false
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.backgroundNode)
self.textNodeContainer.addSubnode(self.textNode)
self.addSubnode(self.textNodeContainer)
self.addSubnode(self.clearIconNode)
self.addSubnode(self.clearButtonNode)
self.addSubnode(self.topSeparator)
self.addSubnode(self.maskNode)
self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside)
self.clearButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.clearIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.clearIconNode.alpha = 0.4
} else {
strongSelf.clearIconNode.alpha = 1.0
strongSelf.clearIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
@objc private func clearButtonPressed() {
guard let theme = self.theme else {
return
}
let font: UIFont
if let fontSize = self.fontSize {
font = Font.regular(fontSize.itemListBaseFontSize)
} else {
font = Font.regular(17.0)
}
let attributedText = NSAttributedString(string: "", font: font, textColor: theme.list.itemPrimaryTextColor)
self.textNode.attributedText = attributedText
self.requestUpdateHeight()
self.updateClearButtonVisibility()
}
func update(width: CGFloat, safeInset: CGFloat, isSettings: Bool, hasPrevious: Bool, hasNext: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat {
self.currentParams = (width, safeInset)
self.fontSize = presentationData.listsFontSize
let titleFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize)
if self.theme !== presentationData.theme {
self.theme = presentationData.theme
self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor
let textColor = presentationData.theme.list.itemPrimaryTextColor
self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: titleFont, NSAttributedString.Key.foregroundColor.rawValue: textColor]
self.textNode.keyboardAppearance = presentationData.theme.rootController.keyboardColor.keyboardAppearance
self.textNode.tintColor = presentationData.theme.list.itemAccentColor
self.textNode.clipsToBounds = true
self.textNode.delegate = self
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
self.clearIconNode.image = PresentationResourcesItemList.itemListClearInputIcon(presentationData.theme)
}
self.topSeparator.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let separatorRightInset: CGFloat = 16.0
let separatorX = safeInset + (hasPrevious ? 16.0 : 0.0)
self.topSeparator.frame = CGRect(origin: CGPoint(x: separatorX, y: 0.0), size: CGSize(width: width - separatorX - safeInset - separatorRightInset, height: UIScreenPixel))
let attributedPlaceholderText = NSAttributedString(string: placeholder, font: titleFont, textColor: presentationData.theme.list.itemPlaceholderTextColor)
if self.textNode.attributedPlaceholderText == nil || !self.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) {
self.textNode.attributedPlaceholderText = attributedPlaceholderText
}
if let updateText = updateText {
let attributedText = NSAttributedString(string: updateText, font: titleFont, textColor: presentationData.theme.list.itemPrimaryTextColor)
self.textNode.attributedText = attributedText
}
var measureText = self.textNode.attributedText?.string ?? ""
if measureText.hasSuffix("\n") || measureText.isEmpty {
measureText += "|"
}
let attributedMeasureText = NSAttributedString(string: measureText, font: titleFont, textColor: .gray)
self.measureTextNode.attributedText = attributedMeasureText
let measureTextSize = self.measureTextNode.updateLayout(CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0 - 38.0, height: .greatestFiniteMagnitude))
self.measureTextNode.frame = CGRect(origin: CGPoint(), size: measureTextSize)
self.currentMeasuredHeight = measureTextSize.height
let verticalInset: CGFloat = 15.0
let height = measureTextSize.height + verticalInset * 2.0
let buttonSize = CGSize(width: 38.0, height: height)
self.clearButtonNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width, y: 0.0), size: buttonSize)
if let image = self.clearIconNode.image {
self.clearIconNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width + floor((buttonSize.width - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size)
}
let textNodeFrame = CGRect(origin: CGPoint(x: safeInset + 16.0, y: 15.0), size: CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0 - 38.0, height: max(height, 1000.0)))
self.textNodeContainer.frame = textNodeFrame
self.textNode.frame = CGRect(origin: CGPoint(), size: textNodeFrame.size)
self.backgroundNode.frame = CGRect(origin: CGPoint(x: safeInset, y: 0.0), size: CGSize(width: max(1.0, width - safeInset * 2.0), height: height))
let hasCorners = safeInset > 0.0 && (!hasPrevious || !hasNext)
let hasTopCorners = hasCorners && !hasPrevious
let hasBottomCorners = hasCorners && !hasNext
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInset, y: 0.0), size: CGSize(width: width - safeInset - safeInset, height: height))
return height
}
func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
self.updateClearButtonVisibility()
}
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
self.updateClearButtonVisibility()
}
private func updateClearButtonVisibility() {
let isHidden = !self.textNode.isFirstResponder() || self.text.isEmpty
self.clearIconNode.isHidden = isHidden
self.clearButtonNode.isHidden = isHidden
self.clearButtonNode.isAccessibilityElement = isHidden
}
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
guard let theme = self.theme else {
return true
}
let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text)
if updatedText.count > 255 {
let attributedText = NSAttributedString(string: String(updatedText[updatedText.startIndex..<updatedText.index(updatedText.startIndex, offsetBy: 255)]), font: Font.regular(17.0), textColor: theme.list.itemPrimaryTextColor)
self.textNode.attributedText = attributedText
self.requestUpdateHeight()
return false
} else {
return true
}
}
func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
if let (width, safeInset) = self.currentParams {
var measureText = self.textNode.attributedText?.string ?? ""
if measureText.hasSuffix("\n") || measureText.isEmpty {
measureText += "|"
}
let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), textColor: .black)
self.measureTextNode.attributedText = attributedMeasureText
let measureTextSize = self.measureTextNode.updateLayout(CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0 - 38.0, height: .greatestFiniteMagnitude))
if let currentMeasuredHeight = self.currentMeasuredHeight, abs(measureTextSize.height - currentMeasuredHeight) > 0.1 {
self.requestUpdateHeight()
}
}
}
func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool {
let text: String? = UIPasteboard.general.string
if let _ = text {
return true
} else {
return false
}
}
}
@@ -0,0 +1,426 @@
import Foundation
import UIKit
import AsyncDisplayKit
import ContextUI
import TelegramPresentationData
import ManagedAnimationNode
import Display
private enum MoreIconNodeState: Equatable {
case more
case search
case sort
case moreToSearch(Float)
}
private final class MoreIconNode: ManagedAnimationNode {
private let duration: Double = 0.21
private var iconState: MoreIconNodeState = .more
init() {
super.init(size: CGSize(width: 30.0, height: 30.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_moretosearch"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.0))
}
func play() {
if case .more = self.iconState {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_moredots"), frames: .range(startFrame: 0, endFrame: 46), duration: 0.76))
}
}
func enqueueState(_ state: MoreIconNodeState, animated: Bool) {
guard self.iconState != state else {
return
}
let previousState = self.iconState
self.iconState = state
var source: ManagedAnimationSource = .local("anim_moretosearch")
let totalLength: Int = 90
if animated {
switch previousState {
case .more:
switch state {
case .more:
break
case .search:
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: totalLength), duration: self.duration))
case .sort:
source = .local("anim_moretosort_l")
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: totalLength), duration: self.duration))
case let .moreToSearch(progress):
let frame = Int(progress * Float(totalLength))
let duration = self.duration * Double(progress)
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: frame), duration: duration))
}
case .search:
switch state {
case .more:
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: 0), duration: self.duration))
case .search:
break
case .sort:
source = .local("anim_sorttosearch")
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: 0), duration: self.duration))
case let .moreToSearch(progress):
let frame = Int(progress * Float(totalLength))
let duration = self.duration * Double((1.0 - progress))
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: frame), duration: duration))
}
case .sort:
switch state {
case .more:
source = .local("anim_moretosort_l")
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: 0), duration: self.duration))
case .search:
source = .local("anim_sorttosearch")
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: totalLength), duration: self.duration))
case .sort:
break
case let .moreToSearch(progress):
let frame = Int(progress * Float(totalLength))
let duration = self.duration * Double((1.0 - progress))
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: frame), duration: duration))
}
case let .moreToSearch(currentProgress):
let currentFrame = Int(currentProgress * Float(totalLength))
switch state {
case .more:
let duration = self.duration * Double(currentProgress)
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: currentFrame, endFrame: 0), duration: duration))
case .search:
let duration = self.duration * (1.0 - Double(currentProgress))
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: currentFrame, endFrame: totalLength), duration: duration))
case .sort:
break
case let .moreToSearch(progress):
let frame = Int(progress * Float(totalLength))
let duration = self.duration * Double(abs(currentProgress - progress))
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: currentFrame, endFrame: frame), duration: duration))
}
}
} else {
switch state {
case .more:
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: 0), duration: 0.0))
case .search:
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: totalLength), duration: 0.0))
case .sort:
source = .local("anim_moretosort_l")
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: totalLength), duration: 0.0))
case let .moreToSearch(progress):
let frame = Int(progress * Float(totalLength))
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: frame, endFrame: frame), duration: 0.0))
}
}
}
}
final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
let containerNode: ContextControllerSourceNode
let contextSourceNode: ContextReferenceContentNode
private let textNode: ImmediateTextNode
private let iconNode: ASImageNode
private let backIconLayer: SimpleShapeLayer
private var animationNode: MoreIconNode?
private let backgroundNode: NavigationBackgroundNode
private var key: PeerInfoHeaderNavigationButtonKey?
private var contentsColor: UIColor = .white
private var canBeExpanded: Bool = false
var action: ((ASDisplayNode, ContextGesture?) -> Void)?
init() {
self.contextSourceNode = ContextReferenceContentNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.animateScale = false
self.textNode = ImmediateTextNode()
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.backIconLayer = SimpleShapeLayer()
self.backIconLayer.lineWidth = 3.0
self.backIconLayer.lineCap = .round
self.backIconLayer.lineJoin = .round
self.backIconLayer.strokeColor = UIColor.white.cgColor
self.backIconLayer.fillColor = nil
self.backIconLayer.isHidden = true
self.backIconLayer.path = try? convertSvgPath("M10.5,2 L1.5,11 L10.5,20 ")
self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: true)
super.init(pointerStyle: .insetRectangle(-8.0, 2.0))
self.isAccessibilityElement = true
self.accessibilityTraits = .button
self.containerNode.addSubnode(self.contextSourceNode)
self.contextSourceNode.addSubnode(self.backgroundNode)
self.contextSourceNode.addSubnode(self.textNode)
self.contextSourceNode.addSubnode(self.iconNode)
self.contextSourceNode.layer.addSublayer(self.backIconLayer)
self.addSubnode(self.containerNode)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
return
}
strongSelf.action?(strongSelf.contextSourceNode, gesture)
}
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
if isReduceTransparencyEnabled() {
self.backgroundNode.isHidden = true
}
}
@objc private func pressed() {
self.animationNode?.play()
self.action?(self.contextSourceNode, nil)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
var boundingRect = self.bounds
if self.textNode.alpha != 0.0 {
boundingRect = boundingRect.union(self.textNode.frame)
}
boundingRect = boundingRect.insetBy(dx: -8.0, dy: -4.0)
if boundingRect.contains(point) {
return super.hitTest(self.bounds.center, with: event)
} else {
return nil
}
}
func updateContentsColor(backgroundColor: UIColor, contentsColor: UIColor, canBeExpanded: Bool, transition: ContainedViewLayoutTransition) {
self.contentsColor = contentsColor
self.canBeExpanded = canBeExpanded
self.backgroundNode.updateColor(color: backgroundColor, transition: transition)
transition.updateTintColor(layer: self.textNode.layer, color: self.contentsColor)
transition.updateTintColor(view: self.iconNode.view, color: self.contentsColor)
transition.updateStrokeColor(layer: self.backIconLayer, strokeColor: self.contentsColor)
switch self.key {
case .back:
transition.updateAlpha(layer: self.textNode.layer, alpha: canBeExpanded ? 1.0 : 0.0)
transition.updateTransformScale(node: self.textNode, scale: canBeExpanded ? 1.0 : 0.001)
var iconTransform = CATransform3DIdentity
iconTransform = CATransform3DScale(iconTransform, canBeExpanded ? 1.0 : 0.8, canBeExpanded ? 1.0 : 0.8, 1.0)
iconTransform = CATransform3DTranslate(iconTransform, canBeExpanded ? -7.0 : 0.0, 0.0, 0.0)
transition.updateTransform(node: self.iconNode, transform: CATransform3DGetAffineTransform(iconTransform))
transition.updateTransform(layer: self.backIconLayer, transform: CATransform3DGetAffineTransform(iconTransform))
transition.updateLineWidth(layer: self.backIconLayer, lineWidth: canBeExpanded ? 3.0 : 2.075)
default:
break
}
if let animationNode = self.animationNode {
transition.updateTintColor(layer: animationNode.imageNode.layer, color: self.contentsColor)
}
}
func update(key: PeerInfoHeaderNavigationButtonKey, presentationData: PresentationData, height: CGFloat) -> CGSize {
let transition: ContainedViewLayoutTransition = .immediate
var iconOffset = CGPoint()
switch key {
case .back:
iconOffset = CGPoint(x: -1.0, y: 0.0)
default:
break
}
let textSize: CGSize
let isFirstTime = self.key == nil
if self.key != key {
self.key = key
let text: String
var accessibilityText: String
var icon: UIImage?
var isBold = false
var isGestureEnabled = false
var isAnimation = false
var animationState: MoreIconNodeState = .more
switch key {
case .back:
text = presentationData.strings.Common_Back
accessibilityText = presentationData.strings.Common_Back
icon = NavigationBar.backArrowImage(color: .white)
case .edit:
text = presentationData.strings.Common_Edit
accessibilityText = text
case .cancel:
text = presentationData.strings.Common_Cancel
accessibilityText = text
isBold = false
case .done, .selectionDone:
text = presentationData.strings.Common_Done
accessibilityText = text
isBold = true
case .select:
text = presentationData.strings.Common_Select
accessibilityText = text
case .search:
text = ""
accessibilityText = presentationData.strings.Common_Search
icon = nil
isAnimation = true
animationState = .search
case .standaloneSearch:
text = ""
accessibilityText = presentationData.strings.Common_Search
icon = PresentationResourcesRootController.navigationCompactSearchWhiteIcon(presentationData.theme)
case .searchWithTags:
text = ""
accessibilityText = presentationData.strings.Common_Search
icon = PresentationResourcesRootController.navigationCompactTagsSearchWhiteIcon(presentationData.theme)
case .editPhoto:
text = presentationData.strings.Settings_EditPhoto
accessibilityText = text
case .editVideo:
text = presentationData.strings.Settings_EditVideo
accessibilityText = text
case .more:
text = ""
accessibilityText = presentationData.strings.Common_More
icon = nil// PresentationResourcesRootController.navigationMoreCircledIcon(presentationData.theme)
isGestureEnabled = true
isAnimation = true
animationState = .more
case .qrCode:
text = ""
accessibilityText = presentationData.strings.PeerInfo_QRCode_Title
icon = PresentationResourcesRootController.navigationQrCodeIcon(presentationData.theme)
case .moreSearchSort:
text = ""
accessibilityText = ""
case .postStory:
text = ""
accessibilityText = presentationData.strings.Story_Privacy_PostStory
icon = PresentationResourcesRootController.navigationPostStoryIcon(presentationData.theme)
case .sort:
text = ""
accessibilityText = presentationData.strings.Common_More
icon = PresentationResourcesRootController.navigationSortIcon(presentationData.theme)
isAnimation = true
animationState = .sort
}
self.accessibilityLabel = accessibilityText
self.containerNode.isGestureEnabled = isGestureEnabled
let font: UIFont = isBold ? Font.semibold(17.0) : Font.regular(17.0)
self.textNode.attributedText = NSAttributedString(string: text, font: font, textColor: .white)
transition.updateTintColor(layer: self.textNode.layer, color: self.contentsColor)
self.iconNode.image = icon
transition.updateTintColor(view: self.iconNode.view, color: self.contentsColor)
if isAnimation {
self.iconNode.isHidden = true
let animationNode: MoreIconNode
if let current = self.animationNode {
animationNode = current
} else {
animationNode = MoreIconNode()
self.animationNode = animationNode
self.contextSourceNode.addSubnode(animationNode)
animationNode.imageNode.layer.layerTintColor = self.contentsColor.cgColor
animationNode.customColor = .white
}
transition.updateTintColor(layer: animationNode.imageNode.layer, color: self.contentsColor)
animationNode.enqueueState(animationState, animated: !isFirstTime)
} else {
self.iconNode.isHidden = false
if let current = self.animationNode {
self.animationNode = nil
current.removeFromSupernode()
}
}
textSize = self.textNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
} else {
textSize = self.textNode.bounds.size
}
let inset: CGFloat = 0.0
var textInset: CGFloat = 0.0
switch key {
case .back:
textInset += 11.0
default:
break
}
let resultSize: CGSize
let textFrame = CGRect(origin: CGPoint(x: inset + textInset, y: floor((height - textSize.height) / 2.0)), size: textSize)
self.textNode.position = textFrame.center
self.textNode.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
if let animationNode = self.animationNode {
let animationSize = CGSize(width: 30.0, height: 30.0)
animationNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((height - animationSize.height) / 2.0)), size: animationSize).offsetBy(dx: iconOffset.x, dy: iconOffset.y)
let size = CGSize(width: animationSize.width + inset * 2.0, height: height)
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size)
resultSize = size
} else if let image = self.iconNode.image {
let iconFrame = CGRect(origin: CGPoint(x: inset, y: floor((height - image.size.height) / 2.0)), size: image.size).offsetBy(dx: iconOffset.x, dy: iconOffset.y)
self.iconNode.position = iconFrame.center
self.iconNode.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
if case .back = key {
self.backIconLayer.position = iconFrame.center
self.backIconLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
self.iconNode.isHidden = true
self.backIconLayer.isHidden = false
} else {
self.iconNode.isHidden = false
self.backIconLayer.isHidden = true
}
let size = CGSize(width: image.size.width + inset * 2.0, height: height)
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size)
resultSize = size
} else {
let size = CGSize(width: textSize.width + inset * 2.0, height: height)
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size)
resultSize = size
}
let diameter: CGFloat = 32.0
let backgroundWidth: CGFloat
if self.iconNode.image != nil || self.animationNode != nil {
backgroundWidth = diameter
} else {
backgroundWidth = max(diameter, resultSize.width + 12.0 * 2.0)
}
let backgroundFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - backgroundWidth) * 0.5), y: floor((resultSize.height - diameter) * 0.5)), size: CGSize(width: backgroundWidth, height: diameter))
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
self.backgroundNode.update(size: backgroundFrame.size, cornerRadius: diameter * 0.5, transition: transition)
self.hitTestSlop = UIEdgeInsets(top: -2.0, left: -12.0, bottom: -2.0, right: -12.0)
return resultSize
}
}
@@ -0,0 +1,309 @@
import Foundation
import UIKit
import AsyncDisplayKit
import ContextUI
import TelegramPresentationData
import Display
enum PeerInfoHeaderNavigationButtonKey {
case back
case edit
case done
case cancel
case select
case selectionDone
case search
case searchWithTags
case standaloneSearch
case editPhoto
case editVideo
case more
case sort
case qrCode
case moreSearchSort
case postStory
}
struct PeerInfoHeaderNavigationButtonSpec: Equatable {
let key: PeerInfoHeaderNavigationButtonKey
let isForExpandedView: Bool
}
final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode {
private var presentationData: PresentationData?
private(set) var leftButtonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:]
private(set) var rightButtonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:]
private var currentLeftButtons: [PeerInfoHeaderNavigationButtonSpec] = []
private var currentRightButtons: [PeerInfoHeaderNavigationButtonSpec] = []
private var backgroundContentColor: UIColor = .clear
private var contentsColor: UIColor = .white
private var canBeExpanded: Bool = false
var performAction: ((PeerInfoHeaderNavigationButtonKey, ContextReferenceContentNode?, ContextGesture?) -> Void)?
func updateContentsColor(backgroundContentColor: UIColor, contentsColor: UIColor, canBeExpanded: Bool, transition: ContainedViewLayoutTransition) {
self.backgroundContentColor = backgroundContentColor
self.contentsColor = contentsColor
self.canBeExpanded = canBeExpanded
for (_, button) in self.leftButtonNodes {
button.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: canBeExpanded, transition: transition)
transition.updateSublayerTransformOffset(layer: button.layer, offset: CGPoint(x: canBeExpanded ? -8.0 : 0.0, y: 0.0))
}
var accumulatedRightButtonOffset: CGFloat = canBeExpanded ? 16.0 : 0.0
for spec in self.currentRightButtons.reversed() {
guard let button = self.rightButtonNodes[spec.key] else {
continue
}
button.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: canBeExpanded, transition: transition)
if !spec.isForExpandedView {
transition.updateSublayerTransformOffset(layer: button.layer, offset: CGPoint(x: accumulatedRightButtonOffset, y: 0.0))
if self.backgroundContentColor.alpha != 0.0 {
accumulatedRightButtonOffset -= 6.0
}
}
}
for (key, button) in self.rightButtonNodes {
if !self.currentRightButtons.contains(where: { $0.key == key }) {
button.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: canBeExpanded, transition: transition)
transition.updateSublayerTransformOffset(layer: button.layer, offset: CGPoint(x: 0.0, y: 0.0))
}
}
}
func update(size: CGSize, presentationData: PresentationData, leftButtons: [PeerInfoHeaderNavigationButtonSpec], rightButtons: [PeerInfoHeaderNavigationButtonSpec], expandFraction: CGFloat, shouldAnimateIn: Bool, transition: ContainedViewLayoutTransition) {
let sideInset: CGFloat = 24.0
let expandedSideInset: CGFloat = 16.0
let maximumExpandOffset: CGFloat = 14.0
let expandOffset: CGFloat = -expandFraction * maximumExpandOffset
if self.currentLeftButtons != leftButtons || presentationData.strings !== self.presentationData?.strings {
self.currentLeftButtons = leftButtons
var nextRegularButtonOrigin = sideInset
var nextExpandedButtonOrigin = sideInset
for spec in leftButtons.reversed() {
let buttonNode: PeerInfoHeaderNavigationButton
var wasAdded = false
if let current = self.leftButtonNodes[spec.key] {
buttonNode = current
} else {
wasAdded = true
buttonNode = PeerInfoHeaderNavigationButton()
self.leftButtonNodes[spec.key] = buttonNode
self.addSubnode(buttonNode)
buttonNode.action = { [weak self] _, gesture in
guard let strongSelf = self, let buttonNode = strongSelf.leftButtonNodes[spec.key] else {
return
}
strongSelf.performAction?(spec.key, buttonNode.contextSourceNode, gesture)
}
}
let buttonSize = buttonNode.update(key: spec.key, presentationData: presentationData, height: size.height)
var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin
let buttonY: CGFloat
if case .back = spec.key {
buttonY = 0.0
} else {
buttonY = expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)
}
let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin, y: buttonY), size: buttonSize)
nextButtonOrigin += buttonSize.width + 4.0
if spec.isForExpandedView {
nextExpandedButtonOrigin = nextButtonOrigin
} else {
nextRegularButtonOrigin = nextButtonOrigin
}
let alphaFactor: CGFloat
if case .back = spec.key {
alphaFactor = 1.0
} else {
alphaFactor = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction)
}
if wasAdded {
buttonNode.frame = buttonFrame
buttonNode.alpha = 0.0
transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor)
buttonNode.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: self.canBeExpanded, transition: .immediate)
transition.updateSublayerTransformOffset(layer: buttonNode.layer, offset: CGPoint(x: canBeExpanded ? -8.0 : 0.0, y: 0.0))
} else {
transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame)
transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor)
}
}
var removeKeys: [PeerInfoHeaderNavigationButtonKey] = []
for (key, _) in self.leftButtonNodes {
if !leftButtons.contains(where: { $0.key == key }) {
removeKeys.append(key)
}
}
for key in removeKeys {
if let buttonNode = self.leftButtonNodes.removeValue(forKey: key) {
buttonNode.removeFromSupernode()
}
}
} else {
var nextRegularButtonOrigin = sideInset
var nextExpandedButtonOrigin = sideInset
for spec in leftButtons.reversed() {
if let buttonNode = self.leftButtonNodes[spec.key] {
let buttonSize = buttonNode.bounds.size
var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin
let buttonY: CGFloat
if case .back = spec.key {
buttonY = 0.0
} else {
buttonY = expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)
}
let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin, y: buttonY), size: buttonSize)
nextButtonOrigin += buttonSize.width + 4.0
if spec.isForExpandedView {
nextExpandedButtonOrigin = nextButtonOrigin
} else {
nextRegularButtonOrigin = nextButtonOrigin
}
transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame)
let alphaFactor: CGFloat
if case .back = spec.key {
alphaFactor = 1.0
} else {
alphaFactor = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction)
}
var buttonTransition = transition
if case let .animated(duration, curve) = buttonTransition, alphaFactor == 0.0 {
buttonTransition = .animated(duration: duration * 0.25, curve: curve)
}
buttonTransition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor)
}
}
}
var accumulatedRightButtonOffset: CGFloat = self.canBeExpanded ? 16.0 : 0.0
if self.currentRightButtons != rightButtons || presentationData.strings !== self.presentationData?.strings {
self.currentRightButtons = rightButtons
var nextRegularButtonOrigin = size.width - sideInset - 8.0
var nextExpandedButtonOrigin = size.width - expandedSideInset
for spec in rightButtons.reversed() {
let buttonNode: PeerInfoHeaderNavigationButton
var wasAdded = false
var key = spec.key
if key == .more || key == .search || key == .sort {
key = .moreSearchSort
}
if let current = self.rightButtonNodes[key] {
buttonNode = current
} else {
wasAdded = true
buttonNode = PeerInfoHeaderNavigationButton()
self.rightButtonNodes[key] = buttonNode
self.addSubnode(buttonNode)
}
buttonNode.action = { [weak self] _, gesture in
guard let strongSelf = self, let buttonNode = strongSelf.rightButtonNodes[key] else {
return
}
strongSelf.performAction?(spec.key, buttonNode.contextSourceNode, gesture)
}
let buttonSize = buttonNode.update(key: spec.key, presentationData: presentationData, height: size.height)
var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin
let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin - buttonSize.width, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize)
nextButtonOrigin -= buttonSize.width + 15.0
if spec.isForExpandedView {
nextExpandedButtonOrigin = nextButtonOrigin
} else {
nextRegularButtonOrigin = nextButtonOrigin
}
let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction)
if wasAdded {
buttonNode.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: self.canBeExpanded, transition: .immediate)
if shouldAnimateIn {
if key == .moreSearchSort || key == .searchWithTags || key == .standaloneSearch {
buttonNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
}
}
buttonNode.frame = buttonFrame
buttonNode.alpha = 0.0
transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor)
if !spec.isForExpandedView {
transition.updateSublayerTransformOffset(layer: buttonNode.layer, offset: CGPoint(x: accumulatedRightButtonOffset, y: 0.0))
if self.backgroundContentColor.alpha != 0.0 {
accumulatedRightButtonOffset -= 6.0
}
} else {
transition.updateSublayerTransformOffset(layer: buttonNode.layer, offset: .zero)
}
} else {
transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame)
transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor)
}
}
var removeKeys: [PeerInfoHeaderNavigationButtonKey] = []
for (key, _) in self.rightButtonNodes {
if key == .moreSearchSort {
if !rightButtons.contains(where: { $0.key == .more || $0.key == .search || $0.key == .sort }) {
removeKeys.append(key)
}
} else if !rightButtons.contains(where: { $0.key == key }) {
removeKeys.append(key)
}
}
for key in removeKeys {
if let buttonNode = self.rightButtonNodes.removeValue(forKey: key) {
if key == .moreSearchSort || key == .searchWithTags || key == .standaloneSearch {
buttonNode.layer.animateAlpha(from: buttonNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak buttonNode] _ in
buttonNode?.removeFromSupernode()
})
buttonNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
} else {
buttonNode.removeFromSupernode()
}
}
}
} else {
var nextRegularButtonOrigin = size.width - sideInset - 8.0
var nextExpandedButtonOrigin = size.width - expandedSideInset
for spec in rightButtons.reversed() {
var key = spec.key
if key == .more || key == .search || key == .sort {
key = .moreSearchSort
}
if let buttonNode = self.rightButtonNodes[key] {
let buttonSize = buttonNode.bounds.size
var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin
let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin - buttonSize.width, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize)
nextButtonOrigin -= buttonSize.width + 15.0
if spec.isForExpandedView {
nextExpandedButtonOrigin = nextButtonOrigin
} else {
nextRegularButtonOrigin = nextButtonOrigin
}
transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame)
let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction)
var buttonTransition = transition
if case let .animated(duration, curve) = buttonTransition, alphaFactor == 0.0 {
buttonTransition = .animated(duration: duration * 0.25, curve: curve)
}
buttonTransition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor)
}
}
}
self.presentationData = presentationData
}
}
@@ -0,0 +1,154 @@
import Foundation
import UIKit
import AsyncDisplayKit
import ContextUI
import TelegramPresentationData
import Display
final class PeerInfoHeaderSingleLineTextFieldNode: ASDisplayNode, PeerInfoHeaderTextFieldNode, UITextFieldDelegate {
private let backgroundNode: ASDisplayNode
private let textNode: TextFieldNode
private let measureTextNode: ImmediateTextNode
private let clearIconNode: ASImageNode
private let clearButtonNode: HighlightableButtonNode
private let topSeparator: ASDisplayNode
private let maskNode: ASImageNode
private var theme: PresentationTheme?
var text: String {
return self.textNode.textField.text ?? ""
}
override init() {
self.backgroundNode = ASDisplayNode()
self.textNode = TextFieldNode()
self.measureTextNode = ImmediateTextNode()
self.measureTextNode.maximumNumberOfLines = 0
self.clearIconNode = ASImageNode()
self.clearIconNode.isLayerBacked = true
self.clearIconNode.displayWithoutProcessing = true
self.clearIconNode.displaysAsynchronously = false
self.clearIconNode.isHidden = true
self.clearButtonNode = HighlightableButtonNode()
self.clearButtonNode.isHidden = true
self.clearButtonNode.isAccessibilityElement = false
self.topSeparator = ASDisplayNode()
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode)
self.addSubnode(self.clearIconNode)
self.addSubnode(self.clearButtonNode)
self.addSubnode(self.topSeparator)
self.addSubnode(self.maskNode)
self.textNode.textField.delegate = self
self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside)
self.clearButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.clearIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.clearIconNode.alpha = 0.4
} else {
strongSelf.clearIconNode.alpha = 1.0
strongSelf.clearIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
@objc private func clearButtonPressed() {
self.textNode.textField.text = ""
self.updateClearButtonVisibility()
}
@objc func textFieldDidBeginEditing(_ textField: UITextField) {
self.updateClearButtonVisibility()
}
@objc func textFieldDidEndEditing(_ textField: UITextField) {
self.updateClearButtonVisibility()
}
private func updateClearButtonVisibility() {
let isHidden = !self.textNode.textField.isFirstResponder || self.text.isEmpty
self.clearIconNode.isHidden = isHidden
self.clearButtonNode.isHidden = isHidden
self.clearButtonNode.isAccessibilityElement = isHidden
}
func update(width: CGFloat, safeInset: CGFloat, isSettings: Bool, hasPrevious: Bool, hasNext: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat {
let titleFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize)
self.textNode.textField.font = titleFont
if self.theme !== presentationData.theme {
self.theme = presentationData.theme
self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor
self.textNode.textField.textColor = presentationData.theme.list.itemPrimaryTextColor
self.textNode.textField.keyboardAppearance = presentationData.theme.rootController.keyboardColor.keyboardAppearance
self.textNode.textField.tintColor = presentationData.theme.list.itemAccentColor
self.clearIconNode.image = PresentationResourcesItemList.itemListClearInputIcon(presentationData.theme)
}
let attributedPlaceholderText = NSAttributedString(string: placeholder, font: titleFont, textColor: presentationData.theme.list.itemPlaceholderTextColor)
if self.textNode.textField.attributedPlaceholder == nil || !self.textNode.textField.attributedPlaceholder!.isEqual(to: attributedPlaceholderText) {
self.textNode.textField.attributedPlaceholder = attributedPlaceholderText
self.textNode.textField.accessibilityHint = attributedPlaceholderText.string
}
if let updateText = updateText {
self.textNode.textField.text = updateText
}
let separatorRightInset: CGFloat = 16.0
if !hasPrevious {
self.topSeparator.isHidden = true
}
self.topSeparator.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let separatorX = safeInset + (hasPrevious ? 16.0 : 0.0)
self.topSeparator.frame = CGRect(origin: CGPoint(x: separatorX, y: 0.0), size: CGSize(width: width - separatorX - safeInset - separatorRightInset, height: UIScreenPixel))
let measureText = "|"
let attributedMeasureText = NSAttributedString(string: measureText, font: titleFont, textColor: .black)
self.measureTextNode.attributedText = attributedMeasureText
let measureTextSize = self.measureTextNode.updateLayout(CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0 - 38.0, height: .greatestFiniteMagnitude))
let verticalInset: CGFloat = 15.0
let height = measureTextSize.height + verticalInset * 2.0
let buttonSize = CGSize(width: 38.0, height: height)
self.clearButtonNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width, y: 0.0), size: buttonSize)
if let image = self.clearIconNode.image {
self.clearIconNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width + floor((buttonSize.width - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size)
}
self.backgroundNode.frame = CGRect(origin: CGPoint(x: safeInset, y: 0.0), size: CGSize(width: max(1.0, width - safeInset * 2.0), height: height))
self.textNode.frame = CGRect(origin: CGPoint(x: safeInset + 16.0, y: floor((height - 40.0) / 2.0)), size: CGSize(width: max(1.0, width - safeInset * 2.0 - 16.0 * 2.0 - 38.0), height: 40.0))
let hasCorners = safeInset > 0.0 && (!hasPrevious || !hasNext)
let hasTopCorners = hasCorners && !hasPrevious
let hasBottomCorners = hasCorners && !hasNext
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInset, y: 0.0), size: CGSize(width: width - safeInset - safeInset, height: height))
self.textNode.isUserInteractionEnabled = isEnabled
self.textNode.alpha = isEnabled ? 1.0 : 0.6
return height
}
}
@@ -0,0 +1,368 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TemporaryCachedPeerDataManager
enum PeerInfoMemberRole {
case creator
case admin
case member
}
enum PeerInfoMember: Equatable {
case channelMember(participant: RenderedChannelParticipant, storyStats: PeerStoryStats?)
case legacyGroupMember(peer: RenderedPeer, role: PeerInfoMemberRole, invitedBy: PeerId?, presence: TelegramUserPresence?, storyStats: PeerStoryStats?)
case account(peer: RenderedPeer)
var id: PeerId {
switch self {
case let .channelMember(participant, _):
return participant.peer.id
case let .legacyGroupMember(peer, _, _, _, _):
return peer.peerId
case let .account(peer):
return peer.peerId
}
}
var peer: Peer {
switch self {
case let .channelMember(participant, _):
return participant.peer
case let .legacyGroupMember(peer, _, _, _, _):
return peer.peers[peer.peerId]!
case let .account(peer):
return peer.peers[peer.peerId]!
}
}
var presence: TelegramUserPresence? {
switch self {
case let .channelMember(participant, _):
return participant.presences[participant.peer.id] as? TelegramUserPresence
case let .legacyGroupMember(_, _, _, presence, _):
return presence
case .account:
return nil
}
}
var role: PeerInfoMemberRole {
switch self {
case let .channelMember(participant, _):
switch participant.participant {
case .creator:
return .creator
case let .member(_, _, adminInfo, _, _, _):
if adminInfo != nil {
return .admin
} else {
return .member
}
}
case let .legacyGroupMember(_, role, _, _, _):
return role
case .account:
return .member
}
}
var rank: String? {
switch self {
case let .channelMember(participant, _):
switch participant.participant {
case let .creator(_, _, rank):
return rank
case let .member(_, _, _, _, rank, _):
return rank
}
case .legacyGroupMember:
return nil
case .account:
return nil
}
}
var storyStats: PeerStoryStats? {
switch self {
case let .channelMember(_, value):
return value
case let .legacyGroupMember(_, _, _, _, value):
return value
case .account:
return nil
}
}
}
enum PeerInfoMembersDataState: Equatable {
case loading(isInitial: Bool)
case ready(canLoadMore: Bool)
}
struct PeerInfoMembersState: Equatable {
var canAddMembers: Bool
var members: [PeerInfoMember]
var dataState: PeerInfoMembersDataState
}
private func membersSortedByPresence(_ members: [PeerInfoMember], accountPeerId: PeerId) -> [PeerInfoMember] {
return members.sorted(by: { lhs, rhs in
if lhs.id == accountPeerId {
return true
} else if rhs.id == accountPeerId {
return false
}
let lhsPresence = lhs.presence
let rhsPresence = rhs.presence
if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
if lhsPresence.status < rhsPresence.status {
return false
} else if lhsPresence.status > rhsPresence.status {
return true
}
} else if let _ = lhsPresence {
return true
} else if let _ = rhsPresence {
return false
}
return lhs.id < rhs.id
})
}
private final class PeerInfoMembersContextImpl {
private let queue: Queue
private let context: AccountContext
private let peerId: PeerId
private var canAddMembers = false
private var members: [PeerInfoMember] = []
private var dataState: PeerInfoMembersDataState = .loading(isInitial: true)
private var removingMemberIds: [PeerId: Disposable] = [:]
private var membersHidden: Bool?
private let stateValue = Promise<PeerInfoMembersState>()
var state: Signal<PeerInfoMembersState, NoError> {
return self.stateValue.get()
}
private let disposable = MetaDisposable()
private let peerDisposable = MetaDisposable()
private var channelMembersControl: PeerChannelMemberCategoryControl?
init(queue: Queue, context: AccountContext, peerId: PeerId) {
self.queue = queue
self.context = context
self.peerId = peerId
self.pushState()
if peerId.namespace == Namespaces.Peer.CloudChannel {
let (disposable, control) = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, requestUpdate: true, updated: { [weak self] state in
queue.async {
guard let strongSelf = self else {
return
}
let unsortedMembers = state.list.map { item -> PeerInfoMember in
return .channelMember(participant: item, storyStats: state.peerStoryStats[item.peer.id])
}
let members: [PeerInfoMember]
if unsortedMembers.count <= 50 {
members = membersSortedByPresence(unsortedMembers, accountPeerId: strongSelf.context.account.peerId)
} else {
members = unsortedMembers
}
strongSelf.members = members
switch state.loadingState {
case let .loading(initial):
strongSelf.dataState = .loading(isInitial: initial)
case let .ready(hasMore):
strongSelf.dataState = .ready(canLoadMore: hasMore)
}
strongSelf.pushState()
}
})
self.disposable.set(disposable)
self.channelMembersControl = control
self.peerDisposable.set((context.account.postbox.peerView(id: peerId)
|> deliverOn(self.queue)).startStrict(next: { [weak self] view in
guard let strongSelf = self else {
return
}
if let channel = peerViewMainPeer(view) as? TelegramChannel {
var canAddMembers = false
switch channel.info {
case .broadcast:
break
case .group:
if channel.flags.contains(.isCreator) || channel.hasPermission(.inviteMembers) {
canAddMembers = true
}
}
strongSelf.canAddMembers = canAddMembers
strongSelf.pushState()
}
var membersHidden: Bool?
if let cachedData = view.cachedData as? CachedChannelData, case let .known(value) = cachedData.membersHidden {
membersHidden = value.value
}
if strongSelf.membersHidden != membersHidden {
let shouldResetList = strongSelf.membersHidden != nil
strongSelf.membersHidden = membersHidden
if shouldResetList, let control = strongSelf.channelMembersControl {
context.peerChannelMemberCategoriesContextsManager.reset(peerId: peerId, control: control)
}
}
}))
} else if peerId.namespace == Namespaces.Peer.CloudGroup {
self.disposable.set((context.account.postbox.peerView(id: peerId)
|> deliverOn(self.queue)).startStrict(next: { [weak self] view in
guard let strongSelf = self, let cachedData = view.cachedData as? CachedGroupData, let participantsData = cachedData.participants else {
return
}
var unsortedMembers: [PeerInfoMember] = []
for participant in participantsData.participants {
if let peer = view.peers[participant.peerId] {
let role: PeerInfoMemberRole
let invitedBy: PeerId?
switch participant {
case .creator:
role = .creator
invitedBy = nil
case let .admin(_, invitedByValue, _):
role = .admin
invitedBy = invitedByValue
case let .member(_, invitedByValue, _):
role = .member
invitedBy = invitedByValue
}
unsortedMembers.append(.legacyGroupMember(peer: RenderedPeer(peer: peer), role: role, invitedBy: invitedBy, presence: view.peerPresences[participant.peerId] as? TelegramUserPresence, storyStats: view.memberStoryStats[participant.peerId]))
}
}
if let group = peerViewMainPeer(view) as? TelegramGroup {
var canAddMembers = false
switch group.role {
case .admin, .creator:
canAddMembers = true
case .member:
break
}
if !group.hasBannedPermission(.banAddMembers) {
canAddMembers = true
}
strongSelf.canAddMembers = canAddMembers
}
strongSelf.members = membersSortedByPresence(unsortedMembers, accountPeerId: strongSelf.context.account.peerId)
strongSelf.dataState = .ready(canLoadMore: false)
strongSelf.pushState()
}))
} else {
self.dataState = .ready(canLoadMore: false)
self.pushState()
}
}
deinit {
self.disposable.dispose()
self.peerDisposable.dispose()
for (_, disposable) in self.removingMemberIds {
disposable.dispose()
}
}
private func pushState() {
if self.removingMemberIds.isEmpty {
self.stateValue.set(.single(PeerInfoMembersState(canAddMembers: self.canAddMembers, members: self.members, dataState: self.dataState)))
} else {
self.stateValue.set(.single(PeerInfoMembersState(canAddMembers: self.canAddMembers, members: self.members.filter { member in
return self.removingMemberIds[member.id] == nil
}, dataState: self.dataState)))
}
}
func loadMore() {
if case .ready(true) = self.dataState, let channelMembersControl = self.channelMembersControl {
self.context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: self.peerId, control: channelMembersControl)
}
}
func removeMember(memberId: PeerId) {
if self.removingMemberIds[memberId] == nil {
let signal: Signal<Never, NoError>
if self.peerId.namespace == Namespaces.Peer.CloudChannel {
signal = context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: self.context.engine, peerId: self.peerId, memberId: memberId, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max))
|> ignoreValues
} else {
signal = self.context.engine.peers.removePeerMember(peerId: self.peerId, memberId: memberId)
|> ignoreValues
}
let completed: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
if let disposable = strongSelf.removingMemberIds.removeValue(forKey: memberId) {
disposable.dispose()
strongSelf.pushState()
}
}
let disposable = MetaDisposable()
self.removingMemberIds[memberId] = disposable
self.pushState()
disposable.set((signal
|> deliverOn(self.queue)).startStrict(completed: {
completed()
}))
}
}
}
final class PeerInfoMembersContext: Equatable {
private let queue = Queue.mainQueue()
private let impl: QueueLocalObject<PeerInfoMembersContextImpl>
var state: Signal<PeerInfoMembersState, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.state.start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
init(context: AccountContext, peerId: PeerId) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return PeerInfoMembersContextImpl(queue: queue, context: context, peerId: peerId)
})
}
func loadMore() {
self.impl.with { impl in
impl.loadMore()
}
}
func removeMember(memberId: PeerId) {
self.impl.with { impl in
impl.removeMember(memberId: memberId)
}
}
static func ==(lhs: PeerInfoMembersContext, rhs: PeerInfoMembersContext) -> Bool {
return lhs === rhs
}
}
@@ -0,0 +1,879 @@
import Foundation
import UIKit
import Display
import SSignalKit
import SwiftSignalKit
import TelegramCore
import AccountContext
import MediaEditor
import MediaEditorScreen
import CameraScreen
import Photos
import PeerInfoAvatarListNode
import MapResourceToAvatarSizes
import AvatarEditorScreen
import OverlayStatusController
import UndoUI
import PeerAvatarGalleryUI
import PresentationDataUtils
import LegacyComponents
import LegacyMediaPickerUI
extension PeerInfoScreenImpl {
func openAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }, completedWithUploadingImage: @escaping (UIImage, Signal<PeerInfoAvatarUploadStatus, NoError>) -> UIView? = { _, _ in nil }) {
guard !self.presentAccountFrozenInfoIfNeeded() else {
return
}
guard let data = self.controllerNode.data, let peer = data.peer, mode != .generic || canEditPeerInfo(context: self.context, peer: peer, chatLocation: self.chatLocation, threadData: data.threadData) else {
return
}
self.view.endEditing(true)
let peerId = self.peerId
var isForum = false
if let peer = peer as? TelegramChannel, peer.isForumOrMonoForum {
isForum = true
}
var currentIsVideo = false
var emojiMarkup: TelegramMediaImage.EmojiMarkup?
let item = self.controllerNode.headerNode.avatarListNode.listContainerNode.currentItemNode?.item
if let item = item, case let .image(_, _, videoRepresentations, _, _, emojiMarkupValue) = item {
currentIsVideo = !videoRepresentations.isEmpty
emojiMarkup = emojiMarkupValue
}
let _ = isForum
let _ = currentIsVideo
let _ = (self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
guard let self, let peer else {
return
}
let keyboardInputData = Promise<AvatarKeyboardInputData>()
keyboardInputData.set(AvatarEditorScreen.inputData(context: self.context, isGroup: peer.id.namespace != Namespaces.Peer.CloudUser))
var hasPhotos = false
if !peer.profileImageRepresentations.isEmpty {
hasPhotos = true
}
var hasDeleteButton = false
if case .generic = mode {
hasDeleteButton = hasPhotos && !fromGallery
} else if case .custom = mode {
hasDeleteButton = peer.profileImageRepresentations.first?.isPersonal == true
} else if case .fallback = mode {
if let cachedData = data.cachedData as? CachedUserData, case let .known(photo) = cachedData.fallbackPhoto {
hasDeleteButton = photo != nil
}
}
struct ConfirmationAlert {
let title: String
let photoText: String
let videoText: String
let action: String
}
let confirmationAlert: ConfirmationAlert?
switch mode {
case .suggest:
confirmationAlert = ConfirmationAlert(
title: self.presentationData.strings.UserInfo_SuggestPhotoTitle(peer.compactDisplayTitle).string,
photoText: self.presentationData.strings.UserInfo_SuggestPhoto_AlertPhotoText(peer.compactDisplayTitle).string,
videoText: self.presentationData.strings.UserInfo_SuggestPhoto_AlertVideoText(peer.compactDisplayTitle).string,
action: self.presentationData.strings.UserInfo_SuggestPhoto_AlertSuggest
)
case .custom:
confirmationAlert = ConfirmationAlert(
title: self.presentationData.strings.UserInfo_SetCustomPhotoTitle(peer.compactDisplayTitle).string,
photoText: self.presentationData.strings.UserInfo_SetCustomPhoto_AlertPhotoText(peer.compactDisplayTitle, peer.compactDisplayTitle).string,
videoText: self.presentationData.strings.UserInfo_SetCustomPhoto_AlertVideoText(peer.compactDisplayTitle, peer.compactDisplayTitle).string,
action: self.presentationData.strings.UserInfo_SetCustomPhoto_AlertSet
)
default:
confirmationAlert = nil
}
let parentController = (self.context.sharedContext.mainWindow?.viewController as? NavigationController)?.topViewController as? ViewController
var dismissImpl: (() -> Void)?
let mainController = self.context.sharedContext.makeAvatarMediaPickerScreen(context: self.context, getSourceRect: { return nil }, canDelete: hasDeleteButton, performDelete: { [weak self] in
self?.openAvatarRemoval(mode: mode, peer: peer, item: item)
}, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, transitionOut, cancelled in
var resultImage: UIImage?
let uploadStatusPromise = Promise<PeerInfoAvatarUploadStatus>(.progress(0.0))
let subject: Signal<MediaEditorScreenImpl.Subject?, NoError>
if let asset = result as? PHAsset {
subject = .single(.asset(asset))
} else if let image = result as? UIImage {
subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight, fromCamera: false))
} else if let result = result as? Signal<CameraScreenImpl.Result, NoError> {
subject = result
|> map { value -> MediaEditorScreenImpl.Subject? in
switch value {
case .pendingImage:
return nil
case let .image(image):
return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft, fromCamera: false)
case let .video(video):
return .video(videoPath: video.videoPath, thumbnail: video.coverImage, mirror: video.mirror, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: video.dimensions, duration: video.duration, videoPositionChanges: [], additionalVideoPosition: .topLeft, fromCamera: false)
default:
return nil
}
}
} else {
let peerType: AvatarEditorScreen.PeerType
if mode == .suggest {
peerType = .suggest
} else if case .legacyGroup = peer {
peerType = .group
} else if case let .channel(channel) = peer {
if case .group = channel.info {
peerType = channel.isForumOrMonoForum ? .forum : .group
} else {
peerType = .channel
}
} else {
peerType = .user
}
let controller = AvatarEditorScreen(context: self.context, inputData: keyboardInputData.get(), peerType: peerType, markup: emojiMarkup)
controller.imageCompletion = { [weak self] image, commit in
resultImage = image
self?.updateProfilePhoto(image, mode: mode, uploadStatus: uploadStatusPromise)
commit()
}
controller.videoCompletion = { [weak self] image, url, values, markup, commit in
resultImage = image
self?.updateProfileVideo(image, video: nil, values: nil, markup: markup, mode: mode, uploadStatus: uploadStatusPromise)
commit()
}
parentController?.push(controller)
//isFromEditor = true
return
}
let editorController = MediaEditorScreenImpl(
context: self.context,
mode: .avatarEditor,
subject: subject,
transitionIn: fromCamera ? .camera : transitionView.flatMap({ .gallery(
MediaEditorScreenImpl.TransitionIn.GalleryTransitionIn(
sourceView: $0,
sourceRect: transitionRect,
sourceImage: transitionImage
)
) }),
transitionOut: { finished, isNew in
if !finished {
if let transitionView {
return MediaEditorScreenImpl.TransitionOut(
destinationView: transitionView,
destinationRect: transitionView.bounds,
destinationCornerRadius: 0.0
)
}
} else if let resultImage, let transitionOutView = completedWithUploadingImage(resultImage, uploadStatusPromise.get()) {
transitionOutView.isHidden = true
return MediaEditorScreenImpl.TransitionOut(
destinationView: transitionOutView,
destinationRect: transitionOutView.bounds,
destinationCornerRadius: transitionOutView.bounds.height * 0.5,
completion: { [weak transitionOutView] in
transitionOutView?.isHidden = false
}
)
}
return nil
},
willComplete: { [weak self, weak parentController] image, isVideo, commit in
if let self, let confirmationAlert, let image {
let controller = photoUpdateConfirmationController(context: self.context, peer: peer, image: image, text: isVideo ? confirmationAlert.videoText : confirmationAlert.photoText, doneTitle: confirmationAlert.action, commit: {
commit()
})
parentController?.presentInGlobalOverlay(controller)
} else {
commit()
}
},
completion: { [weak self] results, commit in
guard let result = results.first else {
return
}
switch result.media {
case let .image(image, _):
resultImage = image
self?.updateProfilePhoto(image, mode: mode, uploadStatus: uploadStatusPromise)
commit({})
case let .video(video, coverImage, values, _, _):
if let coverImage {
resultImage = coverImage
self?.updateProfileVideo(coverImage, video: video, values: values, markup: nil, mode: mode, uploadStatus: uploadStatusPromise)
}
commit({})
default:
break
}
dismissImpl?()
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
)
editorController.cancelled = { _ in
cancelled()
}
if self.navigationController != nil {
self.push(editorController)
} else {
self.parentController?.pushViewController(editorController)
}
}, dismissed: {
})
dismissImpl = { [weak self, weak mainController] in
if let mainController, let navigationController = mainController.navigationController {
var viewControllers = navigationController.viewControllers
viewControllers = viewControllers.filter { c in
return !(c is CameraScreen) && c !== mainController
}
navigationController.setViewControllers(viewControllers, animated: false)
}
if let self, let navigationController = self.parentController, let mainController {
var viewControllers = navigationController.viewControllers
viewControllers = viewControllers.filter { c in
return !(c is CameraScreen) && c !== mainController
}
navigationController.setViewControllers(viewControllers, animated: false)
}
}
mainController.navigationPresentation = .flatModal
mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
if self.navigationController != nil {
self.push(mainController)
} else {
self.parentController?.pushViewController(mainController)
}
})
}
func openAvatarRemoval(mode: PeerInfoAvatarEditingMode, peer: EnginePeer? = nil, item: PeerInfoAvatarListItem? = nil, completion: @escaping () -> Void = {}) {
let proceed = { [weak self] in
guard let strongSelf = self else {
return
}
completion()
if let item = item {
strongSelf.controllerNode.deleteProfilePhoto(item)
}
if mode != .fallback {
if let peer = peer, let _ = peer.smallProfileImage {
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withUpdatingAvatar(nil)
if let (layout, navigationHeight) = strongSelf.controllerNode.validLayout {
strongSelf.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
}
}
}
let postbox = strongSelf.context.account.postbox
let signal: Signal<UpdatePeerPhotoStatus, UploadPeerPhotoError>
if case .custom = mode {
signal = strongSelf.context.engine.contacts.updateContactPhoto(peerId: strongSelf.peerId, resource: nil, videoResource: nil, videoStartTimestamp: nil, markup: nil, mode: .custom, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
})
} else if case .fallback = mode {
signal = strongSelf.context.engine.accountData.removeFallbackPhoto(reference: nil)
|> castError(UploadPeerPhotoError.self)
|> map { _ in
return .complete([])
}
} else {
signal = strongSelf.context.engine.peers.updatePeerPhoto(peerId: strongSelf.peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
})
}
strongSelf.controllerNode.updateAvatarDisposable.set((signal
|> deliverOnMainQueue).startStrict(next: { result in
guard let strongSelf = self else {
return
}
switch result {
case .complete:
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withUpdatingAvatar(nil)
if let (layout, navigationHeight) = strongSelf.controllerNode.validLayout {
strongSelf.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
}
case .progress:
break
}
}))
}
let presentationData = self.presentationData
let actionSheet = ActionSheetController(presentationData: presentationData)
let items: [ActionSheetItem] = [
ActionSheetButtonItem(title: presentationData.strings.Settings_RemoveConfirmation, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
proceed()
})
]
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
(self.navigationController?.topViewController as? ViewController)?.present(actionSheet, in: .window(.root))
}
private func setupProfilePhotoUpload(image: UIImage, mode: PeerInfoAvatarEditingMode, indefiniteProgress: Bool) -> LocalFileMediaResource? {
guard let data = image.jpegData(compressionQuality: 0.6) else {
return nil
}
if self.controllerNode.headerNode.isAvatarExpanded {
self.controllerNode.headerNode.ignoreCollapse = true
self.controllerNode.headerNode.updateIsAvatarExpanded(false, transition: .immediate)
self.controllerNode.updateNavigationExpansionPresentation(isExpanded: false, animated: true)
}
self.controllerNode.scrollNode.view.setContentOffset(CGPoint(), animated: false)
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: mode == .custom)
if [.suggest, .fallback].contains(mode) {
} else {
if indefiniteProgress {
self.controllerNode.state = self.controllerNode.state.withAvatarUploadProgress(.indefinite)
}
self.controllerNode.state = self.controllerNode.state.withUpdatingAvatar(.image(representation))
}
if let (layout, navigationHeight) = self.controllerNode.validLayout {
self.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: mode == .custom ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, additive: false)
}
self.controllerNode.headerNode.ignoreCollapse = false
return resource
}
public func updateProfilePhoto(_ image: UIImage, mode: PeerInfoAvatarEditingMode, uploadStatus: Promise<PeerInfoAvatarUploadStatus>?) {
guard let resource = setupProfilePhotoUpload(image: image, mode: mode, indefiniteProgress: false) else {
uploadStatus?.set(.single(.done))
return
}
let postbox = self.context.account.postbox
let signal: Signal<UpdatePeerPhotoStatus, UploadPeerPhotoError>
if self.isSettings || self.isMyProfile {
if case .fallback = mode {
signal = self.context.engine.accountData.updateFallbackPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
})
} else {
signal = self.context.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
})
}
} else if case .custom = mode {
signal = self.context.engine.contacts.updateContactPhoto(peerId: self.peerId, resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mode: .custom, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
})
} else if case .suggest = mode {
signal = self.context.engine.contacts.updateContactPhoto(peerId: self.peerId, resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
})
} else {
signal = self.context.engine.peers.updatePeerPhoto(peerId: self.peerId, photo: self.context.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
})
}
var dismissStatus: (() -> Void)?
if [.suggest, .fallback, .accept].contains(mode) {
let statusController = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: { [weak self] in
self?.controllerNode.updateAvatarDisposable.set(nil)
dismissStatus?()
}))
dismissStatus = { [weak statusController] in
statusController?.dismiss()
}
if let topController = self.navigationController?.topViewController as? ViewController {
topController.presentInGlobalOverlay(statusController)
} else if let topController = self.parentController?.topViewController as? ViewController {
topController.presentInGlobalOverlay(statusController)
} else {
self.presentInGlobalOverlay(statusController)
}
}
self.controllerNode.updateAvatarDisposable.set((signal
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
guard let strongSelf = self else {
return
}
switch result {
case .complete:
uploadStatus?.set(.single(.done))
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withUpdatingAvatar(nil).withAvatarUploadProgress(nil)
case let .progress(value):
uploadStatus?.set(.single(.progress(value)))
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withAvatarUploadProgress(.value(CGFloat(value)))
}
if let (layout, navigationHeight) = strongSelf.controllerNode.validLayout {
strongSelf.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
}
if case .complete = result {
dismissStatus?()
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.peerId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
if let strongSelf = self, let peer {
switch mode {
case .fallback:
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicPhotoSuccess, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
case .custom:
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessPhotoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone()
case .suggest:
if let navigationController = (strongSelf.navigationController as? NavigationController) {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in
}))
}
case .accept:
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedPhotoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedPhotoSuccessText, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in
if case .info = action {
self?.parentController?.openSettings()
}
return false
}), in: .current)
default:
break
}
}
})
}
}))
}
public func updateProfileVideo(_ image: UIImage, video: MediaEditorScreenImpl.MediaResult.VideoResult?, values: MediaEditorValues?, markup: UploadPeerPhotoMarkup?, mode: PeerInfoAvatarEditingMode, uploadStatus: Promise<PeerInfoAvatarUploadStatus>?) {
var uploadVideo = true
if let _ = markup {
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let uploadVideoValue = data["upload_markup_video"] as? Bool, uploadVideoValue {
uploadVideo = true
} else {
uploadVideo = false
}
}
guard let photoResource = self.setupProfilePhotoUpload(image: image, mode: mode, indefiniteProgress: !uploadVideo) else {
uploadStatus?.set(.single(.done))
return
}
var videoStartTimestamp: Double? = nil
if let values, let coverImageTimestamp = values.coverImageTimestamp, coverImageTimestamp > 0.0 {
videoStartTimestamp = coverImageTimestamp - (values.videoTrimRange?.lowerBound ?? 0.0)
}
let account = self.context.account
let context = self.context
let videoResource: Signal<TelegramMediaResource?, UploadPeerPhotoError>
if uploadVideo, let video, let values {
var exportSubject: Signal<(MediaEditorVideoExport.Subject, Double), NoError>?
switch video {
case let .imageFile(path):
if let image = UIImage(contentsOfFile: path) {
exportSubject = .single((.image(image: image), 3.0))
}
case let .videoFile(path):
let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL)
exportSubject = .single((.video(asset: asset, isStory: false), asset.duration.seconds))
case let .asset(localIdentifier):
exportSubject = Signal { subscriber in
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil)
if fetchResult.count != 0 {
let asset = fetchResult.object(at: 0)
if asset.mediaType == .video {
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
if let avAsset {
subscriber.putNext((.video(asset: avAsset, isStory: true), avAsset.duration.seconds))
subscriber.putCompletion()
}
}
} else {
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
if let image {
subscriber.putNext((.image(image: image), 3.0))
subscriber.putCompletion()
}
}
}
}
return EmptyDisposable
}
}
if let exportSubject {
videoResource = exportSubject
|> castError(UploadPeerPhotoError.self)
|> mapToSignal { exportSubject, duration in
return Signal<TelegramMediaResource?, UploadPeerPhotoError> { subscriber in
let configuration = recommendedVideoExportConfiguration(values: values, duration: duration, forceFullHd: true, frameRate: 60.0, isAvatar: true)
let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4")
let videoExport = MediaEditorVideoExport(postbox: context.account.postbox, subject: exportSubject, configuration: configuration, outputPath: tempFile.path, textScale: 2.0)
let _ = (videoExport.status
|> deliverOnMainQueue).startStandalone(next: { [weak self] status in
guard let self else {
return
}
switch status {
case .completed:
if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path), options: .mappedIfSafe) {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
subscriber.putNext(resource)
subscriber.putCompletion()
}
EngineTempBox.shared.dispose(tempFile)
case let .progress(progress):
Queue.mainQueue().async {
self.controllerNode.state = self.controllerNode.state.withAvatarUploadProgress(.value(CGFloat(progress * 0.45)))
self.requestLayout(transition: .immediate)
}
default:
break
}
})
return EmptyDisposable
}
}
} else {
videoResource = .single(nil)
}
} else {
videoResource = .single(nil)
}
var dismissStatus: (() -> Void)?
if [.suggest, .fallback, .accept].contains(mode) {
let statusController = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: { [weak self] in
self?.controllerNode.updateAvatarDisposable.set(nil)
dismissStatus?()
}))
dismissStatus = { [weak statusController] in
statusController?.dismiss()
}
if let topController = self.navigationController?.topViewController as? ViewController {
topController.presentInGlobalOverlay(statusController)
} else if let topController = self.parentController?.topViewController as? ViewController {
topController.presentInGlobalOverlay(statusController)
} else {
self.presentInGlobalOverlay(statusController)
}
}
let peerId = self.peerId
let isSettings = self.isSettings
let isMyProfile = self.isMyProfile
self.controllerNode.updateAvatarDisposable.set((videoResource
|> mapToSignal { videoResource -> Signal<UpdatePeerPhotoStatus, UploadPeerPhotoError> in
if isSettings || isMyProfile {
if case .fallback = mode {
return context.engine.accountData.updateFallbackPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
})
} else {
return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
})
}
} else if case .custom = mode {
return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .custom, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
})
} else if case .suggest = mode {
return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
})
} else {
return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: videoResource.flatMap { context.engine.peers.uploadedPeerVideo(resource: $0) |> map(Optional.init) }, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
})
}
}
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
guard let strongSelf = self else {
return
}
switch result {
case .complete:
uploadStatus?.set(.single(.done))
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withUpdatingAvatar(nil).withAvatarUploadProgress(nil)
case let .progress(value):
uploadStatus?.set(.single(.progress(value)))
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withAvatarUploadProgress(.value(CGFloat(0.45 + value * 0.55)))
}
if let (layout, navigationHeight) = strongSelf.controllerNode.validLayout {
strongSelf.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
}
if case .complete = result {
dismissStatus?()
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.peerId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
if let strongSelf = self, let peer {
switch mode {
case .fallback:
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicVideoSuccess, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
case .custom:
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessVideoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone()
case .suggest:
if let navigationController = (strongSelf.navigationController as? NavigationController) {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in
}))
}
case .accept:
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccessText, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in
if case .info = action {
self?.parentController?.openSettings()
}
return false
}), in: .current)
default:
break
}
}
})
}
}))
}
public func oldUpdateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?, mode: PeerInfoAvatarEditingMode) {
var markup: UploadPeerPhotoMarkup? = nil
if let fileId = adjustments?.documentId, let backgroundColors = adjustments?.colors as? [Int32], fileId != 0 {
if let packId = adjustments?.stickerPackId, let accessHash = adjustments?.stickerPackAccessHash, packId != 0 {
markup = .sticker(packReference: .id(id: packId, accessHash: accessHash), fileId: fileId, backgroundColors: backgroundColors)
} else {
markup = .emoji(fileId: fileId, backgroundColors: backgroundColors)
}
}
var uploadVideo = true
if let _ = markup {
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let uploadVideoValue = data["upload_markup_video"] as? Bool, uploadVideoValue {
uploadVideo = true
} else {
uploadVideo = false
}
}
guard let photoResource = self.setupProfilePhotoUpload(image: image, mode: mode, indefiniteProgress: !uploadVideo) else {
return
}
var videoStartTimestamp: Double? = nil
if let adjustments = adjustments, adjustments.videoStartValue > 0.0 {
videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue
}
let account = self.context.account
let context = self.context
let videoResource: Signal<TelegramMediaResource?, UploadPeerPhotoError>
if uploadVideo {
videoResource = Signal<TelegramMediaResource?, UploadPeerPhotoError> { [weak self] subscriber in
let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in
if let paintingData = adjustments.paintingData, paintingData.hasAnimation {
return LegacyPaintEntityRenderer(postbox: account.postbox, adjustments: adjustments)
} else {
return nil
}
}
let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4")
let uploadInterface = LegacyLiveUploadInterface(context: context)
let signal: SSignal
if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer {
let durationSignal: SSignal = SSignal(generator: { subscriber in
let disposable = (entityRenderer.duration()).start(next: { duration in
subscriber.putNext(duration)
subscriber.putCompletion()
})
return SBlockDisposable(block: {
disposable.dispose()
})
})
signal = durationSignal.map(toSignal: { duration -> SSignal in
if let duration = duration as? Double {
return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, path: tempFile.path, watcher: nil, entityRenderer: entityRenderer)!
} else {
return SSignal.single(nil)
}
})
} else if let asset = asset as? AVAsset {
signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, path: tempFile.path, watcher: uploadInterface, entityRenderer: entityRenderer)!
} else {
signal = SSignal.complete()
}
let signalDisposable = signal.start(next: { next in
if let result = next as? TGMediaVideoConversionResult {
if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) {
account.postbox.mediaBox.storeResourceData(photoResource.id, data: data)
}
if let timestamp = videoStartTimestamp {
videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05))
}
var value = stat()
if stat(result.fileURL.path, &value) == 0 {
if let data = try? Data(contentsOf: result.fileURL) {
let resource: TelegramMediaResource
if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult {
resource = LocalFileMediaResource(fileId: liveUploadData.id)
} else {
resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
}
account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
subscriber.putNext(resource)
EngineTempBox.shared.dispose(tempFile)
}
}
subscriber.putCompletion()
} else if let strongSelf = self, let progress = next as? NSNumber {
Queue.mainQueue().async {
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withAvatarUploadProgress(.value(CGFloat(progress.floatValue * 0.45)))
if let (layout, navigationHeight) = strongSelf.controllerNode.validLayout {
strongSelf.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
}
}
}
}, error: { _ in
}, completed: nil)
let disposable = ActionDisposable {
signalDisposable?.dispose()
}
return ActionDisposable {
disposable.dispose()
}
}
} else {
videoResource = .single(nil)
}
var dismissStatus: (() -> Void)?
if [.suggest, .fallback, .accept].contains(mode) {
let statusController = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: { [weak self] in
self?.controllerNode.updateAvatarDisposable.set(nil)
dismissStatus?()
}))
dismissStatus = { [weak statusController] in
statusController?.dismiss()
}
if let topController = self.navigationController?.topViewController as? ViewController {
topController.presentInGlobalOverlay(statusController)
} else if let topController = self.parentController?.topViewController as? ViewController {
topController.presentInGlobalOverlay(statusController)
} else {
self.presentInGlobalOverlay(statusController)
}
}
let peerId = self.peerId
let isSettings = self.isSettings
let isMyProfile = self.isMyProfile
self.controllerNode.updateAvatarDisposable.set((videoResource
|> mapToSignal { videoResource -> Signal<UpdatePeerPhotoStatus, UploadPeerPhotoError> in
if isSettings || isMyProfile {
if case .fallback = mode {
return context.engine.accountData.updateFallbackPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
})
} else {
return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
})
}
} else if case .custom = mode {
return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .custom, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
})
} else if case .suggest = mode {
return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
})
} else {
return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: videoResource.flatMap { context.engine.peers.uploadedPeerVideo(resource: $0) |> map(Optional.init) }, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
})
}
}
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
guard let strongSelf = self else {
return
}
switch result {
case .complete:
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withUpdatingAvatar(nil).withAvatarUploadProgress(nil)
case let .progress(value):
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withAvatarUploadProgress(.value(CGFloat(0.45 + value * 0.55)))
}
if let (layout, navigationHeight) = strongSelf.controllerNode.validLayout {
strongSelf.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
}
if case .complete = result {
dismissStatus?()
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.peerId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
if let strongSelf = self, let peer {
switch mode {
case .fallback:
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicVideoSuccess, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
case .custom:
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessVideoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone()
case .suggest:
if let navigationController = (strongSelf.navigationController as? NavigationController) {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in
}))
}
case .accept:
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccessText, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in
if case .info = action {
self?.parentController?.openSettings()
}
return false
}), in: .current)
default:
break
}
}
})
}
}))
}
}
@@ -0,0 +1,27 @@
import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import Display
import AccountContext
extension PeerInfoScreenImpl {
func presentAccountFrozenInfoIfNeeded(delay: Bool = false) -> Bool {
if self.context.isFrozen {
let present = {
self.push(self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context))
}
if delay {
Queue.mainQueue().after(0.3) {
present()
}
} else {
present()
}
return true
}
return false
}
}
@@ -0,0 +1,137 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListUI
import AccountContext
final class PeerInfoScreenMultilineInputItem: PeerInfoScreenItem {
let id: AnyHashable
let text: String
let placeholder: String
let textUpdated: (String) -> Void
let action: () -> Void
let maxLength: Int?
init(
id: AnyHashable,
text: String,
placeholder: String,
textUpdated: @escaping (String) -> Void,
action: @escaping () -> Void,
maxLength: Int?
) {
self.id = id
self.text = text
self.placeholder = placeholder
self.textUpdated = textUpdated
self.action = action
self.maxLength = maxLength
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenMultilineInputItemNode()
}
}
final class PeerInfoScreenMultilineInputItemNode: PeerInfoScreenItemNode {
private let bottomSeparatorNode: ASDisplayNode
private let maskNode: ASImageNode
private(set) var item: PeerInfoScreenMultilineInputItem?
private var itemNode: ItemListMultilineInputItemNode?
override init() {
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
super.init()
self.clipsToBounds = true
self.addSubnode(self.bottomSeparatorNode)
}
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenMultilineInputItem else {
return 10.0
}
self.item = item
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let inputItem = ItemListMultilineInputItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, text: item.text, placeholder: item.placeholder, maxLength: item.maxLength.flatMap { ItemListMultilineInputItemTextLimit(value: $0, display: true) }, sectionId: 0, style: .blocks, returnKeyType: .done, textUpdated: { updatedText in
item.textUpdated(updatedText)
}, action: {
item.action()
}, noInsets: true)
let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0)
let itemNode: ItemListMultilineInputItemNode
if let current = self.itemNode {
itemNode = current
inputItem.updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
apply(ListViewItemApply(isOnScreen: true))
})
} else {
var itemNodeValue: ListViewItemNode?
inputItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in
itemNodeValue = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode = itemNodeValue as! ItemListMultilineInputItemNode
self.itemNode = itemNode
self.addSubnode(itemNode)
}
let height = itemNode.contentSize.height
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size))
var separatorInset: CGFloat = sideInset
if bottomItem != nil {
separatorInset += 49.0
}
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height)))
self.bottomSeparatorNode.isHidden = hasBottomCorners
if self.maskNode.supernode == nil {
self.addSubnode(self.maskNode)
}
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
return height
}
func animateErrorIfNeeded() {
self.itemNode?.animateErrorIfNeeded()
}
func focus() {
self.itemNode?.focus()
}
}
@@ -0,0 +1,90 @@
import Foundation
import UIKit
import AsyncDisplayKit
import ContextUI
import AnimationUI
import Display
import TelegramPresentationData
import ComponentFlow
final class PeerInfoSubtitleBadgeView: HighlightTrackingButton {
private let action: () -> Void
private let backgroundView: BlurredBackgroundView
private let labelView = ComponentView<Empty>()
init(action: @escaping () -> Void) {
self.action = action
self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
self.backgroundView.isUserInteractionEnabled = false
super.init(frame: CGRect())
self.addSubview(self.backgroundView)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.highligthedChanged = { [weak self] highlighted in
if let self, self.bounds.width > 0.0 {
let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width
let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width
if highlighted {
self.layer.removeAnimation(forKey: "opacity")
self.layer.removeAnimation(forKey: "sublayerTransform")
self.alpha = 0.7
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
transition.updateTransformScale(layer: self.layer, scale: topScale)
} else {
self.alpha = 1.0
self.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
let transition: ContainedViewLayoutTransition = .immediate
transition.updateTransformScale(layer: self.layer, scale: 1.0)
self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
})
}
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
self.action()
}
func update(title: String, fillColor: UIColor, foregroundColor: UIColor) -> CGSize {
let labelSize = self.labelView.update(
transition: .immediate,
component: AnyComponent(Text(text: title, font: Font.regular(11.0), color: foregroundColor)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let size = CGSize(width: labelSize.width + 7.0 * 2.0, height: labelSize.height + 4.0 * 2.0)
self.backgroundView.frame = CGRect(origin: CGPoint(), size: size)
self.backgroundView.updateColor(color: fillColor, transition: .immediate)
self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, transition: .immediate)
if let labelComponentView = self.labelView.view {
if labelComponentView.superview == nil {
labelComponentView.isUserInteractionEnabled = false
self.addSubview(labelComponentView)
}
labelComponentView.frame = CGRect(origin: CGPoint(x: floor((size.width - labelSize.width) * 0.5), y: floor((size.height - labelSize.height) * 0.5)), size: labelSize)
}
return size
}
}
@@ -0,0 +1,247 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let text: String
private let textNode: ASTextNode
private let avatarNode: AvatarNode
private let arrowNode: ASImageNode
private let iconNode: ASImageNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer, image: UIImage, text: String, actions: [TextAlertAction]) {
self.strings = strings
self.text = text
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.arrowNode = ASImageNode()
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.iconNode = ASImageNode()
self.iconNode.clipsToBounds = true
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.iconNode.image = image
self.iconNode.cornerRadius = 30.0
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.textNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
self.avatarNode.setPeer(context: context, theme: ptheme, peer: peer)
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let avatarSize = CGSize(width: 60.0, height: 60.0)
self.avatarNode.updateSize(size: avatarSize)
let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize)
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
if let arrowImage = self.arrowNode.image {
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
}
if let _ = self.iconNode.image {
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize)
transition.updateFrame(node: self.iconNode, frame: iconFrame)
origin.y += avatarSize.height + 10.0
}
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultSize = CGSize(width: contentWidth, height: avatarSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
func photoUpdateConfirmationController(context: AccountContext, peer: EnginePeer, image: UIImage, text: String, doneTitle: String, isDark: Bool = true, commit: @escaping () -> Void) -> AlertController {
let theme = defaultDarkColorPresentationTheme
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
if isDark {
presentationData = presentationData.withUpdated(theme: theme)
}
let strings = presentationData.strings
var dismissImpl: ((Bool) -> Void)?
var contentNode: PhotoUpdateConfirmationAlertContentNode?
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
}), TextAlertAction(type: .defaultAction, title: doneTitle, action: {
dismissImpl?(true)
commit()
})]
contentNode = PhotoUpdateConfirmationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, peer: peer, image: image, text: text, actions: actions)
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!)
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}