diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 408938bd30..5786ebcf1d 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -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 } } diff --git a/submodules/ContextUI/Sources/PinchController.swift b/submodules/ContextUI/Sources/PinchController.swift index ba9ade47eb..473031f90a 100644 --- a/submodules/ContextUI/Sources/PinchController.swift +++ b/submodules/ContextUI/Sources/PinchController.swift @@ -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 diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift index 098885f84b..62feee2aed 100644 --- a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -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 diff --git a/submodules/Postbox/Sources/MediaResource.swift b/submodules/Postbox/Sources/MediaResource.swift index bd87e21e24..db25d91a3d 100644 --- a/submodules/Postbox/Sources/MediaResource.swift +++ b/submodules/Postbox/Sources/MediaResource.swift @@ -22,7 +22,7 @@ public struct WrappedMediaResourceId: Hashable { // } public func hash(into hasher: inout Hasher) { - hasher.combine(id.hashValue) + hasher.combine(self.id.hashValue) } } diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index e583fca985..66958f228b 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -1905,7 +1905,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { |> mapToSignal { event -> Signal in return postbox.transaction { transaction -> Signal 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 { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 680a512334..207d79dc2b 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -77,6 +77,11 @@ final class GroupVideoNode: ASDisplayNode { var tapped: (() -> Void)? + private let readyPromise = ValuePromise(false) + var ready: Signal { + 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() 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 @@ -1277,18 +1346,20 @@ public final class VoiceChatController: ViewController { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Tip"), color: theme.actionSheet.primaryTextColor) }), 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 let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, 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)) + 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: @@ -3001,7 +3166,7 @@ public final class VoiceChatController: ViewController { return controlsHidden ? 0.0 : fullscreenBottomAreaHeight } } - + private var bringVideoToBackOnCompletion = false private func updateFloatingHeaderOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) { guard let (layout, _) = self.validLayout else { @@ -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) @@ -3399,7 +3597,7 @@ public final class VoiceChatController: ViewController { effectiveDisplayMode = .fullscreen(controlsHidden: false) } } - + if let videoIndex = self.contentContainer.subnodes?.firstIndex(where: { $0 === self.mainVideoClippingNode }), let listIndex = self.contentContainer.subnodes?.firstIndex(where: { $0 === self.listNode }) { switch effectiveDisplayMode { case .default: @@ -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 @@ -3977,8 +4174,8 @@ public final class VoiceChatController: ViewController { if member.peer.id == self.callState?.myPeerId, let user = memberPeer as? TelegramUser, let photo = self.currentUpdatingAvatar { 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,11 +4187,31 @@ 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 { if processedPeerIds.contains(peer.id) { continue @@ -4013,7 +4230,8 @@ public final class VoiceChatController: ViewController { canManageCall: false, volume: nil, raisedHand: false, - displayRaisedHandStatus: false + displayRaisedHandStatus: false, + pinned: false ))) index += 1 } @@ -4021,6 +4239,26 @@ public final class VoiceChatController: ViewController { guard self.didSetDataReady else { 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 @@ -4087,12 +4353,7 @@ public final class VoiceChatController: ViewController { self.itemInteraction?.isExpanded = self.isExpanded } } - - 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 } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift index 014224cc82..3c881d8f98 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift @@ -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)? 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)?, 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)?, 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,9 +1169,9 @@ 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) +// } } } diff --git a/submodules/TelegramCore/Sources/GroupCalls.swift b/submodules/TelegramCore/Sources/GroupCalls.swift index 7d33fcefbc..c41510ab1b 100644 --- a/submodules/TelegramCore/Sources/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/GroupCalls.swift @@ -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? diff --git a/submodules/TelegramCore/Sources/StickerSetInstallation.swift b/submodules/TelegramCore/Sources/StickerSetInstallation.swift index 9c65ddb251..0abcf37729 100644 --- a/submodules/TelegramCore/Sources/StickerSetInstallation.swift +++ b/submodules/TelegramCore/Sources/StickerSetInstallation.swift @@ -134,7 +134,6 @@ public final class CoveredStickerSet : Equatable { } public func installStickerSetInteractively(account: Account, info: StickerPackCollectionInfo, items: [ItemCollectionItem]) -> Signal { - 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 in diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index fb9a716dae..55a21e25ee 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -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