Additional voice chat UI improvements

This commit is contained in:
Ali 2020-12-18 18:25:37 +04:00
parent 2805b1715c
commit 06a13aab28
16 changed files with 2763 additions and 2030 deletions

View File

@ -6026,3 +6026,8 @@ Sorry for the inconvenience.";
"Group.GroupMembersHeader" = "GROUP MEMBERS"; "Group.GroupMembersHeader" = "GROUP MEMBERS";
"Conversation.VoiceChatMediaRecordingRestricted" = "You can't record voice and video messages during a voice chat."; "Conversation.VoiceChatMediaRecordingRestricted" = "You can't record voice and video messages during a voice chat.";
"CallList.ActiveVoiceChatsHeader" = "ACTIVE VOICE CHATS";
"CallList.RecentCallsHeader" = "RECENT CALLS";
"VoiceChat.PeerJoinedText" = "%@ joined the voice chat";

View File

@ -261,6 +261,16 @@ public struct PresentationGroupCallMembers: Equatable {
} }
} }
public final class PresentationGroupCallMemberEvent {
public let peer: Peer
public let joined: Bool
public init(peer: Peer, joined: Bool) {
self.peer = peer
self.joined = joined
}
}
public protocol PresentationGroupCall: class { public protocol PresentationGroupCall: class {
var account: Account { get } var account: Account { get }
var accountContext: AccountContext { get } var accountContext: AccountContext { get }
@ -277,6 +287,8 @@ public protocol PresentationGroupCall: class {
var myAudioLevel: Signal<Float, NoError> { get } var myAudioLevel: Signal<Float, NoError> { get }
var isMuted: Signal<Bool, NoError> { get } var isMuted: Signal<Bool, NoError> { get }
var memberEvents: Signal<PresentationGroupCallMemberEvent, NoError> { get }
func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError> func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError>
func toggleIsMuted() func toggleIsMuted()

View File

@ -23,6 +23,8 @@ swift_library(
"//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/TelegramNotices:TelegramNotices", "//submodules/TelegramNotices:TelegramNotices",
"//submodules/MergeLists:MergeLists", "//submodules/MergeLists:MergeLists",
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
"//submodules/PeerOnlineMarkerNode:PeerOnlineMarkerNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -12,6 +12,7 @@ import PresentationDataUtils
import AvatarNode import AvatarNode
import TelegramStringFormatting import TelegramStringFormatting
import AccountContext import AccountContext
import ChatListSearchItemHeader
private func callDurationString(strings: PresentationStrings, duration: Int32) -> String { private func callDurationString(strings: PresentationStrings, duration: Int32) -> String {
if duration < 60 { if duration < 60 {
@ -78,7 +79,7 @@ class CallListCallItem: ListViewItem {
let headerAccessoryItem: ListViewAccessoryItem? let headerAccessoryItem: ListViewAccessoryItem?
let header: ListViewItemHeader? let header: ListViewItemHeader?
init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, context: AccountContext, style: ItemListStyle, topMessage: Message, messages: [Message], editing: Bool, revealed: Bool, interaction: CallListNodeInteraction) { init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, context: AccountContext, style: ItemListStyle, topMessage: Message, messages: [Message], editing: Bool, revealed: Bool, displayHeader: Bool, interaction: CallListNodeInteraction) {
self.presentationData = presentationData self.presentationData = presentationData
self.dateTimeFormat = dateTimeFormat self.dateTimeFormat = dateTimeFormat
self.context = context self.context = context
@ -90,7 +91,11 @@ class CallListCallItem: ListViewItem {
self.interaction = interaction self.interaction = interaction
self.headerAccessoryItem = nil self.headerAccessoryItem = nil
self.header = 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) { 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) {
@ -319,7 +324,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
updatedInfoIcon = true updatedInfoIcon = true
} }
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
let statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) let statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
let dateFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) let dateFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
let avatarDiameter = min(40.0, floor(item.presentationData.fontSize.itemListBaseFontSize * 40.0 / 17.0)) let avatarDiameter = min(40.0, floor(item.presentationData.fontSize.itemListBaseFontSize * 40.0 / 17.0))

View File

@ -144,7 +144,7 @@ public final class CallListController: ViewController {
} }
override public func loadDisplayNode() { override public func loadDisplayNode() {
self.displayNode = CallListControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, call: { [weak self] peerId, isVideo in self.displayNode = CallListControllerNode(controller: self, context: self.context, mode: self.mode, presentationData: self.presentationData, call: { [weak self] peerId, isVideo in
if let strongSelf = self { if let strongSelf = self {
strongSelf.call(peerId, isVideo: isVideo) strongSelf.call(peerId, isVideo: isVideo)
} }

View File

@ -63,13 +63,15 @@ final class CallListNodeInteraction {
let openInfo: (PeerId, [Message]) -> Void let openInfo: (PeerId, [Message]) -> Void
let delete: ([MessageId]) -> Void let delete: ([MessageId]) -> Void
let updateShowCallsTab: (Bool) -> Void let updateShowCallsTab: (Bool) -> Void
let openGroupCall: (PeerId) -> Void
init(setMessageIdWithRevealedOptions: @escaping (MessageId?, MessageId?) -> Void, call: @escaping (PeerId, Bool) -> Void, openInfo: @escaping (PeerId, [Message]) -> Void, delete: @escaping ([MessageId]) -> Void, updateShowCallsTab: @escaping (Bool) -> Void) { init(setMessageIdWithRevealedOptions: @escaping (MessageId?, MessageId?) -> Void, call: @escaping (PeerId, Bool) -> Void, openInfo: @escaping (PeerId, [Message]) -> Void, delete: @escaping ([MessageId]) -> Void, updateShowCallsTab: @escaping (Bool) -> Void, openGroupCall: @escaping (PeerId) -> Void) {
self.setMessageIdWithRevealedOptions = setMessageIdWithRevealedOptions self.setMessageIdWithRevealedOptions = setMessageIdWithRevealedOptions
self.call = call self.call = call
self.openInfo = openInfo self.openInfo = openInfo
self.delete = delete self.delete = delete
self.updateShowCallsTab = updateShowCallsTab self.updateShowCallsTab = updateShowCallsTab
self.openGroupCall = openGroupCall
} }
} }
@ -112,16 +114,16 @@ struct CallListNodeState: Equatable {
private func mappedInsertEntries(context: AccountContext, presentationData: ItemListPresentationData, showSettings: Bool, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { private func mappedInsertEntries(context: AccountContext, presentationData: ItemListPresentationData, showSettings: Bool, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] {
return entries.map { entry -> ListViewInsertItem in return entries.map { entry -> ListViewInsertItem in
switch entry.entry { switch entry.entry {
case let .displayTab(theme, text, value): case let .displayTab(_, text, value):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, noCorners: true, sectionId: 0, style: .blocks, updated: { value in return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, noCorners: true, sectionId: 0, style: .blocks, updated: { value in
nodeInteraction.updateShowCallsTab(value) nodeInteraction.updateShowCallsTab(value)
}), directionHint: entry.directionHint) }), directionHint: entry.directionHint)
case let .displayTabInfo(theme, text): case let .displayTabInfo(_, text):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint) return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
case let .groupCall(_, peer): case let .groupCall(peer, editing, isActive):
preconditionFailure() return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: editing, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .messageEntry(topMessage, messages, theme, strings, dateTimeFormat, editing, hasActiveRevealControls): case let .messageEntry(topMessage, messages, _, _, dateTimeFormat, editing, hasActiveRevealControls, displayHeader):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, context: context, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, interaction: nodeInteraction), directionHint: entry.directionHint) return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(presentationData: presentationData, 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): case let .holeEntry(_, theme):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListHoleItem(theme: theme), directionHint: entry.directionHint) return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListHoleItem(theme: theme), directionHint: entry.directionHint)
} }
@ -131,16 +133,16 @@ private func mappedInsertEntries(context: AccountContext, presentationData: Item
private func mappedUpdateEntries(context: AccountContext, presentationData: ItemListPresentationData, showSettings: Bool, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { private func mappedUpdateEntries(context: AccountContext, presentationData: ItemListPresentationData, showSettings: Bool, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] {
return entries.map { entry -> ListViewUpdateItem in return entries.map { entry -> ListViewUpdateItem in
switch entry.entry { switch entry.entry {
case let .displayTab(theme, text, value): case let .displayTab(_, text, value):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, noCorners: true, sectionId: 0, style: .blocks, updated: { value in return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, noCorners: true, sectionId: 0, style: .blocks, updated: { value in
nodeInteraction.updateShowCallsTab(value) nodeInteraction.updateShowCallsTab(value)
}), directionHint: entry.directionHint) }), directionHint: entry.directionHint)
case let .displayTabInfo(theme, text): case let .displayTabInfo(_, text):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint) return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
case let .groupCall(_, peer): case let .groupCall(peer, editing, isActive):
preconditionFailure() return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: editing, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .messageEntry(topMessage, messages, theme, strings, dateTimeFormat, editing, hasActiveRevealControls): case let .messageEntry(topMessage, messages, _, _, dateTimeFormat, editing, hasActiveRevealControls, displayHeader):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, context: context, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, interaction: nodeInteraction), directionHint: entry.directionHint) return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(presentationData: presentationData, 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): case let .holeEntry(_, theme):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListHoleItem(theme: theme), directionHint: entry.directionHint) return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListHoleItem(theme: theme), directionHint: entry.directionHint)
} }
@ -160,6 +162,7 @@ private final class CallListOpaqueTransactionState {
} }
final class CallListControllerNode: ASDisplayNode { final class CallListControllerNode: ASDisplayNode {
private weak var controller: CallListController?
private let context: AccountContext private let context: AccountContext
private let mode: CallListControllerMode private let mode: CallListControllerMode
private var presentationData: PresentationData private var presentationData: PresentationData
@ -201,7 +204,10 @@ final class CallListControllerNode: ASDisplayNode {
private let emptyStatePromise = Promise<Bool>() private let emptyStatePromise = Promise<Bool>()
private let emptyStateDisposable = MetaDisposable() private let emptyStateDisposable = MetaDisposable()
init(context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (PeerId, Bool) -> Void, openInfo: @escaping (PeerId, [Message]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void) { private let openGroupCallDisposable = MetaDisposable()
init(controller: CallListController, context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (PeerId, Bool) -> Void, openInfo: @escaping (PeerId, [Message]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void) {
self.controller = controller
self.context = context self.context = context
self.mode = mode self.mode = mode
self.presentationData = presentationData self.presentationData = presentationData
@ -270,6 +276,63 @@ final class CallListControllerNode: ASDisplayNode {
let _ = ApplicationSpecificNotice.incrementCallsTabTips(accountManager: strongSelf.context.sharedContext.accountManager, count: 4).start() let _ = ApplicationSpecificNotice.incrementCallsTabTips(accountManager: strongSelf.context.sharedContext.accountManager, count: 4).start()
} }
} }
}, openGroupCall: { [weak self] peerId in
guard let strongSelf = self else {
return
}
let disposable = strongSelf.openGroupCallDisposable
let account = strongSelf.context.account
var signal: Signal<CachedChannelData.ActiveCall?, NoError> = strongSelf.context.account.postbox.transaction { transaction -> CachedChannelData.ActiveCall? in
return (transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData)?.activeCall
}
|> mapToSignal { activeCall -> Signal<CachedChannelData.ActiveCall?, NoError> in
if let activeCall = activeCall {
return .single(activeCall)
} else {
return updatedCurrentPeerGroupCall(account: account, 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.context.joinGroupCall(peerId: peerId, activeCall: activeCall)
}
}))
}) })
let viewProcessingQueue = self.viewProcessingQueue let viewProcessingQueue = self.viewProcessingQueue
@ -299,9 +362,56 @@ final class CallListControllerNode: ASDisplayNode {
return value return value
} }
let callListNodeViewTransition = combineLatest(callListViewUpdate, self.statePromise.get(), showCallsTab) let currentGroupCallPeerId: Signal<PeerId?, NoError>
|> mapToQueue { (update, state, showCallsTab) -> Signal<CallListNodeListViewTransition, NoError> in if let callManager = context.sharedContext.callManager {
let processedView = CallListNodeView(originalView: update.view, filteredEntries: callListNodeEntriesForView(update.view, state: state, showSettings: showSettings, showCallsTab: showCallsTab), presentationData: state.presentationData) currentGroupCallPeerId = callManager.currentGroupCallSignal
|> map { call -> PeerId? in
call?.peerId
}
|> distinctUntilChanged
} else {
currentGroupCallPeerId = .single(nil)
}
let groupCalls = context.account.postbox.tailChatListView(groupId: .root, count: 100, summaryComponents: ChatListEntrySummaryComponents())
|> map { view -> [Peer] in
var result: [Peer] = []
for entry in view.0.entries {
switch entry {
case let .MessageEntry(_, _, _, _, _, renderedPeer, _, _, _, _):
if let channel = renderedPeer.peer as? TelegramChannel, channel.flags.contains(.hasActiveVoiceChat) {
result.append(channel)
}
default:
break
}
}
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(isEqual: { lhs, rhs in
if lhs.count != rhs.count {
return false
}
for i in 0 ..< lhs.count {
if !lhs[i].isEqual(rhs[i]) {
return false
}
}
return true
})
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, groupCalls: groupCalls, state: state, showSettings: showSettings, showCallsTab: showCallsTab, isRecentCalls: type == .all, currentGroupCallPeerId: currentGroupCallPeerId), presentationData: state.presentationData)
let previous = previousView.swap(processedView) let previous = previousView.swap(processedView)
let reason: CallListNodeViewTransitionReason let reason: CallListNodeViewTransitionReason
@ -390,6 +500,7 @@ final class CallListControllerNode: ASDisplayNode {
deinit { deinit {
self.callListDisposable.dispose() self.callListDisposable.dispose()
self.emptyStateDisposable.dispose() self.emptyStateDisposable.dispose()
self.openGroupCallDisposable.dispose()
} }
func updateThemeAndStrings(presentationData: PresentationData) { func updateThemeAndStrings(presentationData: PresentationData) {

View File

@ -0,0 +1,484 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
import TelegramCore
import SyncCore
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 context: AccountContext
let style: ItemListStyle
let peer: Peer
let isActive: Bool
let editing: Bool
let interaction: CallListNodeInteraction
let selectable: Bool = true
let headerAccessoryItem: ListViewAccessoryItem?
let header: ListViewItemHeader?
init(presentationData: ItemListPresentationData, context: AccountContext, style: ItemListStyle, peer: Peer, isActive: Bool, editing: Bool, interaction: CallListNodeInteraction) {
self.presentationData = presentationData
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)
}
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: titleLayout.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, short: Bool) {
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 header() -> ListViewItemHeader? {
if let (item, _, _, _, _) = self.layoutParams {
return item.header
} else {
return nil
}
}
@objc private func joinPressed() {
if let item = self.layoutParams?.0 {
item.interaction.openGroupCall(item.peer.id)
}
}
}

View File

@ -27,7 +27,7 @@ enum CallListNodeEntry: Comparable, Identifiable {
enum SortIndex: Comparable { enum SortIndex: Comparable {
case displayTab case displayTab
case displayTabInfo case displayTabInfo
case groupCall(ChatListIndex) case groupCall(PeerId, String)
case message(MessageIndex) case message(MessageIndex)
case hole(MessageIndex) case hole(MessageIndex)
@ -42,12 +42,16 @@ enum CallListNodeEntry: Comparable, Identifiable {
default: default:
return false return false
} }
case let .groupCall(lhsIndex): case let .groupCall(lhsPeerId, lhsTitle):
switch rhs { switch rhs {
case .displayTab, .displayTabInfo: case .displayTab, .displayTabInfo:
return false return false
case let .groupCall(rhsIndex): case let .groupCall(rhsPeerId, rhsTitle):
return lhsIndex > rhsIndex if lhsTitle == rhsTitle {
return lhsPeerId < rhsPeerId
} else {
return lhsTitle < rhsTitle
}
case .message, .hole: case .message, .hole:
return true return true
} }
@ -68,8 +72,6 @@ enum CallListNodeEntry: Comparable, Identifiable {
return lhsIndex < rhsIndex return lhsIndex < rhsIndex
case let .message(rhsIndex): case let .message(rhsIndex):
return lhsIndex < rhsIndex return lhsIndex < rhsIndex
default:
return true
} }
} }
@ -78,8 +80,8 @@ enum CallListNodeEntry: Comparable, Identifiable {
case displayTab(PresentationTheme, String, Bool) case displayTab(PresentationTheme, String, Bool)
case displayTabInfo(PresentationTheme, String) case displayTabInfo(PresentationTheme, String)
case groupCall(index: ChatListIndex, peer: Peer) case groupCall(peer: Peer, editing: Bool, isActive: Bool)
case messageEntry(topMessage: Message, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, editing: Bool, hasActiveRevealControls: Bool) case messageEntry(topMessage: Message, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, editing: Bool, hasActiveRevealControls: Bool, displayHeader: Bool)
case holeEntry(index: MessageIndex, theme: PresentationTheme) case holeEntry(index: MessageIndex, theme: PresentationTheme)
var sortIndex: SortIndex { var sortIndex: SortIndex {
@ -88,9 +90,9 @@ enum CallListNodeEntry: Comparable, Identifiable {
return .displayTab return .displayTab
case .displayTabInfo: case .displayTabInfo:
return .displayTabInfo return .displayTabInfo
case let .groupCall(index, _): case let .groupCall(peer, _, _):
return .groupCall(index) return .groupCall(peer.id, peer.compactDisplayTitle)
case let .messageEntry(message, _, _, _, _, _, _): case let .messageEntry(message, _, _, _, _, _, _, _):
return .message(message.index) return .message(message.index)
case let .holeEntry(index, _): case let .holeEntry(index, _):
return .hole(index) return .hole(index)
@ -103,9 +105,9 @@ enum CallListNodeEntry: Comparable, Identifiable {
return .setting(0) return .setting(0)
case .displayTabInfo: case .displayTabInfo:
return .setting(1) return .setting(1)
case let .groupCall(_, peer): case let .groupCall(peer, _, _):
return .groupCall(peer.id) return .groupCall(peer.id)
case let .messageEntry(message, _, _, _, _, _, _): case let .messageEntry(message, _, _, _, _, _, _, _):
return .message(message.index) return .message(message.index)
case let .holeEntry(index, _): case let .holeEntry(index, _):
return .hole(index) return .hole(index)
@ -130,20 +132,23 @@ enum CallListNodeEntry: Comparable, Identifiable {
} else { } else {
return false return false
} }
case let .groupCall(lhsIndex, lhsPeer): case let .groupCall(lhsPeer, lhsEditing, lhsIsActive):
if case let .groupCall(rhsIndex, rhsPeer) = rhs { if case let .groupCall(rhsPeer, rhsEditing, rhsIsActive) = rhs {
if lhsIndex != rhsIndex { if !lhsPeer.isEqual(rhsPeer) {
return false return false
} }
if !lhsPeer.isEqual(rhsPeer) { if lhsEditing != rhsEditing {
return false
}
if lhsIsActive != rhsIsActive {
return false return false
} }
return true return true
} else { } else {
return false return false
} }
case let .messageEntry(lhsMessage, lhsMessages, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsEditing, lhsHasRevealControls): case let .messageEntry(lhsMessage, lhsMessages, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsEditing, lhsHasRevealControls, lhsDisplayHeader):
if case let .messageEntry(rhsMessage, rhsMessages, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsEditing, rhsHasRevealControls) = rhs { if case let .messageEntry(rhsMessage, rhsMessages, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsEditing, rhsHasRevealControls, rhsDisplayHeader) = rhs {
if lhsTheme !== rhsTheme { if lhsTheme !== rhsTheme {
return false return false
} }
@ -159,6 +164,9 @@ enum CallListNodeEntry: Comparable, Identifiable {
if lhsHasRevealControls != rhsHasRevealControls { if lhsHasRevealControls != rhsHasRevealControls {
return false return false
} }
if lhsDisplayHeader != rhsDisplayHeader {
return false
}
if !areMessagesEqual(lhsMessage, rhsMessage) { if !areMessagesEqual(lhsMessage, rhsMessage) {
return false return false
} }
@ -184,16 +192,30 @@ enum CallListNodeEntry: Comparable, Identifiable {
} }
} }
func callListNodeEntriesForView(_ view: CallListView, state: CallListNodeState, showSettings: Bool, showCallsTab: Bool) -> [CallListNodeEntry] { func callListNodeEntriesForView(view: CallListView, groupCalls: [Peer], state: CallListNodeState, showSettings: Bool, showCallsTab: Bool, isRecentCalls: Bool, currentGroupCallPeerId: PeerId?) -> [CallListNodeEntry] {
var result: [CallListNodeEntry] = [] var result: [CallListNodeEntry] = []
for entry in view.entries { for entry in view.entries {
switch entry { switch entry {
case let .message(topMessage, messages): 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)) 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))
case let .hole(index): case let .hole(index):
result.append(.holeEntry(index: index, theme: state.presentationData.theme)) result.append(.holeEntry(index: index, theme: state.presentationData.theme))
} }
} }
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 showSettings { if showSettings {
result.append(.displayTabInfo(state.presentationData.theme, state.presentationData.strings.CallSettings_TabIconDescription)) result.append(.displayTabInfo(state.presentationData.theme, state.presentationData.strings.CallSettings_TabIconDescription))
result.append(.displayTab(state.presentationData.theme, state.presentationData.strings.CallSettings_TabIcon, showCallsTab)) result.append(.displayTab(state.presentationData.theme, state.presentationData.strings.CallSettings_TabIcon, showCallsTab))

View File

@ -50,19 +50,19 @@ struct CallListNodeViewUpdate {
let scrollPosition: CallListNodeViewScrollPosition? let scrollPosition: CallListNodeViewScrollPosition?
} }
func callListViewForLocationAndType(locationAndType: CallListNodeLocationAndType, account: Account) -> Signal<CallListNodeViewUpdate, NoError> { func callListViewForLocationAndType(locationAndType: CallListNodeLocationAndType, account: Account) -> Signal<(CallListNodeViewUpdate, CallListViewType), NoError> {
switch locationAndType.location { switch locationAndType.location {
case let .initial(count): case let .initial(count):
return account.viewTracker.callListView(type: locationAndType.type, index: MessageIndex.absoluteUpperBound(), count: count) |> map { view -> CallListNodeViewUpdate in return account.viewTracker.callListView(type: locationAndType.type, index: MessageIndex.absoluteUpperBound(), count: count) |> map { view -> (CallListNodeViewUpdate, CallListViewType) in
return CallListNodeViewUpdate(view: view, type: .Generic, scrollPosition: nil) return (CallListNodeViewUpdate(view: view, type: .Generic, scrollPosition: nil), locationAndType.type)
} }
case let .changeType(index): case let .changeType(index):
return account.viewTracker.callListView(type: locationAndType.type, index: index, count: 120) |> map { view -> CallListNodeViewUpdate in return account.viewTracker.callListView(type: locationAndType.type, index: index, count: 120) |> map { view -> (CallListNodeViewUpdate, CallListViewType) in
return CallListNodeViewUpdate(view: view, type: .ReloadAnimated, scrollPosition: nil) return (CallListNodeViewUpdate(view: view, type: .ReloadAnimated, scrollPosition: nil), locationAndType.type)
} }
case let .navigation(index): case let .navigation(index):
var first = true var first = true
return account.viewTracker.callListView(type: locationAndType.type, index: index, count: 120) |> map { view -> CallListNodeViewUpdate in return account.viewTracker.callListView(type: locationAndType.type, index: index, count: 120) |> map { view -> (CallListNodeViewUpdate, CallListViewType) in
let genericType: CallListNodeViewUpdateType let genericType: CallListNodeViewUpdateType
if first { if first {
first = false first = false
@ -70,13 +70,13 @@ func callListViewForLocationAndType(locationAndType: CallListNodeLocationAndType
} else { } else {
genericType = .Generic genericType = .Generic
} }
return CallListNodeViewUpdate(view: view, type: genericType, scrollPosition: nil) return (CallListNodeViewUpdate(view: view, type: genericType, scrollPosition: nil), locationAndType.type)
} }
case let .scroll(index, sourceIndex, scrollPosition, animated): case let .scroll(index, sourceIndex, scrollPosition, animated):
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up
let callScrollPosition: CallListNodeViewScrollPosition = .index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated) let callScrollPosition: CallListNodeViewScrollPosition = .index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated)
var first = true var first = true
return account.viewTracker.callListView(type: locationAndType.type, index: index, count: 120) |> map { view -> CallListNodeViewUpdate in return account.viewTracker.callListView(type: locationAndType.type, index: index, count: 120) |> map { view -> (CallListNodeViewUpdate, CallListViewType) in
let genericType: CallListNodeViewUpdateType let genericType: CallListNodeViewUpdateType
let scrollPosition: CallListNodeViewScrollPosition? = first ? callScrollPosition : nil let scrollPosition: CallListNodeViewScrollPosition? = first ? callScrollPosition : nil
if first { if first {
@ -85,7 +85,7 @@ func callListViewForLocationAndType(locationAndType: CallListNodeLocationAndType
} else { } else {
genericType = .Generic genericType = .Generic
} }
return CallListNodeViewUpdate(view: view, type: genericType, scrollPosition: scrollPosition) return (CallListNodeViewUpdate(view: view, type: genericType, scrollPosition: scrollPosition), locationAndType.type)
} }
} }
} }

View File

@ -23,6 +23,8 @@ public enum ChatListSearchItemHeaderType {
case faq case faq
case messages case messages
case groupMembers case groupMembers
case activeVoiceChats
case recentCalls
fileprivate func title(strings: PresentationStrings) -> String { fileprivate func title(strings: PresentationStrings) -> String {
switch self { switch self {
@ -62,6 +64,10 @@ public enum ChatListSearchItemHeaderType {
return strings.DialogList_SearchSectionMessages return strings.DialogList_SearchSectionMessages
case .groupMembers: case .groupMembers:
return strings.Group_GroupMembersHeader return strings.Group_GroupMembersHeader
case .activeVoiceChats:
return strings.CallList_ActiveVoiceChatsHeader
case .recentCalls:
return strings.CallList_RecentCallsHeader
} }
} }
@ -103,6 +109,10 @@ public enum ChatListSearchItemHeaderType {
return .messages return .messages
case .groupMembers: case .groupMembers:
return .groupMembers return .groupMembers
case .activeVoiceChats:
return .activeVoiceChats
case .recentCalls:
return .recentCalls
} }
} }
} }
@ -130,6 +140,8 @@ private enum ChatListSearchItemHeaderId: Int32 {
case files case files
case music case music
case groupMembers case groupMembers
case activeVoiceChats
case recentCalls
} }
public final class ChatListSearchItemHeader: ListViewItemHeader { public final class ChatListSearchItemHeader: ListViewItemHeader {

View File

@ -3,14 +3,14 @@ import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
private final class VoiceChatIndicatorNode: ASDisplayNode { public final class VoiceChatIndicatorNode: ASDisplayNode {
private let leftLine: ASDisplayNode private let leftLine: ASDisplayNode
private let centerLine: ASDisplayNode private let centerLine: ASDisplayNode
private let rightLine: ASDisplayNode private let rightLine: ASDisplayNode
private var isCurrentlyInHierarchy = true private var isCurrentlyInHierarchy = true
var color: UIColor = UIColor(rgb: 0xffffff) { public var color: UIColor = UIColor(rgb: 0xffffff) {
didSet { didSet {
self.leftLine.backgroundColor = self.color self.leftLine.backgroundColor = self.color
self.centerLine.backgroundColor = self.color self.centerLine.backgroundColor = self.color
@ -18,7 +18,7 @@ private final class VoiceChatIndicatorNode: ASDisplayNode {
} }
} }
override init() { override public init() {
self.leftLine = ASDisplayNode() self.leftLine = ASDisplayNode()
self.leftLine.clipsToBounds = true self.leftLine.clipsToBounds = true
self.leftLine.isLayerBacked = true self.leftLine.isLayerBacked = true
@ -45,17 +45,19 @@ private final class VoiceChatIndicatorNode: ASDisplayNode {
self.addSubnode(self.centerLine) self.addSubnode(self.centerLine)
self.addSubnode(self.rightLine) self.addSubnode(self.rightLine)
self.updateAnimation() if Thread.isMainThread {
self.updateAnimation()
}
} }
override func didEnterHierarchy() { override public func didEnterHierarchy() {
super.didEnterHierarchy() super.didEnterHierarchy()
self.isCurrentlyInHierarchy = true self.isCurrentlyInHierarchy = true
self.updateAnimation() self.updateAnimation()
} }
override func didExitHierarchy() { override public func didExitHierarchy() {
super.didExitHierarchy() super.didExitHierarchy()
self.isCurrentlyInHierarchy = false self.isCurrentlyInHierarchy = false

View File

@ -427,6 +427,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
return self.inivitedPeersPromise.get() return self.inivitedPeersPromise.get()
} }
private let memberEventsPipe = ValuePipe<PresentationGroupCallMemberEvent>()
public var memberEvents: Signal<PresentationGroupCallMemberEvent, NoError> {
return self.memberEventsPipe.signal()
}
private let memberEventsPipeDisposable = MetaDisposable()
private let requestDisposable = MetaDisposable() private let requestDisposable = MetaDisposable()
private var groupCallParticipantUpdatesDisposable: Disposable? private var groupCallParticipantUpdatesDisposable: Disposable?
@ -732,6 +738,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.audioLevelsDisposable.dispose() self.audioLevelsDisposable.dispose()
self.participantsContextStateDisposable.dispose() self.participantsContextStateDisposable.dispose()
self.myAudioLevelDisposable.dispose() self.myAudioLevelDisposable.dispose()
self.memberEventsPipeDisposable.dispose()
self.myAudioLevelTimer?.invalidate() self.myAudioLevelTimer?.invalidate()
self.typingDisposable.dispose() self.typingDisposable.dispose()
@ -1069,6 +1076,22 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
} }
})) }))
let postbox = self.accountContext.account.postbox
self.memberEventsPipeDisposable.set((participantsContext.memberEvents
|> mapToSignal { event -> Signal<PresentationGroupCallMemberEvent, NoError> in
return postbox.transaction { transaction -> Signal<PresentationGroupCallMemberEvent, NoError> in
if let peer = transaction.getPeer(event.peerId) {
return .single(PresentationGroupCallMemberEvent(peer: peer, joined: event.joined))
} else {
return .complete()
}
}
|> switchToLatest
}
|> deliverOnMainQueue).start(next: { [weak self] event in
self?.memberEventsPipe.putNext(event)
}))
if let isCurrentlyConnecting = self.isCurrentlyConnecting, isCurrentlyConnecting { if let isCurrentlyConnecting = self.isCurrentlyConnecting, isCurrentlyConnecting {
self.startCheckingCallIfNeeded() self.startCheckingCallIfNeeded()
} }

