From c78095e2d36e9275c4e7ba87b59701f5a2e27f0c Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 8 Apr 2025 14:38:46 +0400 Subject: [PATCH] Conference updates --- .../CallListUI/Sources/CallListCallItem.swift | 85 +++++- .../Sources/CallListControllerNode.swift | 2 +- submodules/ImageBlur/Sources/ImageBlur.swift | 55 ++++ .../Sources/ItemListPeerActionItem.swift | 123 ++++---- .../VideoChatEncryptionKeyComponent.swift | 268 +++++++++++------- .../Sources/State/AccountViewTracker.swift | 32 ++- .../SyncCore_TelegramMediaAction.swift | 2 + .../TelegramEngine/Calls/GroupCalls.swift | 7 +- .../ChatMessageCallBubbleContentNode/BUILD | 2 + .../ChatMessageCallBubbleContentNode.swift | 95 ++++++- 10 files changed, 492 insertions(+), 179 deletions(-) diff --git a/submodules/CallListUI/Sources/CallListCallItem.swift b/submodules/CallListUI/Sources/CallListCallItem.swift index e32ac96f5d..a949037d2f 100644 --- a/submodules/CallListUI/Sources/CallListCallItem.swift +++ b/submodules/CallListUI/Sources/CallListCallItem.swift @@ -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) diff --git a/submodules/CallListUI/Sources/CallListControllerNode.swift b/submodules/CallListUI/Sources/CallListControllerNode.swift index f06bbc9561..60be775f9b 100644 --- a/submodules/CallListUI/Sources/CallListControllerNode.swift +++ b/submodules/CallListUI/Sources/CallListControllerNode.swift @@ -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) diff --git a/submodules/ImageBlur/Sources/ImageBlur.swift b/submodules/ImageBlur/Sources/ImageBlur.swift index 1340bddc0b..5fe464c179 100644 --- a/submodules/ImageBlur/Sources/ImageBlur.swift +++ b/submodules/ImageBlur/Sources/ImageBlur.swift @@ -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 + } +} diff --git a/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift b/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift index 29ea6129d2..70d4039d64 100644 --- a/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift +++ b/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift @@ -21,6 +21,7 @@ public enum ItemListPeerActionItemColor { public class ItemListPeerActionItem: ListViewItem, ItemListItem { let presentationData: ItemListPresentationData + let style: ItemListStyle let icon: UIImage? let iconSignal: Signal? 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? = 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? = 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) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatEncryptionKeyComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatEncryptionKeyComponent.swift index e4d6c43309..97ccf22070 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatEncryptionKeyComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatEncryptionKeyComponent.swift @@ -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() private var pendingContainerView: EmojiContainerView? - private var pendingEmojiViews: [ComponentView] = [] - private var emojiView: ComponentView? + 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, 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? + var previousEmojiLayer: EmojiContentLayer? if let emoji = component.emoji { - let emojiView: ComponentView - 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 - 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 { diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 9923e728e1..3246214fce 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 9800d6db6b..d57e8b8173 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -727,6 +727,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { peerIds.append(senderId) } return peerIds + case let .conferenceCall(conferenceCall): + return conferenceCall.otherParticipants default: return [] } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index af3a013edb..6ab530d37d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/BUILD index 77e9f52615..4f7d632f42 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/BUILD @@ -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", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift index 38d85daf9c..ccb94e8d77 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift @@ -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 {