mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Conference updates
This commit is contained in:
parent
81ae40bcde
commit
c78095e2d3
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -727,6 +727,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
||||
peerIds.append(senderId)
|
||||
}
|
||||
return peerIds
|
||||
case let .conferenceCall(conferenceCall):
|
||||
return conferenceCall.otherParticipants
|
||||
default:
|
||||
return []
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user