Video Chats Improvements

This commit is contained in:
Ilya Laktyushin 2021-04-21 03:07:13 +03:00
parent d9f3dba292
commit 6cbe2f8f35
10 changed files with 840 additions and 260 deletions

View File

@ -289,10 +289,16 @@ public struct PresentationGroupCallMembers: Equatable {
public final class PresentationGroupCallMemberEvent {
public let peer: Peer
public let isContact: Bool
public let isInChatList: Bool
public let canUnmute: Bool
public let joined: Bool
public init(peer: Peer, joined: Bool) {
public init(peer: Peer, isContact: Bool, isInChatList: Bool, canUnmute: Bool, joined: Bool) {
self.peer = peer
self.isContact = isContact
self.isInChatList = isInChatList
self.canUnmute = canUnmute
self.joined = joined
}
}

View File

@ -168,6 +168,7 @@ public final class PinchSourceContainerNode: ASDisplayNode, UIGestureRecognizerD
public var scaleUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
public var animatedOut: (() -> Void)?
var deactivate: (() -> Void)?
public var deactivated: (() -> Void)?
var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
override public init() {
@ -196,6 +197,7 @@ public final class PinchSourceContainerNode: ASDisplayNode, UIGestureRecognizerD
strongSelf.isActive = false
strongSelf.deactivate?()
strongSelf.deactivated?()
}
self.gesture.updated = { [weak self] scale, pinchLocation, offset in

View File

@ -78,12 +78,33 @@ private class PeerInfoAvatarListLoadingStripNode: ASImageNode {
}
}
private struct CustomListItemResourceId: MediaResourceId {
public var uniqueId: String {
return "customNode"
}
public var hashValue: Int {
return 0
}
public func isEqual(to: MediaResourceId) -> Bool {
if to is CustomListItemResourceId {
return true
} else {
return false
}
}
}
public enum PeerInfoAvatarListItem: Equatable {
case custom(ASDisplayNode)
case topImage([ImageRepresentationWithReference], [VideoRepresentationWithReference], Data?)
case image(TelegramMediaImageReference?, [ImageRepresentationWithReference], [VideoRepresentationWithReference], Data?)
var id: WrappedMediaResourceId {
switch self {
case .custom:
return WrappedMediaResourceId(CustomListItemResourceId())
case let .topImage(representations, _, _):
let representation = largestImageRepresentation(representations.map { $0.representation }) ?? representations[representations.count - 1].representation
return WrappedMediaResourceId(representation.resource.id)
@ -95,6 +116,8 @@ public enum PeerInfoAvatarListItem: Equatable {
var videoRepresentations: [VideoRepresentationWithReference] {
switch self {
case .custom:
return []
case let .topImage(_, videoRepresentations, _):
return videoRepresentations
case let .image(_, _, videoRepresentations, _):
@ -330,6 +353,11 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode {
let immediateThumbnailData: Data?
var id: Int64
switch item {
case let .custom(node):
id = 0
representations = []
videoRepresentations = []
immediateThumbnailData = nil
case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail):
representations = topRepresentations
videoRepresentations = videoRepresentationsValue

View File

@ -22,7 +22,7 @@ public struct WrappedMediaResourceId: Hashable {
// }
public func hash(into hasher: inout Hasher) {
hasher.combine(id.hashValue)
hasher.combine(self.id.hashValue)
}
}

View File

@ -1905,7 +1905,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|> 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))
let isContact = transaction.isPeerContact(peerId: event.peerId)
let isInChatList = transaction.getPeerChatListIndex(event.peerId) != nil
return .single(PresentationGroupCallMemberEvent(peer: peer, isContact: isContact, isInChatList: isInChatList, canUnmute: event.canUnmute, joined: event.joined))
} else {
return .complete()
}
@ -1913,13 +1915,20 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|> switchToLatest
}
|> deliverOnMainQueue).start(next: { [weak self] event in
guard let strongSelf = self else {
guard let strongSelf = self, event.peer.id != strongSelf.stateValue.myPeerId else {
return
}
if event.peer.id == strongSelf.stateValue.myPeerId {
return
var skip = false
if let participantsCount = strongSelf.participantsContext?.immediateState?.totalCount, participantsCount >= 250 {
if event.peer.isVerified || event.isContact || event.isInChatList || (strongSelf.stateValue.defaultParticipantMuteState == .muted && event.canUnmute) {
skip = false
} else {
skip = true
}
}
if !skip {
strongSelf.memberEventsPipe.putNext(event)
}
strongSelf.memberEventsPipe.putNext(event)
}))
if let isCurrentlyConnecting = self.isCurrentlyConnecting, isCurrentlyConnecting {

View File

@ -77,6 +77,11 @@ final class GroupVideoNode: ASDisplayNode {
var tapped: (() -> Void)?
private let readyPromise = ValuePromise(false)
var ready: Signal<Bool, NoError> {
return self.readyPromise.get()
}
init(videoView: PresentationCallVideoView) {
self.videoViewContainer = UIView()
self.videoView = videoView
@ -95,6 +100,7 @@ final class GroupVideoNode: ASDisplayNode {
guard let strongSelf = self else {
return
}
strongSelf.readyPromise.set(true)
if let (size, isLandscape) = strongSelf.validLayout {
strongSelf.updateLayout(size: size, isLandscape: isLandscape, transition: .immediate)
}
@ -170,8 +176,13 @@ final class GroupVideoNode: ASDisplayNode {
rotatedVideoFrame.size.width = ceil(rotatedVideoFrame.size.width)
rotatedVideoFrame.size.height = ceil(rotatedVideoFrame.size.height)
var videoSize = CGSize(width: 1203, height: 677)
transition.updatePosition(layer: self.videoView.view.layer, position: rotatedVideoFrame.center)
transition.updateBounds(layer: self.videoView.view.layer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size))
transition.updateBounds(layer: self.videoView.view.layer, bounds: CGRect(origin: CGPoint(), size: videoSize))
let scale = rotatedVideoFrame.width / videoSize.width
transition.updateTransformScale(layer: self.videoView.view.layer, scale: scale)
let transition: ContainedViewLayoutTransition = .immediate
transition.updateTransformRotation(view: self.videoView.view, angle: angle)
@ -187,6 +198,7 @@ private final class MainVideoContainerNode: ASDisplayNode {
private let topCornersNode: ASImageNode
private let bottomCornersNode: ASImageNode
private let bottomEdgeNode: ASDisplayNode
private let fadeNode: ASImageNode
private var currentPeer: (PeerId, UInt32)?
private var validLayout: (CGSize, CGFloat, Bool)?
@ -208,12 +220,27 @@ private final class MainVideoContainerNode: ASDisplayNode {
self.bottomEdgeNode = ASDisplayNode()
self.bottomEdgeNode.backgroundColor = UIColor(rgb: 0x000000)
self.fadeNode = ASImageNode()
self.fadeNode.displaysAsynchronously = false
self.fadeNode.displayWithoutProcessing = true
self.fadeNode.contentMode = .scaleToFill
self.fadeNode.image = generateImage(CGSize(width: 1.0, height: 50.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let colorsArray = [UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 0.7).cgColor] as CFArray
var locations: [CGFloat] = [0.0, 1.0]
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
super.init()
self.clipsToBounds = true
self.backgroundColor = UIColor(rgb: 0x1c1c1e)
self.addSubnode(self.topCornersNode)
self.addSubnode(self.fadeNode)
self.addSubnode(self.bottomCornersNode)
self.addSubnode(self.bottomEdgeNode)
}
@ -295,6 +322,12 @@ private final class MainVideoContainerNode: ASDisplayNode {
transition.updateFrame(node: self.topCornersNode, frame: CGRect(x: sideInset, y: 0.0, width: size.width - sideInset * 2.0, height: 50.0))
transition.updateFrame(node: self.bottomCornersNode, frame: CGRect(x: sideInset, y: size.height - 6.0 - 50.0, width: size.width - sideInset * 2.0, height: 50.0))
transition.updateFrame(node: self.bottomEdgeNode, frame: CGRect(x: sideInset, y: size.height - 6.0, width: size.width - sideInset * 2.0, height: 6.0))
var fadeHeight: CGFloat = 50.0
if size.width < size.height {
fadeHeight = 140.0
}
transition.updateFrame(node: self.fadeNode, frame: CGRect(x: sideInset, y: size.height - 6.0 - fadeHeight, width: size.width - sideInset * 2.0, height: fadeHeight))
}
}
@ -318,7 +351,7 @@ public final class VoiceChatController: ViewController {
private final class Interaction {
let updateIsMuted: (PeerId, Bool) -> Void
let openPeer: (PeerId) -> Void
let pinPeer: (PeerId, UInt32?) -> Void
let openInvite: () -> Void
let peerContextAction: (PeerEntry, ASDisplayNode, ContextGesture?) -> Void
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
@ -331,14 +364,14 @@ public final class VoiceChatController: ViewController {
init(
updateIsMuted: @escaping (PeerId, Bool) -> Void,
openPeer: @escaping (PeerId) -> Void,
pinPeer: @escaping (PeerId, UInt32?) -> Void,
openInvite: @escaping () -> Void,
peerContextAction: @escaping (PeerEntry, ASDisplayNode, ContextGesture?) -> Void,
setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void,
getPeerVideo: @escaping (UInt32, Bool) -> GroupVideoNode?
) {
self.updateIsMuted = updateIsMuted
self.openPeer = openPeer
self.pinPeer = pinPeer
self.openInvite = openInvite
self.peerContextAction = peerContextAction
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
@ -403,6 +436,7 @@ public final class VoiceChatController: ViewController {
var volume: Int32?
var raisedHand: Bool
var displayRaisedHandStatus: Bool
var pinned: Bool
var stableId: PeerId {
return self.peer.id
@ -448,6 +482,9 @@ public final class VoiceChatController: ViewController {
if lhs.displayRaisedHandStatus != rhs.displayRaisedHandStatus {
return false
}
if lhs.pinned != rhs.pinned {
return false
}
return true
}
@ -532,7 +569,7 @@ public final class VoiceChatController: ViewController {
}
}
func item(context: AccountContext, presentationData: PresentationData, interaction: Interaction, style: VoiceChatParticipantItem.LayoutStyle) -> ListViewItem {
func item(context: AccountContext, presentationData: PresentationData, interaction: Interaction, style: VoiceChatParticipantItem.LayoutStyle, transparent: Bool) -> ListViewItem {
switch self {
case let .invite(_, _, text, isLink):
return VoiceChatActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .generic(UIImage(bundleImageName: isLink ? "Chat/Context Menu/Link" : "Chat/Context Menu/AddUser")!), action: {
@ -611,17 +648,23 @@ public final class VoiceChatController: ViewController {
let revealOptions: [VoiceChatParticipantItem.RevealOption] = []
return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, ssrc: peerEntry.ssrc, presence: peerEntry.presence, text: text, expandedText: expandedText, icon: icon, style: style, enabled: true, transparent: false, selectable: true, getAudioLevel: { return interaction.getAudioLevel(peer.id) }, getVideo: {
return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, ssrc: peerEntry.ssrc, presence: peerEntry.presence, text: text, expandedText: expandedText, icon: icon, style: style, enabled: true, transparent: transparent, pinned: peerEntry.pinned, selectable: true, getAudioLevel: { return interaction.getAudioLevel(peer.id) }, getVideo: {
if let ssrc = peerEntry.ssrc {
return interaction.getPeerVideo(ssrc, style == .tile)
return interaction.getPeerVideo(ssrc, style != .list)
} else {
return nil
}
}, revealOptions: revealOptions, revealed: peerEntry.revealed, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
interaction.setPeerIdWithRevealedOptions(peerId, fromPeerId)
}, action: { node in
interaction.peerContextAction(peerEntry, node, nil)
}, contextAction: nil, getIsExpanded: {
if case .list = style {
interaction.peerContextAction(peerEntry, node, nil)
} else {
interaction.pinPeer(peer.id, peerEntry.ssrc)
}
}, contextAction: style != .list ? { node, gesture in
interaction.peerContextAction(peerEntry, node, gesture)
} : nil, getIsExpanded: {
return interaction.isExpanded
}, getUpdatingAvatar: {
return interaction.updateAvatarPromise.get()
@ -634,8 +677,8 @@ public final class VoiceChatController: ViewController {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction, style: style), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction, style: style), directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction, style: style, transparent: false), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction, style: style, transparent: false), directionHint: nil) }
return ListTransition(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, canInvite: canInvite, crossFade: crossFade, count: toEntries.count, animated: animated)
}
@ -655,6 +698,7 @@ public final class VoiceChatController: ViewController {
private let backgroundNode: ASDisplayNode
private let mainVideoClippingNode: ASDisplayNode
private var mainVideoContainerNode: MainVideoContainerNode?
private var mainParticipantNode: VoiceChatParticipantItemNode
private let listNode: ListView
private let horizontalListNode: ListView
private let topPanelNode: ASDisplayNode
@ -664,6 +708,7 @@ public final class VoiceChatController: ViewController {
private var optionsButtonIsAvatar = false
private let closeButton: VoiceChatHeaderButton
private let topCornersNode: ASImageNode
private let bottomPanelCoverNode: ASDisplayNode
fileprivate let bottomPanelNode: ASDisplayNode
private let bottomPanelBackgroundNode: ASDisplayNode
private let bottomCornersNode: ASImageNode
@ -674,6 +719,7 @@ public final class VoiceChatController: ViewController {
fileprivate let actionButton: VoiceChatActionButton
private let leftBorderNode: ASDisplayNode
private let rightBorderNode: ASDisplayNode
private let transitionContainerNode: ASDisplayNode
private var isScheduling = false
private let timerNode: VoiceChatTimerNode
@ -697,6 +743,11 @@ public final class VoiceChatController: ViewController {
private var isFirstTime = true
private var topInset: CGFloat?
private var animatingInsertion = false
private var animatingExpansion = false
private var animatingAppearance = false
private var panGestureArguments: (topInset: CGFloat, offset: CGFloat)?
private var peer: Peer?
private var currentTitle: String = ""
private var currentTitleIsCustom = false
@ -762,7 +813,10 @@ public final class VoiceChatController: ViewController {
private var requestedVideoSources = Set<UInt32>()
private var videoNodes: [(PeerId, UInt32, GroupVideoNode)] = []
private var currentDominantSpeakerWithVideo: (PeerId, UInt32)?
private var currentForcedSpeakerWithVideo: (PeerId, UInt32)?
private var effectiveSpeakerWithVideo: (PeerId, UInt32)?
private var updateAvatarDisposable = MetaDisposable()
private let updateAvatarPromise = Promise<(TelegramMediaImageRepresentation, Float)?>(nil)
@ -809,6 +863,8 @@ public final class VoiceChatController: ViewController {
self.mainVideoContainerNode = MainVideoContainerNode(context: call.accountContext, call: call)
}
self.mainParticipantNode = VoiceChatParticipantItemNode()
self.listNode = ListView()
self.listNode.alpha = self.isScheduling ? 0.0 : 1.0
self.listNode.isUserInteractionEnabled = !self.isScheduling
@ -854,6 +910,9 @@ public final class VoiceChatController: ViewController {
self.topCornersNode.displayWithoutProcessing = true
self.topCornersNode.image = cornersImage(top: true, bottom: false, dark: false)
self.bottomPanelCoverNode = ASDisplayNode()
self.bottomPanelCoverNode.backgroundColor = fullscreenBackgroundColor
self.bottomPanelNode = ASDisplayNode()
self.bottomPanelNode.clipsToBounds = false
@ -890,6 +949,10 @@ public final class VoiceChatController: ViewController {
self.rightBorderNode.isUserInteractionEnabled = false
self.rightBorderNode.clipsToBounds = false
self.transitionContainerNode = ASDisplayNode()
self.transitionContainerNode.clipsToBounds = true
self.transitionContainerNode.isUserInteractionEnabled = false
self.scheduleTextNode = ImmediateTextNode()
self.scheduleTextNode.isHidden = !self.isScheduling
self.scheduleTextNode.isUserInteractionEnabled = false
@ -945,27 +1008,33 @@ public final class VoiceChatController: ViewController {
self.itemInteraction = Interaction(updateIsMuted: { [weak self] peerId, isMuted in
let _ = self?.call.updateMuteState(peerId: peerId, isMuted: isMuted)
}, openPeer: { [weak self] peerId in
}, pinPeer: { [weak self] peerId, source in
if let strongSelf = self {
for entry in strongSelf.currentEntries {
switch entry {
case let .peer(peer):
if peer.peer.id == peerId {
if let source = peer.ssrc {
if strongSelf.currentDominantSpeakerWithVideo?.0 != peerId || strongSelf.currentDominantSpeakerWithVideo?.1 != source {
strongSelf.currentDominantSpeakerWithVideo = (peerId, source)
strongSelf.call.setFullSizeVideo(peerId: peerId)
strongSelf.mainVideoContainerNode?.updatePeer(peer: (peerId: peerId, source: source), waitForFullSize: false)
} else {
strongSelf.currentDominantSpeakerWithVideo = nil
strongSelf.call.setFullSizeVideo(peerId: nil)
strongSelf.mainVideoContainerNode?.updatePeer(peer: nil, waitForFullSize: false)
}
}
}
default:
break
if peerId != strongSelf.currentForcedSpeakerWithVideo?.0, let source = source {
strongSelf.currentForcedSpeakerWithVideo = (peerId, source)
} else {
strongSelf.currentForcedSpeakerWithVideo = nil
}
strongSelf.updatePinnedParticipant()
var updateLayout = false
if strongSelf.effectiveSpeakerWithVideo != nil && !strongSelf.isExpanded {
strongSelf.isExpanded = true
updateLayout = true
} else if strongSelf.effectiveSpeakerWithVideo == nil && strongSelf.isExpanded {
strongSelf.isExpanded = false
updateLayout = true
}
if updateLayout {
strongSelf.updateIsFullscreen(strongSelf.isExpanded)
strongSelf.animatingExpansion = true
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
}
strongSelf.updateFloatingHeaderOffset(offset: strongSelf.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
strongSelf.animatingExpansion = false
})
}
}
}, openInvite: { [weak self] in
@ -1278,17 +1347,19 @@ public final class VoiceChatController: ViewController {
}), true))
}
if strongSelf.context.sharedContext.immediateExperimentalUISettings.demoVideoChats {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_PinVideo, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pin"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
guard let strongSelf = self else {
return
}
strongSelf.itemInteraction?.openPeer(peer.id)
f(.default)
})))
for (peerId, _, _) in strongSelf.videoNodes {
if peerId == peer.id {
items.append(.action(ContextMenuActionItem(text: strongSelf.currentForcedSpeakerWithVideo?.0 == peer.id ? strongSelf.presentationData.strings.VoiceChat_UnpinVideo : strongSelf.presentationData.strings.VoiceChat_PinVideo, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pin"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
guard let strongSelf = self else {
return
}
strongSelf.itemInteraction?.pinPeer(peer.id, entry.ssrc)
f(.default)
})))
break
}
}
if peer.id == strongSelf.callState?.myPeerId {
@ -1588,16 +1659,18 @@ public final class VoiceChatController: ViewController {
if let mainVideoContainer = self.mainVideoContainerNode {
self.contentContainer.addSubnode(self.mainVideoClippingNode)
self.mainVideoClippingNode.addSubnode(mainVideoContainer)
self.mainVideoClippingNode.addSubnode(self.mainParticipantNode)
}
self.contentContainer.addSubnode(self.listNode)
self.contentContainer.addSubnode(self.topPanelNode)
self.contentContainer.addSubnode(self.leftBorderNode)
self.contentContainer.addSubnode(self.rightBorderNode)
self.contentContainer.addSubnode(self.bottomPanelCoverNode)
self.contentContainer.addSubnode(self.bottomPanelNode)
self.contentContainer.addSubnode(self.timerNode)
self.contentContainer.addSubnode(self.scheduleTextNode)
self.contentContainer.addSubnode(self.horizontalListNode)
self.addSubnode(self.transitionContainerNode)
let invitedPeers: Signal<[Peer], NoError> = self.call.invitedPeers
|> mapToSignal { ids -> Signal<[Peer], NoError> in
@ -1814,6 +1887,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 })
}
}))
self.reconnectedAsEventsDisposable.set((self.call.reconnectedAsEvents
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self else {
@ -1840,7 +1923,7 @@ public final class VoiceChatController: ViewController {
}
let videoNode = GroupVideoNode(videoView: videoView)
strongSelf.videoNodes.append((peerId, source, videoNode))
//strongSelf.addSubnode(videoNode)
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate)
@ -1850,8 +1933,8 @@ public final class VoiceChatController: ViewController {
case let .peer(peerEntry):
if peerEntry.ssrc == source {
let presentationData = strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme)
strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.item(context: strongSelf.context, presentationData: presentationData, interaction: strongSelf.itemInteraction!, style: .list), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil)
strongSelf.horizontalListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.item(context: strongSelf.context, presentationData: presentationData, interaction: strongSelf.itemInteraction!, style: .tile), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil)
strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.item(context: strongSelf.context, presentationData: presentationData, interaction: strongSelf.itemInteraction!, style: .list, transparent: false), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil)
strongSelf.horizontalListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.item(context: strongSelf.context, presentationData: presentationData, interaction: strongSelf.itemInteraction!, style: .tile(isLandscape: strongSelf.isLandscape), transparent: false), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil)
break loop
}
default:
@ -1876,25 +1959,27 @@ public final class VoiceChatController: ViewController {
case let .peer(peerEntry):
if peerEntry.ssrc == ssrc {
let presentationData = strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme)
strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.item(context: strongSelf.context, presentationData: presentationData, interaction: strongSelf.itemInteraction!, style: .list), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil)
strongSelf.horizontalListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.item(context: strongSelf.context, presentationData: presentationData, interaction: strongSelf.itemInteraction!, style: .tile), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil)
strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.item(context: strongSelf.context, presentationData: presentationData, interaction: strongSelf.itemInteraction!, style: .list, transparent: false), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil)
strongSelf.horizontalListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.item(context: strongSelf.context, presentationData: presentationData, interaction: strongSelf.itemInteraction!, style: .tile(isLandscape: strongSelf.isLandscape), transparent: false), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil)
break loop
}
default:
break
}
}
//strongSelf.videoNodes[i].2.removeFromSupernode()
updated = true
}
}
if let (_, source) = strongSelf.currentDominantSpeakerWithVideo {
if let (peerId, source) = strongSelf.effectiveSpeakerWithVideo {
if !validSources.contains(source) {
strongSelf.currentDominantSpeakerWithVideo = nil
strongSelf.call.setFullSizeVideo(peerId: nil)
strongSelf.mainVideoContainerNode?.updatePeer(peer: nil, waitForFullSize: false)
if peerId == strongSelf.currentForcedSpeakerWithVideo?.0 {
strongSelf.currentForcedSpeakerWithVideo = nil
}
if peerId == strongSelf.currentDominantSpeakerWithVideo?.0 {
strongSelf.currentDominantSpeakerWithVideo = nil
}
strongSelf.updatePinnedParticipant()
}
}
@ -1943,10 +2028,7 @@ public final class VoiceChatController: ViewController {
case .default:
strongSelf.displayMode = .fullscreen(controlsHidden: false)
case let .fullscreen(controlsHidden):
if true {
strongSelf.displayMode = .fullscreen(controlsHidden: !controlsHidden)
}
else if controlsHidden && !isLandscape {
if controlsHidden && !isLandscape {
strongSelf.displayMode = .default
} else {
strongSelf.displayMode = .fullscreen(controlsHidden: true)
@ -1956,35 +2038,110 @@ public final class VoiceChatController: ViewController {
if case .default = effectiveDisplayMode, case .fullscreen = strongSelf.displayMode {
strongSelf.horizontalListNode.isHidden = false
var minimalVisiblePeerid: (PeerId, CGFloat)?
var verticalItemNodes: [PeerId: VoiceChatParticipantItemNode] = [:]
strongSelf.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatParticipantItemNode, let item = itemNode.item {
let convertedFrame = itemNode.view.convert(itemNode.bounds, to: strongSelf.transitionContainerNode.view)
if let (_, y) = minimalVisiblePeerid {
if convertedFrame.minY >= 0.0 && convertedFrame.minY < y {
minimalVisiblePeerid = (item.peer.id, convertedFrame.minY)
}
} else {
if convertedFrame.minY >= 0.0 {
minimalVisiblePeerid = (item.peer.id, convertedFrame.minY)
}
}
verticalItemNodes[item.peer.id] = itemNode
}
}
strongSelf.horizontalListNode.forEachVisibleItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatParticipantItemNode, let item = itemNode.item, let otherItemNode = verticalItemNodes[item.peer.id] {
itemNode.transitionIn(from: otherItemNode, containerNode: strongSelf)
strongSelf.animatingExpansion = true
let completion = {
strongSelf.horizontalListNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatParticipantItemNode, let item = itemNode.item, let otherItemNode = verticalItemNodes[item.peer.id] {
itemNode.transitionIn(from: otherItemNode, containerNode: strongSelf)
}
}
strongSelf.updateIsFullscreen(strongSelf.isFullscreen, force: true)
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
strongSelf.updateFloatingHeaderOffset(offset: strongSelf.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut))
}
}
if false, let (peerId, _) = minimalVisiblePeerid {
var index = 0
for item in strongSelf.currentEntries {
if case let .peer(entry) = item, entry.peer.id == peerId {
break
} else {
index += 1
}
}
strongSelf.horizontalListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in
completion()
})
} else {
completion()
}
} else if case .fullscreen = effectiveDisplayMode, case .default = strongSelf.displayMode {
var minimalVisiblePeerid: (PeerId, CGFloat)?
var horizontalItemNodes: [PeerId: VoiceChatParticipantItemNode] = [:]
strongSelf.horizontalListNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatParticipantItemNode, let item = itemNode.item {
let convertedFrame = itemNode.view.convert(itemNode.bounds, to: strongSelf.transitionContainerNode.view)
if let (_, x) = minimalVisiblePeerid {
if convertedFrame.minX >= 0.0 && convertedFrame.minX < x {
minimalVisiblePeerid = (item.peer.id, convertedFrame.minX)
}
} else if convertedFrame.minX >= 0.0 {
minimalVisiblePeerid = (item.peer.id, convertedFrame.minX)
}
horizontalItemNodes[item.peer.id] = itemNode
}
}
strongSelf.listNode.forEachVisibleItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatParticipantItemNode, let item = itemNode.item, let otherItemNode = horizontalItemNodes[item.peer.id] {
itemNode.transitionIn(from: otherItemNode, containerNode: strongSelf)
strongSelf.animatingExpansion = true
let completion = {
strongSelf.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatParticipantItemNode, let item = itemNode.item, let otherItemNode = horizontalItemNodes[item.peer.id] {
itemNode.transitionIn(from: otherItemNode, containerNode: strongSelf.transitionContainerNode)
}
}
strongSelf.updateIsFullscreen(strongSelf.isFullscreen, force: true)
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
strongSelf.updateFloatingHeaderOffset(offset: strongSelf.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut))
}
}
}
if false, let (peerId, _) = minimalVisiblePeerid {
var index = 0
for item in strongSelf.currentEntries {
if case let .peer(entry) = item, entry.peer.id == peerId {
break
} else {
index += 1
}
}
strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in
completion()
})
} else {
completion()
}
} else if case .fullscreen = strongSelf.displayMode {
strongSelf.updateIsFullscreen(strongSelf.isFullscreen, force: true)
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
strongSelf.updateFloatingHeaderOffset(offset: strongSelf.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut))
}
}
}
}
@ -2993,6 +3150,14 @@ public final class VoiceChatController: ViewController {
self.call.switchVideoCamera()
}
private var isLandscape: Bool {
if let (layout, _) = self.validLayout, layout.size.width > layout.size.height, case .compact = layout.metrics.widthClass {
return true
} else {
return false
}
}
private var effectiveBottomAreaHeight: CGFloat {
switch self.displayMode {
case .default:
@ -3073,12 +3238,14 @@ public final class VoiceChatController: ViewController {
let videoClippingFrame: CGRect
let videoContainerFrame: CGRect
let videoInset: CGFloat
let videoHeight: CGFloat
var isFullscreen = false
if isLandscape {
videoInset = 0.0
videoClippingFrame = CGRect(x: layout.safeInsets.left, y: 0.0, width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - fullscreenBottomAreaHeight, height: layout.size.height + 6.0)
videoContainerFrame = CGRect(origin: CGPoint(), size: videoClippingFrame.size)
videoHeight = videoClippingFrame.height
} else {
let videoHeight: CGFloat
let videoY: CGFloat
switch effectiveDisplayMode {
case .default:
@ -3089,16 +3256,34 @@ public final class VoiceChatController: ViewController {
videoInset = 0.0
videoHeight = layout.size.height - (layout.statusBarHeight ?? 0.0) - layout.intrinsicInsets.bottom - fullscreenBottomAreaHeight - 6.0
videoY = layout.statusBarHeight ?? 20.0
isFullscreen = true
}
videoClippingFrame = CGRect(origin: CGPoint(x: videoInset, y: videoY), size: CGSize(width: layout.size.width - videoInset * 2.0, height: self.isFullscreen ? videoHeight : 0.0))
videoContainerFrame = CGRect(origin: CGPoint(x: -videoInset, y: 0.0), size: CGSize(width: layout.size.width, height: videoHeight))
}
let topEdgeY = topPanelFrame.maxY + min(mainVideoHeight, layout.size.width)
let bottomEdgeY = isFullscreen ? layout.size.height : layout.size.height - bottomAreaHeight - layout.intrinsicInsets.bottom
transition.updateFrame(node: self.transitionContainerNode, frame: CGRect(x: sideInset, y: topEdgeY, width: layout.size.width - sideInset * 2.0, height: max(0.0, bottomEdgeY - topEdgeY)))
let offset: CGFloat
if case let .fullscreen(controlsHidden) = effectiveDisplayMode, !isLandscape {
offset = controlsHidden ? 66.0 : 140.0
} else {
offset = 56.0 + 6.0
}
transition.updateFrame(node: self.mainParticipantNode, frame: CGRect(x: 0.0, y: videoClippingFrame.height - offset, width: videoClippingFrame.width, height: 56.0))
transition.updateFrame(node: self.mainVideoClippingNode, frame: videoClippingFrame)
transition.updateFrame(node: mainVideoContainer, frame: videoContainerFrame, completion: { [weak self] _ in
if let strongSelf = self, strongSelf.bringVideoToBackOnCompletion {
strongSelf.bringVideoToBackOnCompletion = false
strongSelf.contentContainer.insertSubnode(strongSelf.mainVideoClippingNode, belowSubnode: strongSelf.horizontalListNode)
if let strongSelf = self {
strongSelf.animatingExpansion = false
if strongSelf.bringVideoToBackOnCompletion {
strongSelf.horizontalListNode.isHidden = true
strongSelf.bringVideoToBackOnCompletion = false
strongSelf.contentContainer.insertSubnode(strongSelf.mainVideoClippingNode, belowSubnode: strongSelf.horizontalListNode)
}
}
})
mainVideoContainer.update(size: videoContainerFrame.size, sideInset: videoInset, isLandscape: isLandscape, transition: transition)
@ -3145,8 +3330,9 @@ public final class VoiceChatController: ViewController {
let listMaxY = listTopInset + listSize.height
let bottomOffset: CGFloat = min(0.0, bottomEdge - listMaxY)
let bottomDelta = self.effectiveBottomAreaHeight - bottomAreaHeight
let bottomCornersFrame = CGRect(origin: CGPoint(x: sideInset, y: -50.0 + bottomOffset), size: CGSize(width: size.width - sideInset * 2.0, height: 50.0))
let bottomCornersFrame = CGRect(origin: CGPoint(x: sideInset, y: -50.0 + bottomOffset + bottomDelta), size: CGSize(width: size.width - sideInset * 2.0, height: 50.0))
let previousBottomCornersFrame = self.bottomCornersNode.frame
if !bottomCornersFrame.equalTo(previousBottomCornersFrame) {
self.bottomCornersNode.frame = bottomCornersFrame
@ -3185,9 +3371,21 @@ public final class VoiceChatController: ViewController {
topEdgeFrame = CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight)
}
var isScheduled = false
if self.isScheduling || self.callState?.scheduleTimestamp != nil {
isScheduled = true
var effectiveDisplayMode = self.displayMode
if case .compact = layout.metrics.widthClass, layout.size.width > layout.size.height {
if case .fullscreen = effectiveDisplayMode {
} else {
effectiveDisplayMode = .fullscreen(controlsHidden: false)
}
}
let backgroundColor: UIColor
if case .fullscreen = effectiveDisplayMode {
backgroundColor = fullscreenBackgroundColor
} else if self.isScheduling || self.callState?.scheduleTimestamp != nil {
backgroundColor = panelBackgroundColor
} else {
backgroundColor = isFullscreen ? panelBackgroundColor : secondaryPanelBackgroundColor
}
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .linear)
@ -3195,7 +3393,7 @@ public final class VoiceChatController: ViewController {
transition.updateCornerRadius(node: self.topPanelEdgeNode, cornerRadius: isFullscreen ? layout.deviceMetrics.screenCornerRadius - 0.5 : 12.0)
transition.updateBackgroundColor(node: self.topPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
transition.updateBackgroundColor(node: self.topPanelEdgeNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
transition.updateBackgroundColor(node: self.backgroundNode, color: isFullscreen || isScheduled ? panelBackgroundColor : secondaryPanelBackgroundColor)
transition.updateBackgroundColor(node: self.backgroundNode, color: backgroundColor)
transition.updateBackgroundColor(node: self.bottomPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
transition.updateBackgroundColor(node: self.leftBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
transition.updateBackgroundColor(node: self.rightBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
@ -3481,12 +3679,21 @@ public final class VoiceChatController: ViewController {
self.horizontalListNode.bounds = CGRect(x: 0.0, y: 0.0, width: horizontalListHeight, height: layout.size.width - layout.safeInsets.left - layout.safeInsets.right)
let horizontalListY = isLandscape ? layout.size.height - layout.intrinsicInsets.bottom - 42.0 : layout.size.height - min(bottomPanelHeight, fullscreenBottomAreaHeight + layout.intrinsicInsets.bottom) - 42.0
transition.updatePosition(node: self.horizontalListNode, position: CGPoint(x: layout.safeInsets.left + layout.size.width / 2.0, y: horizontalListY))
if isLandscape {
transition.updatePosition(node: self.horizontalListNode, position: CGPoint(x: layout.safeInsets.left + layout.size.width / 2.0, y: horizontalListY))
self.horizontalListNode.transform = CATransform3DMakeRotation(0.0, 0.0, 0.0, 1.0)
} else {
transition.updatePosition(node: self.horizontalListNode, position: CGPoint(x: layout.safeInsets.left + layout.size.width / 2.0, y: horizontalListY))
self.horizontalListNode.transform = CATransform3DMakeRotation(-CGFloat(CGFloat.pi / 2.0), 0.0, 0.0, 1.0)
}
self.horizontalListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: horizontalListHeight, height: layout.size.width), insets: UIEdgeInsets(top: 16.0, left: 0.0, bottom: 16.0, right: 0.0), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
transition.updateFrame(node: self.topCornersNode, frame: CGRect(origin: CGPoint(x: sideInset, y: topCornersY), size: CGSize(width: size.width - sideInset * 2.0, height: 50.0)))
var bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelHeight), size: CGSize(width: size.width, height: bottomPanelHeight))
let bottomPanelCoverHeight = bottomAreaHeight + layout.intrinsicInsets.bottom
let bottomPanelCoverFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelCoverHeight), size: CGSize(width: size.width, height: bottomPanelCoverHeight))
if isLandscape {
transition.updateAlpha(node: self.closeButton, alpha: 0.0)
transition.updateAlpha(node: self.optionsButton, alpha: 0.0)
@ -3497,6 +3704,7 @@ public final class VoiceChatController: ViewController {
transition.updateAlpha(node: self.optionsButton, alpha: self.optionsButton.isUserInteractionEnabled ? 1.0 : 0.0)
transition.updateAlpha(node: self.titleNode, alpha: 1.0)
}
transition.updateFrame(node: self.bottomPanelCoverNode, frame: bottomPanelCoverFrame)
transition.updateFrame(node: self.bottomPanelNode, frame: bottomPanelFrame)
if let pickerView = self.pickerView {
@ -3663,23 +3871,6 @@ public final class VoiceChatController: ViewController {
self.updateButtons(animated: !isFirstTime)
/*var currentVideoOrigin = CGPoint(x: 4.0, y: (layout.statusBarHeight ?? 0.0) + 4.0)
for (_, _, videoNode) in self.videoNodes {
let videoSize = CGSize(width: 300.0, height: 500.0)
if currentVideoOrigin.x + videoSize.width > layout.size.width {
currentVideoOrigin.x = 0.0
currentVideoOrigin.y += videoSize.height
}
videoNode.frame = CGRect(origin: currentVideoOrigin, size: videoSize)
videoNode.updateLayout(size: videoSize, transition: .immediate)
if videoNode.supernode == nil {
self.contentContainer.addSubnode(videoNode)
}
currentVideoOrigin.x += videoSize.width + 4.0
}*/
if self.audioButton.supernode === self.bottomPanelNode {
transition.updateFrame(node: self.cameraButton, frame: firstButtonFrame)
transition.updateFrame(node: self.switchCameraButton, frame: firstButtonFrame)
@ -3926,6 +4117,12 @@ public final class VoiceChatController: ViewController {
entries.append(.invite(self.presentationData.theme, self.presentationData.strings, inviteIsLink ? self.presentationData.strings.VoiceChat_Share : self.presentationData.strings.VoiceChat_InviteMember, inviteIsLink))
}
if let _ = self.effectiveSpeakerWithVideo {
index += 1
}
var pinnedEntry: ListEntry?
for member in callMembers.0 {
if processedPeerIds.contains(member.peer.id) {
continue
@ -3978,7 +4175,7 @@ public final class VoiceChatController: ViewController {
memberPeer = user.withUpdatedPhoto([photo])
}
entries.append(.peer(PeerEntry(
let entry: ListEntry = .peer(PeerEntry(
peer: memberPeer,
about: member.about,
isMyPeer: self.callState?.myPeerId == member.peer.id,
@ -3990,9 +4187,29 @@ public final class VoiceChatController: ViewController {
canManageCall: self.callState?.canManageCall ?? false,
volume: member.volume,
raisedHand: member.hasRaiseHand,
displayRaisedHandStatus: self.displayedRaisedHands.contains(member.peer.id)
)))
displayRaisedHandStatus: self.displayedRaisedHands.contains(member.peer.id),
pinned: memberPeer.id == self.effectiveSpeakerWithVideo?.0
))
entries.append(entry)
index += 1
if memberPeer.id == self.effectiveSpeakerWithVideo?.0 {
pinnedEntry = .peer(PeerEntry(
peer: memberPeer,
about: member.about,
isMyPeer: self.callState?.myPeerId == member.peer.id,
ssrc: member.ssrc,
presence: nil,
activityTimestamp: Int32.max - 1 - index,
state: memberState,
muteState: memberMuteState,
canManageCall: self.callState?.canManageCall ?? false,
volume: member.volume,
raisedHand: member.hasRaiseHand,
displayRaisedHandStatus: self.displayedRaisedHands.contains(member.peer.id),
pinned: true
))
}
}
for peer in invitedPeers {
@ -4013,7 +4230,8 @@ public final class VoiceChatController: ViewController {
canManageCall: false,
volume: nil,
raisedHand: false,
displayRaisedHandStatus: false
displayRaisedHandStatus: false,
pinned: false
)))
index += 1
}
@ -4022,6 +4240,26 @@ public final class VoiceChatController: ViewController {
return
}
if let entry = pinnedEntry, let interaction = self.itemInteraction {
self.mainParticipantNode.isHidden = false
let item = entry.item(context: self.context, presentationData: self.presentationData, interaction: interaction, style: .list, transparent: true)
let itemNode = self.mainParticipantNode
item.updateNode(async: { $0() }, node: {
return itemNode
}, params: ListViewItemLayoutParams(width: self.bounds.width, leftInset: 0.0, rightInset: 0.0, availableHeight: self.bounds.height), previousItem: nil, nextItem: nil, animation: .Crossfade, completion: { (layout, apply) in
// let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: width, height: layout.size.height))
//
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
// itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
apply(ListViewItemApply(isOnScreen: true))
})
} else {
self.mainParticipantNode.isHidden = true
}
let previousEntries = self.currentEntries
self.currentEntries = entries
@ -4033,6 +4271,9 @@ public final class VoiceChatController: ViewController {
if lhsPeer.isMyPeer != rhsPeer.isMyPeer {
allEqual = false
break
} else if lhsPeer.pinned || rhsPeer.pinned {
allEqual = false
break
}
} else {
allEqual = false
@ -4051,10 +4292,35 @@ public final class VoiceChatController: ViewController {
let transition = preparedTransition(from: previousEntries, to: entries, isLoading: false, isEmpty: false, canInvite: canInvite, crossFade: false, animated: !disableAnimation, context: self.context, presentationData: presentationData, interaction: self.itemInteraction!, style: .list)
self.enqueueTransition(transition)
let horizontalTransition = preparedTransition(from: previousEntries, to: entries, isLoading: false, isEmpty: false, canInvite: canInvite, crossFade: false, animated: !disableAnimation, context: self.context, presentationData: presentationData, interaction: self.itemInteraction!, style: .tile)
let horizontalTransition = preparedTransition(from: previousEntries, to: entries, isLoading: false, isEmpty: false, canInvite: canInvite, crossFade: false, animated: !disableAnimation, context: self.context, presentationData: presentationData, interaction: self.itemInteraction!, style: .tile(isLandscape: self.isLandscape))
self.enqueueHorizontalTransition(horizontalTransition)
}
private func updatePinnedParticipant() {
let effectivePinnedParticipant = self.currentForcedSpeakerWithVideo ?? self.currentDominantSpeakerWithVideo
guard effectivePinnedParticipant?.0 != self.effectiveSpeakerWithVideo?.0 || effectivePinnedParticipant?.1 != self.effectiveSpeakerWithVideo?.1 else {
return
}
if let (peerId, _) = effectivePinnedParticipant {
for entry in self.currentEntries {
switch entry {
case let .peer(peer):
if peer.peer.id == peerId, let source = peer.ssrc {
self.effectiveSpeakerWithVideo = (peerId, source)
self.call.setFullSizeVideo(peerId: peerId)
self.mainVideoContainerNode?.updatePeer(peer: (peerId: peerId, source: source), waitForFullSize: false)
}
default:
break
}
}
} else {
self.effectiveSpeakerWithVideo = nil
}
self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? ([], nil), invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set())
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UILongPressGestureRecognizer {
return !self.isScheduling
@ -4088,11 +4354,6 @@ public final class VoiceChatController: ViewController {
}
}
private var animatingInsertion = false
private var animatingExpansion = false
private var animatingAppearance = false
private var panGestureArguments: (topInset: CGFloat, offset: CGFloat)?
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
let contentOffset = self.listNode.visibleContentOffset()
let isScheduling = self.isScheduling || self.callState?.scheduleTimestamp != nil
@ -4276,15 +4537,6 @@ public final class VoiceChatController: ViewController {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if let result = result {
for (_, _, videoNode) in self.videoNodes {
if videoNode.view === result || result.isDescendant(of: videoNode.view) {
return result
}
}
}
if result === self.topPanelNode.view {
return self.view
}

View File

@ -20,9 +20,9 @@ import AudioBlob
import PeerInfoAvatarListNode
final class VoiceChatParticipantItem: ListViewItem {
enum LayoutStyle {
enum LayoutStyle: Equatable {
case list
case tile
case tile(isLandscape: Bool)
}
enum ParticipantText {
@ -77,6 +77,7 @@ final class VoiceChatParticipantItem: ListViewItem {
let style: LayoutStyle
let enabled: Bool
let transparent: Bool
let pinned: Bool
public let selectable: Bool
let getAudioLevel: (() -> Signal<Float, NoError>)?
let getVideo: () -> GroupVideoNode?
@ -88,7 +89,7 @@ final class VoiceChatParticipantItem: ListViewItem {
let getIsExpanded: () -> Bool
let getUpdatingAvatar: () -> Signal<(TelegramMediaImageRepresentation, Float)?, NoError>
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, ssrc: UInt32?, presence: PeerPresence?, text: ParticipantText, expandedText: ParticipantText?, icon: Icon, style: LayoutStyle, enabled: Bool, transparent: Bool, selectable: Bool, getAudioLevel: (() -> Signal<Float, NoError>)?, getVideo: @escaping () -> GroupVideoNode?, revealOptions: [RevealOption], revealed: Bool?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, action: ((ASDisplayNode) -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, getIsExpanded: @escaping () -> Bool, getUpdatingAvatar: @escaping () -> Signal<(TelegramMediaImageRepresentation, Float)?, NoError>) {
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, ssrc: UInt32?, presence: PeerPresence?, text: ParticipantText, expandedText: ParticipantText?, icon: Icon, style: LayoutStyle, enabled: Bool, transparent: Bool, pinned: Bool, selectable: Bool, getAudioLevel: (() -> Signal<Float, NoError>)?, getVideo: @escaping () -> GroupVideoNode?, revealOptions: [RevealOption], revealed: Bool?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, action: ((ASDisplayNode) -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, getIsExpanded: @escaping () -> Bool, getUpdatingAvatar: @escaping () -> Signal<(TelegramMediaImageRepresentation, Float)?, NoError>) {
self.presentationData = presentationData
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
@ -102,6 +103,7 @@ final class VoiceChatParticipantItem: ListViewItem {
self.style = style
self.enabled = enabled
self.transparent = transparent
self.pinned = pinned
self.selectable = selectable
self.getAudioLevel = getAudioLevel
self.getVideo = getVideo
@ -160,6 +162,7 @@ final class VoiceChatParticipantItem: ListViewItem {
private let avatarFont = avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0))
private let tileSize = CGSize(width: 84.0, height: 84.0)
private let backgroundCornerRadius: CGFloat = 14.0
private let avatarSize: CGFloat = 40.0
class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
private let topStripeNode: ASDisplayNode
@ -178,6 +181,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
private var extractedVerticalOffset: CGFloat?
fileprivate let avatarNode: AvatarNode
private let pinIconNode: ASImageNode
private let contentWrapperNode: ASDisplayNode
private let titleNode: TextNode
private let statusIconNode: ASImageNode
@ -187,7 +191,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
private var avatarTransitionNode: ASImageNode?
private var avatarListContainerNode: ASDisplayNode?
private var avatarListWrapperNode: ASDisplayNode?
private var avatarListWrapperNode: PinchSourceContainerNode?
private var avatarListNode: PeerInfoAvatarListContainerNode?
private let actionContainerNode: ASDisplayNode
@ -207,8 +211,11 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
private var isExtracted = false
private var wavesColor: UIColor?
private var videoContainerNode: ASDisplayNode
private let videoContainerNode: ASDisplayNode
private let fadeNode: ASImageNode
private var videoNode: GroupVideoNode?
private let videoReadyDisposable = MetaDisposable()
private var videoReadyDelayed = false
private var raiseHandTimer: SwiftSignalKit.Timer?
@ -243,11 +250,32 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 40.0, height: 40.0))
self.pinIconNode = ASImageNode()
self.pinIconNode.alpha = 0.65
self.pinIconNode.displaysAsynchronously = false
self.pinIconNode.displayWithoutProcessing = true
self.pinIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pin"), color: UIColor(rgb: 0xffffff))
self.contentWrapperNode = ASDisplayNode()
self.videoContainerNode = ASDisplayNode()
self.videoContainerNode.clipsToBounds = true
self.fadeNode = ASImageNode()
self.fadeNode.displaysAsynchronously = false
self.fadeNode.displayWithoutProcessing = true
self.fadeNode.contentMode = .scaleToFill
self.fadeNode.image = generateImage(CGSize(width: 1.0, height: 30.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let colorsArray = [UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 0.7).cgColor] as CFArray
var locations: [CGFloat] = [0.0, 1.0]
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
self.videoContainerNode.addSubnode(fadeNode)
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
@ -291,7 +319,8 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
self.contentWrapperNode.addSubnode(self.statusNode)
self.contentWrapperNode.addSubnode(self.expandedStatusNode)
self.contentWrapperNode.addSubnode(self.actionContainerNode)
self.contentWrapperNode.addSubnode(self.actionButtonNode)
self.actionContainerNode.addSubnode(self.actionButtonNode)
self.offsetContainerNode.addSubnode(self.pinIconNode)
self.offsetContainerNode.addSubnode(self.avatarNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
@ -390,11 +419,38 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
let initialScale = avatarInitialRect.width / targetRect.width
avatarInitialRect.origin.y += backgroundCornerRadius / 2.0 * initialScale
let avatarListWrapperNode = ASDisplayNode()
let avatarListWrapperNode = PinchSourceContainerNode()
avatarListWrapperNode.clipsToBounds = true
avatarListWrapperNode.frame = CGRect(x: targetRect.minX, y: targetRect.minY, width: targetRect.width, height: targetRect.height + backgroundCornerRadius)
avatarListWrapperNode.cornerRadius = backgroundCornerRadius
avatarListWrapperNode.activate = { [weak self] sourceNode in
guard let strongSelf = self else {
return
}
strongSelf.avatarListNode?.controlsContainerNode.alpha = 0.0
let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: {
return UIScreen.main.bounds
})
item.context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController)
}
avatarListWrapperNode.deactivated = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.avatarListWrapperNode?.contentNode.layer.animate(from: 0.0 as NSNumber, to: backgroundCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3, completion: { _ in
})
}
avatarListWrapperNode.update(size: targetRect.size, transition: .immediate)
avatarListWrapperNode.frame = CGRect(x: targetRect.minX, y: targetRect.minY, width: targetRect.width, height: targetRect.height + backgroundCornerRadius)
avatarListWrapperNode.animatedOut = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.avatarListNode?.controlsContainerNode.alpha = 1.0
strongSelf.avatarListNode?.controlsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
let transitionNode = ASImageNode()
transitionNode.clipsToBounds = true
transitionNode.displaysAsynchronously = false
@ -405,8 +461,9 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
radiusTransition.updateCornerRadius(node: transitionNode, cornerRadius: 0.0)
strongSelf.avatarNode.isHidden = true
strongSelf.videoContainerNode.isHidden = true
avatarListWrapperNode.addSubnode(transitionNode)
avatarListWrapperNode.contentNode.addSubnode(transitionNode)
strongSelf.avatarTransitionNode = transitionNode
let avatarListContainerNode = ASDisplayNode()
@ -420,6 +477,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
radiusTransition.updateCornerRadius(node: avatarListContainerNode, cornerRadius: 0.0)
let avatarListNode = PeerInfoAvatarListContainerNode(context: item.context)
avatarListWrapperNode.contentNode.clipsToBounds = true
avatarListNode.backgroundColor = .clear
avatarListNode.peer = item.peer
avatarListNode.firstFullSizeOnly = true
@ -434,7 +492,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
avatarListContainerNode.addSubnode(avatarListNode)
avatarListContainerNode.addSubnode(avatarListNode.controlsClippingOffsetNode)
avatarListWrapperNode.addSubnode(avatarListContainerNode)
avatarListWrapperNode.contentNode.addSubnode(avatarListContainerNode)
avatarListNode.update(size: targetRect.size, peer: item.peer, additionalEntry: item.getUpdatingAvatar(), isExpanded: true, transition: .immediate)
strongSelf.offsetContainerNode.supernode?.addSubnode(avatarListWrapperNode)
@ -461,6 +519,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
avatarListContainerNode?.removeFromSupernode()
})
strongSelf.videoContainerNode.isHidden = false
avatarListWrapperNode.layer.animate(from: 1.0 as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false)
avatarListWrapperNode.layer.animate(from: NSValue(cgPoint: avatarListWrapperNode.position), to: NSValue(cgPoint: avatarInitialRect.center), keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak transitionNode, weak self] _ in
transitionNode?.removeFromSupernode()
@ -556,6 +615,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
}
deinit {
self.videoReadyDisposable.dispose()
self.audioLevelDisposable.dispose()
self.raiseHandTimer?.invalidate()
}
@ -565,60 +625,161 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
self.layoutParams?.0.action?(self.contextSourceNode)
}
func transitionIn(from otherNode: VoiceChatParticipantItemNode, containerNode: ASDisplayNode) {
guard let otherItem = otherNode.item, otherItem.style != self.item?.style else {
func transitionIn(from sourceNode: VoiceChatParticipantItemNode, containerNode: ASDisplayNode) {
guard let item = self.item, let sourceItem = sourceNode.item, sourceItem.style != self.item?.style else {
return
}
switch otherItem.style {
switch sourceItem.style {
case .list:
otherNode.avatarNode.alpha = 0.0
let startContainerPosition = otherNode.avatarNode.view.convert(otherNode.avatarNode.bounds, to: containerNode.view).center.offsetBy(dx: 0.0, dy: 9.0)
let initialPosition = self.contextSourceNode.position
let targetContainerPosition = self.contextSourceNode.view.convert(self.contextSourceNode.bounds, to: containerNode.view).center
self.contextSourceNode.position = targetContainerPosition
containerNode.addSubnode(self.contextSourceNode)
self.contextSourceNode.layer.animatePosition(from: startContainerPosition, to: targetContainerPosition, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.contextSourceNode.position = initialPosition
strongSelf.containerNode.addSubnode(strongSelf.contextSourceNode)
}
})
if let videoNode = otherNode.videoNode {
self.avatarNode.alpha = 0.0
otherNode.videoNode = nil
self.videoNode = videoNode
let initialPosition = videoNode.position
videoNode.position = CGPoint(x: self.videoContainerNode.frame.width / 2.0, y: self.videoContainerNode.frame.width / 2.0)
videoNode.layer.animatePosition(from: initialPosition, to: videoNode.position, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
self.videoContainerNode.addSubnode(videoNode)
self.videoContainerNode.layer.animateFrame(from: self.avatarNode.frame, to: self.videoContainerNode.frame, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
self.videoContainerNode.layer.animate(from: (self.avatarNode.frame.width / 2.0) as NSNumber, to: backgroundCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak self] value in
})
var startContainerPosition = sourceNode.avatarNode.view.convert(sourceNode.avatarNode.bounds, to: containerNode.view).center
var animate = true
if startContainerPosition.y > containerNode.frame.height - 238.0 {
animate = false
}
self.backgroundImageNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
self.backgroundImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
self.contentWrapperNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
self.contentWrapperNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
case .tile:
if let otherVideoNode = otherNode.videoNode {
otherNode.videoNode = nil
self.videoNode = otherVideoNode
if let videoNode = sourceNode.videoNode {
if item.transparent {
} else {
if item.pinned {
self.avatarNode.alpha = 1.0
videoNode.alpha = 0.0
} else {
self.avatarNode.alpha = 0.0
}
}
let initialPosition = otherVideoNode.position
otherNode.position = CGPoint(x: self.videoContainerNode.frame.width / 2.0, y: self.videoContainerNode.frame.width / 2.0)
self.videoContainerNode.addSubnode(otherVideoNode)
sourceNode.videoNode = nil
self.videoNode = videoNode
if animate {
self.videoContainerNode.layer.animateScale(from: avatarSize / tileSize.width, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
self.videoContainerNode.insertSubnode(videoNode, at: 0)
if animate {
self.videoContainerNode.layer.animate(from: (tileSize.width / 2.0) as NSNumber, to: backgroundCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { _ in
})
}
} else {
self.avatarNode.alpha = 1.0
startContainerPosition = startContainerPosition.offsetBy(dx: 0.0, dy: 9.0)
}
if animate {
sourceNode.avatarNode.alpha = 0.0
let initialPosition = self.contextSourceNode.position
let targetContainerPosition = self.contextSourceNode.view.convert(self.contextSourceNode.bounds, to: containerNode.view).center
self.contextSourceNode.position = targetContainerPosition
containerNode.addSubnode(self.contextSourceNode)
self.contextSourceNode.layer.animatePosition(from: startContainerPosition, to: targetContainerPosition, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, completion: { [weak self, weak sourceNode] _ in
if let strongSelf = self {
sourceNode?.avatarNode.alpha = 1.0
strongSelf.contextSourceNode.position = initialPosition
strongSelf.containerNode.addSubnode(strongSelf.contextSourceNode)
}
})
self.fadeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
self.backgroundImageNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
self.backgroundImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
self.contentWrapperNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
self.contentWrapperNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
case .tile:
let startContainerAvatarPosition = sourceNode.avatarNode.view.convert(sourceNode.avatarNode.bounds, to: containerNode.view).center
var animate = true
if startContainerAvatarPosition.x < -tileSize.width || startContainerAvatarPosition.x > containerNode.frame.width + tileSize.width {
animate = false
}
if let videoNode = sourceNode.videoNode {
if item.transparent {
} else {
self.avatarNode.alpha = 0.0
}
sourceNode.videoNode = nil
self.videoNode = videoNode
self.videoContainerNode.insertSubnode(videoNode, at: 0)
videoNode.alpha = 1.0
}
if animate {
sourceNode.avatarNode.alpha = 0.0
sourceNode.fadeNode.alpha = 0.0
let initialAvatarPosition = self.avatarNode.position
let targetContainerAvatarPosition = self.avatarNode.view.convert(self.avatarNode.bounds, to: containerNode.view).center
let startContainerBackgroundPosition = sourceNode.backgroundImageNode.view.convert(sourceNode.backgroundImageNode.bounds, to: containerNode.view).center
let startContainerContentPosition = sourceNode.contentWrapperNode.view.convert(sourceNode.contentWrapperNode.bounds, to: containerNode.view).center
let startContainerVideoPosition = sourceNode.videoContainerNode.view.convert(sourceNode.videoContainerNode.bounds, to: containerNode.view).center
let initialBackgroundPosition = sourceNode.backgroundImageNode.position
let initialContentPosition = sourceNode.contentWrapperNode.position
sourceNode.backgroundImageNode.position = targetContainerAvatarPosition
sourceNode.contentWrapperNode.position = targetContainerAvatarPosition
containerNode.addSubnode(sourceNode.backgroundImageNode)
containerNode.addSubnode(sourceNode.contentWrapperNode)
if self.videoNode != nil {
sourceNode.backgroundImageNode.alpha = 0.0
}
sourceNode.backgroundImageNode.layer.animatePosition(from: startContainerBackgroundPosition, to: targetContainerAvatarPosition, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, completion: { [weak sourceNode] _ in
if let sourceNode = sourceNode {
sourceNode.backgroundImageNode.alpha = 1.0
sourceNode.backgroundImageNode.position = initialBackgroundPosition
sourceNode.contextSourceNode.contentNode.insertSubnode(sourceNode.backgroundImageNode, at: 0)
}
})
sourceNode.contentWrapperNode.layer.animatePosition(from: startContainerContentPosition, to: targetContainerAvatarPosition, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, completion: { [weak sourceNode] _ in
if let sourceNode = sourceNode {
sourceNode.avatarNode.alpha = 1.0
sourceNode.fadeNode.alpha = 1.0
sourceNode.contentWrapperNode.position = initialContentPosition
sourceNode.offsetContainerNode.insertSubnode(sourceNode.contentWrapperNode, aboveSubnode: sourceNode.videoContainerNode)
}
})
self.avatarNode.position = targetContainerAvatarPosition
containerNode.addSubnode(self.avatarNode)
self.avatarNode.layer.animatePosition(from: startContainerAvatarPosition, to: targetContainerAvatarPosition, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.avatarNode.position = initialAvatarPosition
strongSelf.offsetContainerNode.addSubnode(strongSelf.avatarNode)
}
})
self.videoContainerNode.position = targetContainerAvatarPosition
containerNode.addSubnode(self.videoContainerNode)
self.videoContainerNode.layer.animatePosition(from: startContainerVideoPosition, to: targetContainerAvatarPosition, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.videoContainerNode.position = initialAvatarPosition
strongSelf.offsetContainerNode.insertSubnode(strongSelf.videoContainerNode, belowSubnode: strongSelf.contentWrapperNode)
}
})
self.videoContainerNode.layer.animateScale(from: 1.0, to: avatarSize / tileSize.width, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
self.videoContainerNode.layer.animate(from: backgroundCornerRadius as NSNumber, to: (tileSize.width / 2.0) as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { _ in
})
self.fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
sourceNode.backgroundImageNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.35, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
sourceNode.backgroundImageNode.layer.animateAlpha(from: sourceNode.backgroundImageNode.alpha, to: 0.0, duration: 0.35, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
sourceNode.contentWrapperNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.35, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
sourceNode.contentWrapperNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
}
}
@ -634,12 +795,11 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
return { item, params, first, last in
var updatedTheme: PresentationTheme?
var updatedName = false
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let titleFont = item.style == .tile ? Font.regular(12.0) : Font.regular(17.0)
var titleFont = item.style == .list ? Font.regular(17.0) : Font.regular(12.0)
let statusFont = Font.regular(14.0)
var titleAttributedString: NSAttributedString?
@ -648,34 +808,39 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
let rightInset: CGFloat = params.rightInset
let titleColor = item.presentationData.theme.list.itemPrimaryTextColor
var titleColor = item.presentationData.theme.list.itemPrimaryTextColor
if item.transparent && item.style == .list {
titleFont = Font.semibold(17.0)
titleColor = UIColor(rgb: 0xffffff, alpha: 0.65)
}
let currentBoldFont: UIFont = titleFont
var updatedTitle = false
if let user = item.peer as? TelegramUser {
if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty {
if item.style == .tile {
let textColor: UIColor
switch item.icon {
case .wantsToSpeak:
textColor = item.presentationData.theme.list.itemAccentColor
default:
textColor = titleColor
}
titleAttributedString = NSAttributedString(string: firstName, font: titleFont, textColor: textColor)
} else {
let string = NSMutableAttributedString()
switch item.nameDisplayOrder {
case .firstLast:
string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor))
string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor))
string.append(NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor))
case .lastFirst:
string.append(NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor))
string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor))
string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor))
}
titleAttributedString = string
switch item.style {
case .list:
let string = NSMutableAttributedString()
switch item.nameDisplayOrder {
case .firstLast:
string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor))
string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor))
string.append(NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor))
case .lastFirst:
string.append(NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor))
string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor))
string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor))
}
titleAttributedString = string
case .tile:
let textColor: UIColor
switch item.icon {
case .wantsToSpeak:
textColor = item.presentationData.theme.list.itemAccentColor
default:
textColor = titleColor
}
titleAttributedString = NSAttributedString(string: firstName, font: titleFont, textColor: textColor)
}
} else if let firstName = user.firstName, !firstName.isEmpty {
titleAttributedString = NSAttributedString(string: firstName, font: currentBoldFont, textColor: titleColor)
@ -712,7 +877,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
statusAttributedString = NSAttributedString(string: item.presentationData.strings.LastSeen_Offline, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
}
case let .text(text, textColor):
let textColorValue: UIColor
var textColorValue: UIColor
switch textColor {
case .generic:
textColorValue = item.presentationData.theme.list.itemSecondaryTextColor
@ -725,6 +890,9 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
textColorValue = UIColor(rgb: 0xff3b30)
wavesColor = textColorValue
}
if item.transparent && item.style == .list {
textColorValue = UIColor(rgb: 0xffffff, alpha: 0.65)
}
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: textColorValue)
case .none:
break
@ -747,10 +915,9 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
expandedStatusAttributedString = statusAttributedString
}
let leftInset: CGFloat = 65.0 + params.leftInset
let leftInset: CGFloat = 58.0 + params.leftInset
let verticalInset: CGFloat = 8.0
let verticalOffset: CGFloat = 0.0
let avatarSize: CGFloat = 40.0
var titleIconsWidth: CGFloat = 0.0
var currentCredibilityIconImage: UIImage?
@ -786,7 +953,6 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - rightInset - 30.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (expandedStatusLayout, expandedStatusApply) = makeExpandedStatusLayout(TextNodeLayoutArguments(attributedString: expandedStatusAttributedString, backgroundColor: nil, maximumNumberOfLines: 6, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - rightInset - expandedRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let titleSpacing: CGFloat = statusLayout.size.height == 0.0 ? 0.0 : 1.0
let minHeight: CGFloat = titleLayout.size.height + verticalInset * 2.0
@ -797,7 +963,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
switch item.style {
case .list:
contentSize = CGSize(width: params.width, height: max(minHeight, rawHeight))
insets = UIEdgeInsets()
insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: item.transparent ? 6.0 : 0.0, right: 0.0)
case .tile:
contentSize = tileSize
insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: !last ? 6.0 : 0.0, right: 0.0)
@ -870,14 +1036,14 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
switch item.style {
case .list:
nonExtractedRect = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: CGSize(width: layout.contentSize.width - 32.0, height: layout.contentSize.height))
avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + 8.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
animationSize = CGSize(width: 36.0, height: 36.0)
animationScale = 1.0
animationFrame = CGRect(x: params.width - animationSize.width - 6.0 - params.rightInset, y: floor((layout.contentSize.height - animationSize.height) / 2.0) + 1.0, width: animationSize.width, height: animationSize.height)
titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + verticalOffset), size: titleLayout.size)
case .tile:
case let .tile(isLandscape):
nonExtractedRect = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
strongSelf.containerNode.transform = CATransform3DMakeRotation(isLandscape ? 0.0 : CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
strongSelf.statusNode.isHidden = true
strongSelf.expandedStatusNode.isHidden = true
avatarFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - avatarSize) / 2.0), y: 13.0), size: CGSize(width: avatarSize, height: avatarSize))
@ -1003,8 +1169,8 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 1)
}
strongSelf.topStripeNode.isHidden = first || item.style == .tile
strongSelf.bottomStripeNode.isHidden = last || item.style == .tile
strongSelf.topStripeNode.isHidden = first || item.style != .list || item.transparent
strongSelf.bottomStripeNode.isHidden = last || item.style != .list || item.transparent
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: leftInset, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: leftInset, y: contentSize.height + -separatorHeight), size: CGSize(width: layoutSize.width - leftInset, height: separatorHeight)))
@ -1065,7 +1231,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
audioLevelView.layer.mask = playbackMaskLayer
audioLevelView.setColor(wavesColor)
audioLevelView.alpha = strongSelf.isExtracted ? 0.0 : 1.0
audioLevelView.alpha = strongSelf.isExtracted || (strongSelf.item?.transparent == true) ? 0.0 : 1.0
strongSelf.audioLevelView = audioLevelView
strongSelf.offsetContainerNode.view.insertSubview(audioLevelView, at: 0)
@ -1124,9 +1290,12 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
nodeToAnimateIn = animationNode
}
var color = color
if color.rgb == 0x979797 && item.style == .tile {
if item.transparent {
color = UIColor(rgb: 0xffffff)
} else if color.rgb == 0x979797 && item.style != .list {
color = UIColor(rgb: 0xffffff)
}
animationNode.alpha = item.transparent && item.style == .list ? 0.65 : 1.0
animationNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, filled: false, color: color), animated: true)
strongSelf.actionButtonNode.isUserInteractionEnabled = false
} else if let animationNode = strongSelf.animationNode {
@ -1204,33 +1373,121 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
}
let videoSize = tileSize
let videoNode = item.getVideo()
if let current = strongSelf.videoNode, current !== videoNode {
current.removeFromSupernode()
strongSelf.videoReadyDisposable.set(nil)
}
let videoNodeUpdated = strongSelf.videoNode !== videoNode
strongSelf.videoNode = videoNode
strongSelf.fadeNode.frame = CGRect(x: 0.0, y: tileSize.height - 30.0, width: tileSize.width, height: 30.0)
strongSelf.videoContainerNode.bounds = CGRect(origin: CGPoint(), size: tileSize)
switch item.style {
case .list:
strongSelf.videoContainerNode.frame = strongSelf.avatarNode.frame
strongSelf.videoContainerNode.cornerRadius = avatarSize / 2.0
strongSelf.fadeNode.alpha = 0.0
strongSelf.videoContainerNode.position = strongSelf.avatarNode.position
strongSelf.videoContainerNode.cornerRadius = tileSize.width / 2.0
strongSelf.videoContainerNode.transform = CATransform3DMakeScale(avatarSize / tileSize.width, avatarSize / tileSize.width, 1.0)
case .tile:
strongSelf.videoContainerNode.frame = CGRect(origin: CGPoint(), size: tileSize)
strongSelf.fadeNode.alpha = 1.0
strongSelf.videoContainerNode.position = CGPoint(x: tileSize.width / 2.0, y: tileSize.height / 2.0)
strongSelf.videoContainerNode.cornerRadius = backgroundCornerRadius
strongSelf.videoContainerNode.transform = CATransform3DMakeScale(1.0, 1.0, 1.0)
}
if let videoNode = videoNode {
strongSelf.avatarNode.alpha = 0.0
if case .tile = item.style {
if currentItem != nil {
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
if item.pinned {
transition.updateAlpha(node: videoNode, alpha: 0.0)
transition.updateAlpha(node: strongSelf.fadeNode, alpha: 0.0)
strongSelf.videoContainerNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2)
transition.updateAlpha(node: strongSelf.avatarNode, alpha: 1.0)
strongSelf.avatarNode.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2)
} else {
transition.updateAlpha(node: videoNode, alpha: 1.0)
transition.updateAlpha(node: strongSelf.fadeNode, alpha: 1.0)
strongSelf.videoContainerNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
transition.updateAlpha(node: strongSelf.avatarNode, alpha: 0.0)
strongSelf.avatarNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2)
}
} else {
if item.pinned {
videoNode.alpha = 0.0
strongSelf.avatarNode.alpha = 1.0
} else {
videoNode.alpha = 1.0
strongSelf.avatarNode.alpha = 0.0
}
}
}
videoNode.updateLayout(size: videoSize, isLandscape: false, transition: .immediate)
if videoNode.supernode !== strongSelf.avatarNode {
if videoNode.supernode !== strongSelf.videoContainerNode {
videoNode.clipsToBounds = true
strongSelf.videoContainerNode.addSubnode(videoNode)
}
videoNode.position = CGPoint(x: strongSelf.videoContainerNode.frame.width / 2.0, y: strongSelf.videoContainerNode.frame.height / 2.0)
videoNode.position = CGPoint(x: videoSize.width / 2.0, y: videoSize.height / 2.0)
videoNode.bounds = CGRect(origin: CGPoint(), size: videoSize)
if videoNodeUpdated {
strongSelf.videoReadyDelayed = false
strongSelf.videoReadyDisposable.set((videoNode.ready
|> deliverOnMainQueue).start(next: { [weak self] ready in
if let strongSelf = self {
if !ready {
strongSelf.videoReadyDelayed = true
}
if let videoNode = strongSelf.videoNode, ready && (strongSelf.item?.transparent != true) {
if strongSelf.videoReadyDelayed {
Queue.mainQueue().after(0.15) {
switch item.style {
case .list:
strongSelf.avatarNode.alpha = 0.0
strongSelf.avatarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
case .tile:
if item.pinned {
strongSelf.avatarNode.alpha = 1.0
videoNode.alpha = 0.0
} else {
strongSelf.avatarNode.alpha = 0.0
strongSelf.avatarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
videoNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
videoNode.alpha = 0.0
}
}
}
} else {
if case .tile = item.style, item.pinned {
strongSelf.avatarNode.alpha = 1.0
} else {
strongSelf.avatarNode.alpha = 0.0
}
}
}
}
}))
}
}
if item.style == .list {
strongSelf.audioLevelView?.alpha = item.transparent ? 0.0 : 0.0
strongSelf.avatarNode.isHidden = item.transparent
strongSelf.videoContainerNode.isHidden = item.transparent
strongSelf.pinIconNode.isHidden = !item.transparent
} else {
strongSelf.pinIconNode.isHidden = true
strongSelf.videoContainerNode.isHidden = item.transparent
if item.transparent {
strongSelf.avatarNode.alpha = 1.0
}
}
if let image = strongSelf.pinIconNode.image {
strongSelf.pinIconNode.frame = CGRect(origin: CGPoint(x: 16.0, y: 17.0), size: image.size)
}
strongSelf.iconNode?.frame = CGRect(origin: CGPoint(), size: animationSize)
@ -1255,36 +1512,51 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
var isHighlighted = false
func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
if self.isHighlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if transition.isAnimated {
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()
}
guard let item = self.item else {
return
}
switch item.style {
case .list:
if self.isHighlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
}
})
self.highlightedBackgroundNode.alpha = 0.0
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
self.highlightedBackgroundNode.removeFromSupernode()
if self.highlightedBackgroundNode.supernode != nil {
if transition.isAnimated {
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()
}
}
}
}
case .tile:
break
// if self.isHighlighted {
// let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring)
// transition.updateSublayerTransformScale(node: self, scale: 0.9)
// } else {
// let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring)
// transition.updateSublayerTransformScale(node: self, scale: 1.0)
// }
}
}

