mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Conference updates
This commit is contained in:
parent
81ae40bcde
commit
c78095e2d3
@ -11,6 +11,7 @@ import AvatarNode
|
|||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import ChatListSearchItemHeader
|
import ChatListSearchItemHeader
|
||||||
|
import AnimatedAvatarSetNode
|
||||||
|
|
||||||
private func callDurationString(strings: PresentationStrings, duration: Int32) -> String {
|
private func callDurationString(strings: PresentationStrings, duration: Int32) -> String {
|
||||||
if duration < 60 {
|
if duration < 60 {
|
||||||
@ -173,6 +174,7 @@ class CallListCallItem: ListViewItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private let avatarFont = avatarPlaceholderFont(size: 16.0)
|
private let avatarFont = avatarPlaceholderFont(size: 16.0)
|
||||||
|
private let multipleAvatarFont = avatarPlaceholderFont(size: 12.0)
|
||||||
|
|
||||||
class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
||||||
private let backgroundNode: ASDisplayNode
|
private let backgroundNode: ASDisplayNode
|
||||||
@ -187,6 +189,10 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private let avatarNode: AvatarNode
|
private let avatarNode: AvatarNode
|
||||||
|
|
||||||
|
private var conferenceAvatarListContext: AnimatedAvatarSetContext?
|
||||||
|
private var conferenceAvatarListNode: AnimatedAvatarSetNode?
|
||||||
|
|
||||||
private let titleNode: TextNode
|
private let titleNode: TextNode
|
||||||
private var credibilityIconNode: ASImageNode?
|
private var credibilityIconNode: ASImageNode?
|
||||||
private let statusNode: TextNode
|
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 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 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 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
|
let editingOffset: CGFloat
|
||||||
var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)?
|
var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)?
|
||||||
@ -376,6 +383,11 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
|||||||
var isConference = false
|
var isConference = false
|
||||||
var conferenceIsDeclined = false
|
var conferenceIsDeclined = false
|
||||||
|
|
||||||
|
let _ = isConference
|
||||||
|
let _ = conferenceIsDeclined
|
||||||
|
|
||||||
|
var conferenceAvatars: [EnginePeer] = []
|
||||||
|
|
||||||
for message in item.messages {
|
for message in item.messages {
|
||||||
inner: for media in message.media {
|
inner: for media in message.media {
|
||||||
if let action = media as? TelegramMediaAction {
|
if let action = media as? TelegramMediaAction {
|
||||||
@ -400,6 +412,16 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
|||||||
} else if case let .conferenceCall(conferenceCall) = action.action {
|
} else if case let .conferenceCall(conferenceCall) = action.action {
|
||||||
isConference = true
|
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)
|
isVideo = conferenceCall.flags.contains(.isVideo)
|
||||||
if message.flags.contains(.Incoming) {
|
if message.flags.contains(.Incoming) {
|
||||||
hasIncoming = true
|
hasIncoming = true
|
||||||
@ -434,7 +456,21 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let peer = item.topMessage.peers[item.topMessage.id.peerId] {
|
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 {
|
if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty {
|
||||||
let string = NSMutableAttributedString()
|
let string = NSMutableAttributedString()
|
||||||
string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor))
|
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)
|
titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: titleColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isConference {
|
if hasMissed {
|
||||||
//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 {
|
|
||||||
statusAttributedString = NSAttributedString(string: item.presentationData.strings.Notification_CallMissedShort, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
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
|
statusAccessibilityString = isVideo ? item.presentationData.strings.Call_VoiceOver_VideoCallMissed : item.presentationData.strings.Call_VoiceOver_VoiceCallMissed
|
||||||
} else if hasIncoming && hasOutgoing {
|
} 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 _ = titleApply()
|
||||||
let titleFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: verticalInset), size: titleLayout.size)
|
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)
|
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
|
||||||
case .createGroupCall:
|
case .createGroupCall:
|
||||||
//TODO:localize
|
//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()
|
nodeInteraction.createGroupCall()
|
||||||
})
|
})
|
||||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
|
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
|
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 {
|
public class ItemListPeerActionItem: ListViewItem, ItemListItem {
|
||||||
let presentationData: ItemListPresentationData
|
let presentationData: ItemListPresentationData
|
||||||
|
let style: ItemListStyle
|
||||||
let icon: UIImage?
|
let icon: UIImage?
|
||||||
let iconSignal: Signal<UIImage?, NoError>?
|
let iconSignal: Signal<UIImage?, NoError>?
|
||||||
let title: String
|
let title: String
|
||||||
@ -34,8 +35,9 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
|
|||||||
public let sectionId: ItemListSectionId
|
public let sectionId: ItemListSectionId
|
||||||
public let action: (() -> Void)?
|
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.presentationData = presentationData
|
||||||
|
self.style = style
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
self.iconSignal = iconSignal
|
self.iconSignal = iconSignal
|
||||||
self.title = title
|
self.title = title
|
||||||
@ -237,10 +239,18 @@ public final class ItemListPeerActionItemNode: ListViewItemNode {
|
|||||||
strongSelf.activateArea.accessibilityLabel = item.title
|
strongSelf.activateArea.accessibilityLabel = item.title
|
||||||
|
|
||||||
if let _ = updatedTheme {
|
if let _ = updatedTheme {
|
||||||
|
switch item.style {
|
||||||
|
case .blocks:
|
||||||
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||||
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||||
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
||||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
|
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()
|
let _ = titleApply()
|
||||||
@ -266,6 +276,22 @@ 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))
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
if strongSelf.backgroundNode.supernode == nil {
|
||||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||||
}
|
}
|
||||||
@ -310,6 +336,7 @@ public final class ItemListPeerActionItemNode: ListViewItemNode {
|
|||||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
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))
|
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)))
|
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)
|
let titleFrame = CGRect(origin: CGPoint(x: leftInset + editingOffset, y: verticalInset + verticalOffset), size: titleLayout.size)
|
||||||
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame)
|
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame)
|
||||||
|
@ -6,6 +6,7 @@ import MultilineTextComponent
|
|||||||
import BalancedTextComponent
|
import BalancedTextComponent
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import CallsEmoji
|
import CallsEmoji
|
||||||
|
import ImageBlur
|
||||||
|
|
||||||
private final class EmojiContainerView: UIView {
|
private final class EmojiContainerView: UIView {
|
||||||
private let maskImageView: UIImageView?
|
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 {
|
private final class EmojiItemComponent: Component {
|
||||||
let emoji: String?
|
let emoji: String?
|
||||||
|
|
||||||
@ -115,8 +210,8 @@ private final class EmojiItemComponent: Component {
|
|||||||
private let containerView: EmojiContainerView
|
private let containerView: EmojiContainerView
|
||||||
private let measureEmojiView = ComponentView<Empty>()
|
private let measureEmojiView = ComponentView<Empty>()
|
||||||
private var pendingContainerView: EmojiContainerView?
|
private var pendingContainerView: EmojiContainerView?
|
||||||
private var pendingEmojiViews: [ComponentView<Empty>] = []
|
private var pendingEmojiLayers: [EmojiContentLayer] = []
|
||||||
private var emojiView: ComponentView<Empty>?
|
private var emojiLayer: EmojiContentLayer?
|
||||||
|
|
||||||
private var component: EmojiItemComponent?
|
private var component: EmojiItemComponent?
|
||||||
private weak var state: EmptyComponentState?
|
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 {
|
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.component = component
|
||||||
self.state = state
|
self.state = state
|
||||||
|
|
||||||
|
let motionBlurTransition: ComponentTransition
|
||||||
|
if transition.animation.isImmediate {
|
||||||
|
motionBlurTransition = .immediate
|
||||||
|
} else {
|
||||||
|
motionBlurTransition = .easeInOut(duration: 0.2)
|
||||||
|
}
|
||||||
|
|
||||||
let size = self.measureEmojiView.update(
|
let size = self.measureEmojiView.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
component: AnyComponent(MultilineTextComponent(
|
component: AnyComponent(MultilineTextComponent(
|
||||||
@ -155,60 +257,41 @@ 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))
|
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.frame = containerFrame
|
||||||
self.containerView.update(size: containerFrame.size, borderWidth: 12.0)
|
self.containerView.update(size: containerFrame.size, borderWidth: 10.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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
let borderEmoji = 2
|
let borderEmoji = 2
|
||||||
let numEmoji = borderEmoji * 2 + 3
|
let numEmoji = borderEmoji * 2 + 3
|
||||||
|
|
||||||
var previousEmojiView: ComponentView<Empty>?
|
var previousEmojiLayer: EmojiContentLayer?
|
||||||
|
|
||||||
if let emoji = component.emoji {
|
if let emoji = component.emoji {
|
||||||
let emojiView: ComponentView<Empty>
|
let emojiLayer: EmojiContentLayer
|
||||||
var emojiViewTransition = transition
|
var emojiLayerTransition = transition
|
||||||
if let current = self.emojiView {
|
if let current = self.emojiLayer {
|
||||||
emojiView = current
|
emojiLayer = current
|
||||||
} else {
|
} else {
|
||||||
emojiViewTransition = .immediate
|
emojiLayerTransition = .immediate
|
||||||
emojiView = ComponentView()
|
emojiLayer = EmojiContentLayer(size: size)
|
||||||
self.emojiView = emojiView
|
self.emojiLayer = emojiLayer
|
||||||
}
|
}
|
||||||
let emojiSize = emojiView.update(
|
emojiLayer.setEmoji(emoji: emoji, font: Font.regular(40.0))
|
||||||
transition: .immediate,
|
let emojiSize = size
|
||||||
component: AnyComponent(MultilineTextComponent(
|
|
||||||
text: .plain(NSAttributedString(string: emoji, font: Font.regular(40.0), textColor: .white))
|
|
||||||
)),
|
|
||||||
environment: {},
|
|
||||||
containerSize: CGSize(width: 200.0, height: 200.0)
|
|
||||||
)
|
|
||||||
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)
|
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 {
|
if emojiLayer.superlayer == nil {
|
||||||
self.containerView.contentView.addSubview(emojiComponentView)
|
self.containerView.contentView.layer.addSublayer(emojiLayer)
|
||||||
}
|
}
|
||||||
emojiViewTransition.setFrame(view: emojiComponentView, frame: emojiFrame)
|
emojiLayerTransition.setFrame(layer: emojiLayer, frame: emojiFrame)
|
||||||
|
|
||||||
|
emojiLayer.setMotionBlurFactor(factor: 0.0, transition: emojiLayerTransition.animation.isImmediate ? .immediate : motionBlurTransition)
|
||||||
|
|
||||||
if let pendingContainerView = self.pendingContainerView {
|
if let pendingContainerView = self.pendingContainerView {
|
||||||
self.pendingContainerView = nil
|
self.pendingContainerView = nil
|
||||||
self.pendingEmojiViews.removeAll()
|
|
||||||
|
for pendingEmojiLayer in self.pendingEmojiLayers {
|
||||||
|
pendingEmojiLayer.setMotionBlurFactor(factor: 0.0, transition: motionBlurTransition)
|
||||||
|
}
|
||||||
|
self.pendingEmojiLayers.removeAll()
|
||||||
|
|
||||||
let currentPendingContainerOffset = pendingContainerView.contentView.layer.presentation()?.position.y ?? pendingContainerView.contentView.layer.position.y
|
let currentPendingContainerOffset = pendingContainerView.contentView.layer.presentation()?.position.y ?? pendingContainerView.contentView.layer.position.y
|
||||||
|
|
||||||
@ -223,17 +306,16 @@ private final class EmojiItemComponent: Component {
|
|||||||
self?.containerView.isMaskEnabled = false
|
self?.containerView.isMaskEnabled = false
|
||||||
})
|
})
|
||||||
|
|
||||||
animateTransition.animatePosition(view: emojiComponentView, from: CGPoint(x: 0.0, y: currentPendingContainerOffset - targetOffset), to: CGPoint(), additive: true)
|
animateTransition.animatePosition(layer: emojiLayer, from: CGPoint(x: 0.0, y: currentPendingContainerOffset - targetOffset), to: CGPoint(), additive: true)
|
||||||
} else {
|
} else {
|
||||||
self.containerView.isMaskEnabled = false
|
self.containerView.isMaskEnabled = false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
self.pendingEmojiValues = nil
|
self.pendingEmojiValues = nil
|
||||||
} else {
|
} else {
|
||||||
if let emojiView = self.emojiView {
|
if let emojiLayer = self.emojiLayer {
|
||||||
self.emojiView = nil
|
self.emojiLayer = nil
|
||||||
previousEmojiView = emojiView
|
previousEmojiLayer = emojiLayer
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.pendingEmojiValues?.count != numEmoji {
|
if self.pendingEmojiValues?.count != numEmoji {
|
||||||
@ -260,31 +342,25 @@ private final class EmojiItemComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i in 0 ..< numEmoji {
|
for i in 0 ..< numEmoji {
|
||||||
let pendingEmojiView: ComponentView<Empty>
|
let pendingEmojiLayer: EmojiContentLayer
|
||||||
if self.pendingEmojiViews.count > i {
|
if self.pendingEmojiLayers.count > i {
|
||||||
pendingEmojiView = self.pendingEmojiViews[i]
|
pendingEmojiLayer = self.pendingEmojiLayers[i]
|
||||||
} else {
|
} else {
|
||||||
pendingEmojiView = ComponentView()
|
pendingEmojiLayer = EmojiContentLayer(size: size)
|
||||||
self.pendingEmojiViews.append(pendingEmojiView)
|
self.pendingEmojiLayers.append(pendingEmojiLayer)
|
||||||
}
|
}
|
||||||
let pendingEmojiViewSize = pendingEmojiView.update(
|
pendingEmojiLayer.setEmoji(emoji: pendingEmojiValues[i], font: Font.regular(40.0))
|
||||||
transition: .immediate,
|
let pendingEmojiViewSize = size
|
||||||
component: AnyComponent(MultilineTextComponent(
|
if pendingEmojiLayer.superlayer == nil {
|
||||||
text: .plain(NSAttributedString(string: pendingEmojiValues[i], font: Font.regular(40.0), textColor: .white))
|
pendingContainerView.contentView.layer.addSublayer(pendingEmojiLayer)
|
||||||
)),
|
|
||||||
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.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.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 {
|
if pendingContainerView.superview == nil {
|
||||||
self.containerView.contentView.addSubview(pendingContainerView)
|
self.containerView.contentView.addSubview(pendingContainerView)
|
||||||
@ -292,11 +368,11 @@ private final class EmojiItemComponent: Component {
|
|||||||
let startTime = CACurrentMediaTime()
|
let startTime = CACurrentMediaTime()
|
||||||
|
|
||||||
var loopAnimationOffset: Double = 0.0
|
var loopAnimationOffset: Double = 0.0
|
||||||
if let previousEmojiComponentView = previousEmojiView?.view {
|
if let previousEmojiLayerValue = previousEmojiLayer {
|
||||||
previousEmojiView = nil
|
previousEmojiLayer = nil
|
||||||
|
|
||||||
pendingContainerView.contentView.addSubview(previousEmojiComponentView)
|
pendingContainerView.contentView.layer.addSublayer(previousEmojiLayerValue)
|
||||||
previousEmojiComponentView.center = previousEmojiComponentView.center.offsetBy(dx: 0.0, dy: CGFloat(numEmoji) * size.height)
|
previousEmojiLayerValue.position = previousEmojiLayerValue.position.offsetBy(dx: 0.0, dy: CGFloat(numEmoji) * size.height)
|
||||||
|
|
||||||
let animation = CABasicAnimation(keyPath: "position.y")
|
let animation = CABasicAnimation(keyPath: "position.y")
|
||||||
loopAnimationOffset = 0.25
|
loopAnimationOffset = 0.25
|
||||||
@ -311,11 +387,13 @@ private final class EmojiItemComponent: Component {
|
|||||||
animation.beginTime = pendingContainerView.contentView.layer.convertTime(startTime, from: nil)
|
animation.beginTime = pendingContainerView.contentView.layer.convertTime(startTime, from: nil)
|
||||||
animation.isAdditive = true
|
animation.isAdditive = true
|
||||||
|
|
||||||
animation.completion = { [weak previousEmojiComponentView] _ in
|
animation.completion = { [weak previousEmojiLayerValue] _ in
|
||||||
previousEmojiComponentView?.removeFromSuperview()
|
previousEmojiLayerValue?.removeFromSuperlayer()
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingContainerView.contentView.layer.add(animation, forKey: "offsetCyclePre")
|
pendingContainerView.contentView.layer.add(animation, forKey: "offsetCyclePre")
|
||||||
|
|
||||||
|
previousEmojiLayerValue.setMotionBlurFactor(factor: 1.0, transition: motionBlurTransition)
|
||||||
}
|
}
|
||||||
|
|
||||||
let animation = CABasicAnimation(keyPath: "position.y")
|
let animation = CABasicAnimation(keyPath: "position.y")
|
||||||
@ -346,14 +424,14 @@ private final class EmojiItemComponent: Component {
|
|||||||
self.pendingContainerView = nil
|
self.pendingContainerView = nil
|
||||||
pendingContainerView.removeFromSuperview()
|
pendingContainerView.removeFromSuperview()
|
||||||
|
|
||||||
for emojiView in self.pendingEmojiViews {
|
for emojiLayer in self.pendingEmojiLayers {
|
||||||
emojiView.view?.removeFromSuperview()
|
emojiLayer.removeFromSuperlayer()
|
||||||
}
|
}
|
||||||
self.pendingEmojiViews.removeAll()
|
self.pendingEmojiLayers.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
if let previousEmojiView {
|
if let previousEmojiLayer = previousEmojiLayer {
|
||||||
previousEmojiView.view?.removeFromSuperview()
|
previousEmojiLayer.removeFromSuperlayer()
|
||||||
}
|
}
|
||||||
|
|
||||||
return size
|
return size
|
||||||
@ -470,7 +548,7 @@ final class VideoChatEncryptionKeyComponent: Component {
|
|||||||
self.isUpdating = false
|
self.isUpdating = false
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG && false
|
#if DEBUG && true
|
||||||
if self.component == nil {
|
if self.component == nil {
|
||||||
self.mockStateTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 4.0, repeats: true, block: { [weak self] _ in
|
self.mockStateTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 4.0, repeats: true, block: { [weak self] _ in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
|
@ -2384,6 +2384,7 @@ public final class AccountViewTracker {
|
|||||||
var lhsVideo = false
|
var lhsVideo = false
|
||||||
var lhsMissed = false
|
var lhsMissed = false
|
||||||
var lhsOther = false
|
var lhsOther = false
|
||||||
|
var lhsConferenceId: Int64?
|
||||||
inner: for media in lhs.media {
|
inner: for media in lhs.media {
|
||||||
if let action = media as? TelegramMediaAction {
|
if let action = media as? TelegramMediaAction {
|
||||||
if case let .phoneCall(_, discardReason, _, video) = action.action {
|
if case let .phoneCall(_, discardReason, _, video) = action.action {
|
||||||
@ -2394,12 +2395,15 @@ public final class AccountViewTracker {
|
|||||||
lhsOther = true
|
lhsOther = true
|
||||||
}
|
}
|
||||||
break inner
|
break inner
|
||||||
|
} else if case let .conferenceCall(conferenceCall) = action.action {
|
||||||
|
lhsConferenceId = conferenceCall.callId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var rhsVideo = false
|
var rhsVideo = false
|
||||||
var rhsMissed = false
|
var rhsMissed = false
|
||||||
var rhsOther = false
|
var rhsOther = false
|
||||||
|
var rhsConferenceId: Int64?
|
||||||
inner: for media in rhs.media {
|
inner: for media in rhs.media {
|
||||||
if let action = media as? TelegramMediaAction {
|
if let action = media as? TelegramMediaAction {
|
||||||
if case let .phoneCall(_, discardReason, _, video) = action.action {
|
if case let .phoneCall(_, discardReason, _, video) = action.action {
|
||||||
@ -2410,10 +2414,12 @@ public final class AccountViewTracker {
|
|||||||
rhsOther = true
|
rhsOther = true
|
||||||
}
|
}
|
||||||
break inner
|
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 false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@ -727,6 +727,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
|||||||
peerIds.append(senderId)
|
peerIds.append(senderId)
|
||||||
}
|
}
|
||||||
return peerIds
|
return peerIds
|
||||||
|
case let .conferenceCall(conferenceCall):
|
||||||
|
return conferenceCall.otherParticipants
|
||||||
default:
|
default:
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -3111,14 +3111,9 @@ func _internal_refreshInlineGroupCall(account: Account, messageId: MessageId) ->
|
|||||||
|
|
||||||
for i in 0 ..< updatedMedia.count {
|
for i in 0 ..< updatedMedia.count {
|
||||||
if let action = updatedMedia[i] as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action {
|
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
|
var duration: Int32? = conferenceCall.duration
|
||||||
if let result {
|
if let result {
|
||||||
for id in result.participants {
|
|
||||||
if id != account.peerId {
|
|
||||||
otherParticipants.append(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
duration = result.duration
|
duration = result.duration
|
||||||
} else {
|
} else {
|
||||||
duration = nil
|
duration = nil
|
||||||
|
@ -20,6 +20,8 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
|
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
|
||||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
|
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
|
||||||
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
|
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
|
||||||
|
"//submodules/AnimatedAvatarSetNode",
|
||||||
|
"//submodules/AvatarNode",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -10,9 +10,12 @@ import ChatMessageBubbleContentNode
|
|||||||
import ChatMessageItemCommon
|
import ChatMessageItemCommon
|
||||||
import ChatMessageDateAndStatusNode
|
import ChatMessageDateAndStatusNode
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
import AnimatedAvatarSetNode
|
||||||
|
import AvatarNode
|
||||||
|
|
||||||
private let titleFont: UIFont = Font.medium(16.0)
|
private let titleFont: UIFont = Font.medium(16.0)
|
||||||
private let labelFont: UIFont = Font.regular(13.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 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))
|
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 {
|
public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||||
private let titleNode: TextNode
|
private let titleNode: TextNode
|
||||||
private let labelNode: TextNode
|
private let labelNode: TextNode
|
||||||
|
|
||||||
|
private var peopleAvatarsContext: AnimatedAvatarSetContext?
|
||||||
|
private var peopleAvatarsNode: AnimatedAvatarSetNode?
|
||||||
|
private var peopleTextNode: TextNode?
|
||||||
|
|
||||||
private let iconNode: ASImageNode
|
private let iconNode: ASImageNode
|
||||||
private let buttonNode: HighlightableButtonNode
|
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))) {
|
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 makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||||
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||||
|
let makePeopleTextLayout = TextNode.asyncLayout(self.peopleTextNode)
|
||||||
|
|
||||||
return { item, layoutConstants, _, _, _, _ in
|
return { item, layoutConstants, _, _, _, _ in
|
||||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
||||||
@ -91,8 +100,16 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
|
let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
|
||||||
let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset, height: constrainedSize.height)
|
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
|
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 titleString: String?
|
||||||
var callDuration: Int32?
|
var callDuration: Int32?
|
||||||
var callSuccessful = true
|
var callSuccessful = true
|
||||||
@ -135,6 +152,20 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
} else if let action = media as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action {
|
} else if let action = media as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action {
|
||||||
isVideo = conferenceCall.flags.contains(.isVideo)
|
isVideo = conferenceCall.flags.contains(.isVideo)
|
||||||
callDuration = conferenceCall.duration
|
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
|
//TODO:localize
|
||||||
let missedTimeout: Int32
|
let missedTimeout: Int32
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@ -217,18 +248,26 @@ 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 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 {
|
if let callDuration = callDuration, callDuration > 1 {
|
||||||
statusText = item.presentationData.strings.Notification_CallFormat(dateText, callDurationString(strings: item.presentationData.strings, value: callDuration)).string
|
statusText = item.presentationData.strings.Notification_CallFormat(dateText, callDurationString(strings: item.presentationData.strings, value: callDuration)).string
|
||||||
} else {
|
} else {
|
||||||
statusText = dateText
|
statusText = dateText
|
||||||
}
|
}
|
||||||
|
if peopleTextString != nil || !peopleAvatars.isEmpty {
|
||||||
|
statusText.append(",")
|
||||||
|
}
|
||||||
|
|
||||||
let attributedLabel = NSAttributedString(string: statusText, font: labelFont, textColor: messageTheme.fileDurationColor)
|
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 (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()))
|
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 titleSize = titleLayout.size
|
||||||
let labelSize = labelLayout.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)
|
labelFrame = labelFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top + titleSize.height + 4.0)
|
||||||
|
|
||||||
var boundingSize: CGSize
|
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.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
|
||||||
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
|
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
|
||||||
|
|
||||||
@ -258,6 +311,44 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
strongSelf.titleNode.frame = titleFrame
|
strongSelf.titleNode.frame = titleFrame
|
||||||
strongSelf.labelNode.frame = labelFrame
|
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 let callIcon = callIcon {
|
||||||
if strongSelf.iconNode.image != callIcon {
|
if strongSelf.iconNode.image != callIcon {
|
||||||
strongSelf.iconNode.image = callIcon
|
strongSelf.iconNode.image = callIcon
|
||||||
|
Loading…
x
Reference in New Issue
Block a user