View File

@ -451,6 +451,8 @@ public final class VoiceChatController: ViewController {
private let inviteDisposable = MetaDisposable() private let inviteDisposable = MetaDisposable()
private let memberEventsDisposable = MetaDisposable()
init(controller: VoiceChatController, sharedContext: SharedAccountContext, call: PresentationGroupCall) { init(controller: VoiceChatController, sharedContext: SharedAccountContext, call: PresentationGroupCall) {
self.controller = controller self.controller = controller
self.sharedContext = sharedContext self.sharedContext = sharedContext
@ -596,7 +598,7 @@ public final class VoiceChatController: ViewController {
dismissController?() dismissController?()
if strongSelf.call.invitePeer(participant.peer.id) { if strongSelf.call.invitePeer(participant.peer.id) {
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: participant.peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), elevatedLayout: false, action: { _ in return false }), in: .current) strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: participant.peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false })
} }
} else { } else {
strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: strongSelf.presentationData.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), groupPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: { strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: strongSelf.presentationData.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), groupPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: {
@ -669,7 +671,7 @@ public final class VoiceChatController: ViewController {
dismissController?() dismissController?()
if strongSelf.call.invitePeer(peer.id) { if strongSelf.call.invitePeer(peer.id) {
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), elevatedLayout: false, action: { _ in return false }), in: .current) strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false })
} }
})) }))
} else if let groupPeer = groupPeer as? TelegramGroup { } else if let groupPeer = groupPeer as? TelegramGroup {
@ -737,7 +739,7 @@ public final class VoiceChatController: ViewController {
dismissController?() dismissController?()
if strongSelf.call.invitePeer(peer.id) { if strongSelf.call.invitePeer(peer.id) {
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), elevatedLayout: false, action: { _ in return false }), in: .current) strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false })
} }
})) }))
} }
@ -771,7 +773,7 @@ public final class VoiceChatController: ViewController {
if let link = link { if let link = link {
UIPasteboard.general.string = link UIPasteboard.general.string = link
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .linkCopied(text: strongSelf.presentationData.strings.VoiceChat_InviteLinkCopiedText), elevatedLayout: false, action: { _ in return false }), in: .current) strongSelf.presentUndoOverlay(content: .linkCopied(text: strongSelf.presentationData.strings.VoiceChat_InviteLinkCopiedText), action: { _ in return false })
} }
}) })
} }
@ -856,7 +858,7 @@ public final class VoiceChatController: ViewController {
let _ = strongSelf.context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: strongSelf.context.account, peerId: strongSelf.call.peerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start() let _ = strongSelf.context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: strongSelf.context.account, peerId: strongSelf.call.peerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start()
strongSelf.call.removedPeer(peer.id) strongSelf.call.removedPeer(peer.id)
strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .banned(text: strongSelf.presentationData.strings.VoiceChat_RemovedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), elevatedLayout: false, action: { _ in return false }), in: .current) strongSelf.presentUndoOverlay(content: .banned(text: strongSelf.presentationData.strings.VoiceChat_RemovedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false })
})) }))
actionSheet.setItemGroups([ actionSheet.setItemGroups([
@ -1170,6 +1172,16 @@ public final class VoiceChatController: ViewController {
} }
} }
} }
self.memberEventsDisposable.set((self.call.memberEvents
|> deliverOnMainQueue).start(next: { [weak self] event in
guard let strongSelf = self else {
return
}
if event.joined {
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: event.peer, text: strongSelf.presentationData.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false })
}
}))
} }
deinit { deinit {
@ -1230,6 +1242,18 @@ public final class VoiceChatController: ViewController {
} }
} }
private func presentUndoOverlay(content: UndoOverlayContent, action: @escaping (UndoOverlayAction) -> Bool) {
var animateInAsReplacement = false
self.controller?.forEachController { c in
if let c = c as? UndoOverlayController {
animateInAsReplacement = true
c.dismiss()
}
return true
}
self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: action), in: .current)
}
private var pressTimer: SwiftSignalKit.Timer? private var pressTimer: SwiftSignalKit.Timer?
private func startPressTimer() { private func startPressTimer() {
self.pressTimer?.invalidate() self.pressTimer?.invalidate()

View File

@ -681,6 +681,16 @@ public final class GroupCallParticipantsContext {
case call(isTerminated: Bool, defaultParticipantsAreMuted: State.DefaultParticipantsAreMuted) case call(isTerminated: Bool, defaultParticipantsAreMuted: State.DefaultParticipantsAreMuted)
} }
public final class MemberEvent {
public let peerId: PeerId
public let joined: Bool
public init(peerId: PeerId, joined: Bool) {
self.peerId = peerId
self.joined = joined
}
}
private let account: Account private let account: Account
private let id: Int64 private let id: Int64
private let accessHash: Int64 private let accessHash: Int64
@ -724,6 +734,11 @@ public final class GroupCallParticipantsContext {
return self.activeSpeakersPromise.get() return self.activeSpeakersPromise.get()
} }
private let memberEventsPipe = ValuePipe<MemberEvent>()
public var memberEvents: Signal<MemberEvent, NoError> {
return self.memberEventsPipe.signal()
}
private var updateQueue: [Update.StateUpdate] = [] private var updateQueue: [Update.StateUpdate] = []
private var isProcessingUpdate: Bool = false private var isProcessingUpdate: Bool = false
private let disposable = MetaDisposable() private let disposable = MetaDisposable()
@ -964,6 +979,7 @@ public final class GroupCallParticipantsContext {
if let index = updatedParticipants.firstIndex(where: { $0.peer.id == participantUpdate.peerId }) { if let index = updatedParticipants.firstIndex(where: { $0.peer.id == participantUpdate.peerId }) {
updatedParticipants.remove(at: index) updatedParticipants.remove(at: index)
updatedTotalCount = max(0, updatedTotalCount - 1) updatedTotalCount = max(0, updatedTotalCount - 1)
strongSelf.memberEventsPipe.putNext(MemberEvent(peerId: participantUpdate.peerId, joined: false))
} else if isVersionUpdate { } else if isVersionUpdate {
updatedTotalCount = max(0, updatedTotalCount - 1) updatedTotalCount = max(0, updatedTotalCount - 1)
} }
@ -976,8 +992,9 @@ public final class GroupCallParticipantsContext {
if let index = updatedParticipants.firstIndex(where: { $0.peer.id == participantUpdate.peerId }) { if let index = updatedParticipants.firstIndex(where: { $0.peer.id == participantUpdate.peerId }) {
previousActivityTimestamp = updatedParticipants[index].activityTimestamp previousActivityTimestamp = updatedParticipants[index].activityTimestamp
updatedParticipants.remove(at: index) updatedParticipants.remove(at: index)
} else if case .left = participantUpdate.participationStatusChange { } else if case .joined = participantUpdate.participationStatusChange {
updatedTotalCount += 1 updatedTotalCount += 1
strongSelf.memberEventsPipe.putNext(MemberEvent(peerId: participantUpdate.peerId, joined: true))
} }
var activityTimestamp: Double? var activityTimestamp: Double?
@ -1253,3 +1270,12 @@ public func inviteToGroupCall(account: Account, callId: Int64, accessHash: Int64
} }
} }
} }
public func updatedCurrentPeerGroupCall(account: Account, peerId: PeerId) -> Signal<CachedChannelData.ActiveCall?, NoError> {
return fetchAndUpdateCachedPeerData(accountPeerId: account.peerId, peerId: peerId, network: account.network, postbox: account.postbox)
|> mapToSignal { _ -> Signal<CachedChannelData.ActiveCall?, NoError> in
return account.postbox.transaction { transaction -> CachedChannelData.ActiveCall? in
return (transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData)?.activeCall
}
}
}