Conference updates

This commit is contained in:
Isaac 2025-04-08 14:38:46 +04:00
parent 81ae40bcde
commit c78095e2d3
10 changed files with 492 additions and 179 deletions

View File

@ -11,6 +11,7 @@ import AvatarNode
import TelegramStringFormatting
import AccountContext
import ChatListSearchItemHeader
import AnimatedAvatarSetNode
private func callDurationString(strings: PresentationStrings, duration: Int32) -> String {
if duration < 60 {
@ -173,6 +174,7 @@ class CallListCallItem: ListViewItem {
}
private let avatarFont = avatarPlaceholderFont(size: 16.0)
private let multipleAvatarFont = avatarPlaceholderFont(size: 12.0)
class CallListCallItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
@ -187,6 +189,10 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
}
private let avatarNode: AvatarNode
private var conferenceAvatarListContext: AnimatedAvatarSetContext?
private var conferenceAvatarListNode: AnimatedAvatarSetNode?
private let titleNode: TextNode
private var credibilityIconNode: ASImageNode?
private let statusNode: TextNode
@ -321,6 +327,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
let statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
let dateFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
let avatarDiameter = min(40.0, floor(item.presentationData.fontSize.itemListBaseFontSize * 40.0 / 17.0))
let multipleAvatarDiameter = min(30.0, floor(item.presentationData.fontSize.itemListBaseFontSize * 30.0 / 17.0))
let editingOffset: CGFloat
var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)?
@ -376,6 +383,11 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
var isConference = false
var conferenceIsDeclined = false
let _ = isConference
let _ = conferenceIsDeclined
var conferenceAvatars: [EnginePeer] = []
for message in item.messages {
inner: for media in message.media {
if let action = media as? TelegramMediaAction {
@ -399,6 +411,16 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
}
} else if case let .conferenceCall(conferenceCall) = action.action {
isConference = true
if let peer = message.author, !conferenceAvatars.contains(where: { $0.id == peer.id }) {
conferenceAvatars.append(peer)
}
for id in conferenceCall.otherParticipants {
if let peer = message.peers[id], !conferenceAvatars.contains(where: { $0.id == peer.id }) {
conferenceAvatars.append(EnginePeer(peer))
}
}
isVideo = conferenceCall.flags.contains(.isVideo)
if message.flags.contains(.Incoming) {
@ -434,7 +456,21 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
}
if let peer = item.topMessage.peers[item.topMessage.id.peerId] {
if let user = peer as? TelegramUser {
if conferenceAvatars.count > 1 {
var peersString = ""
for peer in conferenceAvatars {
if !peersString.isEmpty {
peersString.append(", ")
}
if peer.id == item.context.account.peerId {
//TODO:localize
peersString += "You"
} else {
peersString += peer.compactDisplayTitle
}
}
titleAttributedString = NSAttributedString(string: peersString, font: titleFont, textColor: titleColor)
} else if let user = peer as? TelegramUser {
if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty {
let string = NSMutableAttributedString()
string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor))
@ -457,18 +493,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: titleColor)
}
if isConference {
//TODO:localize
if conferenceIsDeclined {
statusAttributedString = NSAttributedString(string: "Declined Group Call", font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
} else if hasMissed {
statusAttributedString = NSAttributedString(string: "Missed Group Call", font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
} else {
statusAttributedString = NSAttributedString(string: "Group call", font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
}
statusAccessibilityString = statusAttributedString?.string ?? ""
} else if hasMissed {
if hasMissed {
statusAttributedString = NSAttributedString(string: item.presentationData.strings.Notification_CallMissedShort, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
statusAccessibilityString = isVideo ? item.presentationData.strings.Call_VoiceOver_VideoCallMissed : item.presentationData.strings.Call_VoiceOver_VoiceCallMissed
} else if hasIncoming && hasOutgoing {
@ -658,7 +683,39 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
}
transition.updateFrameAdditive(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 52.0, y: floor((contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)))
let avatarFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset - 52.0, y: floor((contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter))
transition.updateFrameAdditive(node: strongSelf.avatarNode, frame: avatarFrame)
if conferenceAvatars.count > 1 {
strongSelf.avatarNode.isHidden = true
let conferenceAvatarListContext: AnimatedAvatarSetContext
if let current = strongSelf.conferenceAvatarListContext {
conferenceAvatarListContext = current
} else {
conferenceAvatarListContext = AnimatedAvatarSetContext()
strongSelf.conferenceAvatarListContext = conferenceAvatarListContext
}
let conferenceAvatarListNode: AnimatedAvatarSetNode
if let current = strongSelf.conferenceAvatarListNode {
conferenceAvatarListNode = current
} else {
conferenceAvatarListNode = AnimatedAvatarSetNode()
strongSelf.conferenceAvatarListNode = conferenceAvatarListNode
strongSelf.containerNode.addSubnode(conferenceAvatarListNode)
}
let avatarListContents = conferenceAvatarListContext.update(peers: conferenceAvatars, animated: false)
let avatarListSize = conferenceAvatarListNode.update(context: item.context, content: avatarListContents, itemSize: CGSize(width: CGFloat(multipleAvatarDiameter), height: CGFloat(multipleAvatarDiameter)), customSpacing: multipleAvatarDiameter - 8.0, font: multipleAvatarFont, animated: false, synchronousLoad: synchronousLoads)
conferenceAvatarListNode.frame = CGRect(origin: CGPoint(x: avatarFrame.minX + floor((avatarFrame.width - avatarListSize.width) / 2.0), y: avatarFrame.minY + floor((avatarFrame.height - avatarListSize.height) / 2.0)), size: avatarListSize)
} else {
strongSelf.avatarNode.isHidden = false
strongSelf.conferenceAvatarListContext = nil
if let conferenceAvatarListNode = strongSelf.conferenceAvatarListNode {
strongSelf.conferenceAvatarListNode = nil
conferenceAvatarListNode.removeFromSupernode()
}
}
let _ = titleApply()
let titleFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: verticalInset), size: titleLayout.size)

View File

@ -127,7 +127,7 @@ private func mappedInsertEntries(context: AccountContext, presentationData: Item
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
case .createGroupCall:
//TODO:localize
let item = ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: "New Call Link", hasSeparator: false, sectionId: 1, noInsets: true, editing: false, action: {
let item = ItemListPeerActionItem(presentationData: presentationData, style: showSettings ? .blocks : .plain, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: "New Call Link", hasSeparator: false, sectionId: 1, noInsets: true, editing: false, action: {
nodeInteraction.createGroupCall()
})
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)

View File

@ -59,3 +59,58 @@ public func blurredImage(_ image: UIImage, radius: CGFloat, iterations: Int = 3)
return nil
}
}
public func verticalBlurredImage(_ image: UIImage, radius: CGFloat, iterations: Int = 3) -> UIImage? {
guard let cgImage = image.cgImage, let providerData = cgImage.dataProvider?.data else {
return nil
}
if image.size.width <= 0.0 || image.size.height <= 0 || radius <= 0 {
return image
}
var boxSize = UInt32(radius)
if boxSize % 2 == 0 {
boxSize += 1
}
let bytes = cgImage.bytesPerRow * cgImage.height
let inData = malloc(bytes)
var inBuffer = imageBuffer(from: inData, width: vImagePixelCount(cgImage.width), height: vImagePixelCount(cgImage.height), rowBytes: cgImage.bytesPerRow)
let outData = malloc(bytes)
var outBuffer = imageBuffer(from: outData, width: vImagePixelCount(cgImage.width), height: vImagePixelCount(cgImage.height), rowBytes: cgImage.bytesPerRow)
let tempSize = vImageBoxConvolve_ARGB8888(&inBuffer, &outBuffer, nil, 0, 0, boxSize, 1, nil, vImage_Flags(kvImageEdgeExtend + kvImageGetTempBufferSize))
let tempData = malloc(tempSize)
defer {
free(inData)
free(outData)
free(tempData)
}
let source = CFDataGetBytePtr(providerData)
memcpy(inBuffer.data, source, bytes)
for _ in 0 ..< iterations {
vImageBoxConvolve_ARGB8888(&inBuffer, &outBuffer, tempData, 0, 0, boxSize, 1, nil, vImage_Flags(kvImageEdgeExtend))
let temp = inBuffer.data
inBuffer.data = outBuffer.data
outBuffer.data = temp
}
let context = cgImage.colorSpace.flatMap {
CGContext(data: inBuffer.data, width: cgImage.width, height: cgImage.height, bitsPerComponent: cgImage.bitsPerComponent, bytesPerRow: cgImage.bytesPerRow, space: $0, bitmapInfo: cgImage.bitmapInfo.rawValue)
}
let blurredCGImage = context?.makeImage()
if let blurredCGImage = blurredCGImage {
return UIImage(cgImage: blurredCGImage, scale: image.scale, orientation: image.imageOrientation)
} else {
return nil
}
}

View File

@ -21,6 +21,7 @@ public enum ItemListPeerActionItemColor {
public class ItemListPeerActionItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let style: ItemListStyle
let icon: UIImage?
let iconSignal: Signal<UIImage?, NoError>?
let title: String
@ -34,8 +35,9 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
public let sectionId: ItemListSectionId
public let action: (() -> Void)?
public init(presentationData: ItemListPresentationData, icon: UIImage?, iconSignal: Signal<UIImage?, NoError>? = nil, title: String, additionalBadgeIcon: UIImage? = nil, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, noInsets: Bool = false, editing: Bool = false, action: (() -> Void)?) {
public init(presentationData: ItemListPresentationData, style: ItemListStyle = .blocks, icon: UIImage?, iconSignal: Signal<UIImage?, NoError>? = nil, title: String, additionalBadgeIcon: UIImage? = nil, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, noInsets: Bool = false, editing: Bool = false, action: (() -> Void)?) {
self.presentationData = presentationData
self.style = style
self.icon = icon
self.iconSignal = iconSignal
self.title = title
@ -237,10 +239,18 @@ public final class ItemListPeerActionItemNode: ListViewItemNode {
strongSelf.activateArea.accessibilityLabel = item.title
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
switch item.style {
case .blocks:
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
case .plain:
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
}
let _ = titleApply()
@ -266,50 +276,67 @@ public final class ItemListPeerActionItemNode: ListViewItemNode {
transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + editingOffset + floor((leftInset - params.leftInset - imageSize.width) / 2.0) + iconOffset, y: floor((contentSize.height - imageSize.height) / 2.0)), size: imageSize))
}
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset + editingOffset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = !item.hasSeparator
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners || !item.hasSeparator
}
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset + editingOffset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = !item.hasSeparator
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners || !item.hasSeparator
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
}
let titleFrame = CGRect(origin: CGPoint(x: leftInset + editingOffset, y: verticalInset + verticalOffset), size: titleLayout.size)
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame)

View File

@ -6,6 +6,7 @@ import MultilineTextComponent
import BalancedTextComponent
import TelegramPresentationData
import CallsEmoji
import ImageBlur
private final class EmojiContainerView: UIView {
private let maskImageView: UIImageView?
@ -97,6 +98,100 @@ private final class EmojiContainerView: UIView {
}
}
private final class EmojiContentLayer: SimpleLayer {
private let size: CGSize
private(set) var emoji: String?
private var image: UIImage?
private var motionBlurLayer: SimpleLayer?
init(size: CGSize) {
self.size = size
super.init()
self.contentsGravity = .center
self.contentsScale = UIScreenScale
}
override init(layer: Any) {
if let layer = layer as? EmojiContentLayer {
self.size = layer.size
} else {
self.size = CGSize()
}
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setEmoji(emoji: String, font: UIFont) {
if self.emoji == emoji {
return
}
self.emoji = emoji
let attributedText = NSAttributedString(string: emoji, attributes: [
NSAttributedString.Key.font: font,
NSAttributedString.Key.foregroundColor: UIColor.black
])
var boundingRect = attributedText.boundingRect(with: CGSize(width: 200.0, height: 200.0), options: .usesLineFragmentOrigin, context: nil)
boundingRect.size.width = ceil(boundingRect.size.width)
boundingRect.size.height = ceil(boundingRect.size.height)
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: boundingRect.size))
let image = renderer.image { context in
UIGraphicsPushContext(context.cgContext)
attributedText.draw(at: CGPoint())
UIGraphicsPopContext()
}
self.image = image
self.contents = image.cgImage
if let motionBlurLayer = self.motionBlurLayer {
motionBlurLayer.contents = verticalBlurredImage(image, radius: 6.0)?.cgImage
}
}
func setMotionBlurFactor(factor: CGFloat, transition: ComponentTransition) {
if factor != 0.0 {
let motionBlurLayer: SimpleLayer
if let current = self.motionBlurLayer {
motionBlurLayer = current
} else {
motionBlurLayer = SimpleLayer()
if let image = self.image {
motionBlurLayer.contents = verticalBlurredImage(image, radius: 6.0)?.cgImage
}
motionBlurLayer.contentsScale = self.contentsScale
self.motionBlurLayer = motionBlurLayer
self.addSublayer(motionBlurLayer)
motionBlurLayer.position = CGPoint(x: self.size.width * 0.5, y: self.size.height * 0.5)
motionBlurLayer.bounds = CGRect(origin: CGPoint(), size: self.size)
motionBlurLayer.opacity = 0.0
}
let scaleFactor = 1.0 * (1.0 - factor) + 2.0 * factor
let opacityFactor = 0.0 * (1.0 - factor) + 0.6 * factor
transition.setTransform(layer: motionBlurLayer, transform: CATransform3DMakeScale(1.0, scaleFactor, 1.0))
transition.setAlpha(layer: motionBlurLayer, alpha: opacityFactor)
} else if let motionBlurLayer = self.motionBlurLayer {
self.motionBlurLayer = nil
transition.setAlpha(layer: motionBlurLayer, alpha: 0.0, completion: { [weak motionBlurLayer] _ in
motionBlurLayer?.removeFromSuperlayer()
})
transition.setTransform(layer: motionBlurLayer, transform: CATransform3DIdentity)
}
}
}
private final class EmojiItemComponent: Component {
let emoji: String?
@ -115,8 +210,8 @@ private final class EmojiItemComponent: Component {
private let containerView: EmojiContainerView
private let measureEmojiView = ComponentView<Empty>()
private var pendingContainerView: EmojiContainerView?
private var pendingEmojiViews: [ComponentView<Empty>] = []
private var emojiView: ComponentView<Empty>?
private var pendingEmojiLayers: [EmojiContentLayer] = []
private var emojiLayer: EmojiContentLayer?
private var component: EmojiItemComponent?
private weak var state: EmptyComponentState?
@ -139,11 +234,18 @@ private final class EmojiItemComponent: Component {
}
func update(component: EmojiItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let pendingContainerInset: CGFloat = 6.0
let pendingContainerInset: CGFloat = 8.0
self.component = component
self.state = state
let motionBlurTransition: ComponentTransition
if transition.animation.isImmediate {
motionBlurTransition = .immediate
} else {
motionBlurTransition = .easeInOut(duration: 0.2)
}
let size = self.measureEmojiView.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
@ -155,85 +257,65 @@ private final class EmojiItemComponent: Component {
let containerFrame = CGRect(origin: CGPoint(x: -pendingContainerInset, y: -pendingContainerInset), size: CGSize(width: size.width + pendingContainerInset * 2.0, height: size.height + pendingContainerInset * 2.0))
self.containerView.frame = containerFrame
self.containerView.update(size: containerFrame.size, borderWidth: 12.0)
/*let maxBlur: CGFloat = 4.0
if component.emoji == nil, (self.containerView.contentView.layer.filters == nil || self.containerView.contentView.layer.filters?.count == 0) {
if let blurFilter = CALayer.blur() {
blurFilter.setValue(maxBlur as NSNumber, forKey: "inputRadius")
self.containerView.contentView.layer.filters = [blurFilter]
self.containerView.contentView.layer.animate(from: 0.0 as NSNumber, to: maxBlur as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2, removeOnCompletion: true)
}
} else if self.containerView.contentView.layer.filters != nil && self.containerView.contentView.layer.filters?.count != 0 {
if let blurFilter = CALayer.blur() {
blurFilter.setValue(0.0 as NSNumber, forKey: "inputRadius")
self.containerView.contentView.layer.filters = [blurFilter]
self.containerView.contentView.layer.animate(from: maxBlur as NSNumber, to: 0.0 as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak self] flag in
if flag, let self {
self.containerView.contentView.layer.filters = nil
}
})
}
}*/
self.containerView.update(size: containerFrame.size, borderWidth: 10.0)
let borderEmoji = 2
let numEmoji = borderEmoji * 2 + 3
var previousEmojiView: ComponentView<Empty>?
var previousEmojiLayer: EmojiContentLayer?
if let emoji = component.emoji {
let emojiView: ComponentView<Empty>
var emojiViewTransition = transition
if let current = self.emojiView {
emojiView = current
let emojiLayer: EmojiContentLayer
var emojiLayerTransition = transition
if let current = self.emojiLayer {
emojiLayer = current
} else {
emojiViewTransition = .immediate
emojiView = ComponentView()
self.emojiView = emojiView
emojiLayerTransition = .immediate
emojiLayer = EmojiContentLayer(size: size)
self.emojiLayer = emojiLayer
}
let emojiSize = emojiView.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: emoji, font: Font.regular(40.0), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 200.0)
)
emojiLayer.setEmoji(emoji: emoji, font: Font.regular(40.0))
let emojiSize = size
let emojiFrame = CGRect(origin: CGPoint(x: pendingContainerInset + floor((size.width - emojiSize.width) * 0.5), y: pendingContainerInset + floor((size.height - emojiSize.height) * 0.5)), size: emojiSize)
if let emojiComponentView = emojiView.view {
if emojiComponentView.superview == nil {
self.containerView.contentView.addSubview(emojiComponentView)
if emojiLayer.superlayer == nil {
self.containerView.contentView.layer.addSublayer(emojiLayer)
}
emojiLayerTransition.setFrame(layer: emojiLayer, frame: emojiFrame)
emojiLayer.setMotionBlurFactor(factor: 0.0, transition: emojiLayerTransition.animation.isImmediate ? .immediate : motionBlurTransition)
if let pendingContainerView = self.pendingContainerView {
self.pendingContainerView = nil
for pendingEmojiLayer in self.pendingEmojiLayers {
pendingEmojiLayer.setMotionBlurFactor(factor: 0.0, transition: motionBlurTransition)
}
emojiViewTransition.setFrame(view: emojiComponentView, frame: emojiFrame)
self.pendingEmojiLayers.removeAll()
if let pendingContainerView = self.pendingContainerView {
self.pendingContainerView = nil
self.pendingEmojiViews.removeAll()
let currentPendingContainerOffset = pendingContainerView.contentView.layer.presentation()?.position.y ?? pendingContainerView.contentView.layer.position.y
pendingContainerView.contentView.layer.removeAnimation(forKey: "offsetCycle")
pendingContainerView.contentView.layer.position.y = currentPendingContainerOffset
let currentPendingContainerOffset = pendingContainerView.contentView.layer.presentation()?.position.y ?? pendingContainerView.contentView.layer.position.y
pendingContainerView.contentView.layer.removeAnimation(forKey: "offsetCycle")
pendingContainerView.contentView.layer.position.y = currentPendingContainerOffset
let animateTransition: ComponentTransition = .spring(duration: 0.4)
let targetOffset: CGFloat = CGFloat(borderEmoji - 1) * size.height
animateTransition.setPosition(layer: pendingContainerView.contentView.layer, position: CGPoint(x: 0.0, y: targetOffset), completion: { [weak self, weak pendingContainerView] _ in
pendingContainerView?.removeFromSuperview()
let animateTransition: ComponentTransition = .spring(duration: 0.4)
let targetOffset: CGFloat = CGFloat(borderEmoji - 1) * size.height
animateTransition.setPosition(layer: pendingContainerView.contentView.layer, position: CGPoint(x: 0.0, y: targetOffset), completion: { [weak self, weak pendingContainerView] _ in
pendingContainerView?.removeFromSuperview()
self?.containerView.isMaskEnabled = false
})
self?.containerView.isMaskEnabled = false
})
animateTransition.animatePosition(view: emojiComponentView, from: CGPoint(x: 0.0, y: currentPendingContainerOffset - targetOffset), to: CGPoint(), additive: true)
} else {
self.containerView.isMaskEnabled = false
}
animateTransition.animatePosition(layer: emojiLayer, from: CGPoint(x: 0.0, y: currentPendingContainerOffset - targetOffset), to: CGPoint(), additive: true)
} else {
self.containerView.isMaskEnabled = false
}
self.pendingEmojiValues = nil
} else {
if let emojiView = self.emojiView {
self.emojiView = nil
previousEmojiView = emojiView
if let emojiLayer = self.emojiLayer {
self.emojiLayer = nil
previousEmojiLayer = emojiLayer
}
if self.pendingEmojiValues?.count != numEmoji {
@ -260,31 +342,25 @@ private final class EmojiItemComponent: Component {
}
for i in 0 ..< numEmoji {
let pendingEmojiView: ComponentView<Empty>
if self.pendingEmojiViews.count > i {
pendingEmojiView = self.pendingEmojiViews[i]
let pendingEmojiLayer: EmojiContentLayer
if self.pendingEmojiLayers.count > i {
pendingEmojiLayer = self.pendingEmojiLayers[i]
} else {
pendingEmojiView = ComponentView()
self.pendingEmojiViews.append(pendingEmojiView)
pendingEmojiLayer = EmojiContentLayer(size: size)
self.pendingEmojiLayers.append(pendingEmojiLayer)
}
let pendingEmojiViewSize = pendingEmojiView.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: pendingEmojiValues[i], font: Font.regular(40.0), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 200.0)
)
if let pendingEmojiComponentView = pendingEmojiView.view {
if pendingEmojiComponentView.superview == nil {
pendingContainerView.contentView.addSubview(pendingEmojiComponentView)
}
pendingEmojiComponentView.frame = CGRect(origin: CGPoint(x: pendingContainerInset, y: pendingContainerInset + CGFloat(i) * size.height), size: pendingEmojiViewSize)
pendingEmojiLayer.setEmoji(emoji: pendingEmojiValues[i], font: Font.regular(40.0))
let pendingEmojiViewSize = size
if pendingEmojiLayer.superlayer == nil {
pendingContainerView.contentView.layer.addSublayer(pendingEmojiLayer)
}
pendingEmojiLayer.frame = CGRect(origin: CGPoint(x: pendingContainerInset, y: pendingContainerInset + CGFloat(i) * size.height), size: pendingEmojiViewSize)
pendingEmojiLayer.setMotionBlurFactor(factor: 1.0, transition: motionBlurTransition)
}
pendingContainerView.frame = CGRect(origin: CGPoint(), size: containerFrame.size)
pendingContainerView.update(size: containerFrame.size, borderWidth: 12.0)
pendingContainerView.update(size: containerFrame.size, borderWidth: 10.0)
if pendingContainerView.superview == nil {
self.containerView.contentView.addSubview(pendingContainerView)
@ -292,11 +368,11 @@ private final class EmojiItemComponent: Component {
let startTime = CACurrentMediaTime()
var loopAnimationOffset: Double = 0.0
if let previousEmojiComponentView = previousEmojiView?.view {
previousEmojiView = nil
if let previousEmojiLayerValue = previousEmojiLayer {
previousEmojiLayer = nil
pendingContainerView.contentView.addSubview(previousEmojiComponentView)
previousEmojiComponentView.center = previousEmojiComponentView.center.offsetBy(dx: 0.0, dy: CGFloat(numEmoji) * size.height)
pendingContainerView.contentView.layer.addSublayer(previousEmojiLayerValue)
previousEmojiLayerValue.position = previousEmojiLayerValue.position.offsetBy(dx: 0.0, dy: CGFloat(numEmoji) * size.height)
let animation = CABasicAnimation(keyPath: "position.y")
loopAnimationOffset = 0.25
@ -311,11 +387,13 @@ private final class EmojiItemComponent: Component {
animation.beginTime = pendingContainerView.contentView.layer.convertTime(startTime, from: nil)
animation.isAdditive = true
animation.completion = { [weak previousEmojiComponentView] _ in
previousEmojiComponentView?.removeFromSuperview()
animation.completion = { [weak previousEmojiLayerValue] _ in
previousEmojiLayerValue?.removeFromSuperlayer()
}
pendingContainerView.contentView.layer.add(animation, forKey: "offsetCyclePre")
previousEmojiLayerValue.setMotionBlurFactor(factor: 1.0, transition: motionBlurTransition)
}
let animation = CABasicAnimation(keyPath: "position.y")
@ -346,14 +424,14 @@ private final class EmojiItemComponent: Component {
self.pendingContainerView = nil
pendingContainerView.removeFromSuperview()
for emojiView in self.pendingEmojiViews {
emojiView.view?.removeFromSuperview()
for emojiLayer in self.pendingEmojiLayers {
emojiLayer.removeFromSuperlayer()
}
self.pendingEmojiViews.removeAll()
self.pendingEmojiLayers.removeAll()
}
if let previousEmojiView {
previousEmojiView.view?.removeFromSuperview()
if let previousEmojiLayer = previousEmojiLayer {
previousEmojiLayer.removeFromSuperlayer()
}
return size
@ -470,7 +548,7 @@ final class VideoChatEncryptionKeyComponent: Component {
self.isUpdating = false
}
#if DEBUG && false
#if DEBUG && true
if self.component == nil {
self.mockStateTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 4.0, repeats: true, block: { [weak self] _ in
guard let self else {

View File

@ -2384,6 +2384,7 @@ public final class AccountViewTracker {
var lhsVideo = false
var lhsMissed = false
var lhsOther = false
var lhsConferenceId: Int64?
inner: for media in lhs.media {
if let action = media as? TelegramMediaAction {
if case let .phoneCall(_, discardReason, _, video) = action.action {
@ -2394,12 +2395,15 @@ public final class AccountViewTracker {
lhsOther = true
}
break inner
} else if case let .conferenceCall(conferenceCall) = action.action {
lhsConferenceId = conferenceCall.callId
}
}
}
var rhsVideo = false
var rhsMissed = false
var rhsOther = false
var rhsConferenceId: Int64?
inner: for media in rhs.media {
if let action = media as? TelegramMediaAction {
if case let .phoneCall(_, discardReason, _, video) = action.action {
@ -2410,10 +2414,12 @@ public final class AccountViewTracker {
rhsOther = true
}
break inner
} else if case let .conferenceCall(conferenceCall) = action.action {
rhsConferenceId = conferenceCall.callId
}
}
}
if lhsMissed != rhsMissed || lhsOther != rhsOther || lhsVideo != rhsVideo {
if lhsMissed != rhsMissed || lhsOther != rhsOther || lhsVideo != rhsVideo || lhsConferenceId != rhsConferenceId {
return false
}
return true
@ -2454,22 +2460,22 @@ public final class AccountViewTracker {
var currentMessages: [Message] = []
for entry in view.entries {
switch entry {
case .hole:
case .hole:
if !currentMessages.isEmpty {
entries.append(.message(currentMessages[currentMessages.count - 1], currentMessages))
currentMessages.removeAll()
}
//entries.append(.hole(index))
case let .message(message):
if currentMessages.isEmpty || groupingPredicate(message, currentMessages[currentMessages.count - 1]) {
currentMessages.append(message)
} else {
if !currentMessages.isEmpty {
entries.append(.message(currentMessages[currentMessages.count - 1], currentMessages))
currentMessages.removeAll()
}
//entries.append(.hole(index))
case let .message(message):
if currentMessages.isEmpty || groupingPredicate(message, currentMessages[currentMessages.count - 1]) {
currentMessages.append(message)
} else {
if !currentMessages.isEmpty {
entries.append(.message(currentMessages[currentMessages.count - 1], currentMessages))
currentMessages.removeAll()
}
currentMessages.append(message)
}
currentMessages.append(message)
}
}
}
if !currentMessages.isEmpty {

View File

@ -727,6 +727,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
peerIds.append(senderId)
}
return peerIds
case let .conferenceCall(conferenceCall):
return conferenceCall.otherParticipants
default:
return []
}

View File

@ -3111,14 +3111,9 @@ func _internal_refreshInlineGroupCall(account: Account, messageId: MessageId) ->
for i in 0 ..< updatedMedia.count {
if let action = updatedMedia[i] as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action {
var otherParticipants: [PeerId] = []
let otherParticipants: [PeerId] = conferenceCall.otherParticipants
var duration: Int32? = conferenceCall.duration
if let result {
for id in result.participants {
if id != account.peerId {
otherParticipants.append(id)
}
}
duration = result.duration
} else {
duration = nil

View File

@ -20,6 +20,8 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/AnimatedAvatarSetNode",
"//submodules/AvatarNode",
],
visibility = [
"//visibility:public",

View File

@ -10,9 +10,12 @@ import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import ChatMessageDateAndStatusNode
import SwiftSignalKit
import AnimatedAvatarSetNode
import AvatarNode
private let titleFont: UIFont = Font.medium(16.0)
private let labelFont: UIFont = Font.regular(13.0)
private let avatarFont: UIFont = avatarPlaceholderFont(size: 8.0)
private let incomingGreenIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/CallIncomingArrow"), color: UIColor(rgb: 0x36c033))
private let incomingRedIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/CallIncomingArrow"), color: UIColor(rgb: 0xff4747))
@ -23,6 +26,11 @@ private let outgoingRedIcon = generateTintedImage(image: UIImage(bundleImageName
public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
private let titleNode: TextNode
private let labelNode: TextNode
private var peopleAvatarsContext: AnimatedAvatarSetContext?
private var peopleAvatarsNode: AnimatedAvatarSetNode?
private var peopleTextNode: TextNode?
private let iconNode: ASImageNode
private let buttonNode: HighlightableButtonNode
@ -82,6 +90,7 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makePeopleTextLayout = TextNode.asyncLayout(self.peopleTextNode)
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
@ -90,8 +99,16 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset, height: constrainedSize.height)
let avatarsLeftInset: CGFloat = 5.0
let avatarsRightInset: CGFloat = 5.0
let peopleAvatarSize: CGFloat = 16.0
let peopleAvatarSpacing: CGFloat = 10.0
let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
var peopleTextString: String?
var peopleAvatars: [Peer] = []
var titleString: String?
var callDuration: Int32?
@ -135,6 +152,20 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
} else if let action = media as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action {
isVideo = conferenceCall.flags.contains(.isVideo)
callDuration = conferenceCall.duration
if conferenceCall.otherParticipants.count > 0 {
//TODO:localize
peopleTextString = "\(conferenceCall.otherParticipants.count + 1) people"
if let peer = item.message.author {
peopleAvatars.append(peer)
}
for id in conferenceCall.otherParticipants {
if let peer = item.message.peers[id] {
peopleAvatars.append(peer)
}
}
}
//TODO:localize
let missedTimeout: Int32
#if DEBUG
@ -217,17 +248,25 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData)
let statusText: String
var statusText: String
if let callDuration = callDuration, callDuration > 1 {
statusText = item.presentationData.strings.Notification_CallFormat(dateText, callDurationString(strings: item.presentationData.strings, value: callDuration)).string
} else {
statusText = dateText
}
if peopleTextString != nil || !peopleAvatars.isEmpty {
statusText.append(",")
}
let attributedLabel = NSAttributedString(string: statusText, font: labelFont, textColor: messageTheme.fileDurationColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedLabel, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var peopleTextLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let peopleTextString {
peopleTextLayoutAndApply = makePeopleTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: peopleTextString, font: labelFont, textColor: messageTheme.fileDurationColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
}
let titleSize = titleLayout.size
let labelSize = labelLayout.size
@ -239,7 +278,21 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
labelFrame = labelFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top + titleSize.height + 4.0)
var boundingSize: CGSize
boundingSize = CGSize(width: max(titleFrame.size.width, labelFrame.size.width + 14.0), height: 47.0)
var labelsWidth: CGFloat = labelFrame.size.width
var avatarsWidth: CGFloat = 0.0
if !peopleAvatars.isEmpty {
avatarsWidth += avatarsLeftInset
avatarsWidth += 1.0 * peopleAvatarSize + CGFloat(peopleAvatars.count - 1) * peopleAvatarSpacing
avatarsWidth += avatarsRightInset
labelsWidth += avatarsWidth
}
if let peopleTextLayoutAndApply {
labelsWidth += peopleTextLayoutAndApply.0.size.width
}
boundingSize = CGSize(width: max(titleFrame.size.width, labelsWidth + 14.0), height: 47.0)
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
@ -257,6 +310,44 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.titleNode.frame = titleFrame
strongSelf.labelNode.frame = labelFrame
if !peopleAvatars.isEmpty {
let peopleAvatarsContext: AnimatedAvatarSetContext
if let current = strongSelf.peopleAvatarsContext {
peopleAvatarsContext = current
} else {
peopleAvatarsContext = AnimatedAvatarSetContext()
strongSelf.peopleAvatarsContext = peopleAvatarsContext
}
let peopleAvatarsNode: AnimatedAvatarSetNode
if let current = strongSelf.peopleAvatarsNode {
peopleAvatarsNode = current
} else {
peopleAvatarsNode = AnimatedAvatarSetNode()
strongSelf.peopleAvatarsNode = peopleAvatarsNode
strongSelf.addSubnode(peopleAvatarsNode)
}
let peopleAvatarsContent = peopleAvatarsContext.update(peers: peopleAvatars.map(EnginePeer.init), animated: false)
let peopleAvatarsSize = peopleAvatarsNode.update(context: item.context, content: peopleAvatarsContent, itemSize: CGSize(width: peopleAvatarSize, height: peopleAvatarSize), customSpacing: peopleAvatarSize - peopleAvatarSpacing, font: avatarFont, animated: false, synchronousLoad: false)
peopleAvatarsNode.frame = CGRect(origin: CGPoint(x: labelFrame.maxX + avatarsLeftInset, y: labelFrame.minY - 1.0), size: peopleAvatarsSize)
} else {
strongSelf.peopleAvatarsContext = nil
if let peopleAvatarsNode = strongSelf.peopleAvatarsNode {
strongSelf.peopleAvatarsNode = nil
peopleAvatarsNode.removeFromSupernode()
}
}
if let peopleTextLayoutAndApply {
let peopleTextNode = peopleTextLayoutAndApply.1()
if strongSelf.peopleTextNode !== peopleTextNode {
strongSelf.peopleTextNode?.removeFromSupernode()
strongSelf.peopleTextNode = peopleTextNode
strongSelf.addSubnode(peopleTextNode)
}
peopleTextNode.frame = CGRect(origin: CGPoint(x: labelFrame.maxX + avatarsWidth, y: labelFrame.minY), size: peopleTextLayoutAndApply.0.size)
}
if let callIcon = callIcon {
if strongSelf.iconNode.image != callIcon {