Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "CallListUI",
module_name = "CallListUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/AccountContext:AccountContext",
"//submodules/ItemListUI:ItemListUI",
"//submodules/AvatarNode:AvatarNode",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/AlertUI:AlertUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/TelegramNotices:TelegramNotices",
"//submodules/MergeLists:MergeLists",
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
"//submodules/PeerOnlineMarkerNode:PeerOnlineMarkerNode",
"//submodules/ContextUI:ContextUI",
"//submodules/TelegramBaseController:TelegramBaseController",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/ItemListPeerActionItem",
"//submodules/InviteLinksUI",
"//submodules/UndoUI",
"//submodules/TelegramCallsUI",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,898 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AvatarNode
import TelegramStringFormatting
import AccountContext
import ChatListSearchItemHeader
import AnimatedAvatarSetNode
private func callDurationString(strings: PresentationStrings, duration: Int32) -> String {
if duration < 60 {
return strings.Call_ShortSeconds(duration)
} else {
return strings.Call_ShortMinutes(duration / 60)
}
}
private func callListNeighbors(item: ListViewItem, topItem: ListViewItem?, bottomItem: ListViewItem?) -> ItemListNeighbors {
let topNeighbor: ItemListNeighbor
if let topItem = topItem {
if let item = item as? ItemListItem, let topItem = topItem as? ItemListItem {
if topItem.sectionId != item.sectionId {
topNeighbor = .otherSection(topItem.requestsNoInset ? .none : .full)
} else {
topNeighbor = .sameSection(alwaysPlain: topItem.isAlwaysPlain)
}
} else {
if item is CallListCallItem && topItem is CallListCallItem {
topNeighbor = .sameSection(alwaysPlain: false)
} else {
topNeighbor = .otherSection(.full)
}
}
} else {
topNeighbor = .none
}
let bottomNeighbor: ItemListNeighbor
if let bottomItem = bottomItem {
if let item = item as? ItemListItem, let bottomItem = bottomItem as? ItemListItem {
if bottomItem.sectionId != item.sectionId {
bottomNeighbor = .otherSection(bottomItem.requestsNoInset ? .none : .full)
} else {
bottomNeighbor = .sameSection(alwaysPlain: bottomItem.isAlwaysPlain)
}
} else {
if item is CallListCallItem && bottomItem is CallListCallItem {
bottomNeighbor = .sameSection(alwaysPlain: false)
} else {
bottomNeighbor = .otherSection(.full)
}
}
} else {
bottomNeighbor = .none
}
return ItemListNeighbors(top: topNeighbor, bottom: bottomNeighbor)
}
class CallListCallItem: ListViewItem {
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let dateTimeFormat: PresentationDateTimeFormat
let context: AccountContext
let style: ItemListStyle
let topMessage: EngineMessage
let messages: [EngineMessage]
let editing: Bool
let revealed: Bool
let interaction: CallListNodeInteraction
let selectable: Bool = true
let headerAccessoryItem: ListViewAccessoryItem?
let header: ListViewItemHeader?
init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle, dateTimeFormat: PresentationDateTimeFormat, context: AccountContext, style: ItemListStyle, topMessage: EngineMessage, messages: [EngineMessage], editing: Bool, revealed: Bool, displayHeader: Bool, interaction: CallListNodeInteraction) {
self.presentationData = presentationData
self.systemStyle = systemStyle
self.dateTimeFormat = dateTimeFormat
self.context = context
self.style = style
self.topMessage = topMessage
self.messages = messages
self.editing = editing
self.revealed = revealed
self.interaction = interaction
self.headerAccessoryItem = nil
if displayHeader {
self.header = ChatListSearchItemHeader(type: .recentCalls, theme: presentationData.theme, strings: presentationData.strings)
} else {
self.header = nil
}
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = CallListCallItemNode()
let makeLayout = node.asyncLayout()
let (first, last, firstWithHeader) = CallListCallItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (nodeLayout, nodeApply) = makeLayout(self, params, first, last, firstWithHeader, callListNeighbors(item: self, topItem: previousItem, bottomItem: nextItem))
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
nodeApply(synchronousLoads).1(false)
})
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? CallListCallItemNode {
let layout = nodeValue.asyncLayout()
async {
let (first, last, firstWithHeader) = CallListCallItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader, callListNeighbors(item: self, topItem: previousItem, bottomItem: nextItem))
var animated = true
if case .None = animation {
animated = false
}
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply(false).1(animated)
})
}
}
}
}
}
func selected(listView: ListView) {
listView.clearHighlightAnimated(true)
self.interaction.call(self.topMessage)
}
static func mergeType(item: CallListCallItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool) {
var first = false
var last = false
var firstWithHeader = false
if let previousItem = previousItem {
if let header = item.header {
if let previousItem = previousItem as? CallListCallItem {
firstWithHeader = header.id != previousItem.header?.id
} else {
firstWithHeader = true
}
}
} else {
first = true
firstWithHeader = item.header != nil
}
if let nextItem = nextItem {
if let header = item.header {
if let nextItem = nextItem as? CallListCallItem {
last = header.id != nextItem.header?.id
} else {
last = true
}
}
} else {
last = true
}
return (first, last, firstWithHeader)
}
}
private let avatarFont = avatarPlaceholderFont(size: 16.0)
private let multipleAvatarFont = avatarPlaceholderFont(size: 12.0)
class CallListCallItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private let containerNode: ASDisplayNode
override var controlsContainer: ASDisplayNode {
return self.containerNode
}
private let avatarNode: AvatarNode
private var conferenceAvatarListContext: AnimatedAvatarSetContext?
private var conferenceAvatarListNode: AnimatedAvatarSetNode?
private let titleNode: TextNode
private var credibilityIconNode: ASImageNode?
private let statusNode: TextNode
private let dateNode: TextNode
private let typeIconNode: ASImageNode
private let infoButtonNode: HighlightableButtonNode
var editableControlNode: ItemListEditableControlNode?
private let accessibilityArea: AccessibilityAreaNode
private var layoutParams: (CallListCallItem, ListViewItemLayoutParams, Bool, Bool, Bool)?
required init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.containerNode = ASDisplayNode()
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.isLayerBacked = !smartInvertColorsEnabled()
self.titleNode = TextNode()
self.statusNode = TextNode()
self.dateNode = TextNode()
self.typeIconNode = ASImageNode()
self.typeIconNode.isLayerBacked = true
self.typeIconNode.displayWithoutProcessing = true
self.typeIconNode.displaysAsynchronously = false
self.infoButtonNode = HighlightableButtonNode()
self.infoButtonNode.hitTestSlop = UIEdgeInsets(top: -6.0, left: -6.0, bottom: -6.0, right: -10.0)
self.accessibilityArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.avatarNode)
self.containerNode.addSubnode(self.typeIconNode)
self.containerNode.addSubnode(self.titleNode)
self.containerNode.addSubnode(self.statusNode)
self.containerNode.addSubnode(self.dateNode)
self.containerNode.addSubnode(self.infoButtonNode)
self.addSubnode(self.accessibilityArea)
self.infoButtonNode.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside)
self.accessibilityArea.activate = { [weak self] in
guard let item = self?.layoutParams?.0 else {
return false
}
item.interaction.call(item.topMessage)
return true
}
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let (item, _, _, _, _) = self.layoutParams {
let (first, last, firstWithHeader) = CallListCallItem.mergeType(item: item, previousItem: previousItem, nextItem: nextItem)
self.layoutParams = (item, params, first, last, firstWithHeader)
let makeLayout = self.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(item, params, first, last, firstWithHeader, callListNeighbors(item: item, topItem: previousItem, bottomItem: nextItem))
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply(false)
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
if self.backgroundNode.supernode != nil {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.backgroundNode)
} else {
self.insertSubnode(self.highlightedBackgroundNode, at: 0)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
func asyncLayout() -> (_ item: CallListCallItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> (Signal<Void, NoError>?, (Bool) -> Void)) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
let makeDateLayout = TextNode.asyncLayout(self.dateNode)
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
let currentItem = self.layoutParams?.0
return { [weak self] item, params, first, last, firstWithHeader, neighbors in
var updatedTheme: PresentationTheme?
var updatedInfoIcon = false
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
updatedInfoIcon = true
}
let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
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)?
if item.editing {
let sizeAndApply = editableControlLayout(item.presentationData.theme, false)
editableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0
} else {
editingOffset = 0.0
}
var leftInset: CGFloat = 46.0 + avatarDiameter + params.leftInset
let rightInset: CGFloat = 13.0 + params.rightInset
var infoIconRightInset: CGFloat = rightInset - 1.0
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
var dateRightInset: CGFloat = 46.0 + params.rightInset
if item.editing {
leftInset += editingOffset
dateRightInset += 5.0
infoIconRightInset -= 36.0
}
var titleAttributedString: NSAttributedString?
var statusAttributedString: NSAttributedString?
var statusAccessibilityString = ""
var titleColor = item.presentationData.theme.list.itemPrimaryTextColor
var hasMissed = false
var hasIncoming = false
var hasOutgoing = false
var isVideo = false
var hadDuration = false
var callDuration: Int32?
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 {
if case let .phoneCall(_, discardReason, duration, video) = action.action {
isVideo = video
if message.flags.contains(.Incoming) {
hasIncoming = true
if let discardReason = discardReason, case .missed = discardReason {
titleColor = item.presentationData.theme.list.itemDestructiveColor
hasMissed = true
}
} else {
hasOutgoing = true
}
if callDuration == nil && !hadDuration {
hadDuration = true
callDuration = duration
} else {
callDuration = nil
}
} else if case let .conferenceCall(conferenceCall) = action.action {
isConference = true
if let peer = message.peers[message.id.peerId], !conferenceAvatars.contains(where: { $0.id == peer.id }) {
conferenceAvatars.append(EnginePeer(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) {
hasIncoming = true
let missedTimeout: Int32
#if DEBUG && false
missedTimeout = 5
#else
missedTimeout = 30
#endif
let currentTime = Int32(Date().timeIntervalSince1970)
if conferenceCall.flags.contains(.isMissed) {
titleColor = item.presentationData.theme.list.itemDestructiveColor
conferenceIsDeclined = true
} else if message.timestamp < currentTime - missedTimeout {
titleColor = item.presentationData.theme.list.itemDestructiveColor
hasMissed = true
}
} else {
hasOutgoing = true
}
if callDuration == nil && !hadDuration {
hadDuration = true
callDuration = conferenceCall.duration
} else {
callDuration = nil
}
}
break inner
}
}
}
if let peer = item.topMessage.peers[item.topMessage.id.peerId] {
if conferenceAvatars.count > 1 {
var peersString = ""
for peer in conferenceAvatars {
if !peersString.isEmpty {
peersString.append(", ")
}
if peer.id == item.context.account.peerId {
peersString += item.presentationData.strings.DialogList_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))
string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor))
string.append(NSAttributedString(string: lastName, font: titleFont, textColor: titleColor))
if item.messages.count > 1 {
string.append(NSAttributedString(string: " (\(item.messages.count))", font: titleFont, textColor: titleColor))
}
titleAttributedString = string
} else if let firstName = user.firstName, !firstName.isEmpty {
titleAttributedString = NSAttributedString(string: firstName, font: titleFont, textColor: titleColor)
} else if let lastName = user.lastName, !lastName.isEmpty {
titleAttributedString = NSAttributedString(string: lastName, font: titleFont, textColor: titleColor)
} else {
titleAttributedString = NSAttributedString(string: item.presentationData.strings.User_DeletedAccount, font: titleFont, textColor: titleColor)
}
} else if let group = peer as? TelegramGroup {
titleAttributedString = NSAttributedString(string: group.title, font: titleFont, textColor: titleColor)
} else if let channel = peer as? TelegramChannel {
titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: titleColor)
}
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 {
statusAttributedString = NSAttributedString(string: item.presentationData.strings.Notification_CallOutgoingShort + ", " + item.presentationData.strings.Notification_CallIncomingShort, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
statusAccessibilityString = isVideo ? (item.presentationData.strings.Call_VoiceOver_VideoCallOutgoing + ", " + item.presentationData.strings.Call_VoiceOver_VideoCallIncoming) : (item.presentationData.strings.Call_VoiceOver_VoiceCallOutgoing + ", " + item.presentationData.strings.Call_VoiceOver_VoiceCallIncoming)
} else if hasIncoming {
if let callDuration = callDuration, callDuration != 0 {
statusAttributedString = NSAttributedString(string: item.presentationData.strings.Notification_CallTimeFormat(item.presentationData.strings.Notification_CallIncomingShort, callDurationString(strings: item.presentationData.strings, duration: callDuration)).string, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
statusAccessibilityString = item.presentationData.strings.Notification_CallTimeFormat(isVideo ? item.presentationData.strings.Call_VoiceOver_VideoCallIncoming : item.presentationData.strings.Call_VoiceOver_VoiceCallIncoming, callDurationString(strings: item.presentationData.strings, duration: callDuration)).string
} else {
statusAttributedString = NSAttributedString(string: item.presentationData.strings.Notification_CallIncomingShort, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
statusAccessibilityString = isVideo ? item.presentationData.strings.Call_VoiceOver_VideoCallIncoming : item.presentationData.strings.Call_VoiceOver_VoiceCallIncoming
}
} else {
if let callDuration = callDuration, callDuration != 0 {
statusAttributedString = NSAttributedString(string: item.presentationData.strings.Notification_CallTimeFormat(item.presentationData.strings.Notification_CallOutgoingShort, callDurationString(strings: item.presentationData.strings, duration: callDuration)).string, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
statusAccessibilityString = item.presentationData.strings.Notification_CallTimeFormat(isVideo ? item.presentationData.strings.Call_VoiceOver_VideoCallOutgoing : item.presentationData.strings.Call_VoiceOver_VoiceCallOutgoing, callDurationString(strings: item.presentationData.strings, duration: callDuration)).string
} else {
statusAttributedString = NSAttributedString(string: item.presentationData.strings.Notification_CallOutgoingShort, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
statusAccessibilityString = isVideo ? item.presentationData.strings.Call_VoiceOver_VideoCallOutgoing : item.presentationData.strings.Call_VoiceOver_VoiceCallOutgoing
}
}
}
var t = Int(item.topMessage.timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo)
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.topMessage.timestamp, relativeTo: timestamp, dateTimeFormat: item.dateTimeFormat)
let (dateLayout, dateApply) = makeDateLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: dateText, font: dateFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 })
var currentCredibilityIconImage: UIImage?
if let peer = item.topMessage.peers[item.topMessage.id.peerId], peer.id != item.context.account.peerId {
if peer.isScam {
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
} else if peer.isFake {
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
} else if peer.isVerified {
currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme)
} else if peer.isPremium && !premiumConfiguration.isPremiumDisabled {
currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme)
}
}
var additionalTitleInset: CGFloat = 0.0
if let currentCredibilityIconImage = currentCredibilityIconImage {
additionalTitleInset += 3.0 + currentCredibilityIconImage.size.width
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - additionalTitleInset - leftInset - dateRightInset - dateLayout.size.width - (item.editing ? -30.0 : 10.0)), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - dateRightInset - dateLayout.size.width - (item.editing ? -30.0 : 10.0)), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let titleSpacing: CGFloat = -1.0
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
verticalInset = 10.0
case .legacy:
verticalInset = 6.0
}
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: titleLayout.size.height + titleSpacing + statusLayout.size.height + verticalInset * 2.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0))
let outgoingVoiceIcon = PresentationResourcesCallList.outgoingIcon(item.presentationData.theme)
let outgoingVideoIcon = PresentationResourcesCallList.outgoingVideoIcon(item.presentationData.theme)
let infoIcon = PresentationResourcesCallList.infoButton(item.presentationData.theme)
let outgoingIcon = isVideo ? outgoingVideoIcon : outgoingVoiceIcon
let contentSize = nodeLayout.contentSize
return (nodeLayout, { [weak self] synchronousLoads in
if let strongSelf = self {
if let peer = item.topMessage.peers[item.topMessage.id.peerId] {
var overrideImage: AvatarNodeImageOverride?
if peer.isDeleted {
overrideImage = .deletedIcon
}
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: EnginePeer(peer), overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads)
}
return (strongSelf.avatarNode.ready, { [weak strongSelf] animated in
if let strongSelf = strongSelf {
strongSelf.layoutParams = (item, params, first, last, firstWithHeader)
let revealOffset = strongSelf.revealOffset
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
if let editableControlSizeAndApply = editableControlSizeAndApply {
let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: nodeLayout.contentSize.height))
if strongSelf.editableControlNode == nil {
let editableControlNode = editableControlSizeAndApply.1(nodeLayout.contentSize.height)
editableControlNode.tapped = {
if let strongSelf = self {
strongSelf.setRevealOptionsOpened(true, animated: true)
strongSelf.revealOptionsInteractivelyOpened()
}
}
strongSelf.editableControlNode = editableControlNode
strongSelf.addSubnode(editableControlNode)
editableControlNode.frame = editableControlFrame
transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY))
editableControlNode.alpha = 0.0
transition.updateAlpha(node: editableControlNode, alpha: 1.0)
} else {
strongSelf.editableControlNode?.frame = editableControlFrame
}
} else if let editableControlNode = strongSelf.editableControlNode {
var editableControlFrame = editableControlNode.frame
editableControlFrame.origin.x = -editableControlFrame.size.width
strongSelf.editableControlNode = nil
transition.updateAlpha(node: editableControlNode, alpha: 0.0)
transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in
editableControlNode?.removeFromSupernode()
})
}
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if !last && strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 1)
} else if last && strongSelf.bottomStripeNode.supernode != nil {
strongSelf.bottomStripeNode.removeFromSupernode()
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
transition.updateFrameAdditive(node: 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.addSubnode(strongSelf.maskNode)
}
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
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : 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: nodeLayout.size.width, height: separatorHeight))
transition.updateFrameAdditive(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight)))
}
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: Array(conferenceAvatars.prefix(3)), 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)
transition.updateFrameAdditive(node: strongSelf.titleNode, frame: titleFrame)
let _ = statusApply()
transition.updateFrameAdditive(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size))
if let currentCredibilityIconImage = currentCredibilityIconImage {
let iconNode: ASImageNode
if let current = strongSelf.credibilityIconNode {
iconNode = current
} else {
iconNode = ASImageNode()
iconNode.isLayerBacked = true
iconNode.displaysAsynchronously = false
iconNode.displayWithoutProcessing = true
strongSelf.credibilityIconNode = iconNode
}
iconNode.image = currentCredibilityIconImage
transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - currentCredibilityIconImage.size.height / 2.0) - UIScreenPixel), size: currentCredibilityIconImage.size))
} else if let credibilityIconNode = strongSelf.credibilityIconNode {
strongSelf.credibilityIconNode = nil
credibilityIconNode.removeFromSupernode()
}
let _ = dateApply()
transition.updateFrameAdditive(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + params.width - dateRightInset - dateLayout.size.width, y: floor((nodeLayout.contentSize.height - dateLayout.size.height) / 2.0) + 2.0), size: dateLayout.size))
if let outgoingIcon = outgoingIcon {
if strongSelf.typeIconNode.image !== outgoingIcon {
strongSelf.typeIconNode.image = outgoingIcon
}
transition.updateFrameAdditive(node: strongSelf.typeIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 79.0, y: floor((nodeLayout.contentSize.height - outgoingIcon.size.height) / 2.0)), size: outgoingIcon.size))
}
strongSelf.typeIconNode.isHidden = !hasOutgoing
if let infoIcon = infoIcon {
if updatedInfoIcon {
strongSelf.infoButtonNode.setImage(infoIcon, for: [])
}
transition.updateFrameAdditive(node: strongSelf.infoButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - infoIconRightInset - infoIcon.size.width, y: floor((nodeLayout.contentSize.height - infoIcon.size.height) / 2.0)), size: infoIcon.size))
}
transition.updateAlpha(node: strongSelf.infoButtonNode, alpha: item.editing ? 0.0 : 1.0)
let topHighlightInset: CGFloat = (first || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height))
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: strongSelf.backgroundNode.frame.size)
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset))
strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
strongSelf.accessibilityArea.accessibilityLabel = titleAttributedString?.string
strongSelf.accessibilityArea.accessibilityValue = statusAccessibilityString
strongSelf.accessibilityArea.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
strongSelf.infoButtonNode.accessibilityLabel = item.presentationData.strings.Conversation_Info
strongSelf.view.accessibilityCustomActions = [UIAccessibilityCustomAction(name: item.presentationData.strings.Common_Delete, target: strongSelf, selector: #selector(strongSelf.performLocalAccessibilityCustomAction(_:)))]
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
strongSelf.setRevealOptionsOpened(item.revealed, animated: animated)
}
})
} else {
return (nil, { _ in })
}
})
}
}
override func layoutHeaderAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) {
let bounds = self.bounds
accessoryItemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -29.0), size: CGSize(width: bounds.size.width, height: 29.0))
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.3, removeOnCompletion: false)
}
override public func headers() -> [ListViewItemHeader]? {
if let (item, _, _, _, _) = self.layoutParams {
return item.header.flatMap { [$0] }
} else {
return nil
}
}
@objc func infoPressed() {
if let item = self.layoutParams?.0 {
item.interaction.openInfo(item.topMessage.id.peerId, item.messages)
}
}
override func revealOptionsInteractivelyOpened() {
if let item = self.layoutParams?.0 {
item.interaction.setMessageIdWithRevealedOptions(item.topMessage.id, nil)
}
}
override func revealOptionsInteractivelyClosed() {
if let item = self.layoutParams?.0 {
item.interaction.setMessageIdWithRevealedOptions(nil, item.topMessage.id)
}
}
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
if let (item, params, _, _, _) = self.layoutParams {
let revealOffset = offset
let editingOffset: CGFloat
if let editableControlNode = self.editableControlNode {
editingOffset = editableControlNode.bounds.size.width
var editableControlFrame = editableControlNode.frame
editableControlFrame.origin.x = params.leftInset + offset
transition.updateFrame(node: editableControlNode, frame: editableControlFrame)
} else {
editingOffset = 0.0
}
let leftInset: CGFloat = 86.0 + params.leftInset + editingOffset
let rightInset: CGFloat = 13.0 + params.rightInset
var infoIconRightInset: CGFloat = rightInset - 1.0
var dateRightInset: CGFloat = 46.0 + params.rightInset
if item.editing {
dateRightInset += 5.0
infoIconRightInset -= 36.0
}
var avatarFrame = self.avatarNode.frame
avatarFrame.origin.x = revealOffset + leftInset - 52.0
transition.updateFrameAdditive(node: self.avatarNode, frame: avatarFrame)
if let conferenceAvatarListNode = self.conferenceAvatarListNode {
var conferenceAvatarFrame = conferenceAvatarListNode.frame
conferenceAvatarFrame.origin.x = avatarFrame.minX + floor((avatarFrame.width - conferenceAvatarFrame.width) / 2.0)
transition.updateFrameAdditive(node: conferenceAvatarListNode, frame: conferenceAvatarFrame)
}
transition.updateFrameAdditive(node: self.titleNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size))
transition.updateFrameAdditive(node: self.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: self.statusNode.frame.minY), size: self.statusNode.bounds.size))
transition.updateFrameAdditive(node: self.dateNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + self.bounds.size.width - dateRightInset - self.dateNode.bounds.size.width, y: self.dateNode.frame.minY), size: self.dateNode.bounds.size))
transition.updateFrameAdditive(node: self.typeIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 81.0, y: self.typeIconNode.frame.minY), size: self.typeIconNode.bounds.size))
transition.updateFrameAdditive(node: self.infoButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + self.bounds.size.width - infoIconRightInset - self.infoButtonNode.bounds.width, y: self.infoButtonNode.frame.minY), size: self.infoButtonNode.bounds.size))
}
}
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
if let item = self.layoutParams?.0 {
item.interaction.delete(item.messages.map { $0.id })
}
}
@objc private func performLocalAccessibilityCustomAction(_ action: UIAccessibilityCustomAction) {
if let item = self.layoutParams?.0 {
item.interaction.delete(item.messages.map { $0.id })
}
}
}
@@ -0,0 +1,801 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import AlertUI
import AppBundle
import LocalizedPeerData
import ContextUI
import TelegramBaseController
import InviteLinksUI
import UndoUI
import TelegramCallsUI
import TelegramUIPreferences
public enum CallListControllerMode {
case tab
case navigation
}
private final class DeleteAllButtonNode: ASDisplayNode {
private let pressed: () -> Void
let contentNode: ContextExtractedContentContainingNode
private let buttonNode: HighlightableButtonNode
private let titleNode: ImmediateTextNode
init(presentationData: PresentationData, pressed: @escaping () -> Void) {
self.pressed = pressed
self.contentNode = ContextExtractedContentContainingNode()
self.buttonNode = HighlightableButtonNode()
self.titleNode = ImmediateTextNode()
super.init()
self.addSubnode(self.contentNode)
self.buttonNode.addSubnode(self.titleNode)
self.contentNode.contentNode.addSubnode(self.buttonNode)
self.titleNode.attributedText = NSAttributedString(string: presentationData.strings.CallList_DeleteAll, font: Font.regular(17.0), textColor: presentationData.theme.rootController.navigationBar.accentTextColor)
//self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
@objc private func buttonPressed() {
self.pressed()
}
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let titleSize = self.titleNode.updateLayout(constrainedSize)
self.titleNode.frame = CGRect(origin: CGPoint(), size: titleSize)
self.buttonNode.frame = CGRect(origin: CGPoint(), size: titleSize)
return titleSize
}
override public func layout() {
super.layout()
let size = self.bounds.size
self.contentNode.frame = CGRect(origin: CGPoint(), size: size)
self.contentNode.contentRect = CGRect(origin: CGPoint(), size: size)
}
}
public final class CallListController: TelegramBaseController {
private var controllerNode: CallListControllerNode {
return self.displayNode as! CallListControllerNode
}
private let _ready = Promise<Bool>(false)
override public var ready: Promise<Bool> {
return self._ready
}
private let context: AccountContext
private let mode: CallListControllerMode
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let peerViewDisposable = MetaDisposable()
private let segmentedTitleView: ItemListControllerSegmentedTitleView
private var isEmpty: Bool?
private var editingMode: Bool = false
private let createActionDisposable = MetaDisposable()
private let clearDisposable = MetaDisposable()
private var createConferenceCallDisposable: Disposable?
public init(context: AccountContext, mode: CallListControllerMode) {
self.context = context
self.mode = mode
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.segmentedTitleView = ItemListControllerSegmentedTitleView(theme: self.presentationData.theme, segments: [self.presentationData.strings.Calls_All, self.presentationData.strings.Calls_Missed], selectedIndex: 0)
super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .none, locationBroadcastPanelSource: .none, groupCallPanelSource: .none)
self.tabBarItemContextActionType = .always
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
if case .tab = self.mode {
//self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed))
self.navigationItem.rightBarButtonItem = nil
let icon: UIImage?
if useSpecialTabBarIcons() {
icon = UIImage(bundleImageName: "Chat List/Tabs/Holiday/IconCalls")
} else {
icon = UIImage(bundleImageName: "Chat List/Tabs/IconCalls")
}
self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle
self.tabBarItem.image = icon
self.tabBarItem.selectedImage = icon
if !self.presentationData.reduceMotion {
self.tabBarItem.animationName = "TabCalls"
}
}
self.segmentedTitleView.indexUpdated = { [weak self] index in
if let strongSelf = self {
strongSelf.segmentedTitleView.index = index
strongSelf.controllerNode.updateType(index == 0 ? .all : .missed)
}
}
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings()
}
}
}).strict()
self.scrollToTop = { [weak self] in
self?.controllerNode.scrollToLatest()
}
self.navigationItem.titleView = self.segmentedTitleView
if case .navigation = self.mode {
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.createActionDisposable.dispose()
self.presentationDataDisposable?.dispose()
self.peerViewDisposable.dispose()
self.clearDisposable.dispose()
self.createConferenceCallDisposable?.dispose()
}
private func updateThemeAndStrings() {
let index = self.segmentedTitleView.index
self.segmentedTitleView.segments = [self.presentationData.strings.Calls_All, self.presentationData.strings.Calls_Missed]
self.segmentedTitleView.theme = self.presentationData.theme
self.segmentedTitleView.index = index
self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle
if !self.presentationData.reduceMotion {
self.tabBarItem.animationName = "TabCalls"
} else {
self.tabBarItem.animationName = nil
}
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
switch self.mode {
case .tab:
if let isEmpty = self.isEmpty, isEmpty {
} else {
if self.editingMode {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
} else {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
}
}
//self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed))
case .navigation:
if self.editingMode {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
}
}
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
if self.isNodeLoaded {
self.controllerNode.updateThemeAndStrings(presentationData: self.presentationData)
}
}
private func createGroupCall(peerIds: [EnginePeer.Id], isVideo: Bool, completion: (() -> Void)? = nil) {
self.view.window?.endEditing(true)
guard !self.presentAccountFrozenInfoIfNeeded() else {
return
}
if self.createConferenceCallDisposable != nil {
return
}
var cancelImpl: (() -> Void)?
var signal = self.context.engine.calls.createConferenceCall()
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
self?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.3, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
signal = signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = { [weak self] in
guard let self else {
return
}
self.createConferenceCallDisposable?.dispose()
self.createConferenceCallDisposable = nil
}
self.createConferenceCallDisposable?.dispose()
self.createConferenceCallDisposable = (signal
|> deliverOnMainQueue).startStrict(next: { [weak self] call in
guard let self else {
return
}
self.createConferenceCallDisposable?.dispose()
self.createConferenceCallDisposable = nil
let openCall: () -> Void = { [weak self] in
guard let self else {
return
}
let _ = self.context.sharedContext.callManager?.joinConferenceCall(
accountContext: self.context,
initialCall: EngineGroupCallDescription(
id: call.callInfo.id,
accessHash: call.callInfo.accessHash,
title: call.callInfo.title,
scheduleTimestamp: nil,
subscribedToScheduled: false,
isStream: false
),
reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash),
beginWithVideo: isVideo,
invitePeerIds: peerIds,
endCurrentIfAny: true,
unmuteByDefault: true
)
completion?()
}
if !peerIds.isEmpty {
openCall()
} else {
let controller = InviteLinkInviteController(
context: self.context,
updatedPresentationData: nil,
mode: .groupCall(InviteLinkInviteController.Mode.GroupCall(callId: call.callInfo.id, accessHash: call.callInfo.accessHash, isRecentlyCreated: true, canRevoke: true)),
initialInvite: .link(link: call.link, title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: self.context.account.peerId, date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil),
parentNavigationController: self.navigationController as? NavigationController,
completed: { [weak self] result in
guard let self else {
return
}
if let result {
switch result {
case .linkCopied:
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: presentationData.strings.CallList_ToastCallLinkCopied_Text, customUndoText: presentationData.strings.CallList_ToastCallLinkCopied_Action, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
if case .undo = action {
openCall()
}
return false
}), in: .window(.root))
case .openCall:
openCall()
}
}
}
)
self.present(controller, in: .window(.root), with: nil)
}
})
}
override public func loadDisplayNode() {
self.displayNode = CallListControllerNode(controller: self, context: self.context, mode: self.mode, presentationData: self.presentationData, call: { [weak self] message in
guard let self else {
return
}
for media in message.media {
if let action = media as? TelegramMediaAction {
if case let .phoneCall(_, _, _, isVideo) = action.action {
self.call(message.id.peerId, isVideo: isVideo)
} else if case .conferenceCall = action.action {
self.openGroupCall(message: message)
}
}
}
}, joinGroupCall: { [weak self] peerId, activeCall in
if let self {
guard !self.presentAccountFrozenInfoIfNeeded() else {
return
}
self.joinGroupCall(peerId: peerId, invite: nil, activeCall: activeCall)
}
}, openInfo: { [weak self] peerId, messages in
if let strongSelf = self {
let _ = (strongSelf.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> deliverOnMainQueue).startStandalone(next: { peer in
if let strongSelf = self, let peer = peer, let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .calls(messages: messages.map({ $0._asMessage() })), avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
(strongSelf.navigationController as? NavigationController)?.pushViewController(controller)
}
})
}
}, emptyStateUpdated: { [weak self] empty in
if let strongSelf = self {
if empty != strongSelf.isEmpty {
strongSelf.isEmpty = empty
if empty {
switch strongSelf.mode {
case .tab:
strongSelf.navigationItem.setLeftBarButton(nil, animated: true)
strongSelf.navigationItem.setRightBarButton(nil, animated: true)
case .navigation:
strongSelf.navigationItem.setRightBarButton(nil, animated: true)
}
} else {
var pressedImpl: (() -> Void)?
let buttonNode = DeleteAllButtonNode(presentationData: strongSelf.presentationData, pressed: {
pressedImpl?()
})
pressedImpl = { [weak self, weak buttonNode] in
guard let strongSelf = self, let buttonNode = buttonNode else {
return
}
strongSelf.deleteAllPressed(buttonNode: buttonNode)
}
switch strongSelf.mode {
case .tab:
if strongSelf.editingMode {
strongSelf.navigationItem.setLeftBarButton(UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed)), animated: true)
strongSelf.navigationItem.setRightBarButton(UIBarButtonItem(customDisplayNode: buttonNode), animated: true)
strongSelf.navigationItem.rightBarButtonItem?.setCustomAction({
pressedImpl?()
})
} else {
strongSelf.navigationItem.setLeftBarButton(UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed)), animated: true)
//strongSelf.navigationItem.setRightBarButton(UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(strongSelf.presentationData.theme), style: .plain, target: self, action: #selector(strongSelf.callPressed)), animated: true)
strongSelf.navigationItem.setRightBarButton(nil, animated: true)
}
case .navigation:
if strongSelf.editingMode {
strongSelf.navigationItem.setLeftBarButton(UIBarButtonItem(customDisplayNode: buttonNode), animated: true)
strongSelf.navigationItem.leftBarButtonItem?.setCustomAction({
pressedImpl?()
})
strongSelf.navigationItem.setRightBarButton(UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed)), animated: true)
} else {
strongSelf.navigationItem.setRightBarButton(UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed)), animated: true)
}
}
}
}
}
}, openNewCall: { [weak self] in
if let strongSelf = self {
strongSelf.callPressed()
}
})
if case .navigation = self.mode {
self.controllerNode.navigationBar = self.navigationBar
self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
}
self.controllerNode.startNewCall = { [weak self] in
self?.beginCallImpl()
}
self._ready.set(self.controllerNode.ready)
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@objc func callPressed() {
self.beginCallImpl()
}
@objc private func deleteAllPressed(buttonNode: DeleteAllButtonNode) {
var items: [ContextMenuItem] = []
let beginClear: (Bool) -> Void = { [weak self] forEveryone in
guard let strongSelf = self else {
return
}
var signal = strongSelf.context.engine.messages.clearCallHistory(forEveryone: forEveryone)
var cancelImpl: (() -> Void)?
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
signal = signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
self?.clearDisposable.set(nil)
}
strongSelf.clearDisposable.set((signal
|> deliverOnMainQueue).startStrict(completed: {
}))
}
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.CallList_DeleteAllForMe, textColor: .destructive, icon: { _ in
return nil
}, action: { _, f in
f(.default)
beginClear(false)
})))
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.CallList_DeleteAllForEveryone, textColor: .destructive, icon: { _ in
return nil
}, action: { _, f in
f(.default)
beginClear(true)
})))
final class ExtractedContentSourceImpl: ContextExtractedContentSource {
var keepInPlace: Bool
let ignoreContentTouches: Bool = true
let blurBackground: Bool
private let controller: ViewController
private let sourceNode: ContextExtractedContentContainingNode
init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool, blurBackground: Bool) {
self.controller = controller
self.sourceNode = sourceNode
self.keepInPlace = keepInPlace
self.blurBackground = blurBackground
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
let contextController = ContextController(presentationData: self.presentationData, source: .extracted(ExtractedContentSourceImpl(controller: self, sourceNode: buttonNode.contentNode, keepInPlace: false, blurBackground: false)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
self.presentInGlobalOverlay(contextController)
}
private func beginCallImpl() {
guard !self.presentAccountFrozenInfoIfNeeded() else {
return
}
var dismissSelectionController: (() -> Void)?
let options = [ContactListAdditionalOption(title: self.presentationData.strings.CallList_NewCallLink, icon: .generic(PresentationResourcesItemList.linkIcon(presentationData.theme)!), action: { [weak self] in
guard let self else {
return
}
dismissSelectionController?()
self.createGroupCall(peerIds: [], isVideo: false)
}, clearHighlightAutomatically: true)]
let controller = self.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(
context: self.context,
title: self.presentationData.strings.Calls_NewCall,
mode: .groupCreation(isCall: true),
options: .single(options),
filters: [.excludeSelf],
onlyWriteable: true,
isGroupInvitation: false,
isPeerEnabled: nil,
attemptDisabledItemSelection: nil,
alwaysEnabled: false,
limit: nil,
reachedLimit: nil,
openProfile: nil,
sendMessage: nil
))
controller.navigationPresentation = .modal
if let navigationController = self.context.sharedContext.mainWindow?.viewController as? NavigationController {
navigationController.pushViewController(controller)
}
dismissSelectionController = { [weak controller] in
controller?.dismiss()
}
let _ = (controller.result
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak controller, weak self] result in
guard let self else {
controller?.dismiss()
return
}
guard case let .result(rawPeerIds, _) = result else {
controller?.dismiss()
return
}
let peerIds = rawPeerIds.compactMap { id -> EnginePeer.Id? in
if case let .peer(id) = id {
return id
}
return nil
}
if peerIds.isEmpty {
controller?.dismiss()
return
}
let isVideo = controller?.isCallVideoOptionSelected ?? false
if peerIds.count == 1 {
controller?.dismiss()
self.call(peerIds[0], isVideo: isVideo, began: { [weak self] in
if let strongSelf = self {
let _ = (strongSelf.context.sharedContext.hasOngoingCall.get()
|> filter { $0 }
|> timeout(1.0, queue: Queue.mainQueue(), alternate: .single(true))
|> delay(0.5, queue: Queue.mainQueue())
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] _ in
if let _ = self, let controller = controller, let navigationController = controller.navigationController as? NavigationController {
if navigationController.viewControllers.last === controller {
let _ = navigationController.popViewController(animated: true)
}
}
})
}
})
} else {
self.createGroupCall(peerIds: peerIds, isVideo: isVideo, completion: {
controller?.dismiss()
})
}
})
}
private func presentAccountFrozenInfoIfNeeded(delay: Bool = false) -> Bool {
if self.context.isFrozen {
let present = {
self.push(self.context.sharedContext.makeAccountFreezeInfoScreen(context: self.context))
}
if delay {
Queue.mainQueue().after(0.3) {
present()
}
} else {
present()
}
return true
}
return false
}
@objc func editPressed() {
self.editingMode = true
var pressedImpl: (() -> Void)?
let buttonNode = DeleteAllButtonNode(presentationData: self.presentationData, pressed: {
pressedImpl?()
})
pressedImpl = { [weak self, weak buttonNode] in
guard let strongSelf = self, let buttonNode = buttonNode else {
return
}
strongSelf.deleteAllPressed(buttonNode: buttonNode)
}
switch self.mode {
case .tab:
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)), animated: true)
self.navigationItem.setRightBarButton(UIBarButtonItem(customDisplayNode: buttonNode), animated: true)
self.navigationItem.rightBarButtonItem?.setCustomAction({
pressedImpl?()
})
case .navigation:
self.navigationItem.setLeftBarButton(UIBarButtonItem(customDisplayNode: buttonNode), animated: true)
self.navigationItem.leftBarButtonItem?.setCustomAction({
pressedImpl?()
})
self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)), animated: true)
}
self.controllerNode.updateState { state in
return state.withUpdatedEditing(true)
}
}
@objc func donePressed() {
self.editingMode = false
switch self.mode {
case .tab:
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)), animated: true)
self.navigationItem.setRightBarButton(nil, animated: true)
//self.navigationItem.setRightBarButton(UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed)), animated: true)
case .navigation:
self.navigationItem.setLeftBarButton(nil, animated: true)
self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)), animated: true)
}
self.controllerNode.updateState { state in
return state.withUpdatedEditing(false).withUpdatedMessageIdWithRevealedOptions(nil)
}
}
private func call(_ peerId: EnginePeer.Id, isVideo: Bool, began: (() -> Void)? = nil) {
guard !self.presentAccountFrozenInfoIfNeeded() else {
return
}
self.peerViewDisposable.set((self.context.account.viewTracker.peerView(peerId)
|> take(1)
|> deliverOnMainQueue).startStrict(next: { [weak self] view in
if let strongSelf = self {
guard let peer = peerViewMainPeer(view) else {
return
}
if let cachedUserData = view.cachedData as? CachedUserData, cachedUserData.callsPrivate {
strongSelf.push(strongSelf.context.sharedContext.makeSendInviteLinkScreen(context: strongSelf.context, subject: .groupCall(.create), peers: [TelegramForbiddenInvitePeer(
peer: EnginePeer(peer),
canInviteWithPremium: false,
premiumRequiredToContact: false
)], theme: strongSelf.presentationData.theme))
return
}
strongSelf.context.requestCall(peerId: peerId, isVideo: isVideo, completion: {
began?()
})
}
}))
}
private func openGroupCall(message: EngineMessage) {
var action: TelegramMediaAction?
for media in message.media {
if let media = media as? TelegramMediaAction {
action = media
break
}
}
guard case let .conferenceCall(conferenceCall) = action?.action else {
return
}
if let currentGroupCallController = self.context.sharedContext as? VoiceChatController, case let .group(groupCall) = currentGroupCallController.call, let currentCallId = groupCall.callId, currentCallId == conferenceCall.callId {
self.context.sharedContext.navigateToCurrentCall()
return
}
let signal = self.context.engine.peers.joinCallInvitationInformation(messageId: message.id)
let _ = (signal
|> deliverOnMainQueue).startStandalone(next: { [weak self] resolvedCallLink in
guard let self else {
return
}
let _ = (self.context.engine.calls.getGroupCallPersistentSettings(callId: resolvedCallLink.id)
|> deliverOnMainQueue).startStandalone(next: { [weak self] value in
guard let self else {
return
}
let value: PresentationGroupCallPersistentSettings = value?.get(PresentationGroupCallPersistentSettings.self) ?? PresentationGroupCallPersistentSettings.default
self.context.joinConferenceCall(call: resolvedCallLink, isVideo: conferenceCall.flags.contains(.isVideo), unmuteByDefault: value.isMicrophoneEnabledByDefault)
})
}, error: { [weak self] error in
guard let self else {
return
}
switch error {
case .doesNotExist:
self.context.sharedContext.openCreateGroupCallUI(context: self.context, peerIds: conferenceCall.otherParticipants, parentController: self)
default:
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.present(textAlertController(context: self.context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}
})
}
override public func tabBarItemContextAction(sourceView: ContextExtractedContentContainingView, gesture: ContextGesture) {
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Calls_StartNewCall, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, f in
c?.dismiss(completion: { [weak self] in
guard let self else {
return
}
self.callPressed()
})
})))
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Calls_HideCallsTab, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/HideIcon"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, f in
c?.dismiss(completion: { [weak self] in
guard let self else {
return
}
let _ = updateCallListSettingsInteractively(accountManager: self.context.sharedContext.accountManager, {
$0.withUpdatedShowTab(false)
}).start()
})
})))
let controller = ContextController(presentationData: self.presentationData, source: .reference(CallListTabBarContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
}
}
private final class CallListTabBarContextReferenceContentSource: ContextReferenceContentSource {
let keepInPlace: Bool = true
private let controller: ViewController
private let sourceView: ContextExtractedContentContainingView
init(controller: ViewController, sourceView: ContextExtractedContentContainingView) {
self.controller = controller
self.sourceView = sourceView
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(
referenceView: self.sourceView.contentView,
contentAreaInScreenSpace: UIScreen.main.bounds,
actionsPosition: .top
)
}
}
@@ -0,0 +1,949 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import TelegramNotices
import ChatListSearchItemHeader
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import AppBundle
import ItemListPeerActionItem
private struct CallListNodeListViewTransition {
let callListView: CallListNodeView
let deleteItems: [ListViewDeleteItem]
let insertItems: [ListViewInsertItem]
let updateItems: [ListViewUpdateItem]
let options: ListViewDeleteAndInsertOptions
let scrollToItem: ListViewScrollToItem?
let stationaryItemRange: (Int, Int)?
}
private extension EngineCallList.Item {
var lowestIndex: EngineMessage.Index {
switch self {
case let .hole(index):
return index
case let .message(_, messages):
var lowest = messages[0].index
for i in 1 ..< messages.count {
let index = messages[i].index
if index < lowest {
lowest = index
}
}
return lowest
}
}
var highestIndex: EngineMessage.Index {
switch self {
case let .hole(index):
return index
case let .message(_, messages):
var highest = messages[0].index
for i in 1 ..< messages.count {
let index = messages[i].index
if index > highest {
highest = index
}
}
return highest
}
}
}
final class CallListNodeInteraction {
let setMessageIdWithRevealedOptions: (EngineMessage.Id?, EngineMessage.Id?) -> Void
let call: (EngineMessage) -> Void
let openInfo: (EnginePeer.Id, [EngineMessage]) -> Void
let delete: ([EngineMessage.Id]) -> Void
let updateShowCallsTab: (Bool) -> Void
let openGroupCall: (EnginePeer.Id) -> Void
let openNewCall: () -> Void
init(setMessageIdWithRevealedOptions: @escaping (EngineMessage.Id?, EngineMessage.Id?) -> Void, call: @escaping (EngineMessage) -> Void, openInfo: @escaping (EnginePeer.Id, [EngineMessage]) -> Void, delete: @escaping ([EngineMessage.Id]) -> Void, updateShowCallsTab: @escaping (Bool) -> Void, openGroupCall: @escaping (EnginePeer.Id) -> Void, openNewCall: @escaping () -> Void) {
self.setMessageIdWithRevealedOptions = setMessageIdWithRevealedOptions
self.call = call
self.openInfo = openInfo
self.delete = delete
self.updateShowCallsTab = updateShowCallsTab
self.openGroupCall = openGroupCall
self.openNewCall = openNewCall
}
}
struct CallListNodeState: Equatable {
let presentationData: ItemListPresentationData
let dateTimeFormat: PresentationDateTimeFormat
let disableAnimations: Bool
let editing: Bool
let messageIdWithRevealedOptions: EngineMessage.Id?
func withUpdatedPresentationData(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, disableAnimations: Bool) -> CallListNodeState {
return CallListNodeState(presentationData: presentationData, dateTimeFormat: dateTimeFormat, disableAnimations: disableAnimations, editing: self.editing, messageIdWithRevealedOptions: self.messageIdWithRevealedOptions)
}
func withUpdatedEditing(_ editing: Bool) -> CallListNodeState {
return CallListNodeState(presentationData: self.presentationData, dateTimeFormat: self.dateTimeFormat, disableAnimations: self.disableAnimations, editing: editing, messageIdWithRevealedOptions: self.messageIdWithRevealedOptions)
}
func withUpdatedMessageIdWithRevealedOptions(_ messageIdWithRevealedOptions: EngineMessage.Id?) -> CallListNodeState {
return CallListNodeState(presentationData: self.presentationData, dateTimeFormat: self.dateTimeFormat, disableAnimations: self.disableAnimations, editing: self.editing, messageIdWithRevealedOptions: messageIdWithRevealedOptions)
}
static func ==(lhs: CallListNodeState, rhs: CallListNodeState) -> Bool {
if lhs.presentationData != rhs.presentationData {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
if lhs.editing != rhs.editing {
return false
}
if lhs.messageIdWithRevealedOptions != rhs.messageIdWithRevealedOptions {
return false
}
return true
}
}
private func mappedInsertEntries(context: AccountContext, presentationData: ItemListPresentationData, showSettings: Bool, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] {
return entries.map { entry -> ListViewInsertItem in
switch entry.entry {
case let .displayTab(_, text, value):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enabled: true, noCorners: false, sectionId: 0, style: .blocks, updated: { value in
nodeInteraction.updateShowCallsTab(value)
}), directionHint: entry.directionHint)
case let .displayTabInfo(_, text):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
case .openNewCall:
let item = ItemListPeerActionItem(presentationData: presentationData, style: showSettings ? .blocks : .plain, systemStyle: .glass, icon: PresentationResourcesRootController.callListCallIcon(presentationData.theme), title: presentationData.strings.CallList_NewCall, hasSeparator: false, sectionId: 1, height: .generic, noInsets: !showSettings, editing: false, action: {
nodeInteraction.openNewCall()
})
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
case let .groupCall(peer, _, isActive):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, systemStyle: .glass, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: false, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .messageEntry(topMessage, messages, _, _, dateTimeFormat, editing, hasActiveRevealControls, displayHeader, _):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, context: context, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, displayHeader: displayHeader, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .holeEntry(_, theme):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListHoleItem(theme: theme), directionHint: entry.directionHint)
}
}
}
private func mappedUpdateEntries(context: AccountContext, presentationData: ItemListPresentationData, showSettings: Bool, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] {
return entries.map { entry -> ListViewUpdateItem in
switch entry.entry {
case let .displayTab(_, text, value):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enabled: true, noCorners: false, sectionId: 0, style: .blocks, updated: { value in
nodeInteraction.updateShowCallsTab(value)
}), directionHint: entry.directionHint)
case let .displayTabInfo(_, text):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
case .openNewCall:
let item = ItemListPeerActionItem(presentationData: presentationData, style: showSettings ? .blocks : .plain, systemStyle: .glass, icon: PresentationResourcesRootController.callListCallIcon(presentationData.theme), title: presentationData.strings.CallList_NewCall, hasSeparator: false, sectionId: 1, height: .generic, noInsets: !showSettings, editing: false, action: {
nodeInteraction.openNewCall()
})
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
case let .groupCall(peer, _, isActive):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, systemStyle: .glass, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: false, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .messageEntry(topMessage, messages, _, _, dateTimeFormat, editing, hasActiveRevealControls, displayHeader, _):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, context: context, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, displayHeader: displayHeader, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .holeEntry(_, theme):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListHoleItem(theme: theme), directionHint: entry.directionHint)
}
}
}
private func mappedCallListNodeViewListTransition(context: AccountContext, presentationData: ItemListPresentationData, showSettings: Bool, nodeInteraction: CallListNodeInteraction, transition: CallListNodeViewTransition) -> CallListNodeListViewTransition {
return CallListNodeListViewTransition(callListView: transition.callListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, presentationData: presentationData, showSettings: showSettings, nodeInteraction: nodeInteraction, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, presentationData: presentationData, showSettings: showSettings, nodeInteraction: nodeInteraction, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange)
}
private final class CallListOpaqueTransactionState {
let callListView: CallListNodeView
init(callListView: CallListNodeView) {
self.callListView = callListView
}
}
final class CallListControllerNode: ASDisplayNode {
private weak var controller: CallListController?
private let context: AccountContext
private let mode: CallListControllerMode
private var presentationData: PresentationData
private var containerLayout: (ContainerViewLayout, CGFloat)?
private let _ready = ValuePromise<Bool>()
private var didSetReady = false
var ready: Signal<Bool, NoError> {
return _ready.get()
}
weak var navigationBar: NavigationBar?
var peerSelected: ((EnginePeer.Id) -> Void)?
var activateSearch: (() -> Void)?
var deletePeerChat: ((EnginePeer.Id) -> Void)?
var startNewCall: (() -> Void)?
private let viewProcessingQueue = Queue()
private var callListView: CallListNodeView?
private var dequeuedInitialTransitionOnLayout = false
private var enqueuedTransition: (CallListNodeListViewTransition, () -> Void)?
private var currentState: CallListNodeState
private let statePromise: ValuePromise<CallListNodeState>
private var currentLocationAndType = CallListNodeLocationAndType(location: .initial(count: 50), scope: .all)
private let callListLocationAndType = ValuePromise<CallListNodeLocationAndType>()
private let callListDisposable = MetaDisposable()
private let listNode: ListView
private let leftOverlayNode: ASDisplayNode
private let rightOverlayNode: ASDisplayNode
private let emptyTextNode: ImmediateTextNode
private let emptyAnimationNode: AnimatedStickerNode
private var emptyAnimationSize = CGSize()
private let emptyButtonNode: HighlightTrackingButtonNode
private let emptyButtonIconNode: ASImageNode
private let emptyButtonTextNode: ImmediateTextNode
private let call: (EngineMessage) -> Void
private let joinGroupCall: (EnginePeer.Id, EngineGroupCallDescription) -> Void
private let openNewCall: () -> Void
private let openInfo: (EnginePeer.Id, [EngineMessage]) -> Void
private let emptyStateUpdated: (Bool) -> Void
private let emptyStatePromise = Promise<Bool>()
private let emptyStateDisposable = MetaDisposable()
private let openGroupCallDisposable = MetaDisposable()
private var previousContentOffset: ListViewVisibleContentOffset?
init(controller: CallListController, context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (EngineMessage) -> Void, joinGroupCall: @escaping (EnginePeer.Id, EngineGroupCallDescription) -> Void, openInfo: @escaping (EnginePeer.Id, [EngineMessage]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void, openNewCall: @escaping () -> Void) {
self.controller = controller
self.context = context
self.mode = mode
self.presentationData = presentationData
self.call = call
self.joinGroupCall = joinGroupCall
self.openInfo = openInfo
self.emptyStateUpdated = emptyStateUpdated
self.openNewCall = openNewCall
self.currentState = CallListNodeState(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: true, editing: false, messageIdWithRevealedOptions: nil)
self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true)
self.listNode = ListView()
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
self.leftOverlayNode = ASDisplayNode()
self.leftOverlayNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
self.rightOverlayNode = ASDisplayNode()
self.rightOverlayNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
self.emptyTextNode = ImmediateTextNode()
self.emptyTextNode.alpha = 0.0
self.emptyTextNode.isUserInteractionEnabled = false
self.emptyTextNode.displaysAsynchronously = false
self.emptyTextNode.textAlignment = .center
self.emptyTextNode.maximumNumberOfLines = 3
self.emptyAnimationNode = DefaultAnimatedStickerNodeImpl()
self.emptyAnimationNode.alpha = 0.0
self.emptyAnimationNode.isUserInteractionEnabled = false
self.emptyButtonNode = HighlightTrackingButtonNode()
self.emptyButtonNode.isUserInteractionEnabled = false
self.emptyButtonTextNode = ImmediateTextNode()
self.emptyButtonTextNode.isUserInteractionEnabled = false
self.emptyButtonIconNode = ASImageNode()
self.emptyButtonIconNode.displaysAsynchronously = false
self.emptyButtonIconNode.isUserInteractionEnabled = false
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.addSubnode(self.listNode)
self.addSubnode(self.emptyTextNode)
self.addSubnode(self.emptyAnimationNode)
self.addSubnode(self.emptyButtonTextNode)
self.addSubnode(self.emptyButtonIconNode)
self.addSubnode(self.emptyButtonNode)
switch self.mode {
case .tab:
self.backgroundColor = presentationData.theme.chatList.backgroundColor
self.listNode.backgroundColor = presentationData.theme.chatList.backgroundColor
case .navigation:
self.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.listNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
}
self.emptyAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "CallsPlaceholder"), width: 256, height: 256, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.emptyAnimationSize = CGSize(width: 148.0, height: 148.0)
self.emptyButtonIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call List/CallIcon"), color: presentationData.theme.list.itemAccentColor)
self.emptyButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.emptyButtonIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.emptyButtonIconNode.alpha = 0.4
strongSelf.emptyButtonTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.emptyButtonTextNode.alpha = 0.4
} else {
strongSelf.emptyButtonIconNode.alpha = 1.0
strongSelf.emptyButtonIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.emptyButtonTextNode.alpha = 1.0
strongSelf.emptyButtonTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.emptyButtonNode.addTarget(self, action: #selector(self.emptyButtonPressed), forControlEvents: .touchUpInside)
let nodeInteraction = CallListNodeInteraction(setMessageIdWithRevealedOptions: { [weak self] messageId, fromMessageId in
if let strongSelf = self {
strongSelf.updateState { state in
if (messageId == nil && fromMessageId == state.messageIdWithRevealedOptions) || (messageId != nil && fromMessageId == nil) {
return state.withUpdatedMessageIdWithRevealedOptions(messageId)
} else {
return state
}
}
}
}, call: { [weak self] message in
self?.call(message)
}, openInfo: { [weak self] peerId, messages in
self?.openInfo(peerId, messages)
}, delete: { [weak self] messageIds in
guard let peerId = messageIds.first?.peerId else {
return
}
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> deliverOnMainQueue).startStandalone(next: { peer in
guard let strongSelf = self, let peer = peer else {
return
}
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: strongSelf.presentationData.strings.CallList_DeleteConfirmation, parseMarkdown: true))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_DeleteMessagesFor(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: .forEveryone).startStandalone()
}))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: .forLocalPeer).startStandalone()
}))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.controller?.present(actionSheet, in: .window(.root))
})
}, updateShowCallsTab: { [weak self] value in
if let strongSelf = self {
let _ = updateCallListSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, {
$0.withUpdatedShowTab(value)
}).startStandalone()
if value {
let _ = ApplicationSpecificNotice.incrementCallsTabTips(accountManager: strongSelf.context.sharedContext.accountManager, count: 4).startStandalone()
}
}
}, openGroupCall: { [weak self] peerId in
guard let strongSelf = self else {
return
}
let disposable = strongSelf.openGroupCallDisposable
let engine = strongSelf.context.engine
var signal: Signal<EngineGroupCallDescription?, NoError> = context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.GroupCallDescription(id: peerId)
)
|> mapToSignal { activeCall -> Signal<EngineGroupCallDescription?, NoError> in
if let activeCall = activeCall {
return .single(activeCall)
} else {
return engine.calls.updatedCurrentPeerGroupCall(peerId: peerId)
}
}
var cancelImpl: (() -> Void)?
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
if let strongSelf = self {
strongSelf.controller?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
signal = signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
disposable.set(nil)
}
disposable.set((signal
|> deliverOnMainQueue).start(next: { activeCall in
guard let strongSelf = self else {
return
}
if let activeCall = activeCall {
strongSelf.joinGroupCall(peerId, activeCall)
}
}))
}, openNewCall: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.openNewCall()
})
let viewProcessingQueue = self.viewProcessingQueue
let callListViewUpdate = self.callListLocationAndType.get()
|> distinctUntilChanged
|> mapToSignal { locationAndType in
return callListViewForLocationAndType(locationAndType: locationAndType, engine: context.engine)
}
let previousView = Atomic<CallListNodeView?>(value: nil)
let previousType = Atomic<EngineCallList.Scope?>(value: nil)
let showSettings: Bool
switch mode {
case .tab:
showSettings = false
case .navigation:
showSettings = true
}
let showCallsTab = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.callListSettings])
|> map { sharedData -> Bool in
var value = CallListSettings.defaultSettings.showTab
if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.callListSettings]?.get(CallListSettings.self) {
value = settings.showTab
}
return value
}
let currentGroupCallPeerId: Signal<EnginePeer.Id?, NoError>
if let callManager = context.sharedContext.callManager {
currentGroupCallPeerId = callManager.currentGroupCallSignal
|> map { call -> EnginePeer.Id? in
guard case let .group(call) = call else {
return nil
}
return call.peerId
}
|> distinctUntilChanged
} else {
currentGroupCallPeerId = .single(nil)
}
let groupCalls: Signal<[EnginePeer], NoError> = context.engine.messages.chatList(group: .root, count: 100)
|> map { chatList -> [EnginePeer] in
var result: [EnginePeer] = []
for item in chatList.items {
if case let .channel(channel) = item.renderedPeer.peer, channel.flags.contains(.hasActiveVoiceChat) {
result.append(.channel(channel))
} else if case let .legacyGroup(group) = item.renderedPeer.peer, group.flags.contains(.hasActiveVoiceChat) {
result.append(.legacyGroup(group))
}
}
return result.sorted(by: { lhs, rhs in
let lhsTitle = lhs.compactDisplayTitle
let rhsTitle = rhs.compactDisplayTitle
if lhsTitle != rhsTitle {
return lhsTitle < rhsTitle
}
return lhs.id < rhs.id
})
}
|> distinctUntilChanged
let callListNodeViewTransition = combineLatest(
callListViewUpdate,
self.statePromise.get(),
groupCalls,
showCallsTab,
currentGroupCallPeerId
)
|> mapToQueue { (updateAndType, state, groupCalls, showCallsTab, currentGroupCallPeerId) -> Signal<CallListNodeListViewTransition, NoError> in
let (update, type) = updateAndType
let processedView = CallListNodeView(originalView: update.view, filteredEntries: callListNodeEntriesForView(view: update.view, displayOpenNewCall: type == .all, groupCalls: groupCalls, state: state, showSettings: showSettings, showCallsTab: showCallsTab, isRecentCalls: type == .all, currentGroupCallPeerId: currentGroupCallPeerId), presentationData: state.presentationData)
let previous = previousView.swap(processedView)
let previousType = previousType.swap(type)
let reason: CallListNodeViewTransitionReason
var prepareOnMainQueue = false
var previousWasEmptyOrSingleHole = false
if let previous = previous {
if previous.filteredEntries.count == 1 {
if case .holeEntry = previous.filteredEntries[0] {
previousWasEmptyOrSingleHole = true
}
}
} else {
previousWasEmptyOrSingleHole = true
}
var disableAnimations = false
if previousWasEmptyOrSingleHole {
reason = .initial
if previous == nil {
prepareOnMainQueue = true
}
} else {
if previous?.originalView === update.view {
let previousCalls = previous?.filteredEntries.compactMap { item -> EnginePeer.Id? in
switch item {
case let .groupCall(peer, _, _):
return peer.id
default:
return nil
}
}
let updatedCalls = processedView.filteredEntries.compactMap { item -> EnginePeer.Id? in
switch item {
case let .groupCall(peer, _, _):
return peer.id
default:
return nil
}
}
reason = .interactiveChanges
if previousCalls != updatedCalls {
disableAnimations = true
}
} else {
switch update.type {
case .Initial:
reason = .initial
prepareOnMainQueue = true
case .Generic:
reason = .interactiveChanges
case .UpdateVisible:
reason = .reload
case .Reload:
reason = .reload
case .ReloadAnimated:
reason = .reloadAnimated
}
}
}
var scrollPosition = update.scrollPosition
if previousType != type {
scrollPosition = .top(animated: false)
}
return preparedCallListNodeViewTransition(from: previous, to: processedView, reason: reason, disableAnimations: disableAnimations, context: context, scrollPosition: scrollPosition)
|> map({ mappedCallListNodeViewListTransition(context: context, presentationData: state.presentationData, showSettings: showSettings, nodeInteraction: nodeInteraction, transition: $0) })
|> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue)
}
let appliedTransition = callListNodeViewTransition
|> deliverOnMainQueue
|> mapToQueue { [weak self] transition -> Signal<Void, NoError> in
if let strongSelf = self {
return strongSelf.enqueueTransition(transition)
}
return .complete()
}
self.listNode.displayedItemRangeChanged = { [weak self] range, transactionOpaqueState in
if let strongSelf = self, let range = range.loadedRange, let view = (transactionOpaqueState as? CallListOpaqueTransactionState)?.callListView.originalView {
var location: CallListNodeLocation?
if range.firstIndex < 5 && view.hasLater {
location = .navigation(index: view.items[view.items.count - 1].highestIndex)
} else if range.firstIndex >= 5 && range.lastIndex >= view.items.count - 5 && view.hasEarlier {
location = .navigation(index: view.items[0].lowestIndex)
}
if let location = location, location != strongSelf.currentLocationAndType.location {
strongSelf.currentLocationAndType = CallListNodeLocationAndType(location: location, scope: strongSelf.currentLocationAndType.scope)
strongSelf.callListLocationAndType.set(strongSelf.currentLocationAndType)
}
}
}
self.callListDisposable.set(appliedTransition.startStrict())
self.callListLocationAndType.set(self.currentLocationAndType)
let emptySignal = self.emptyStatePromise.get() |> distinctUntilChanged
let typeSignal = self.callListLocationAndType.get() |> map { locationAndType -> EngineCallList.Scope in
return locationAndType.scope
}
|> distinctUntilChanged
self.emptyStateDisposable.set((combineLatest(emptySignal, typeSignal, self.statePromise.get()) |> deliverOnMainQueue).startStrict(next: { [weak self] isEmpty, type, state in
if let strongSelf = self {
strongSelf.updateEmptyPlaceholder(theme: state.presentationData.theme, strings: state.presentationData.strings, type: type, isHidden: !isEmpty)
}
}))
if case .navigation = mode {
self.listNode.itemNodeHitTest = { [weak self] point in
if let strongSelf = self {
return point.x > strongSelf.leftOverlayNode.frame.maxX && point.x < strongSelf.rightOverlayNode.frame.minX
} else {
return true
}
}
self.listNode.visibleContentOffsetChanged = { [weak self] offset in
if let strongSelf = self {
var previousContentOffsetValue: CGFloat?
if let previousContentOffset = strongSelf.previousContentOffset, case let .known(value) = previousContentOffset {
previousContentOffsetValue = value
}
switch offset {
case let .known(value):
let transition: ContainedViewLayoutTransition
if let previousContentOffsetValue = previousContentOffsetValue, value <= 0.0, previousContentOffsetValue > 30.0 {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
strongSelf.navigationBar?.updateBackgroundAlpha(min(30.0, value) / 30.0, transition: transition)
case .unknown, .none:
strongSelf.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate)
}
strongSelf.previousContentOffset = offset
}
}
}
}
deinit {
self.callListDisposable.dispose()
self.emptyStateDisposable.dispose()
self.openGroupCallDisposable.dispose()
}
func updateThemeAndStrings(presentationData: PresentationData) {
if presentationData.theme !== self.currentState.presentationData.theme || presentationData.strings !== self.currentState.presentationData.strings {
self.presentationData = presentationData
self.leftOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.rightOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
switch self.mode {
case .tab:
self.backgroundColor = presentationData.theme.chatList.backgroundColor
self.listNode.backgroundColor = presentationData.theme.chatList.backgroundColor
case .navigation:
self.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.listNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
}
self.emptyButtonIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call List/CallIcon"), color: presentationData.theme.list.itemAccentColor)
self.updateEmptyPlaceholder(theme: presentationData.theme, strings: presentationData.strings, type: self.currentLocationAndType.scope, isHidden: self.emptyTextNode.alpha.isZero)
self.updateState {
return $0.withUpdatedPresentationData(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: true)
}
self.listNode.forEachItemHeaderNode({ itemHeaderNode in
if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode {
itemHeaderNode.updateTheme(theme: presentationData.theme)
}
})
}
}
private let textFont = Font.regular(16.0)
private let buttonFont = Font.regular(17.0)
func updateEmptyPlaceholder(theme: PresentationTheme, strings: PresentationStrings, type: EngineCallList.Scope, isHidden: Bool) {
let alpha: CGFloat = isHidden ? 0.0 : 1.0
let previousAlpha = self.emptyTextNode.alpha
self.emptyTextNode.alpha = alpha
self.emptyTextNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.25)
if previousAlpha.isZero && !alpha.isZero {
self.emptyAnimationNode.visibility = true
}
self.emptyAnimationNode.alpha = alpha
self.emptyAnimationNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.25, completion: { [weak self] _ in
if let strongSelf = self {
if !previousAlpha.isZero && strongSelf.emptyAnimationNode.alpha.isZero {
strongSelf.emptyAnimationNode.visibility = false
}
}
})
self.emptyButtonIconNode.alpha = alpha
self.emptyButtonIconNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.25)
self.emptyButtonTextNode.alpha = alpha
self.emptyButtonTextNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.25)
self.emptyButtonNode.isUserInteractionEnabled = !isHidden
self.listNode.alpha = 1.0 - alpha
self.listNode.layer.animateAlpha(from: 1.0 - previousAlpha, to: 1.0 - alpha, duration: 0.25)
if !isHidden {
let type = self.currentLocationAndType.scope
let emptyText: String
let buttonText = strings.Calls_StartNewCall
if type == .missed {
emptyText = strings.Calls_NoMissedCallsPlacehoder
} else {
emptyText = strings.Calls_NoVoiceAndVideoCallsPlaceholder
}
let color: UIColor
switch self.mode {
case .tab:
self.backgroundColor = theme.chatList.backgroundColor
self.listNode.backgroundColor = theme.chatList.backgroundColor
color = theme.list.freeTextColor
case .navigation:
self.backgroundColor = theme.list.blocksBackgroundColor
self.listNode.backgroundColor = theme.list.blocksBackgroundColor
color = theme.list.freeTextColor
}
self.emptyTextNode.attributedText = NSAttributedString(string: emptyText, font: textFont, textColor: color, paragraphAlignment: .center)
self.emptyButtonTextNode.attributedText = NSAttributedString(string: buttonText, font: buttonFont, textColor: theme.list.itemAccentColor, paragraphAlignment: .center)
if let layout = self.containerLayout {
self.updateLayout(layout.0, navigationBarHeight: layout.1, transition: .immediate)
}
}
}
func updateState(_ f: (CallListNodeState) -> CallListNodeState) {
let state = f(self.currentState)
if state != self.currentState {
self.currentState = state
self.statePromise.set(state)
}
}
func updateType(_ type: EngineCallList.Scope) {
if type != self.currentLocationAndType.scope {
if let view = self.callListView?.originalView {
var index: EngineMessage.Index
if !view.items.isEmpty {
index = view.items[view.items.count - 1].highestIndex
} else {
index = EngineMessage.Index.absoluteUpperBound()
}
self.currentLocationAndType = CallListNodeLocationAndType(location: .changeType(index: index), scope: type)
self.emptyStatePromise.set(.single(false))
self.callListLocationAndType.set(self.currentLocationAndType)
}
}
}
private func enqueueTransition(_ transition: CallListNodeListViewTransition) -> Signal<Void, NoError> {
return Signal { [weak self] subscriber in
if let strongSelf = self {
if let _ = strongSelf.enqueuedTransition {
preconditionFailure()
}
strongSelf.enqueuedTransition = (transition, {
subscriber.putCompletion()
})
if strongSelf.isNodeLoaded {
strongSelf.dequeueTransition()
} else {
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(true)
}
}
} else {
subscriber.putCompletion()
}
return EmptyDisposable
} |> runOn(Queue.mainQueue())
}
private func dequeueTransition() {
if let (transition, completion) = self.enqueuedTransition {
self.enqueuedTransition = nil
let completion: (ListViewDisplayedItemRange) -> Void = { [weak self] visibleRange in
if let strongSelf = self {
strongSelf.callListView = transition.callListView
let empty = countMeaningfulCallListEntries(transition.callListView.filteredEntries) == 0
strongSelf.emptyStateUpdated(empty)
strongSelf.emptyStatePromise.set(.single(empty))
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(true)
}
completion()
}
}
self.listNode.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: CallListOpaqueTransactionState(callListView: transition.callListView), completion: completion)
}
}
func scrollToLatest() {
if let view = self.callListView?.originalView, !view.hasLater {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
} else {
let location: CallListNodeLocation = .scroll(index: EngineMessage.Index.absoluteUpperBound(), sourceIndex: EngineMessage.Index.absoluteLowerBound(), scrollPosition: .top(0.0), animated: true)
self.currentLocationAndType = CallListNodeLocationAndType(location: location, scope: self.currentLocationAndType.scope)
self.callListLocationAndType.set(self.currentLocationAndType)
}
}
@objc private func emptyButtonPressed() {
self.startNewCall?()
}
func updateLayout(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
var insets = layout.insets(options: [.input])
insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
insets.left += layout.safeInsets.left
insets.right += layout.safeInsets.right
if self.mode == .navigation {
insets.top += 64.0
}
let size = layout.size
let contentRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom))
let sideInset: CGFloat = 64.0
let emptyAnimationHeight = self.emptyAnimationSize.height
let emptyAnimationSpacing: CGFloat = 13.0
let emptyTextSpacing: CGFloat = 23.0
let emptyTextSize = self.emptyTextNode.updateLayout(CGSize(width: contentRect.width - sideInset * 2.0, height: size.height))
let emptyButtonSize = self.emptyButtonTextNode.updateLayout(CGSize(width: contentRect.width - sideInset * 2.0, height: size.height))
let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTextSize.height + emptyTextSpacing + emptyButtonSize.height
let emptyAnimationY = contentRect.minY + floorToScreenPixels((contentRect.height - emptyTotalHeight) / 2.0)
let textTransition = ContainedViewLayoutTransition.immediate
textTransition.updateFrame(node: self.emptyAnimationNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + (contentRect.width - self.emptyAnimationSize.width) / 2.0, y: emptyAnimationY), size: self.emptyAnimationSize))
textTransition.updateFrame(node: self.emptyTextNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + (contentRect.width - emptyTextSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTextSize))
let emptyButtonSpacing: CGFloat = 14.0
let emptyButtonIconSize = (self.emptyButtonIconNode.image?.size ?? CGSize())
let emptyButtonWidth = emptyButtonIconSize.width + emptyButtonSpacing + emptyButtonSize.width
let emptyButtonX = floor(contentRect.width - emptyButtonWidth) / 2.0
textTransition.updateFrame(node: self.emptyButtonIconNode, frame: CGRect(origin: CGPoint(x: emptyButtonX, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTextSize.height + emptyTextSpacing), size: emptyButtonIconSize))
textTransition.updateFrame(node: self.emptyButtonTextNode, frame: CGRect(origin: CGPoint(x: emptyButtonX + emptyButtonIconSize.width + emptyButtonSpacing, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTextSize.height + emptyTextSpacing + 4.0), size: emptyButtonSize))
textTransition.updateFrame(node: self.emptyButtonNode, frame: CGRect(origin: CGPoint(x: emptyButtonX, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTextSize.height + emptyTextSpacing), size: CGSize(width: emptyButtonWidth, height: 44.0)))
self.emptyAnimationNode.updateLayout(size: self.emptyAnimationSize)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.input])
insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
let inset: CGFloat
if layout.size.width >= 375.0 {
inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
} else {
inset = 0.0
}
if case .navigation = self.mode {
insets.left += inset
insets.right += inset
self.leftOverlayNode.frame = CGRect(x: 0.0, y: 0.0, width: insets.left, height: layout.size.height)
self.rightOverlayNode.frame = CGRect(x: layout.size.width - insets.right, y: 0.0, width: insets.right, height: layout.size.height)
if self.leftOverlayNode.supernode == nil {
self.insertSubnode(self.leftOverlayNode, aboveSubnode: self.listNode)
}
if self.rightOverlayNode.supernode == nil {
self.insertSubnode(self.rightOverlayNode, aboveSubnode: self.listNode)
}
} else {
insets.left += layout.safeInsets.left
insets.right += layout.safeInsets.right
}
self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
self.updateLayout(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if !self.dequeuedInitialTransitionOnLayout {
self.dequeuedInitialTransitionOnLayout = true
self.dequeueTransition()
}
}
}
@@ -0,0 +1,484 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AvatarNode
import TelegramStringFormatting
import AccountContext
import ChatListSearchItemHeader
import PeerOnlineMarkerNode
private func callListNeighbors(item: ListViewItem, topItem: ListViewItem?, bottomItem: ListViewItem?) -> ItemListNeighbors {
let topNeighbor: ItemListNeighbor
if let topItem = topItem {
if let item = item as? ItemListItem, let topItem = topItem as? ItemListItem {
if topItem.sectionId != item.sectionId {
topNeighbor = .otherSection(topItem.requestsNoInset ? .none : .full)
} else {
topNeighbor = .sameSection(alwaysPlain: topItem.isAlwaysPlain)
}
} else {
if item is CallListGroupCallItem && topItem is CallListGroupCallItem {
topNeighbor = .sameSection(alwaysPlain: false)
} else {
topNeighbor = .otherSection(.full)
}
}
} else {
topNeighbor = .none
}
let bottomNeighbor: ItemListNeighbor
if let bottomItem = bottomItem {
if let item = item as? ItemListItem, let bottomItem = bottomItem as? ItemListItem {
if bottomItem.sectionId != item.sectionId {
bottomNeighbor = .otherSection(bottomItem.requestsNoInset ? .none : .full)
} else {
bottomNeighbor = .sameSection(alwaysPlain: bottomItem.isAlwaysPlain)
}
} else {
if item is CallListGroupCallItem && bottomItem is CallListGroupCallItem {
bottomNeighbor = .sameSection(alwaysPlain: false)
} else {
bottomNeighbor = .otherSection(.full)
}
}
} else {
bottomNeighbor = .none
}
return ItemListNeighbors(top: topNeighbor, bottom: bottomNeighbor)
}
class CallListGroupCallItem: ListViewItem {
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let context: AccountContext
let style: ItemListStyle
let peer: EnginePeer
let isActive: Bool
let editing: Bool
let interaction: CallListNodeInteraction
let selectable: Bool = true
let headerAccessoryItem: ListViewAccessoryItem?
let header: ListViewItemHeader?
init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, context: AccountContext, style: ItemListStyle, peer: EnginePeer, isActive: Bool, editing: Bool, interaction: CallListNodeInteraction) {
self.presentationData = presentationData
self.systemStyle = systemStyle
self.context = context
self.style = style
self.peer = peer
self.isActive = isActive
self.editing = editing
self.interaction = interaction
self.headerAccessoryItem = nil
self.header = ChatListSearchItemHeader(type: .activeVoiceChats, theme: presentationData.theme, strings: presentationData.strings)
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = CallListGroupCallItemNode()
let makeLayout = node.asyncLayout()
let (first, last, firstWithHeader) = CallListGroupCallItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (nodeLayout, nodeApply) = makeLayout(self, params, first, last, firstWithHeader, callListNeighbors(item: self, topItem: previousItem, bottomItem: nextItem))
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
nodeApply(synchronousLoads).1(false)
})
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? CallListGroupCallItemNode {
let layout = nodeValue.asyncLayout()
async {
let (first, last, firstWithHeader) = CallListGroupCallItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader, callListNeighbors(item: self, topItem: previousItem, bottomItem: nextItem))
var animated = true
if case .None = animation {
animated = false
}
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply(false).1(animated)
})
}
}
}
}
}
func selected(listView: ListView) {
listView.clearHighlightAnimated(true)
self.interaction.openGroupCall(self.peer.id)
}
static func mergeType(item: CallListGroupCallItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool) {
var first = false
var last = false
var firstWithHeader = false
if let previousItem = previousItem {
if let header = item.header {
if let previousItem = previousItem as? CallListGroupCallItem {
firstWithHeader = header.id != previousItem.header?.id
} else {
firstWithHeader = true
}
}
} else {
first = true
firstWithHeader = item.header != nil
}
if let nextItem = nextItem {
if let header = item.header {
if let nextItem = nextItem as? CallListGroupCallItem {
last = header.id != nextItem.header?.id
} else {
last = true
}
}
} else {
last = true
}
return (first, last, firstWithHeader)
}
}
private let avatarFont = avatarPlaceholderFont(size: 16.0)
class CallListGroupCallItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let indicatorNode: VoiceChatIndicatorNode
private let avatarNode: AvatarNode
private let titleNode: TextNode
private let joinButtonNode: HighlightableButtonNode
private let joinTitleNode: TextNode
private let joinBackgroundNode: ASImageNode
private let accessibilityArea: AccessibilityAreaNode
private var layoutParams: (CallListGroupCallItem, ListViewItemLayoutParams, Bool, Bool, Bool)?
required init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.indicatorNode = VoiceChatIndicatorNode()
self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.isLayerBacked = !smartInvertColorsEnabled()
self.titleNode = TextNode()
self.joinButtonNode = HighlightableButtonNode()
self.joinButtonNode.hitTestSlop = UIEdgeInsets(top: -6.0, left: -6.0, bottom: -6.0, right: -10.0)
self.joinTitleNode = TextNode()
self.joinBackgroundNode = ASImageNode()
self.joinButtonNode.addSubnode(self.joinBackgroundNode)
self.joinButtonNode.addSubnode(self.joinTitleNode)
self.accessibilityArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.indicatorNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.joinButtonNode)
self.addSubnode(self.accessibilityArea)
self.joinButtonNode.addTarget(self, action: #selector(self.joinPressed), forControlEvents: .touchUpInside)
self.accessibilityArea.activate = { [weak self] in
guard let item = self?.layoutParams?.0 else {
return false
}
item.interaction.openGroupCall(item.peer.id)
return true
}
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let (item, _, _, _, _) = self.layoutParams {
let (first, last, firstWithHeader) = CallListGroupCallItem.mergeType(item: item, previousItem: previousItem, nextItem: nextItem)
self.layoutParams = (item, params, first, last, firstWithHeader)
let makeLayout = self.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(item, params, first, last, firstWithHeader, callListNeighbors(item: item, topItem: previousItem, bottomItem: nextItem))
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply(false)
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
if self.backgroundNode.supernode != nil {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.backgroundNode)
} else {
self.insertSubnode(self.highlightedBackgroundNode, at: 0)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
func asyncLayout() -> (_ item: CallListGroupCallItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> (Signal<Void, NoError>?, (Bool) -> Void)) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeJoinTitleLayout = TextNode.asyncLayout(self.joinTitleNode)
let currentItem = self.layoutParams?.0
return { [weak self] item, params, first, last, firstWithHeader, neighbors in
var updatedTheme: PresentationTheme?
var updatedJoinBackground: UIImage?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
updatedJoinBackground = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.list.itemCheckColors.fillColor)
}
let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
let avatarDiameter = min(40.0, floor(item.presentationData.fontSize.itemListBaseFontSize * 40.0 / 17.0))
let editingOffset: CGFloat
if item.editing {
editingOffset = 16.0
} else {
editingOffset = 0.0
}
var leftInset: CGFloat = 46.0 + avatarDiameter + params.leftInset
let rightInset: CGFloat = 13.0 + params.rightInset
var infoIconRightInset: CGFloat = rightInset
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
var dateRightInset: CGFloat = 46.0 + params.rightInset
if item.editing {
leftInset += editingOffset
dateRightInset += 5.0
infoIconRightInset -= 36.0
}
var titleAttributedString: NSAttributedString?
let titleColor = item.presentationData.theme.list.itemPrimaryTextColor
titleAttributedString = NSAttributedString(string: item.peer.compactDisplayTitle, font: titleFont, textColor: titleColor)
let (joinTitleLayout, joinTitleApply) = makeJoinTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.VoiceChat_PanelJoin.uppercased(), font: Font.semibold(15.0), textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let joinButtonSize = CGSize(width: joinTitleLayout.size.width + 20.0, height: 28.0)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - joinButtonSize.width - 8.0 - (item.editing ? -30.0 : 10.0)), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat = 11.0
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: titleLayout.size.height + verticalInset * 2.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0))
let contentSize = nodeLayout.contentSize
return (nodeLayout, { [weak self] synchronousLoads in
if let strongSelf = self {
let peer = item.peer
var overrideImage: AvatarNodeImageOverride?
if peer.isDeleted {
overrideImage = .deletedIcon
}
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads)
return (strongSelf.avatarNode.ready, { [weak strongSelf] animated in
if let strongSelf = strongSelf {
strongSelf.layoutParams = (item, params, first, last, firstWithHeader)
let revealOffset = strongSelf.revealOffset
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if !last && strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 1)
} else if last && strongSelf.bottomStripeNode.supernode != nil {
strongSelf.bottomStripeNode.removeFromSupernode()
}
transition.updateFrameAdditive(node: 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)
}
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
strongSelf.topStripeNode.isHidden = false
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
default:
bottomStripeInset = 0.0
}
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.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: nodeLayout.size.width, height: separatorHeight))
transition.updateFrameAdditive(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width - bottomStripeInset, height: separatorHeight)))
}
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)
strongSelf.indicatorNode.color = item.presentationData.theme.chatList.checkmarkColor
let indicatorSize: CGFloat = 22.0
transition.updateFrameAdditive(node: strongSelf.indicatorNode, frame: CGRect(origin: CGPoint(x: avatarFrame.minX - 6.0 - indicatorSize, y: floor(avatarFrame.midY - indicatorSize / 2.0)), size: CGSize(width: indicatorSize, height: indicatorSize)))
let _ = titleApply()
transition.updateFrameAdditive(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: verticalInset), size: titleLayout.size))
let joinButtonFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightInset - joinButtonSize.width, y: floor((contentSize.height - 28.0) / 2.0)), size: joinButtonSize)
transition.updateFrameAdditive(node: strongSelf.joinButtonNode, frame: joinButtonFrame)
transition.updateAlpha(node: strongSelf.joinButtonNode, alpha: item.isActive ? 0.0 : 1.0)
if let image = updatedJoinBackground {
strongSelf.joinBackgroundNode.image = image
}
transition.updateFrameAdditive(node: strongSelf.joinBackgroundNode, frame: CGRect(origin: CGPoint(), size: joinButtonFrame.size))
let _ = joinTitleApply()
transition.updateFrameAdditive(node: strongSelf.joinTitleNode, frame: CGRect(origin: CGPoint(x: floor((joinButtonSize.width - joinTitleLayout.size.width) / 2.0), y: floor((joinButtonSize.height - joinTitleLayout.size.height) / 2.0) + 1.0), size: joinTitleLayout.size))
let topHighlightInset: CGFloat = (first || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset))
strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
strongSelf.accessibilityArea.accessibilityTraits = .button
strongSelf.accessibilityArea.accessibilityLabel = titleAttributedString?.string
strongSelf.accessibilityArea.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
strongSelf.joinButtonNode.accessibilityLabel = item.presentationData.strings.VoiceChat_PanelJoin
}
})
} else {
return (nil, { _ in })
}
})
}
}
override func layoutHeaderAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) {
let bounds = self.bounds
accessoryItemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -29.0), size: CGSize(width: bounds.size.width, height: 29.0))
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.3, removeOnCompletion: false)
}
override public func headers() -> [ListViewItemHeader]? {
if let (item, _, _, _, _) = self.layoutParams {
return item.header.flatMap { [$0] }
} else {
return nil
}
}
@objc private func joinPressed() {
if let item = self.layoutParams?.0 {
item.interaction.openGroupCall(item.peer.id)
}
}
}
@@ -0,0 +1,127 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
private let titleFont = Font.regular(17.0)
class CallListHoleItem: ListViewItem {
let theme: PresentationTheme
let selectable: Bool = false
init(theme: PresentationTheme) {
self.theme = theme
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = CallListHoleItemNode()
node.relativePosition = (first: previousItem == nil, last: nextItem == nil)
node.insets = UIEdgeInsets()
node.layoutForParams(params, item: self, previousItem: previousItem, nextItem: nextItem)
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
assert(node() is CallListHoleItemNode)
if let nodeValue = node() as? CallListHoleItemNode {
let layout = nodeValue.asyncLayout()
async {
let first = previousItem == nil
let last = nextItem == nil
let (nodeLayout, apply) = layout(self, params, first, last)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply()
if let nodeValue = node() as? CallListHoleItemNode {
nodeValue.updateBackgroundAndSeparatorsLayout()
}
})
}
}
}
}
}
}
private let separatorHeight = 1.0 / UIScreen.main.scale
class CallListHoleItemNode: ListViewItemNode {
let separatorNode: ASDisplayNode
let labelNode: TextNode
var relativePosition: (first: Bool, last: Bool) = (false, false)
required init() {
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = UIColor(rgb: 0xc8c7cc)
self.separatorNode.isLayerBacked = true
self.labelNode = TextNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.separatorNode)
self.addSubnode(self.labelNode)
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (_, apply) = layout(item as! CallListHoleItem, params, self.relativePosition.first, self.relativePosition.last)
apply()
}
func asyncLayout() -> (_ item: CallListHoleItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) {
let labelNodeLayout = TextNode.asyncLayout(self.labelNode)
return { item, params, first, last in
let baseWidth = params.width - params.leftInset - params.rightInset
let (labelLayout, labelApply) = labelNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "", font: titleFont, textColor: item.theme.chatList.messageTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let insets = UIEdgeInsets()
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 68.0), insets: insets)
let separatorInset: CGFloat
if last {
separatorInset = 0.0
} else {
separatorInset = 80.0 + params.leftInset
}
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.relativePosition = (first, last)
let _ = labelApply()
strongSelf.separatorNode.backgroundColor = item.theme.chatList.itemSeparatorColor
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: floor((params.width - labelLayout.size.width) / 2.0), y: floor((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: separatorInset, y: 68.0 - separatorHeight), size: CGSize(width: params.width - separatorInset, height: separatorHeight))
strongSelf.contentSize = layout.contentSize
strongSelf.insets = layout.insets
strongSelf.updateBackgroundAndSeparatorsLayout()
}
})
}
}
func updateBackgroundAndSeparatorsLayout() {
//let size = self.bounds.size
//let insets = self.insets
}
}
@@ -0,0 +1,263 @@
import Foundation
import UIKit
import TelegramCore
import TelegramPresentationData
import MergeLists
enum CallListNodeEntryId: Hashable {
case setting(Int32)
case groupCall(EnginePeer.Id)
case hole(EngineMessage.Index)
case message(EngineMessage.Index)
}
private func areMessagesEqual(_ lhsMessage: EngineMessage, _ rhsMessage: EngineMessage) -> Bool {
if lhsMessage.stableVersion != rhsMessage.stableVersion {
return false
}
if lhsMessage.id != rhsMessage.id || lhsMessage.flags != rhsMessage.flags {
return false
}
return true
}
enum CallListNodeEntry: Comparable, Identifiable {
enum SortIndex: Comparable {
case displayTab
case displayTabInfo
case openNewCall
case groupCall(EnginePeer.Id, String)
case message(EngineMessage.Index)
case hole(EngineMessage.Index)
static func <(lhs: SortIndex, rhs: SortIndex) -> Bool {
switch lhs {
case .displayTab:
return false
case .displayTabInfo:
switch rhs {
case .displayTab:
return true
default:
return false
}
case .openNewCall:
switch rhs {
case .displayTab, .displayTabInfo:
return true
default:
return false
}
case let .groupCall(lhsPeerId, lhsTitle):
switch rhs {
case .displayTab, .displayTabInfo, .openNewCall:
return true
case let .groupCall(rhsPeerId, rhsTitle):
if lhsTitle == rhsTitle {
return lhsPeerId < rhsPeerId
} else {
return lhsTitle < rhsTitle
}
case .message, .hole:
return true
}
case let .hole(lhsIndex):
switch rhs {
case .displayTab, .displayTabInfo, .groupCall, .openNewCall:
return true
case let .hole(rhsIndex):
return lhsIndex < rhsIndex
case let .message(rhsIndex):
return lhsIndex < rhsIndex
}
case let .message(lhsIndex):
switch rhs {
case .displayTab, .displayTabInfo, .groupCall, .openNewCall:
return true
case let .hole(rhsIndex):
return lhsIndex < rhsIndex
case let .message(rhsIndex):
return lhsIndex < rhsIndex
}
}
}
}
case displayTab(PresentationTheme, String, Bool)
case displayTabInfo(PresentationTheme, String)
case openNewCall
case groupCall(peer: EnginePeer, editing: Bool, isActive: Bool)
case messageEntry(topMessage: EngineMessage, messages: [EngineMessage], theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, editing: Bool, hasActiveRevealControls: Bool, displayHeader: Bool, missed: Bool)
case holeEntry(index: EngineMessage.Index, theme: PresentationTheme)
var sortIndex: SortIndex {
switch self {
case .displayTab:
return .displayTab
case .displayTabInfo:
return .displayTabInfo
case .openNewCall:
return .openNewCall
case let .groupCall(peer, _, _):
return .groupCall(peer.id, peer.compactDisplayTitle)
case let .messageEntry(message, _, _, _, _, _, _, _, _):
return .message(message.index)
case let .holeEntry(index, _):
return .hole(index)
}
}
var stableId: CallListNodeEntryId {
switch self {
case .displayTab:
return .setting(0)
case .displayTabInfo:
return .setting(1)
case .openNewCall:
return .setting(2)
case let .groupCall(peer, _, _):
return .groupCall(peer.id)
case let .messageEntry(message, _, _, _, _, _, _, _, _):
return .message(message.index)
case let .holeEntry(index, _):
return .hole(index)
}
}
static func <(lhs: CallListNodeEntry, rhs: CallListNodeEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
static func ==(lhs: CallListNodeEntry, rhs: CallListNodeEntry) -> Bool {
switch lhs {
case let .displayTab(lhsTheme, lhsText, lhsValue):
if case let .displayTab(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .displayTabInfo(lhsTheme, lhsText):
if case let .displayTabInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case .openNewCall:
if case .openNewCall = rhs {
return true
} else {
return false
}
case let .groupCall(lhsPeer, lhsEditing, lhsIsActive):
if case let .groupCall(rhsPeer, rhsEditing, rhsIsActive) = rhs {
if lhsPeer != rhsPeer {
return false
}
if lhsEditing != rhsEditing {
return false
}
if lhsIsActive != rhsIsActive {
return false
}
return true
} else {
return false
}
case let .messageEntry(lhsMessage, lhsMessages, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsEditing, lhsHasRevealControls, lhsDisplayHeader, lhsMissed):
if case let .messageEntry(rhsMessage, rhsMessages, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsEditing, rhsHasRevealControls, rhsDisplayHeader, rhsMissed) = rhs {
if lhsTheme !== rhsTheme {
return false
}
if lhsStrings !== rhsStrings {
return false
}
if lhsDateTimeFormat != rhsDateTimeFormat {
return false
}
if lhsMissed != rhsMissed {
return false
}
if lhsEditing != rhsEditing {
return false
}
if lhsHasRevealControls != rhsHasRevealControls {
return false
}
if lhsDisplayHeader != rhsDisplayHeader {
return false
}
if !areMessagesEqual(lhsMessage, rhsMessage) {
return false
}
if lhsMessages.count != rhsMessages.count {
return false
}
for i in 0 ..< lhsMessages.count {
if !areMessagesEqual(lhsMessages[i], rhsMessages[i]) {
return false
}
}
return true
} else {
return false
}
case let .holeEntry(lhsIndex, lhsTheme):
if case let .holeEntry(rhsIndex, rhsTheme) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme {
return true
} else {
return false
}
}
}
}
func callListNodeEntriesForView(view: EngineCallList, displayOpenNewCall: Bool, groupCalls: [EnginePeer], state: CallListNodeState, showSettings: Bool, showCallsTab: Bool, isRecentCalls: Bool, currentGroupCallPeerId: EnginePeer.Id?) -> [CallListNodeEntry] {
var result: [CallListNodeEntry] = []
for entry in view.items {
switch entry {
case let .message(topMessage, messages):
result.append(.messageEntry(topMessage: topMessage, messages: messages, theme: state.presentationData.theme, strings: state.presentationData.strings, dateTimeFormat: state.dateTimeFormat, editing: state.editing, hasActiveRevealControls: state.messageIdWithRevealedOptions == topMessage.id, displayHeader: !showSettings && isRecentCalls, missed: !isRecentCalls))
case let .hole(index):
result.append(.holeEntry(index: index, theme: state.presentationData.theme))
}
}
if !view.hasLater {
if !showSettings && isRecentCalls {
for peer in groupCalls.sorted(by: { lhs, rhs in
let lhsTitle = lhs.compactDisplayTitle
let rhsTitle = rhs.compactDisplayTitle
if lhsTitle != rhsTitle {
return lhsTitle < rhsTitle
}
return lhs.id < rhs.id
}).reversed() {
result.append(.groupCall(peer: peer, editing: state.editing, isActive: currentGroupCallPeerId == peer.id))
}
}
if displayOpenNewCall {
result.append(.openNewCall)
}
if showSettings {
result.append(.displayTabInfo(state.presentationData.theme, state.presentationData.strings.CallSettings_TabIconDescription))
result.append(.displayTab(state.presentationData.theme, state.presentationData.strings.CallSettings_TabIcon, showCallsTab))
}
}
return result
}
func countMeaningfulCallListEntries(_ entries: [CallListNodeEntry]) -> Int {
var count: Int = 0
for entry in entries {
switch entry.stableId {
case .setting:
break
default:
count += 1
}
}
return count
}
@@ -0,0 +1,105 @@
import Foundation
import UIKit
import TelegramCore
import SwiftSignalKit
import Display
enum CallListNodeLocation: Equatable {
case initial(count: Int)
case changeType(index: EngineMessage.Index)
case navigation(index: EngineMessage.Index)
case scroll(index: EngineMessage.Index, sourceIndex: EngineMessage.Index, scrollPosition: ListViewScrollPosition, animated: Bool)
static func ==(lhs: CallListNodeLocation, rhs: CallListNodeLocation) -> Bool {
switch lhs {
case let .navigation(index):
switch rhs {
case .navigation(index):
return true
default:
return false
}
default:
return false
}
}
}
struct CallListNodeLocationAndType: Equatable {
let location: CallListNodeLocation
let scope: EngineCallList.Scope
}
enum CallListNodeViewUpdateType {
case Initial
case Generic
case Reload
case ReloadAnimated
case UpdateVisible
}
struct CallListNodeViewUpdate {
let view: EngineCallList
let type: CallListNodeViewUpdateType
let scrollPosition: CallListNodeViewScrollPosition?
}
func callListViewForLocationAndType(locationAndType: CallListNodeLocationAndType, engine: TelegramEngine) -> Signal<(CallListNodeViewUpdate, EngineCallList.Scope), NoError> {
switch locationAndType.location {
case let .initial(count):
return engine.messages.callList(
scope: locationAndType.scope,
index: EngineMessage.Index.absoluteUpperBound(),
itemCount: count
)
|> map { view -> (CallListNodeViewUpdate, EngineCallList.Scope) in
return (CallListNodeViewUpdate(view: view, type: .Generic, scrollPosition: nil), locationAndType.scope)
}
case let .changeType(index):
return engine.messages.callList(
scope: locationAndType.scope,
index: index,
itemCount: 120
)
|> map { view -> (CallListNodeViewUpdate, EngineCallList.Scope) in
return (CallListNodeViewUpdate(view: view, type: .ReloadAnimated, scrollPosition: nil), locationAndType.scope)
}
case let .navigation(index):
var first = true
return engine.messages.callList(
scope: locationAndType.scope,
index: index,
itemCount: 120
)
|> map { view -> (CallListNodeViewUpdate, EngineCallList.Scope) in
let genericType: CallListNodeViewUpdateType
if first {
first = false
genericType = .UpdateVisible
} else {
genericType = .Generic
}
return (CallListNodeViewUpdate(view: view, type: genericType, scrollPosition: nil), locationAndType.scope)
}
case let .scroll(index, sourceIndex, scrollPosition, animated):
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up
let callScrollPosition: CallListNodeViewScrollPosition = .index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated)
var first = true
return engine.messages.callList(
scope: locationAndType.scope,
index: index,
itemCount: 120
)
|> map { view -> (CallListNodeViewUpdate, EngineCallList.Scope) in
let genericType: CallListNodeViewUpdateType
let scrollPosition: CallListNodeViewScrollPosition? = first ? callScrollPosition : nil
if first {
first = false
genericType = .UpdateVisible
} else {
genericType = .Generic
}
return (CallListNodeViewUpdate(view: view, type: genericType, scrollPosition: scrollPosition), locationAndType.scope)
}
}
}
@@ -0,0 +1,173 @@
import Foundation
import UIKit
import TelegramCore
import SwiftSignalKit
import Display
import MergeLists
import ItemListUI
import AccountContext
struct CallListNodeView {
let originalView: EngineCallList
let filteredEntries: [CallListNodeEntry]
let presentationData: ItemListPresentationData
}
enum CallListNodeViewTransitionReason {
case initial
case interactiveChanges
case reload
case reloadAnimated
}
struct CallListNodeViewTransitionInsertEntry {
let index: Int
let previousIndex: Int?
let entry: CallListNodeEntry
let directionHint: ListViewItemOperationDirectionHint?
}
struct CallListNodeViewTransitionUpdateEntry {
let index: Int
let previousIndex: Int
let entry: CallListNodeEntry
let directionHint: ListViewItemOperationDirectionHint?
}
struct CallListNodeViewTransition {
let callListView: CallListNodeView
let deleteItems: [ListViewDeleteItem]
let insertEntries: [CallListNodeViewTransitionInsertEntry]
let updateEntries: [CallListNodeViewTransitionUpdateEntry]
let options: ListViewDeleteAndInsertOptions
let scrollToItem: ListViewScrollToItem?
let stationaryItemRange: (Int, Int)?
}
enum CallListNodeViewScrollPosition {
case top(animated: Bool)
case index(index: EngineMessage.Index, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool)
}
func preparedCallListNodeViewTransition(from fromView: CallListNodeView?, to toView: CallListNodeView, reason: CallListNodeViewTransitionReason, disableAnimations: Bool, context: AccountContext, scrollPosition: CallListNodeViewScrollPosition?) -> Signal<CallListNodeViewTransition, NoError> {
return Signal { subscriber in
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries, allUpdated: fromView?.presentationData != toView.presentationData)
var adjustedDeleteIndices: [ListViewDeleteItem] = []
let previousCount: Int
if let fromView = fromView {
previousCount = fromView.filteredEntries.count
} else {
previousCount = 0;
}
for index in deleteIndices {
adjustedDeleteIndices.append(ListViewDeleteItem(index: previousCount - 1 - index, directionHint: nil))
}
var adjustedIndicesAndItems: [CallListNodeViewTransitionInsertEntry] = []
var adjustedUpdateItems: [CallListNodeViewTransitionUpdateEntry] = []
let updatedCount = toView.filteredEntries.count
var options: ListViewDeleteAndInsertOptions = []
var maxAnimatedInsertionIndex = -1
let stationaryItemRange: (Int, Int)? = nil
var scrollToItem: ListViewScrollToItem? = nil
var wasEmpty = false
if let fromView = fromView, fromView.originalView.items.isEmpty {
wasEmpty = true
}
switch reason {
case .initial:
let _ = options.insert(.LowLatency)
let _ = options.insert(.Synchronous)
let _ = options.insert(.PreferSynchronousResourceLoading)
case .interactiveChanges:
if wasEmpty {
let _ = options.insert(.Synchronous)
let _ = options.insert(.PreferSynchronousResourceLoading)
} else {
let _ = options.insert(.AnimateAlpha)
if !disableAnimations {
let _ = options.insert(.AnimateInsertion)
}
for (index, _, _) in indicesAndItems.sorted(by: { $0.0 > $1.0 }) {
let adjustedIndex = updatedCount - 1 - index
if adjustedIndex == maxAnimatedInsertionIndex + 1 {
maxAnimatedInsertionIndex += 1
}
}
}
case .reload:
break
case .reloadAnimated:
let _ = options.insert(.LowLatency)
let _ = options.insert(.Synchronous)
let _ = options.insert(.AnimateCrossfade)
let _ = options.insert(.PreferSynchronousResourceLoading)
}
for (index, entry, previousIndex) in indicesAndItems {
let adjustedIndex = updatedCount - 1 - index
let adjustedPrevousIndex: Int?
if let previousIndex = previousIndex {
adjustedPrevousIndex = previousCount - 1 - previousIndex
} else {
adjustedPrevousIndex = nil
}
var directionHint: ListViewItemOperationDirectionHint?
if maxAnimatedInsertionIndex >= 0 && adjustedIndex <= maxAnimatedInsertionIndex {
directionHint = .Down
}
adjustedIndicesAndItems.append(CallListNodeViewTransitionInsertEntry(index: adjustedIndex, previousIndex: adjustedPrevousIndex, entry: entry, directionHint: directionHint))
}
for (index, entry, previousIndex) in updateIndices {
let adjustedIndex = updatedCount - 1 - index
let adjustedPreviousIndex = previousCount - 1 - previousIndex
let directionHint: ListViewItemOperationDirectionHint? = nil
adjustedUpdateItems.append(CallListNodeViewTransitionUpdateEntry(index: adjustedIndex, previousIndex: adjustedPreviousIndex, entry: entry, directionHint: directionHint))
}
if let scrollPosition = scrollPosition {
switch scrollPosition {
case let .top(animated):
scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: animated, curve: .Default(duration: nil), directionHint: .Up)
case let .index(scrollIndex, position, directionHint, animated):
var index = toView.filteredEntries.count - 1
for entry in toView.filteredEntries {
if case let .message(messageIndex) = entry.sortIndex {
if messageIndex >= scrollIndex {
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default(duration: nil), directionHint: directionHint)
break
}
}
index -= 1
}
if scrollToItem == nil {
var index = 0
for entry in toView.filteredEntries.reversed() {
if case let .message(messageIndex) = entry.sortIndex {
if messageIndex < scrollIndex {
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default(duration: nil), directionHint: directionHint)
break
}
}
index += 1
}
}
}
}
subscriber.putNext(CallListNodeViewTransition(callListView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange))
subscriber.putCompletion()
return EmptyDisposable
}
}