mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Render and play InstantPageBlock.audio in the InstantPage V2 renderer
(rich-data message bubbles + the rich send preview), where audio was
previously an inert grey placeholder.
- New InstantPageV2AudioContentNode replicates the standard music message
bubble (ChatMessageInteractiveFileNode's music layout): a SemanticStatusNode
control (album art via playerAlbumArt + play/pause) with a download/progress
overlay, title + "duration · performer" lines, exact fonts/colors from
theme.chat.message.{incoming|outgoing}. Tap is driven by a
UITapGestureRecognizer (ASControl .touchUpInside is cancelled by the chat
ListView's gesture system). V1's InstantPageAudioNode is unchanged.
- Playback runs on InstantPageMediaPlaylist with a discriminated, message-scoped
InstantPageMediaPlaylistId (.instantPage / .richMessage) so concurrent
rich-message audio bubbles don't collide; the big control's play/pause comes
from filteredPlaylistState, the overlay's download/progress from
messageMediaFileStatus, and fetch goes through the fetch manager.
- Rich-message audio fetches via a MessageReference (threaded through the V2
render context) instead of the synthesized webpage; FetchedMediaResource's
.message revalidation arm now also searches RichTextMessageAttribute instant
pages, so a stale instant-page audio/image reference can recover. Corrected a
dormant inverted InstantPagePlaylistLocation.isEqual.
- New .mediaAudio laid-out item + layout/reveal-cost arms; the audio block lays
out full-width at the file node's music normHeight.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
495 lines
21 KiB
Swift
495 lines
21 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import SwiftSignalKit
|
|
import AccountContext
|
|
import TelegramCore
|
|
import TelegramPresentationData
|
|
import GalleryUI
|
|
import UniversalMediaPlayer
|
|
|
|
// Mutable weak box: lets a wrapper hand its `openMedia` closure a back-reference to itself,
|
|
// filled in after `super.init` (when `self` becomes usable). SwiftSignalKit's `Weak<T>` requires
|
|
// a non-optional value at init time, so it can't be used here.
|
|
private final class WrapperRef {
|
|
weak var view: UIView?
|
|
}
|
|
|
|
// MARK: - Shared media node factory
|
|
|
|
// Hosts a V1 `InstantPageImageNode` inside a V2 UIView wrapper. The caller sizes its own
|
|
// frame from `item.frame` and adds the returned node's view as a subview.
|
|
func makeMediaWrapper(
|
|
frame: CGRect,
|
|
media: InstantPageMedia,
|
|
webPage: TelegramMediaWebpage,
|
|
attributes: [InstantPageImageAttribute],
|
|
renderContext: InstantPageV2RenderContext,
|
|
theme: InstantPageTheme,
|
|
openMedia: @escaping (InstantPageMedia) -> Void,
|
|
longPressMedia: @escaping (InstantPageMedia) -> Void
|
|
) -> InstantPageImageNode {
|
|
let imageNode = InstantPageImageNode(
|
|
context: renderContext.context,
|
|
sourceLocation: renderContext.sourceLocation,
|
|
theme: theme,
|
|
webPage: webPage,
|
|
media: media,
|
|
attributes: attributes,
|
|
interactive: true,
|
|
roundCorners: false,
|
|
fit: false,
|
|
openMedia: openMedia,
|
|
longPressMedia: longPressMedia,
|
|
activatePinchPreview: nil,
|
|
pinchPreviewFinished: nil,
|
|
imageReferenceForMedia: renderContext.imageReference,
|
|
fileReferenceForMedia: renderContext.fileReference,
|
|
getPreloadedResource: { _ in nil }
|
|
)
|
|
imageNode.frame = CGRect(origin: .zero, size: frame.size)
|
|
return imageNode
|
|
}
|
|
|
|
// Walks up the superview chain from `start` to find the nearest enclosing `InstantPageV2View`.
|
|
private func findEnclosingV2View(from start: UIView?) -> InstantPageV2View? {
|
|
var view: UIView? = start
|
|
while view != nil {
|
|
if let v2 = view as? InstantPageV2View {
|
|
return v2
|
|
}
|
|
view = view?.superview
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Registers `wrapper` in the root V2View's `mediaRegistry` under `mediaIndex`. The root is
|
|
// reached by walking up the superview chain to the nearest `InstantPageV2View`, then walking
|
|
// its `rootMediaRegistryHost` chain transitively (nested details blocks can leave an inner
|
|
// body's host pointing at an intermediate body — see `trueRegistryRoot`). No-op if the wrapper
|
|
// isn't yet attached to a V2View ancestor.
|
|
func registerInRootRegistry(wrapper: UIView, mediaIndex: Int) {
|
|
guard let v2 = findEnclosingV2View(from: wrapper.superview) else { return }
|
|
v2.trueRegistryRoot.mediaRegistry[mediaIndex] = Weak(wrapper)
|
|
}
|
|
|
|
// Routes a tap on `tapped` through `openInstantPageMedia`, sourcing sibling medias from the
|
|
// root V2View's `currentLayout`. No-op if the wrapper isn't currently in a V2View tree.
|
|
func handleOpenMediaTap(
|
|
tapped: InstantPageMedia,
|
|
wrapper: UIView,
|
|
renderContext: InstantPageV2RenderContext
|
|
) {
|
|
guard let v2 = findEnclosingV2View(from: wrapper.superview) else { return }
|
|
let root = v2.trueRegistryRoot
|
|
guard let layout = root.currentLayout else { return }
|
|
openInstantPageMedia(
|
|
media: tapped,
|
|
allMedias: layout.allMedias(),
|
|
webPage: renderContext.webpage,
|
|
context: renderContext.context,
|
|
userLocation: renderContext.sourceLocation.userLocation,
|
|
present: renderContext.present,
|
|
push: renderContext.push,
|
|
openUrl: renderContext.openUrl,
|
|
baseNavigationController: renderContext.baseNavigationController,
|
|
transitionArgsForMedia: { [weak root] tappedSibling -> GalleryTransitionArguments? in
|
|
guard let root else { return nil }
|
|
return root.transitionArgsFor(tappedSibling, addToTransitionSurface: { [weak root] view in
|
|
root?.superview?.addSubview(view)
|
|
})
|
|
},
|
|
hiddenMediaCallback: { [weak root] hidden in
|
|
root?.applyHiddenMedia(hidden)
|
|
}
|
|
)
|
|
}
|
|
|
|
// MARK: - Concrete wrapper classes
|
|
|
|
final class InstantPageV2MediaImageView: UIView, InstantPageItemView {
|
|
private(set) var item: InstantPageV2MediaImageItem
|
|
var itemFrame: CGRect { return self.item.frame }
|
|
let wrappedNode: InstantPageImageNode
|
|
|
|
init(item: InstantPageV2MediaImageItem, renderContext: InstantPageV2RenderContext, theme: InstantPageTheme) {
|
|
self.item = item
|
|
|
|
// The tap closure can't capture `[weak self]` before `super.init`, so we route through
|
|
// a `WrapperRef` box that gets filled in after `super.init`. The box's weak storage
|
|
// breaks the wrapper → wrappedNode → closure → wrapper retain cycle that would otherwise
|
|
// form (the wrapper owns wrappedNode, which owns the closure, which holds the wrapper).
|
|
let wrapperRef = WrapperRef()
|
|
let renderContextRef = renderContext
|
|
let openMedia: (InstantPageMedia) -> Void = { tapped in
|
|
guard let wrapper = wrapperRef.view else { return }
|
|
handleOpenMediaTap(tapped: tapped, wrapper: wrapper, renderContext: renderContextRef)
|
|
}
|
|
self.wrappedNode = makeMediaWrapper(
|
|
frame: item.frame,
|
|
media: item.media,
|
|
webPage: item.webPage,
|
|
attributes: item.attributes,
|
|
renderContext: renderContext,
|
|
theme: theme,
|
|
openMedia: openMedia,
|
|
longPressMedia: { _ in }
|
|
)
|
|
|
|
super.init(frame: item.frame)
|
|
self.backgroundColor = .clear // structural
|
|
self.addSubview(self.wrappedNode.view) // structural
|
|
wrapperRef.view = self // structural: back-reference for the openMedia closure
|
|
self.update(item: item, theme: theme, renderContext: renderContext)
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
override func didMoveToWindow() {
|
|
super.didMoveToWindow()
|
|
guard self.window != nil else { return }
|
|
registerInRootRegistry(wrapper: self, mediaIndex: self.item.media.index)
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
self.wrappedNode.frame = self.bounds
|
|
}
|
|
|
|
func update(item: InstantPageV2MediaImageItem, theme: InstantPageTheme, renderContext: InstantPageV2RenderContext) {
|
|
self.item = item
|
|
self.layer.cornerRadius = item.cornerRadius
|
|
self.clipsToBounds = item.cornerRadius > 0.0
|
|
let strings = renderContext.context.sharedContext.currentPresentationData.with { $0 }.strings
|
|
self.wrappedNode.update(strings: strings, theme: theme)
|
|
}
|
|
|
|
func instantPageTransitionNode(for media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
|
return self.wrappedNode.transitionNode(media: media)
|
|
}
|
|
|
|
func instantPageUpdateHiddenMedia(_ media: InstantPageMedia?) {
|
|
self.wrappedNode.updateHiddenMedia(media: media)
|
|
}
|
|
}
|
|
|
|
final class InstantPageV2MediaVideoView: UIView, InstantPageItemView {
|
|
private(set) var item: InstantPageV2MediaVideoItem
|
|
var itemFrame: CGRect { return self.item.frame }
|
|
let wrappedNode: InstantPageImageNode
|
|
|
|
init(item: InstantPageV2MediaVideoItem, renderContext: InstantPageV2RenderContext, theme: InstantPageTheme) {
|
|
self.item = item
|
|
|
|
let wrapperRef = WrapperRef()
|
|
let renderContextRef = renderContext
|
|
let openMedia: (InstantPageMedia) -> Void = { tapped in
|
|
guard let wrapper = wrapperRef.view else { return }
|
|
handleOpenMediaTap(tapped: tapped, wrapper: wrapper, renderContext: renderContextRef)
|
|
}
|
|
self.wrappedNode = makeMediaWrapper(
|
|
frame: item.frame,
|
|
media: item.media,
|
|
webPage: item.webPage,
|
|
attributes: item.attributes,
|
|
renderContext: renderContext,
|
|
theme: theme,
|
|
openMedia: openMedia,
|
|
longPressMedia: { _ in }
|
|
)
|
|
|
|
super.init(frame: item.frame)
|
|
self.backgroundColor = .clear // structural
|
|
self.addSubview(self.wrappedNode.view) // structural
|
|
wrapperRef.view = self // structural: back-reference for the openMedia closure
|
|
self.update(item: item, theme: theme, renderContext: renderContext)
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
override func didMoveToWindow() {
|
|
super.didMoveToWindow()
|
|
guard self.window != nil else { return }
|
|
registerInRootRegistry(wrapper: self, mediaIndex: self.item.media.index)
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
self.wrappedNode.frame = self.bounds
|
|
}
|
|
|
|
func update(item: InstantPageV2MediaVideoItem, theme: InstantPageTheme, renderContext: InstantPageV2RenderContext) {
|
|
self.item = item
|
|
self.layer.cornerRadius = item.cornerRadius
|
|
self.clipsToBounds = item.cornerRadius > 0.0
|
|
let strings = renderContext.context.sharedContext.currentPresentationData.with { $0 }.strings
|
|
self.wrappedNode.update(strings: strings, theme: theme)
|
|
}
|
|
|
|
func instantPageTransitionNode(for media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
|
return self.wrappedNode.transitionNode(media: media)
|
|
}
|
|
|
|
func instantPageUpdateHiddenMedia(_ media: InstantPageMedia?) {
|
|
self.wrappedNode.updateHiddenMedia(media: media)
|
|
}
|
|
}
|
|
|
|
final class InstantPageV2MediaMapView: UIView, InstantPageItemView {
|
|
private(set) var item: InstantPageV2MediaMapItem
|
|
var itemFrame: CGRect { return self.item.frame }
|
|
let wrappedNode: InstantPageImageNode
|
|
|
|
init(item: InstantPageV2MediaMapItem, renderContext: InstantPageV2RenderContext, theme: InstantPageTheme) {
|
|
self.item = item
|
|
|
|
let wrapperRef = WrapperRef()
|
|
let renderContextRef = renderContext
|
|
let openMedia: (InstantPageMedia) -> Void = { tapped in
|
|
guard let wrapper = wrapperRef.view else { return }
|
|
handleOpenMediaTap(tapped: tapped, wrapper: wrapper, renderContext: renderContextRef)
|
|
}
|
|
self.wrappedNode = makeMediaWrapper(
|
|
frame: item.frame,
|
|
media: item.media,
|
|
webPage: item.webPage,
|
|
attributes: item.attributes,
|
|
renderContext: renderContext,
|
|
theme: theme,
|
|
openMedia: openMedia,
|
|
longPressMedia: { _ in }
|
|
)
|
|
|
|
super.init(frame: item.frame)
|
|
self.backgroundColor = .clear // structural
|
|
self.addSubview(self.wrappedNode.view) // structural
|
|
wrapperRef.view = self // structural: back-reference for the openMedia closure
|
|
self.update(item: item, theme: theme, renderContext: renderContext)
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
override func didMoveToWindow() {
|
|
super.didMoveToWindow()
|
|
guard self.window != nil else { return }
|
|
registerInRootRegistry(wrapper: self, mediaIndex: self.item.media.index)
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
self.wrappedNode.frame = self.bounds
|
|
}
|
|
|
|
func update(item: InstantPageV2MediaMapItem, theme: InstantPageTheme, renderContext: InstantPageV2RenderContext) {
|
|
self.item = item
|
|
self.layer.cornerRadius = item.cornerRadius
|
|
self.clipsToBounds = item.cornerRadius > 0.0
|
|
let strings = renderContext.context.sharedContext.currentPresentationData.with { $0 }.strings
|
|
self.wrappedNode.update(strings: strings, theme: theme)
|
|
}
|
|
|
|
func instantPageTransitionNode(for media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
|
return self.wrappedNode.transitionNode(media: media)
|
|
}
|
|
|
|
func instantPageUpdateHiddenMedia(_ media: InstantPageMedia?) {
|
|
self.wrappedNode.updateHiddenMedia(media: media)
|
|
}
|
|
}
|
|
|
|
final class InstantPageV2MediaCoverImageView: UIView, InstantPageItemView {
|
|
private(set) var item: InstantPageV2MediaCoverImageItem
|
|
var itemFrame: CGRect { return self.item.frame }
|
|
let wrappedNode: InstantPageImageNode
|
|
|
|
init(item: InstantPageV2MediaCoverImageItem, renderContext: InstantPageV2RenderContext, theme: InstantPageTheme) {
|
|
self.item = item
|
|
|
|
let wrapperRef = WrapperRef()
|
|
let renderContextRef = renderContext
|
|
let openMedia: (InstantPageMedia) -> Void = { tapped in
|
|
guard let wrapper = wrapperRef.view else { return }
|
|
handleOpenMediaTap(tapped: tapped, wrapper: wrapper, renderContext: renderContextRef)
|
|
}
|
|
self.wrappedNode = makeMediaWrapper(
|
|
frame: item.frame,
|
|
media: item.media,
|
|
webPage: item.webPage,
|
|
attributes: item.attributes,
|
|
renderContext: renderContext,
|
|
theme: theme,
|
|
openMedia: openMedia,
|
|
longPressMedia: { _ in }
|
|
)
|
|
|
|
super.init(frame: item.frame)
|
|
self.backgroundColor = .clear // structural
|
|
self.addSubview(self.wrappedNode.view) // structural
|
|
wrapperRef.view = self // structural: back-reference for the openMedia closure
|
|
self.update(item: item, theme: theme, renderContext: renderContext)
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
override func didMoveToWindow() {
|
|
super.didMoveToWindow()
|
|
guard self.window != nil else { return }
|
|
registerInRootRegistry(wrapper: self, mediaIndex: self.item.media.index)
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
self.wrappedNode.frame = self.bounds
|
|
}
|
|
|
|
func update(item: InstantPageV2MediaCoverImageItem, theme: InstantPageTheme, renderContext: InstantPageV2RenderContext) {
|
|
self.item = item
|
|
self.layer.cornerRadius = item.cornerRadius
|
|
self.clipsToBounds = item.cornerRadius > 0.0
|
|
let strings = renderContext.context.sharedContext.currentPresentationData.with { $0 }.strings
|
|
self.wrappedNode.update(strings: strings, theme: theme)
|
|
}
|
|
|
|
func instantPageTransitionNode(for media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
|
return self.wrappedNode.transitionNode(media: media)
|
|
}
|
|
|
|
func instantPageUpdateHiddenMedia(_ media: InstantPageMedia?) {
|
|
self.wrappedNode.updateHiddenMedia(media: media)
|
|
}
|
|
}
|
|
|
|
// Sets up shared-media playback for an audio tap. Mirrors V1's
|
|
// `InstantPageControllerNode.openMedia(_:)` audio branch: collect the page's voice/music
|
|
// medias from the root V2View's current layout, build an `InstantPageMediaPlaylist` keyed by
|
|
// `playlistId`, and start playback. No-op if the wrapper isn't currently in a V2View tree.
|
|
func handleOpenAudioTap(
|
|
tapped: InstantPageMedia,
|
|
wrapper: UIView,
|
|
renderContext: InstantPageV2RenderContext,
|
|
playlistId: InstantPageMediaPlaylistId
|
|
) {
|
|
guard case let .file(tappedFile) = tapped.media, tappedFile.isVoice || tappedFile.isMusic else { return }
|
|
guard let v2 = findEnclosingV2View(from: wrapper.superview) else { return }
|
|
let root = v2.trueRegistryRoot
|
|
guard let layout = root.currentLayout else { return }
|
|
|
|
var audioMedias: [InstantPageMedia] = []
|
|
var initialIndex = 0
|
|
for media in layout.allMedias() {
|
|
if case let .file(file) = media.media, (file.isVoice || file.isMusic) {
|
|
if media.index == tapped.index {
|
|
initialIndex = audioMedias.count
|
|
}
|
|
audioMedias.append(media)
|
|
}
|
|
}
|
|
|
|
let playlist = InstantPageMediaPlaylist(
|
|
playlistId: playlistId,
|
|
webPage: renderContext.webpage,
|
|
messageReference: renderContext.message,
|
|
items: audioMedias,
|
|
initialItemIndex: initialIndex
|
|
)
|
|
renderContext.context.sharedContext.mediaManager.setPlaylist(
|
|
(renderContext.context, playlist),
|
|
type: tappedFile.isVoice ? .voice : .music,
|
|
control: .playback(.play)
|
|
)
|
|
}
|
|
|
|
final class InstantPageV2MediaAudioView: UIView, InstantPageItemView {
|
|
private(set) var item: InstantPageV2MediaAudioItem
|
|
var itemFrame: CGRect { return self.item.frame }
|
|
private let audioNode: InstantPageV2AudioContentNode
|
|
|
|
init(item: InstantPageV2MediaAudioItem, renderContext: InstantPageV2RenderContext, theme: InstantPageTheme) {
|
|
self.item = item
|
|
|
|
// `.richMessage(messageId)` isolates playback state per chat message; the preview (no
|
|
// message) falls back to the webpage-keyed id (only one preview is ever on screen).
|
|
let playlistId: InstantPageMediaPlaylistId
|
|
if let messageId = renderContext.message?.id {
|
|
playlistId = .richMessage(messageId: messageId)
|
|
} else {
|
|
playlistId = .instantPage(webpageId: renderContext.webpage.webpageId)
|
|
}
|
|
|
|
let wrapperRef = WrapperRef()
|
|
let renderContextRef = renderContext
|
|
let itemMedia = item.media
|
|
|
|
let presentationData = renderContext.context.sharedContext.currentPresentationData.with { $0 }
|
|
let incoming = renderContext.message?.isIncoming == true
|
|
let audioFile: TelegramMediaFile
|
|
if case let .file(f) = item.media.media { audioFile = f } else { audioFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: EmptyMediaResource(), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/mpeg", size: nil, attributes: [], alternativeRepresentations: []) }
|
|
self.audioNode = InstantPageV2AudioContentNode(context: renderContext.context, message: renderContext.message, file: audioFile, incoming: incoming, presentationData: presentationData)
|
|
|
|
super.init(frame: item.frame)
|
|
self.backgroundColor = .clear // structural
|
|
self.addSubview(self.audioNode.view) // structural
|
|
wrapperRef.view = self // structural: back-reference for the play closure
|
|
|
|
self.audioNode.play = {
|
|
guard let wrapper = wrapperRef.view else { return }
|
|
handleOpenAudioTap(tapped: itemMedia, wrapper: wrapper, renderContext: renderContextRef, playlistId: playlistId)
|
|
}
|
|
|
|
let fetchContext = renderContext.context
|
|
let fetchMessage = renderContext.message
|
|
let fetchMedia = item.media
|
|
self.audioNode.fetch = {
|
|
guard case let .file(file) = fetchMedia.media, let message = fetchMessage, let messageId = message.id else { return }
|
|
// Route through the fetch manager (not freeMediaFileInteractiveFetched) so the
|
|
// messageMediaFileStatus signal — which keys progress off the fetch manager's
|
|
// `hasEntry` — surfaces .Fetching, letting the overlay show the animated ring.
|
|
let _ = messageMediaFileInteractiveFetched(fetchManager: fetchContext.fetchManager, messageId: messageId, messageReference: message, file: file, userInitiated: true, priority: .userInitiated).startStandalone()
|
|
}
|
|
|
|
let mediaForPlayback = item.media
|
|
let playlistTypeForPlayback: MediaManagerPlayerType
|
|
if case let .file(f) = mediaForPlayback.media, f.isVoice { playlistTypeForPlayback = .voice } else { playlistTypeForPlayback = .music }
|
|
let contextForPlayback = renderContext.context
|
|
|
|
self.audioNode.togglePlayPause = {
|
|
contextForPlayback.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: playlistTypeForPlayback)
|
|
}
|
|
|
|
let stateSignal = contextForPlayback.sharedContext.mediaManager.filteredPlaylistState(accountId: contextForPlayback.account.id, playlistId: playlistId, itemId: InstantPageMediaPlaylistItemId(index: mediaForPlayback.index), type: playlistTypeForPlayback)
|
|
self.audioNode.setPlaybackStatusSignal(stateSignal)
|
|
|
|
self.update(item: item, theme: theme, renderContext: renderContext)
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
self.audioNode.frame = self.bounds
|
|
self.audioNode.updateLayout(width: self.bounds.width)
|
|
}
|
|
|
|
func update(item: InstantPageV2MediaAudioItem, theme: InstantPageTheme, renderContext: InstantPageV2RenderContext) {
|
|
self.item = item
|
|
let presentationData = renderContext.context.sharedContext.currentPresentationData.with { $0 }
|
|
let incoming = renderContext.message?.isIncoming == true
|
|
self.audioNode.updatePresentationData(presentationData, incoming: incoming)
|
|
self.audioNode.updateLayout(width: self.bounds.width)
|
|
}
|
|
|
|
// Audio is not a gallery item: explicit nil/no-op witnesses (per the existing pattern of
|
|
// explicit per-class witnesses rather than a shared protocol-extension override).
|
|
func instantPageTransitionNode(for media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
|
return nil
|
|
}
|
|
|
|
func instantPageUpdateHiddenMedia(_ media: InstantPageMedia?) {
|
|
}
|
|
}
|