diff --git a/Telegram/Telegram-iOS/Resources/UserAvatarMask.tgs b/Telegram/Telegram-iOS/Resources/UserAvatarMask.tgs new file mode 100644 index 0000000000..4bd975f15c Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/UserAvatarMask.tgs differ diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index b5f4eb2dc5..4d73371cf8 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -1023,6 +1023,9 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { final class PeerInfoAvatarListNode: ASDisplayNode { private let isSettings: Bool let pinchSourceNode: PinchSourceContainerNode + let bottomCoverNode: ASDisplayNode + fileprivate let maskNode: DynamicIslandMaskNode + fileprivate let topCoverNode: DynamicIslandBlurNode let avatarContainerNode: PeerInfoAvatarTransformContainerNode let listContainerTransformNode: ASDisplayNode let listContainerNode: PeerInfoAvatarListContainerNode @@ -1038,6 +1041,10 @@ final class PeerInfoAvatarListNode: ASDisplayNode { init(context: AccountContext, readyWhenGalleryLoads: Bool, isSettings: Bool) { self.isSettings = isSettings + self.bottomCoverNode = ASDisplayNode() + self.bottomCoverNode.backgroundColor = .black + + self.maskNode = DynamicIslandMaskNode(size: CGSize(width: 512.0, height: 512.0)) self.pinchSourceNode = PinchSourceContainerNode() self.avatarContainerNode = PeerInfoAvatarTransformContainerNode(context: context) @@ -1046,12 +1053,16 @@ final class PeerInfoAvatarListNode: ASDisplayNode { self.listContainerNode.clipsToBounds = true self.listContainerNode.isHidden = true + self.topCoverNode = DynamicIslandBlurNode() + super.init() + self.addSubnode(self.bottomCoverNode) self.addSubnode(self.pinchSourceNode) self.pinchSourceNode.contentNode.addSubnode(self.avatarContainerNode) self.listContainerTransformNode.addSubnode(self.listContainerNode) self.pinchSourceNode.contentNode.addSubnode(self.listContainerTransformNode) + self.addSubnode(self.topCoverNode) let avatarReady = (self.avatarContainerNode.avatarNode.ready |> mapToSignal { _ -> Signal in @@ -2542,7 +2553,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { private var currentCredibilityIcon: CredibilityIcon? private var currentPanelStatusData: PeerInfoStatusData? - func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, threadData: MessageHistoryThreadData?, notificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: (PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, metrics: LayoutMetrics, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat { + func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, threadData: MessageHistoryThreadData?, notificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: (PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat { self.state = state self.peer = peer self.threadData = threadData @@ -2714,7 +2725,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { var transitionSourceTitleFrame = CGRect() var transitionSourceSubtitleFrame = CGRect() - let avatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize) / 2.0), y: statusBarHeight + 13.0), size: CGSize(width: avatarSize, height: avatarSize)) + let avatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize) / 2.0), y: statusBarHeight + 22.0), size: CGSize(width: avatarSize, height: avatarSize)) self.backgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) @@ -3103,7 +3114,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { let titleMinScale: CGFloat = 0.6 let subtitleMinScale: CGFloat = 0.8 - let avatarMinScale: CGFloat = 0.7 + let avatarMinScale: CGFloat = 0.55 let apparentTitleLockOffset = (1.0 - titleCollapseFraction) * 0.0 + titleCollapseFraction * titleMaxLockOffset @@ -3197,13 +3208,13 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } - let avatarCornerRadius: CGFloat + var isForum = false if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { - avatarCornerRadius = floor(avatarSize * 0.25) - } else { - avatarCornerRadius = avatarSize / 2.0 + isForum = true } - + + let avatarCornerRadius: CGFloat = isForum ? floor(avatarSize * 0.25) : avatarSize / 2.0 + if self.isAvatarExpanded { self.avatarListNode.listContainerNode.isHidden = false if let transitionSourceAvatarFrame = transitionSourceAvatarFrame { @@ -3294,6 +3305,29 @@ final class PeerInfoHeaderNode: ASDisplayNode { transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerTransformNode, scale: avatarListContainerScale) } + if deviceMetrics.hasDynamicIsland && !isForum && self.forumTopicThreadId == nil { + self.avatarListNode.maskNode.frame = CGRect(origin: CGPoint(x: -85.5, y: -self.avatarListNode.frame.minY + 48.0), size: CGSize(width: 171.0, height: 171.0)) + self.avatarListNode.bottomCoverNode.frame = self.avatarListNode.maskNode.frame + self.avatarListNode.topCoverNode.frame = self.avatarListNode.maskNode.frame + + let maskValue = max(0.0, min(1.0, contentOffset / 120.0)) + if maskValue > 0.03 { + self.avatarListNode.bottomCoverNode.isHidden = false + self.avatarListNode.topCoverNode.isHidden = false + self.avatarListNode.view.mask = self.avatarListNode.maskNode.view + } else { + self.avatarListNode.bottomCoverNode.isHidden = true + self.avatarListNode.topCoverNode.isHidden = true + self.avatarListNode.view.mask = nil + } + self.avatarListNode.maskNode.update(maskValue) + self.avatarListNode.topCoverNode.update(maskValue) + } else { + self.avatarListNode.bottomCoverNode.isHidden = true + self.avatarListNode.topCoverNode.isHidden = true + self.avatarListNode.view.mask = nil + } + self.avatarListNode.listContainerNode.update(size: expandedAvatarListSize, peer: peer, isExpanded: self.isAvatarExpanded, transition: transition) if self.avatarListNode.listContainerNode.isCollapsing && !self.ignoreCollapse { self.avatarListNode.avatarContainerNode.canAttachVideo = false @@ -3639,3 +3673,83 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } } + +private class DynamicIslandMaskNode: ManagedAnimationNode { + func update(_ value: CGFloat) { + let lowerBound = 0 + let upperBound = 180 + let frameIndex = lowerBound + Int(value * CGFloat(upperBound - lowerBound)) + + self.trackTo(item: ManagedAnimationItem(source: .local("UserAvatarMask"), frames: .range(startFrame: frameIndex, endFrame: frameIndex), duration: 0.001)) + } +} + +private class DynamicIslandBlurNode: ASDisplayNode { + private var effectView: UIVisualEffectView? + private let fadeNode = ASDisplayNode() + private let gradientNode = ASImageNode() + + override func didLoad() { + super.didLoad() + + self.fadeNode.backgroundColor = .black + self.fadeNode.alpha = 0.0 + + self.gradientNode.displaysAsynchronously = false + let gradientImage = generateImage(CGSize(width: 100.0, height: 100.0), rotatedContext: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + var locations: [CGFloat] = [0.0, 0.87, 1.0] + let colors: [CGColor] = [UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 1.0).cgColor] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + let endRadius: CGFloat = 90.0 + let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0 + 38.0) + context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: endRadius, options: .drawsAfterEndLocation) + }) + self.gradientNode.image = gradientImage + + let effectView = UIVisualEffectView(effect: nil) + self.effectView = effectView + self.view.insertSubview(effectView, at: 0) + + self.addSubnode(self.gradientNode) + self.addSubnode(self.fadeNode) + } + + func prepare() { + guard let effectView = self.effectView, effectView.layer.animation(forKey: "effect") == nil else { + return + } + UIView.animate(withDuration: 1.0) { + effectView.effect = UIBlurEffect(style: .dark) + } + effectView.layer.speed = 0.0 + } + + func update(_ value: CGFloat) { + if value > 0.0 { + self.prepare() + self.effectView?.layer.timeOffset = max(0.0, -0.1 + value * 1.1) + } else { + self.effectView?.layer.removeAllAnimations() + self.effectView?.layer.speed = 1.0 + self.effectView?.layer.timeOffset = 0.0 + self.effectView?.effect = nil + } + + self.fadeNode.alpha = min(1.0, max(0.0, -0.25 + value * 1.55)) + } + + override func layout() { + super.layout() + + self.effectView?.frame = self.bounds + self.fadeNode.frame = self.bounds + + let gradientSize = CGSize(width: 100.0, height: 100.0) + self.gradientNode.frame = CGRect(origin: CGPoint(x: (self.bounds.width - gradientSize.width) / 2.0, y: 0.0), size: gradientSize) + } +} diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index a0390749ad..585697f82a 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -8688,7 +8688,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } let headerInset = sectionInset - var headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, notificationSettings: self.data?.notificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, transition: transition, additive: additive) + var headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, notificationSettings: self.data?.notificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: additive) if !self.isSettings && !self.state.isEditing { headerHeight += 71.0 } @@ -9053,7 +9053,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } let headerInset = sectionInset - let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, notificationSettings: self.data?.notificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, transition: transition, additive: additive) + let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, notificationSettings: self.data?.notificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: additive) } let paneAreaExpansionDistance: CGFloat = 32.0 @@ -10262,7 +10262,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig } let headerInset = sectionInset - topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, threadData: self.screenNode.data?.threadData, notificationSettings: self.screenNode.data?.notificationSettings, globalNotificationSettings: self.screenNode.data?.globalNotificationSettings, statusData: self.screenNode.data?.status, panelStatusData: (nil, nil, nil), isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, metrics: layout.metrics, transition: transition, additive: false) + topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, threadData: self.screenNode.data?.threadData, notificationSettings: self.screenNode.data?.notificationSettings, globalNotificationSettings: self.screenNode.data?.globalNotificationSettings, statusData: self.screenNode.data?.status, panelStatusData: (nil, nil, nil), isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: false) } let titleScale = (fraction * previousTitleNode.view.bounds.height + (1.0 - fraction) * self.headerNode.titleNodeRawContainer.bounds.height) / previousTitleNode.view.bounds.height