View File

@ -1172,10 +1172,12 @@ public final class GroupCallParticipantsContext {
public final class MemberEvent {
public let peerId: PeerId
public let canUnmute: Bool
public let joined: Bool
public init(peerId: PeerId, joined: Bool) {
public init(peerId: PeerId, canUnmute: Bool, joined: Bool) {
self.peerId = peerId
self.canUnmute = canUnmute
self.joined = joined
}
}
@ -1627,7 +1629,7 @@ public final class GroupCallParticipantsContext {
if let index = updatedParticipants.firstIndex(where: { $0.peer.id == participantUpdate.peerId }) {
updatedParticipants.remove(at: index)
updatedTotalCount = max(0, updatedTotalCount - 1)
strongSelf.memberEventsPipe.putNext(MemberEvent(peerId: participantUpdate.peerId, joined: false))
strongSelf.memberEventsPipe.putNext(MemberEvent(peerId: participantUpdate.peerId, canUnmute: false, joined: false))
} else if isVersionUpdate {
updatedTotalCount = max(0, updatedTotalCount - 1)
}
@ -1650,7 +1652,7 @@ public final class GroupCallParticipantsContext {
updatedParticipants.remove(at: index)
} else if case .joined = participantUpdate.participationStatusChange {
updatedTotalCount += 1
strongSelf.memberEventsPipe.putNext(MemberEvent(peerId: participantUpdate.peerId, joined: true))
strongSelf.memberEventsPipe.putNext(MemberEvent(peerId: participantUpdate.peerId, canUnmute: participantUpdate.muteState?.canUnmute ?? true, joined: true))
}
var activityTimestamp: Double?

View File

@ -134,7 +134,6 @@ public final class CoveredStickerSet : Equatable {
}
public func installStickerSetInteractively(account: Account, info: StickerPackCollectionInfo, items: [ItemCollectionItem]) -> Signal<InstallStickerSetResult, InstallStickerSetError> {
return account.network.request(Api.functions.messages.installStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash), archived: .boolFalse)) |> mapError { _ -> InstallStickerSetError in
return .generic
} |> mapToSignal { result -> Signal<InstallStickerSetResult, InstallStickerSetError> in

View File

@ -406,6 +406,11 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
let immediateThumbnailData: Data?
var id: Int64
switch item {
case .custom:
representations = []
videoRepresentations = []
immediateThumbnailData = nil
id = 0
case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail):
representations = topRepresentations
videoRepresentations = videoRepresentationsValue
@ -696,6 +701,11 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
let immediateThumbnailData: Data?
var id: Int64
switch item {
case .custom:
representations = []
videoRepresentations = []
immediateThumbnailData = nil
id = 0
case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail):
representations = topRepresentations
videoRepresentations = videoRepresentationsValue