diff --git a/submodules/AvatarNode/Sources/PeerAvatar.swift b/submodules/AvatarNode/Sources/PeerAvatar.swift index c37effc85b..b32eaeee81 100644 --- a/submodules/AvatarNode/Sources/PeerAvatar.swift +++ b/submodules/AvatarNode/Sources/PeerAvatar.swift @@ -87,9 +87,9 @@ public func peerAvatarImageData(account: Account, peerReference: PeerReference?, } } -public func peerAvatarCompleteImage(account: Account, peer: Peer, size: CGSize, round: Bool = true, font: UIFont = avatarPlaceholderFont(size: 13.0), drawLetters: Bool = true, fullSize: Bool = false) -> Signal { +public func peerAvatarCompleteImage(account: Account, peer: Peer, size: CGSize, round: Bool = true, font: UIFont = avatarPlaceholderFont(size: 13.0), drawLetters: Bool = true, fullSize: Bool = false, blurred: Bool = false) -> Signal { let iconSignal: Signal - if let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: peer.profileImageRepresentations.first, displayDimensions: size, round: round, inset: 0.0, emptyColor: nil, synchronousLoad: fullSize) { + if let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: peer.profileImageRepresentations.first, displayDimensions: size, round: round, blurred: blurred, inset: 0.0, emptyColor: nil, synchronousLoad: fullSize) { if fullSize, let fullSizeSignal = peerAvatarImage(account: account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: peer.profileImageRepresentations.last, displayDimensions: size, emptyColor: nil, synchronousLoad: true) { iconSignal = combineLatest(.single(nil) |> then(signal), .single(nil) |> then(fullSizeSignal)) |> mapToSignal { thumbnailImage, fullSizeImage -> Signal in @@ -120,6 +120,10 @@ public func peerAvatarCompleteImage(account: Account, peer: Peer, size: CGSize, let image = generateImage(size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) drawPeerAvatarLetters(context: context, size: CGSize(width: size.width, height: size.height), round: round, font: font, letters: displayLetters, peerId: peerId) + if blurred { + context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.45).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + } })?.withRenderingMode(.alwaysOriginal) subscriber.putNext(image) @@ -130,7 +134,7 @@ public func peerAvatarCompleteImage(account: Account, peer: Peer, size: CGSize, return iconSignal } -public func peerAvatarImage(account: Account, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), round: Bool = true, inset: CGFloat = 0.0, emptyColor: UIColor? = nil, synchronousLoad: Bool = false, provideUnrounded: Bool = false) -> Signal<(UIImage, UIImage)?, NoError>? { +public func peerAvatarImage(account: Account, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), round: Bool = true, blurred: Bool = false, inset: CGFloat = 0.0, emptyColor: UIColor? = nil, synchronousLoad: Bool = false, provideUnrounded: Bool = false) -> Signal<(UIImage, UIImage)?, NoError>? { if let imageData = peerAvatarImageData(account: account, peerReference: peerReference, authorOfMessage: authorOfMessage, representation: representation, synchronousLoad: synchronousLoad) { return imageData |> mapToSignal { data -> Signal<(UIImage, UIImage)?, NoError> in @@ -149,11 +153,22 @@ public func peerAvatarImage(account: Account, peerReference: PeerReference?, aut context.clip() } + var shouldBlur = false if case .blurred = dataType { + shouldBlur = true + } else if blurred { + shouldBlur = true + } + if shouldBlur { let imageContextSize = CGSize(width: 64.0, height: 64.0) let imageContext = DrawingContext(size: imageContextSize, scale: 1.0, premultiplied: true, clear: true) imageContext.withFlippedContext { c in c.draw(dataImage, in: CGRect(origin: CGPoint(), size: imageContextSize)) + + context.setBlendMode(.saturation) + context.setFillColor(UIColor(rgb: 0xffffff, alpha: 1.0).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) } telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes) @@ -162,6 +177,12 @@ public func peerAvatarImage(account: Account, peerReference: PeerReference?, aut } context.draw(dataImage, in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) + if blurred { + context.setBlendMode(.normal) + context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.45).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + } if round { if displayDimensions.width == 60.0 { context.setBlendMode(.destinationOut) diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 803561a788..ea41030e1e 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -1400,7 +1400,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi contentHeight = layout.size.height } else { let totalHeight = originalContentFrame.height + originalActionsFrame.height - contentContainerFrame = CGRect(origin: CGPoint(x: originalContentFrame.minX - contentParentNode.contentRect.minX, y: floor((layout.size.height - totalHeight) / 2.0)), size: originalContentFrame.size) + contentContainerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - originalContentFrame.width) / 2.0), y: floor((layout.size.height - totalHeight) / 2.0)), size: originalContentFrame.size) originalActionsFrame.origin.y = contentContainerFrame.maxY + contentActionsSpacing } } else if keepInPlace { diff --git a/submodules/Display/Source/ImageNode.swift b/submodules/Display/Source/ImageNode.swift index 30cf507002..ecf6f87aea 100644 --- a/submodules/Display/Source/ImageNode.swift +++ b/submodules/Display/Source/ImageNode.swift @@ -126,7 +126,7 @@ public class ImageNode: ASDisplayNode { private let hasImage: ValuePromise? private var first = true private let enableEmpty: Bool - private let enableAnimatedTransition: Bool + public var enableAnimatedTransition: Bool private let _contentReady = Promise() private var didSetReady: Bool = false diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 12694353fe..3416521010 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -365,7 +365,7 @@ public final class VoiceChatController: ViewController { } } - func tileItem(context: AccountContext, presentationData: PresentationData, interaction: Interaction, videoEndpointId: String) -> VoiceChatTileItem? { + func tileItem(context: AccountContext, presentationData: PresentationData, interaction: Interaction, videoEndpointId: String, videoReady: Bool) -> VoiceChatTileItem? { guard case let .peer(peerEntry, _) = self else { return nil } @@ -373,6 +373,7 @@ public final class VoiceChatController: ViewController { let icon: VoiceChatTileItem.Icon var text: VoiceChatParticipantItem.ParticipantText + var additionalText: VoiceChatParticipantItem.ParticipantText? var speaking = false var textIcon = VoiceChatParticipantItem.ParticipantText.TextIcon() @@ -404,6 +405,7 @@ public final class VoiceChatController: ViewController { } if let muteState = peerEntry.muteState, muteState.mutedByYou { icon = .microphone(true) + additionalText = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) } else { icon = .microphone(peerEntry.muteState != nil) } @@ -411,6 +413,7 @@ public final class VoiceChatController: ViewController { if let muteState = peerEntry.muteState, muteState.mutedByYou { text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) icon = .microphone(true) + additionalText = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) } else { if peerEntry.volume != nil { textIcon.insert(.volume) @@ -433,7 +436,7 @@ public final class VoiceChatController: ViewController { text = .text(about, textIcon, .generic) } - return VoiceChatTileItem(peer: peerEntry.peer, videoEndpointId: videoEndpointId, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, speaking: speaking, icon: icon, text: text, action: { + return VoiceChatTileItem(account: context.account, peer: peerEntry.peer, videoEndpointId: videoEndpointId, videoReady: videoReady, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, speaking: speaking, icon: icon, text: text, additionalText: additionalText, action: { interaction.switchToPeer(peer.id, videoEndpointId, true) }, contextAction: { node, gesture in interaction.peerContextAction(peerEntry, node, gesture) @@ -528,7 +531,7 @@ public final class VoiceChatController: ViewController { text = .text(about, textIcon, .generic) } - return VoiceChatFullscreenParticipantItem(presentationData: ItemListPresentationData(presentationData), nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peerEntry.peer, icon: icon, text: text, textColor: textColor, color: color, isLandscape: peerEntry.isLandscape, active: peerEntry.active, getAudioLevel: { return interaction.getAudioLevel(peerEntry.peer.id) }, getVideo: { + return VoiceChatFullscreenParticipantItem(presentationData: ItemListPresentationData(presentationData), nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peerEntry.peer, videoEndpointId: peerEntry.effectiveVideoEndpointId, icon: icon, text: text, textColor: textColor, color: color, isLandscape: peerEntry.isLandscape, active: peerEntry.active, getAudioLevel: { return interaction.getAudioLevel(peerEntry.peer.id) }, getVideo: { if let endpointId = peerEntry.effectiveVideoEndpointId { return interaction.getPeerVideo(endpointId, .list) } else { @@ -3575,7 +3578,7 @@ public final class VoiceChatController: ViewController { let isFirstTime = self.validLayout == nil let previousLayout = self.validLayout?.0 self.validLayout = (layout, navigationHeight) - + var size = layout.size let contentWidth: CGFloat if case .regular = layout.metrics.widthClass { @@ -3696,6 +3699,15 @@ public final class VoiceChatController: ViewController { self.fullscreenListNode.transform = fullscreenListTransform self.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: fullscreenListUpdateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + var childrenLayout = layout + var childrenInsets = childrenLayout.intrinsicInsets + if !isLandscape, case .fullscreen = effectiveDisplayMode { + childrenInsets.bottom += self.effectiveBottomAreaHeight + fullscreenListHeight + 30.0 + } + childrenLayout.intrinsicInsets = childrenInsets + self.controller?.presentationContext.containerLayoutUpdated(childrenLayout, transition: transition) + transition.updateFrame(node: self.topCornersNode, frame: CGRect(origin: CGPoint(x: sideInset + floorToScreenPixels((size.width - contentWidth) / 2.0), y: topCornersY), size: CGSize(width: contentWidth - 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)) @@ -4278,11 +4290,11 @@ public final class VoiceChatController: ViewController { var isTile = false if let interaction = self.itemInteraction { - if let videoEndpointId = member.presentationEndpointId, self.readyVideoNodes.contains(videoEndpointId) { + if let videoEndpointId = member.presentationEndpointId { if !self.videoOrder.contains(videoEndpointId) { self.videoOrder.append(videoEndpointId) } - if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, videoEndpointId: videoEndpointId) { + if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, videoEndpointId: videoEndpointId, videoReady: self.readyVideoNodes.contains(videoEndpointId)) { isTile = true tileByVideoEndpoint[videoEndpointId] = tileItem } @@ -4290,11 +4302,11 @@ public final class VoiceChatController: ViewController { latestWideVideo = videoEndpointId } } - if let videoEndpointId = member.videoEndpointId, self.readyVideoNodes.contains(videoEndpointId) { + if let videoEndpointId = member.videoEndpointId { if !self.videoOrder.contains(videoEndpointId) { self.videoOrder.append(videoEndpointId) } - if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, videoEndpointId: videoEndpointId) { + if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, videoEndpointId: videoEndpointId, videoReady: self.readyVideoNodes.contains(videoEndpointId)) { isTile = true tileByVideoEndpoint[videoEndpointId] = tileItem } @@ -4680,7 +4692,7 @@ public final class VoiceChatController: ViewController { self.mainStageNode.setControlsHidden(true, animated: true) self.fullscreenListNode.alpha = 0.0 - self.fullscreenListNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, completion: { [weak self] _ in + self.fullscreenListNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, completion: { [weak self] finished in self?.attachTileVideos() }) @@ -4729,6 +4741,11 @@ public final class VoiceChatController: ViewController { } } case .fullscreen: + if fabs(translation) > 32.0 { + if self.fullscreenListNode.layer.animationKeys()?.contains("opacity") == true { + self.fullscreenListNode.layer.removeAllAnimations() + } + } var bounds = self.mainStageContainerNode.bounds bounds.origin.y = -translation self.mainStageContainerNode.bounds = bounds @@ -5264,42 +5281,44 @@ public final class VoiceChatController: ViewController { } private func attachFullscreenVideos() { - var verticalItemNodes: [PeerId: ASDisplayNode] = [:] + var verticalItemNodes: [String: ASDisplayNode] = [:] self.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? VoiceChatTilesGridItemNode { for tileNode in itemNode.tileNodes { if let item = tileNode.item { - verticalItemNodes[item.peer.id] = tileNode + verticalItemNodes[String(item.peer.id.toInt64()) + "_" + item.videoEndpointId] = tileNode } - if tileNode.item?.peer.id == self.effectiveSpeaker?.0 { + if tileNode.item?.peer.id == self.effectiveSpeaker?.0 && tileNode.item?.videoEndpointId == self.effectiveSpeaker?.1 { tileNode.isHidden = false } } } } self.fullscreenListNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item, let otherItemNode = verticalItemNodes[item.peer.id] { + if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item, let otherItemNode = verticalItemNodes[String(item.peer.id.toInt64()) + "_" + (item.videoEndpointId ?? "")] { itemNode.animateTransitionIn(from: otherItemNode, containerNode: self.transitionContainerNode, transition: .immediate, animate: false) } } } private func attachTileVideos() { - var fullscreenItemNodes: [PeerId: VoiceChatFullscreenParticipantItemNode] = [:] + var fullscreenItemNodes: [String: VoiceChatFullscreenParticipantItemNode] = [:] self.fullscreenListNode.forEachItemNode { itemNode in if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item { - fullscreenItemNodes[item.peer.id] = itemNode + fullscreenItemNodes[String(item.peer.id.toInt64()) + "_" + (item.videoEndpointId ?? "")] = itemNode } } self.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? VoiceChatTilesGridItemNode { for tileNode in itemNode.tileNodes { - if let item = tileNode.item, let otherItemNode = fullscreenItemNodes[item.peer.id] { - tileNode.animateTransitionIn(from: otherItemNode, containerNode: self.transitionContainerNode, transition: .immediate, animate: false) + if let item = tileNode.item { + if let otherItemNode = fullscreenItemNodes[String(item.peer.id.toInt64()) + "_" + item.videoEndpointId] { + tileNode.animateTransitionIn(from: otherItemNode, containerNode: self.transitionContainerNode, transition: .immediate, animate: false) + } - if tileNode.item?.peer.id == self.effectiveSpeaker?.0 { + if tileNode.item?.peer.id == self.effectiveSpeaker?.0 && tileNode.item?.videoEndpointId == self.effectiveSpeaker?.1 { tileNode.isHidden = true } } @@ -5336,7 +5355,7 @@ public final class VoiceChatController: ViewController { self.updateDecorationsLayout(transition: .immediate) var minimalVisiblePeerid: (PeerId, CGPoint)? - var verticalItemNodes: [PeerId: ASDisplayNode] = [:] + var verticalItemNodes: [String: ASDisplayNode] = [:] self.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? VoiceChatTilesGridItemNode { for tileNode in itemNode.tileNodes { @@ -5351,7 +5370,7 @@ public final class VoiceChatController: ViewController { minimalVisiblePeerid = (item.peer.id, convertedFrame.origin) } } - verticalItemNodes[item.peer.id] = tileNode + verticalItemNodes[String(item.peer.id.toInt64()) + "_" + item.videoEndpointId] = tileNode } } } else if let itemNode = itemNode as? VoiceChatParticipantItemNode, let item = itemNode.item { @@ -5365,7 +5384,7 @@ public final class VoiceChatController: ViewController { minimalVisiblePeerid = (item.peer.id, convertedFrame.origin) } } - verticalItemNodes[item.peer.id] = itemNode + verticalItemNodes[String(item.peer.id.toInt64()) + "_"] = itemNode } } @@ -5373,7 +5392,7 @@ public final class VoiceChatController: ViewController { let completion = { let effectiveSpeakerPeerId = self.effectiveSpeaker?.0 - if let effectiveSpeakerPeerId = effectiveSpeakerPeerId, let otherItemNode = verticalItemNodes[effectiveSpeakerPeerId] { + if let effectiveSpeakerPeerId = effectiveSpeakerPeerId, let otherItemNode = verticalItemNodes[String(effectiveSpeakerPeerId.toInt64()) + "_" + (self.effectiveSpeaker?.1 ?? "")] { self.mainStageNode.animateTransitionIn(from: otherItemNode, transition: transition) self.mainStageBackgroundNode.alpha = 1.0 @@ -5382,7 +5401,7 @@ public final class VoiceChatController: ViewController { self.fullscreenListNode.forEachItemNode { itemNode in if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item { - itemNode.animateTransitionIn(from: verticalItemNodes[item.peer.id], containerNode: self.transitionContainerNode, transition: transition, animate: item.peer.id != effectiveSpeakerPeerId) + itemNode.animateTransitionIn(from: verticalItemNodes[String(item.peer.id.toInt64()) + "_" + (item.videoEndpointId ?? "")], containerNode: self.transitionContainerNode, transition: transition, animate: item.peer.id != effectiveSpeakerPeerId) } } @@ -5422,7 +5441,7 @@ public final class VoiceChatController: ViewController { }) } else if case .fullscreen = previousDisplayMode, case .modal = self.displayMode { var minimalVisiblePeerid: (PeerId, CGFloat)? - var fullscreenItemNodes: [PeerId: VoiceChatFullscreenParticipantItemNode] = [:] + var fullscreenItemNodes: [String: VoiceChatFullscreenParticipantItemNode] = [:] self.fullscreenListNode.forEachItemNode { itemNode in if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item { let convertedFrame = itemNode.view.convert(itemNode.bounds, to: self.transitionContainerNode.view) @@ -5433,7 +5452,7 @@ public final class VoiceChatController: ViewController { } else if convertedFrame.minX >= 0.0 { minimalVisiblePeerid = (item.peer.id, convertedFrame.minX) } - fullscreenItemNodes[item.peer.id] = itemNode + fullscreenItemNodes[String(item.peer.id.toInt64()) + "_" + (item.videoEndpointId ?? "")] = itemNode } } @@ -5448,17 +5467,19 @@ public final class VoiceChatController: ViewController { self.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? VoiceChatTilesGridItemNode { for tileNode in itemNode.tileNodes { - if let item = tileNode.item, let otherItemNode = fullscreenItemNodes[item.peer.id] { - if !fromPan || item.peer.id == effectiveSpeakerPeerId { - tileNode.animateTransitionIn(from: otherItemNode, containerNode: self.transitionContainerNode, transition: transition, animate: item.peer.id != effectiveSpeakerPeerId) + if let item = tileNode.item { + if let otherItemNode = fullscreenItemNodes[String(item.peer.id.toInt64()) + "_" + item.videoEndpointId] { + if !fromPan || item.peer.id == effectiveSpeakerPeerId { + tileNode.animateTransitionIn(from: otherItemNode, containerNode: self.transitionContainerNode, transition: transition, animate: item.peer.id != effectiveSpeakerPeerId) + } } - if item.peer.id == effectiveSpeakerPeerId { + if item.peer.id == effectiveSpeakerPeerId, item.videoEndpointId == self.effectiveSpeaker?.1 { targetTileNode = tileNode } } } - } else if let itemNode = itemNode as? VoiceChatParticipantItemNode, let item = itemNode.item, let otherItemNode = fullscreenItemNodes[item.peer.id] { + } else if let itemNode = itemNode as? VoiceChatParticipantItemNode, let item = itemNode.item, let otherItemNode = fullscreenItemNodes[String(item.peer.id.toInt64()) + "_"] { if !fromPan { itemNode.animateTransitionIn(from: otherItemNode, containerNode: self.transitionContainerNode, transition: transition) } @@ -5643,6 +5664,7 @@ public final class VoiceChatController: ViewController { super.init(navigationBarPresentationData: nil) + self.automaticallyControlPresentationContextLayout = false self.blocksBackgroundWhenInOverlay = true self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift index a9f3fe84ec..bad7327a80 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift @@ -67,6 +67,7 @@ final class VoiceChatFullscreenParticipantItem: ListViewItem { let nameDisplayOrder: PresentationPersonNameOrder let context: AccountContext let peer: Peer + let videoEndpointId: String? let icon: Icon let text: VoiceChatParticipantItem.ParticipantText let textColor: Color @@ -81,11 +82,12 @@ final class VoiceChatFullscreenParticipantItem: ListViewItem { public let selectable: Bool = true - public init(presentationData: ItemListPresentationData, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, icon: Icon, text: VoiceChatParticipantItem.ParticipantText, textColor: Color, color: Color, isLandscape: Bool, active: Bool, getAudioLevel: (() -> Signal)?, getVideo: @escaping () -> GroupVideoNode?, action: ((ASDisplayNode?) -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, getUpdatingAvatar: @escaping () -> Signal<(TelegramMediaImageRepresentation, Float)?, NoError>) { + public init(presentationData: ItemListPresentationData, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, videoEndpointId: String?, icon: Icon, text: VoiceChatParticipantItem.ParticipantText, textColor: Color, color: Color, isLandscape: Bool, active: Bool, getAudioLevel: (() -> Signal)?, getVideo: @escaping () -> GroupVideoNode?, action: ((ASDisplayNode?) -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, getUpdatingAvatar: @escaping () -> Signal<(TelegramMediaImageRepresentation, Float)?, NoError>) { self.presentationData = presentationData self.nameDisplayOrder = nameDisplayOrder self.context = context self.peer = peer + self.videoEndpointId = videoEndpointId self.icon = icon self.text = text self.textColor = textColor diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift index 6df1d842b7..ff83f85ab3 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift @@ -24,6 +24,7 @@ private let backArrowImage = NavigationBarTheme.generateBackArrowImage(color: .w private let backgroundCornerRadius: CGFloat = 11.0 private let fadeColor = UIColor(rgb: 0x000000, alpha: 0.5) private let fadeHeight: CGFloat = 50.0 +private let destructiveColor: UIColor = UIColor(rgb: 0xff3b30) final class VoiceChatMainStageNode: ASDisplayNode { private let context: AccountContext @@ -503,28 +504,27 @@ final class VoiceChatMainStageNode: ASDisplayNode { func update(peerEntry: VoiceChatPeerEntry, pinned: Bool) { let previousPeerEntry = self.currentPeerEntry self.currentPeerEntry = peerEntry + + let peer = peerEntry.peer + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } if !arePeersEqual(previousPeerEntry?.peer, peerEntry.peer) { - let peer = peerEntry.peer - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.backdropAvatarNode.setSignal(peerAvatarCompleteImage(account: self.context.account, peer: peer, size: CGSize(width: 180.0, height: 180.0), round: false, font: avatarPlaceholderFont(size: 78.0), drawLetters: false)) self.avatarNode.setSignal(peerAvatarCompleteImage(account: self.context.account, peer: peer, size: CGSize(width: 180.0, height: 180.0), font: avatarPlaceholderFont(size: 78.0), fullSize: true)) - self.titleNode.attributedText = NSAttributedString(string: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: .white) - if let (size, sideInset, bottomInset, isLandscape) = self.validLayout { - self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: isLandscape, transition: .immediate) - } } - + var gradient: VoiceChatBlobNode.Gradient = .active var muted = false var state = peerEntry.state if let muteState = peerEntry.muteState, case .speaking = state, muteState.mutedByYou || !muteState.canUnmute { state = .listening } + var mutedForYou = false switch state { case .listening: if let muteState = peerEntry.muteState, muteState.mutedByYou { gradient = .muted muted = true + mutedForYou = true } else { gradient = .active muted = peerEntry.muteState != nil @@ -533,6 +533,7 @@ final class VoiceChatMainStageNode: ASDisplayNode { if let muteState = peerEntry.muteState, muteState.mutedByYou { gradient = .muted muted = true + mutedForYou = true } else { gradient = .speaking muted = false @@ -540,6 +541,21 @@ final class VoiceChatMainStageNode: ASDisplayNode { default: muted = true } + + var microphoneColor = UIColor.white + var titleAttributedString = NSAttributedString(string: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: .white) + if mutedForYou { + microphoneColor = destructiveColor + + let updatedString = NSMutableAttributedString(attributedString: titleAttributedString) + updatedString.append(NSAttributedString(string: " \(presentationData.strings.VoiceChat_StatusMutedForYou)", font: Font.regular(15.0), textColor: UIColor.white)) + titleAttributedString = updatedString + } + self.titleNode.attributedText = titleAttributedString + if let (size, sideInset, bottomInset, isLandscape) = self.validLayout { + self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: isLandscape, transition: .immediate) + } + self.audioLevelNode.updateGlowAndGradientAnimations(type: gradient, animated: true) self.pinButtonTitleNode.isHidden = !pinned @@ -571,7 +587,7 @@ final class VoiceChatMainStageNode: ASDisplayNode { })) } - self.microphoneNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, filled: true, color: .white), animated: true) + self.microphoneNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, filled: true, color: microphoneColor), animated: true) } private func setAvatarHidden(_ hidden: Bool) { @@ -598,6 +614,7 @@ final class VoiceChatMainStageNode: ASDisplayNode { if previousPeer?.0 == peer?.0 && self.appeared { delayTransition = true } + let appeared = self.appeared if !delayTransition { self.setAvatarHidden(true) @@ -671,8 +688,10 @@ final class VoiceChatMainStageNode: ASDisplayNode { }) } } else { - if let previousVideoNode = previousVideoNode { - previousVideoNode.removeFromSupernode() + Queue.mainQueue().after(0.07) { + if let previousVideoNode = previousVideoNode { + previousVideoNode.removeFromSupernode() + } } } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatTileGridNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatTileGridNode.swift index 073174dd6c..6ed552b17a 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatTileGridNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatTileGridNode.swift @@ -15,6 +15,8 @@ final class VoiceChatTileGridNode: ASDisplayNode { fileprivate var itemNodes: [String: VoiceChatTileItemNode] = [:] private var isFirstTime = true + private var absoluteLocation: (CGRect, CGSize)? + init(context: AccountContext) { self.context = context @@ -23,6 +25,16 @@ final class VoiceChatTileGridNode: ASDisplayNode { self.clipsToBounds = true } + func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.absoluteLocation = (rect, containerSize) + for itemNode in self.itemNodes.values { + var localRect = rect + localRect.origin = localRect.origin.offsetBy(dx: itemNode.frame.minX, dy: itemNode.frame.minY) + localRect.size = itemNode.frame.size + itemNode.updateAbsoluteRect(localRect, within: containerSize) + } + } + func update(size: CGSize, items: [VoiceChatTileItem], transition: ContainedViewLayoutTransition) -> CGSize { self.items = items @@ -72,6 +84,13 @@ final class VoiceChatTileGridNode: ASDisplayNode { } else { transition.updateFrame(node: itemNode, frame: itemFrame) } + + if let (rect, containerSize) = self.absoluteLocation { + var localRect = rect + localRect.origin = localRect.origin.offsetBy(dx: itemFrame.minX, dy: itemFrame.minY) + localRect.size = itemFrame.size + itemNode.updateAbsoluteRect(localRect, within: containerSize) + } } } @@ -147,6 +166,8 @@ final class VoiceChatTilesGridItemNode: ListViewItemNode { let backgroundNode: ASDisplayNode let cornersNode: ASImageNode + private var absoluteLocation: (CGRect, CGSize)? + var tileNodes: [VoiceChatTileItemNode] { if let values = self.tileGridNode?.itemNodes.values { return Array(values) @@ -208,10 +229,15 @@ final class VoiceChatTilesGridItemNode: ListViewItemNode { strongSelf.tileGridNode = tileGridNode } + + if let (rect, size) = strongSelf.absoluteLocation { + tileGridNode.updateAbsoluteRect(rect, within: size) + } + let transition: ContainedViewLayoutTransition = currentItem == nil ? .immediate : .animated(duration: 0.3, curve: .easeInOut) let tileGridSize = tileGridNode.update(size: CGSize(width: params.width - params.leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), items: item.tiles, transition: transition) if currentItem == nil { - tileGridNode.frame = CGRect(x: params.leftInset, y: 0.0, width: tileGridSize.width, height: 0.0) + tileGridNode.frame = CGRect(x: params.leftInset, y: 0.0, width: tileGridSize.width, height: tileGridSize.height) strongSelf.backgroundNode.frame = tileGridNode.frame strongSelf.cornersNode.frame = CGRect(x: params.leftInset, y: layout.size.height, width: tileGridSize.width, height: 50.0) } else { @@ -223,4 +249,9 @@ final class VoiceChatTilesGridItemNode: ListViewItemNode { }) } } + + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.absoluteLocation = (rect, containerSize) + self.tileGridNode?.updateAbsoluteRect(rect, within: containerSize) + } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatTileItemNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatTileItemNode.swift index bef3144367..30e115b23f 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatTileItemNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatTileItemNode.swift @@ -9,10 +9,13 @@ import TelegramCore import AccountContext import TelegramUIPreferences import TelegramPresentationData +import AvatarNode private let backgroundCornerRadius: CGFloat = 11.0 private let borderLineWidth: CGFloat = 2.0 +private let destructiveColor: UIColor = UIColor(rgb: 0xff3b30) + final class VoiceChatTileItem: Equatable { enum Icon: Equatable { case none @@ -20,12 +23,15 @@ final class VoiceChatTileItem: Equatable { case presentation } + let account: Account let peer: Peer let videoEndpointId: String + let videoReady: Bool let strings: PresentationStrings let nameDisplayOrder: PresentationPersonNameOrder let icon: Icon let text: VoiceChatParticipantItem.ParticipantText + let additionalText: VoiceChatParticipantItem.ParticipantText? let speaking: Bool let action: () -> Void let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? @@ -36,13 +42,16 @@ final class VoiceChatTileItem: Equatable { return self.videoEndpointId } - init(peer: Peer, videoEndpointId: String, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, speaking: Bool, icon: Icon, text: VoiceChatParticipantItem.ParticipantText, action: @escaping () -> Void, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?, getVideo: @escaping () -> GroupVideoNode?, getAudioLevel: (() -> Signal)?) { + init(account: Account, peer: Peer, videoEndpointId: String, videoReady: Bool, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, speaking: Bool, icon: Icon, text: VoiceChatParticipantItem.ParticipantText, additionalText: VoiceChatParticipantItem.ParticipantText?, action: @escaping () -> Void, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?, getVideo: @escaping () -> GroupVideoNode?, getAudioLevel: (() -> Signal)?) { + self.account = account self.peer = peer self.videoEndpointId = videoEndpointId + self.videoReady = videoReady self.strings = strings self.nameDisplayOrder = nameDisplayOrder self.icon = icon self.text = text + self.additionalText = additionalText self.speaking = speaking self.action = action self.contextAction = contextAction @@ -57,9 +66,18 @@ final class VoiceChatTileItem: Equatable { if lhs.videoEndpointId != rhs.videoEndpointId { return false } + if lhs.videoReady != rhs.videoReady { + return false + } if lhs.speaking != rhs.speaking { return false } + if lhs.text != rhs.text { + return false + } + if lhs.additionalText != rhs.additionalText { + return false + } if lhs.icon != rhs.icon { return false } @@ -93,6 +111,7 @@ final class VoiceChatTileItemNode: ASDisplayNode { var videoNode: GroupVideoNode? let infoNode: ASDisplayNode let fadeNode: ASDisplayNode + private var shimmerNode: VoiceChatTileShimmeringNode? private let titleNode: ImmediateTextNode private let iconNode: ASImageNode private var animationNode: VoiceChatMicrophoneNode? @@ -197,7 +216,9 @@ final class VoiceChatTileItemNode: ASDisplayNode { } @objc private func tap() { - self.item?.action() + if let item = self.item, item.videoReady { + item.action() + } } private func updateIsExtracted(_ isExtracted: Bool, transition: ContainedViewLayoutTransition) { @@ -232,6 +253,14 @@ final class VoiceChatTileItemNode: ASDisplayNode { } } + private var absoluteLocation: (CGRect, CGSize)? + func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.absoluteLocation = (rect, containerSize) + if let shimmerNode = self.shimmerNode { + shimmerNode.updateAbsoluteRect(rect, within: containerSize) + } + } + func update(size: CGSize, availableWidth: CGFloat, item: VoiceChatTileItem, transition: ContainedViewLayoutTransition) { guard self.validLayout?.0 != size || self.validLayout?.1 != availableWidth || self.item != item else { return @@ -244,6 +273,28 @@ final class VoiceChatTileItemNode: ASDisplayNode { let previousItem = self.item self.item = item + if !item.videoReady { + let shimmerNode: VoiceChatTileShimmeringNode + if let current = self.shimmerNode { + shimmerNode = current + } else { + shimmerNode = VoiceChatTileShimmeringNode(account: item.account, peer: item.peer) + self.contentNode.insertSubnode(shimmerNode, aboveSubnode: self.fadeNode) + self.shimmerNode = shimmerNode + + if let (rect, containerSize) = self.absoluteLocation { + shimmerNode.updateAbsoluteRect(rect, within: containerSize) + } + } + transition.updateFrame(node: shimmerNode, frame: CGRect(origin: CGPoint(), size: size)) + shimmerNode.update(shimmeringColor: UIColor.white, size: size, transition: transition) + } else if let shimmerNode = self.shimmerNode { + self.shimmerNode = nil + shimmerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak shimmerNode] _ in + shimmerNode?.removeFromSupernode() + }) + } + if let getAudioLevel = item.getAudioLevel { self.audioLevelDisposable.set((getAudioLevel() |> deliverOnMainQueue).start(next: { [weak self] value in @@ -255,11 +306,7 @@ final class VoiceChatTileItemNode: ASDisplayNode { } let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) - if item.speaking { - transition.updateAlpha(node: self.highlightNode, alpha: 1.0) - } else { - transition.updateAlpha(node: self.highlightNode, alpha: 0.0) - } + transition.updateAlpha(node: self.highlightNode, alpha: item.speaking ? 1.0 : 0.0) if previousItem?.videoEndpointId != item.videoEndpointId || self.videoNode == nil { if let current = self.videoNode { @@ -275,6 +322,7 @@ final class VoiceChatTileItemNode: ASDisplayNode { } let titleFont = Font.semibold(13.0) + let subtitleFont = Font.regular(13.0) let titleColor = UIColor.white var titleAttributedString: NSAttributedString? if let user = item.peer as? TelegramUser { @@ -303,6 +351,13 @@ final class VoiceChatTileItemNode: ASDisplayNode { } else if let channel = item.peer as? TelegramChannel { titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: titleColor) } + + var microphoneColor = UIColor.white + if let additionalText = item.additionalText, case let .text(text, _, color) = additionalText { + if case .destructive = color { + microphoneColor = destructiveColor + } + } self.titleNode.attributedText = titleAttributedString if case let .microphone(muted) = item.icon { @@ -315,7 +370,7 @@ final class VoiceChatTileItemNode: ASDisplayNode { self.infoNode.addSubnode(animationNode) } animationNode.alpha = 1.0 - animationNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, filled: true, color: UIColor.white), animated: true) + animationNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, filled: true, color: microphoneColor), animated: true) } else if let animationNode = self.animationNode { self.animationNode = nil animationNode.removeFromSupernode() @@ -342,7 +397,7 @@ final class VoiceChatTileItemNode: ASDisplayNode { if self.videoContainerNode.supernode === self.contentNode { if let videoNode = self.videoNode { - transition.updateFrame(node: videoNode, frame: bounds) + itemTransition.updateFrame(node: videoNode, frame: bounds) videoNode.updateLayout(size: size, layoutMode: .fillOrFitToSquare, transition: itemTransition) } transition.updateFrame(node: self.videoContainerNode, frame: bounds) @@ -584,3 +639,208 @@ class VoiceChatTileHighlightNode: ASDisplayNode { self.updateAnimations() } } + +private final class ShimmerEffectForegroundNode: ASDisplayNode { + private var currentForegroundColor: UIColor? + private let imageNodeContainer: ASDisplayNode + private let imageNode: ASImageNode + + private var absoluteLocation: (CGRect, CGSize)? + private var isCurrentlyInHierarchy = false + private var shouldBeAnimating = false + + private let size: CGFloat + + init(size: CGFloat) { + self.size = size + + self.imageNodeContainer = ASDisplayNode() + self.imageNodeContainer.isLayerBacked = true + + self.imageNode = ASImageNode() + self.imageNode.isLayerBacked = true + self.imageNode.displaysAsynchronously = false + self.imageNode.displayWithoutProcessing = true + self.imageNode.contentMode = .scaleToFill + + super.init() + + self.isLayerBacked = true + self.clipsToBounds = true + + self.imageNodeContainer.addSubnode(self.imageNode) + self.addSubnode(self.imageNodeContainer) + } + + override func didEnterHierarchy() { + super.didEnterHierarchy() + + self.isCurrentlyInHierarchy = true + self.updateAnimation() + } + + override func didExitHierarchy() { + super.didExitHierarchy() + + self.isCurrentlyInHierarchy = false + self.updateAnimation() + } + + func update(foregroundColor: UIColor) { + if let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) { + return + } + self.currentForegroundColor = foregroundColor + + let image = generateImage(CGSize(width: self.size, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.clip(to: CGRect(origin: CGPoint(), size: size)) + + let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor + let peakColor = foregroundColor.cgColor + + var locations: [CGFloat] = [0.0, 0.5, 1.0] + let colors: [CGColor] = [transparentColor, peakColor, transparentColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) + }) + self.imageNode.image = image + } + + func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize { + return + } + let sizeUpdated = self.absoluteLocation?.1 != containerSize + let frameUpdated = self.absoluteLocation?.0 != rect + self.absoluteLocation = (rect, containerSize) + + if sizeUpdated { + if self.shouldBeAnimating { + self.imageNode.layer.removeAnimation(forKey: "shimmer") + self.addImageAnimation() + } else { + self.updateAnimation() + } + } + + if frameUpdated { + self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize) + } + } + + private func updateAnimation() { + let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil + if shouldBeAnimating != self.shouldBeAnimating { + self.shouldBeAnimating = shouldBeAnimating + if shouldBeAnimating { + self.addImageAnimation() + } else { + self.imageNode.layer.removeAnimation(forKey: "shimmer") + } + } + } + + private func addImageAnimation() { + guard let containerSize = self.absoluteLocation?.1 else { + return + } + let gradientHeight: CGFloat = self.size + self.imageNode.frame = CGRect(origin: CGPoint(x: -gradientHeight, y: 0.0), size: CGSize(width: gradientHeight, height: containerSize.height)) + let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.width + gradientHeight) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = Float.infinity + animation.beginTime = 1.0 + self.imageNode.layer.add(animation, forKey: "shimmer") + } +} + +private class VoiceChatTileShimmeringNode: ASDisplayNode { + private let backgroundNode: ImageNode + private let effectNode: ShimmerEffectForegroundNode + + private let borderNode: ASDisplayNode + private var borderMaskView: UIView? + private let borderEffectNode: ShimmerEffectForegroundNode + + private var currentShimmeringColor: UIColor? + private var currentSize: CGSize? + + public init(account: Account, peer: Peer) { + self.backgroundNode = ImageNode(enableHasImage: false, enableEmpty: false, enableAnimatedTransition: true) + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.contentMode = .scaleAspectFill + + self.effectNode = ShimmerEffectForegroundNode(size: 220.0) + + self.borderNode = ASDisplayNode() + self.borderEffectNode = ShimmerEffectForegroundNode(size: 320.0) + + super.init() + + self.clipsToBounds = true + self.cornerRadius = backgroundCornerRadius + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.effectNode) + self.addSubnode(self.borderNode) + self.borderNode.addSubnode(self.borderEffectNode) + + self.backgroundNode.setSignal(peerAvatarCompleteImage(account: account, peer: peer, size: CGSize(width: 180.0, height: 180.0), round: false, font: Font.regular(16.0), drawLetters: false, fullSize: false, blurred: true)) + } + + public override func didLoad() { + super.didLoad() + + self.effectNode.layer.compositingFilter = "screenBlendMode" + self.borderEffectNode.layer.compositingFilter = "screenBlendMode" + + let borderMaskView = UIView() + borderMaskView.layer.borderWidth = 1.0 + borderMaskView.layer.borderColor = UIColor.white.cgColor + borderMaskView.layer.cornerRadius = backgroundCornerRadius + self.borderMaskView = borderMaskView + + if let size = self.currentSize { + borderMaskView.frame = CGRect(origin: CGPoint(), size: size) + } + + self.borderNode.view.mask = borderMaskView + + if #available(iOS 13.0, *) { + self.layer.cornerCurve = .continuous + borderMaskView.layer.cornerCurve = .continuous + } + } + + public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.effectNode.updateAbsoluteRect(rect, within: containerSize) + self.borderEffectNode.updateAbsoluteRect(rect, within: containerSize) + } + + public func update(shimmeringColor: UIColor, size: CGSize, transition: ContainedViewLayoutTransition) { + if let currentShimmeringColor = self.currentShimmeringColor, currentShimmeringColor.isEqual(shimmeringColor) && self.currentSize == size { + return + } + + self.currentShimmeringColor = shimmeringColor + self.currentSize = size + + let bounds = CGRect(origin: CGPoint(), size: size) + + self.effectNode.update(foregroundColor: shimmeringColor.withAlphaComponent(0.3)) + self.effectNode.frame = bounds + + self.borderEffectNode.update(foregroundColor: shimmeringColor.withAlphaComponent(0.5)) + self.borderEffectNode.frame = bounds + + transition.updateFrame(node: self.backgroundNode, frame: bounds) + transition.updateFrame(node: self.borderNode, frame: bounds) + if let borderMaskView = self.borderMaskView { + transition.updateFrame(view: borderMaskView, frame: bounds) + } + } +}