Rich-message media in gallery/shared-media/preview pipelines via Message.effectiveMedia

Add Message/EngineMessage.effectiveMedia (= message.media when non-empty, else
richText.instantPage.allMedia()) and route the media-consuming sites through it
so a rich message's instant-page media participates in the same pipelines as
normal message.media: shared-media grids/file-rows, search media grid, gallery
open + item nodes + footer, the peer audio/voice playlist, secret-media preview,
resource-by-id resolution, recent downloads, downloaded-media store, delete-time
resource cleanup, cache-usage stats, the in-chat download manager, and the
context-menu / share actions (Save to Camera Roll, copy image, save audio/music
to files). For normal messages effectiveMedia == message.media, so each swap is
behavior-preserving; rich messages render their own bubble via
ChatMessageRichDataBubbleContentNode (not the text/file bubbles), so those paths
are deliberately untouched, as are the forward path (the attribute travels with
the forward) and the markdown-based rich-edit path. First-media scope for now.

See docs/instantpage-richtext.md for the full architecture + invariants.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
isaac 2026-06-04 23:46:56 +02:00
parent c95e014681
commit 0050cc7a08
30 changed files with 130 additions and 64 deletions

View file

@ -62,7 +62,7 @@ Rare exceptions: top-level view-controller views integrating with the system's f
## InstantPage V2 & rich-text messages
Typed markdown with structure the regular message-entity set can't represent (headings, lists, tables, formulas, nested blockquotes) is sent as a **rich message** — a `RichTextMessageAttribute` carrying an `InstantPage`, drawn by `ChatMessageRichDataBubbleContentNode` via the **InstantPage V2** renderer (with AI-streaming progressive reveal, inline custom emoji, and entity cases). The detailed architecture and non-obvious invariants — streaming reveal, V2 table/text-box layout, custom-emoji & entity round-trips, task-list checkboxes, nested blockquotes, thinking blocks, and the markdown send / edit / copy / paste paths — live in [`docs/instantpage-richtext.md`](docs/instantpage-richtext.md).
Typed markdown with structure the regular message-entity set can't represent (headings, lists, tables, formulas, nested blockquotes) is sent as a **rich message** — a `RichTextMessageAttribute` carrying an `InstantPage`, drawn by `ChatMessageRichDataBubbleContentNode` via the **InstantPage V2** renderer (with AI-streaming progressive reveal, inline custom emoji, and entity cases). The detailed architecture and non-obvious invariants — streaming reveal, V2 table/text-box layout, custom-emoji & entity round-trips, task-list checkboxes, nested blockquotes, thinking blocks, the markdown send / edit / copy / paste paths, and surfacing rich-message media through the shared-media/gallery/preview pipelines via `Message.effectiveMedia` — live in [`docs/instantpage-richtext.md`](docs/instantpage-richtext.md).
## Postbox → TelegramEngine refactor (in progress)

View file

@ -426,3 +426,44 @@ A server-sent rich message can arrive **partial** when the content is long: the
- **`showMoreExpanded` is part of BOTH layout caches.** It is in the `currentPageLayout` cache key **and** the `pageView` content key (`pageViewMessageKey`). This is required because the cached-expand path (full page already on the attribute) performs **no postbox write**, so `stableVersion` does not bump — without the key, the cached partial layout/content would shadow the expand.
- **Tap (`activateShowMore`):** if `fullInstantPage` is already cached → set expanded + `requestMessageUpdate` immediately (no network, no shimmer); otherwise shimmer the link and fetch, expanding only once the full page lands. Guards against a second in-flight request and against re-expanding.
- **Expand grows the bubble downward in screen space** (top fixed) via `info?.setInvertOffsetDirection()` on the `ListViewItemApply` in the apply closure, fired only on the `appliedShowMoreExpanded → showMoreExpanded` transition (never on first apply). Same mechanism as `ChatMessageInteractiveFileNode`'s audio-transcription expand and the text/fact-check bubbles; the ListView clamps it to what fits.
## Rich-message media in the gallery / shared-media / preview pipelines (`Message.effectiveMedia`)
A rich message's media (images / videos / audio / documents) lives in `attribute.instantPage.media`, **not** in `message.media` (which is empty — rich messages are sent with `text: ""` and no media reference). To make that media participate in the *same* shared-media-index, gallery, file-list, playback, download, and save/copy pipelines that normal `message.media` flows through, there is one shared accessor and a set of opt-in call-site swaps.
### The accessor
`Message.effectiveMedia: [Media]` (+ a delegating `EngineMessage.effectiveMedia`) in `submodules/TelegramCore/Sources/Utils/MessageUtils.swift`:
```swift
var effectiveMedia: [Media] {
if !self.media.isEmpty { return self.media } // normal message: identical to message.media
if let richText = self.richText { return richText.instantPage.allMedia() } // rich: the instant-page media
return self.media
}
```
`Message.richText` (same file) is already a typed `RichTextMessageAttribute?`; `InstantPage.allMedia()` (`SyncCore_InstantPage.swift`) recursively gathers media from the page's blocks (audio/collage/cover/details/image/list/slideshow/video) via its `[MediaId: Media]` dict. **For a normal message `effectiveMedia == message.media`**, so swapping a `message.media` read for `message.effectiveMedia` is behavior-preserving for non-rich content and only adds the rich media where the site should consider it. **Scope is first-media** for now (call sites keep their `.first` / iterate-and-break logic; the helper returns all media but callers stop at the first match — the `//TODO:rewrite to take all media` markers remain).
### Where things live
| Layer | What |
|---|---|
| **Discovery / index** | `tagsForStoreMessage` (`StoreMessage_Telegram.swift`) indexes rich media into `MessageTags` (photo/video/gif/voice/file). **This is the linchpin**: it makes rich messages *appear* in every tag-queried surface (shared-media tabs, search, downloads) — which is exactly why each rendering-side site below then needs `effectiveMedia`, or it renders the surfaced message blank. |
| **Extraction helper** | `Message.effectiveMedia` (above). |
| **Shared-media grids / rows** | `PeerInfoVisualMediaPaneNode`, `PeerInfoGifPaneNode`, `ListMessageItem` (row-type selection) + `ListMessageFileItemNode` (file/music/voice row), `ChatListSearchMediaNode` (search media grid). |
| **Gallery open + items** | `GalleryController` (`tagsForMessage` + `mediaForMessage` — the duplicated `message.media`/`message.richText` blocks were collapsed into one `effectiveMedia` loop), `GalleryData.chatMessageGalleryControllerData`, `SecretMediaPreviewController` (its own local `mediaForMessage`), and the gallery item nodes `ChatDocumentGalleryItem` / `ChatExternalFileGalleryItem` / `ChatAnimationGalleryItem` (these re-derive from `message.media` in `node()`, so a rich doc/animation rendered **blank** without the swap) + `UniversalVideoGalleryItem` secondary affordances + `ChatItemGalleryFooterContentNode`. |
| **Playback** | `PeerMessagesMediaPlaylist.extractFileMedia` (the peer music/voice playlist), `OverlayAudioPlayerControllerNode` (audio context menu). |
| **Resolution / downloads / cleanup** | `FetchedMediaResource.findMediaResourceById(message:)`, `SyncCore_RecentDownloadItem`, `StoreDownloadedMedia`, `DeleteMessages.addMessageMediaResourceIdsToRemove(message:)` (rich media was **leaking on delete**), `CollectCacheUsageStats`, `ChatHistoryListNode` (download manager), `ChatListSearch{ListPaneNode,ContainerNode}`. |
| **Actions** | `ChatInterfaceStateContextMenus` (Save-to-Camera-Roll, copy-image, save-audio/music-to-files, debug/premium), `ChatControllerNode` (post-suggestion media ref), `ChatControllerLoadDisplayNode` (edit send-validation), `ShareController.saveToCameraRoll`. |
### Non-obvious invariants
- **The tag-index change is what creates the work.** `tagsForStoreMessage` surfacing rich messages into tag-queried lists, *without* the rendering-side `effectiveMedia` swaps, produces visible **blank cells / blank rows / wrong row types**. Index and render must move together.
- **The rich message's own in-chat bubble + in-bubble gallery do NOT read `message.media`** — a rich message renders via `ChatMessageRichDataBubbleContentNode` (InstantPage V2), in-bubble image/video tap opens `InstantPageGalleryController` (reads the instant page directly), and in-bubble audio uses `InstantPageV2AudioContentNode`. So the text-bubble / interactive-file / interactive-media nodes' `message.media` reads are **never reached by a rich message** and are deliberately left alone.
- **Do NOT route the FORWARD path through `effectiveMedia`** (`ChatControllerNode` `forwardedMessages` ~556/560/568). The `RichTextMessageAttribute` already travels with a forward, so the forwarded copy reconstructs from the attribute; injecting the instant-page media as top-level `message.media` there would **double-render** (rich bubble + a separate media attachment). That `message.media` processing is caption-hiding / poll-stripping only, both irrelevant to rich — left as `message.media`.
- **Rich messages are edited as reconstructed MARKDOWN, not via the media-caption edit path.** So `ChatControllerLoadDisplayNode`'s edit caption-max-length / original-media-reference reads (~1241/1775/4463) stay on `message.media` — they belong to the `.media` edit state a rich message never enters. (The send-*validation* `.contains` at ~2273 IS swapped, so an edit that leaves only media isn't wrongly rejected.)
- **`RichTextMessageAttribute.associatedMediaIds` stays `[]` — intentionally.** `MessageHistoryTable` resolves `associatedMediaIds` via `getMedia(id)` in the postbox **media table**, but rich-message media is embedded inside the attribute blob, not the table — so returning the keys would be a no-op without also inserting the media into the table. The embedded-blob approach is self-contained.
- **`fullInstantPage` is not indexed** (the server doesn't index it either, and it's fetched on demand after store-time). The first media lives in the partial `instantPage` anyway.
- **Only switch the loop SOURCE, never the per-type branches.** Many swapped loops still contain `TelegramMediaPoll`/`TelegramMediaPaidContent`/`TelegramMediaWebpage` branches that rich messages never match — that's fine and intentional; only the `for … in <msg>.media` source changes.
- **Build-only completeness gate.** Every swap is type-identical (`[Media]``[Media]`), so the only compile risk is a receiver that is neither `Message` nor `EngineMessage`; the full Bazel build is the gate (no per-module build / unit tests). Deferred, NOT done: chat-list/reply/pinned/notification/forward thumbnail **previews** and the "Photo"/"Video" media-kind **labels** (`messageContentKind`/`ChatListItemStrings`) — those are preview surfaces, not blank-cell breakage — and **multi-media** (first-media-only is the current scope).

View file

@ -1416,7 +1416,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
var resourceIds = Set<EngineMediaResource.Id>()
for message in messages {
for media in message.media {
for media in message.effectiveMedia {
if let file = media as? TelegramMediaFile {
resourceIds.insert(EngineMediaResource.Id(file.resource.id))
}

View file

@ -2106,7 +2106,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let queryTokens = stringIndexTokens(query ?? "", transliteration: .combined)
func messageMatchesTokens(message: EngineMessage, tokens: [ValueBoxKey]) -> Bool {
for media in message.media {
for media in message.effectiveMedia {
if let file = media as? TelegramMediaFile {
if let fileName = file.fileName {
if matchStringIndexTokens(stringIndexTokens(fileName, transliteration: .none), with: tokens) {

View file

@ -117,7 +117,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
if case .tap = gesture {
if let _ = self.item {
var media: EngineRawMedia?
for value in message.media {
for value in message.effectiveMedia {
if let image = value as? TelegramMediaImage {
media = image
break
@ -126,7 +126,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
break
}
}
if let media = media {
if let file = media as? TelegramMediaFile {
if isMediaStreamable(message: EngineMessage(message), media: file) {
@ -150,7 +150,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
}
var media: EngineRawMedia?
for value in message.media {
for value in message.effectiveMedia {
if let image = value as? TelegramMediaImage {
media = image
break
@ -159,7 +159,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
break
}
}
if let resourceStatus = self.resourceStatus, let file = media as? TelegramMediaFile {
switch resourceStatus {
case .Fetching:
@ -186,7 +186,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
self.theme = theme
var media: EngineRawMedia?
if let message = item.message {
for value in message.media {
for value in message.effectiveMedia {
if let image = value as? TelegramMediaImage {
media = image
break
@ -413,7 +413,7 @@ private final class VisualMediaItem {
var aspectRatio: CGFloat = 1.0
var dimensions = CGSize(width: 100.0, height: 100.0)
for media in message.media {
for media in message.effectiveMedia {
if let file = media as? TelegramMediaFile {
if let dimensionsValue = file.dimensions, dimensions.height > 1 {
dimensions = dimensionsValue.cgSize

View file

@ -125,7 +125,7 @@ public func chatMessageGalleryControllerData(
}
}
}
for media in message.media {
for media in message.effectiveMedia {
if let poll = media as? TelegramMediaPoll {
standalone = true
galleryMedia = poll

View file

@ -870,7 +870,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
var canEdit = false
var isImage = false
var isVideo = false
for media in message.media {
for media in message.effectiveMedia {
if media is TelegramMediaImage {
canEdit = true
isImage = true

View file

@ -18,26 +18,27 @@ import UndoUI
import TranslateUI
private func tagsForMessage(_ message: Message) -> MessageTags? {
for media in message.media {
//TODO:rewrite to take all media (effectiveMedia returns all rich-text media; we stop at the first)
for media in message.effectiveMedia {
switch media {
case _ as TelegramMediaImage:
return .photoOrVideo
case let file as TelegramMediaFile:
if file.isVideo {
if file.isAnimated {
return .gif
} else {
return .photoOrVideo
}
} else if file.isVoice {
return .voiceOrInstantVideo
} else if file.isSticker {
return nil
case _ as TelegramMediaImage:
return .photoOrVideo
case let file as TelegramMediaFile:
if file.isVideo {
if file.isAnimated {
return .gif
} else {
return .file
return .photoOrVideo
}
default:
break
} else if file.isVoice {
return .voiceOrInstantVideo
} else if file.isSticker {
return nil
} else {
return .file
}
default:
break
}
}
return nil
@ -61,7 +62,8 @@ private func galleryMediaForMedia(media: Media) -> Media? {
}
func mediaForMessage(message: Message, mediaSubject: GalleryMediaSubject? = nil) -> [(Media, TelegramMediaImage?)] {
for media in message.media {
//TODO:rewrite to take all media (effectiveMedia returns all rich-text media; we return the first match)
for media in message.effectiveMedia {
if let result = galleryMediaForMedia(media: media) {
return [(result, nil)]
} else if let poll = media as? TelegramMediaPoll {
@ -115,6 +117,7 @@ func mediaForMessage(message: Message, mediaSubject: GalleryMediaSubject? = nil)
}
}
}
return []
}

View file

@ -33,7 +33,7 @@ class ChatAnimationGalleryItem: GalleryItem {
func node(synchronous: Bool) -> GalleryItemNode {
let node = ChatAnimationGalleryItemNode(context: self.context, presentationData: self.presentationData)
for media in self.message.media {
for media in self.message.effectiveMedia {
if let file = media as? TelegramMediaFile {
node.setFile(context: self.context, fileReference: .message(message: MessageReference(self.message), media: file))
break

View file

@ -29,7 +29,7 @@ class ChatDocumentGalleryItem: GalleryItem {
func node(synchronous: Bool) -> GalleryItemNode {
let node = ChatDocumentGalleryItemNode(context: self.context, presentationData: self.presentationData)
for media in self.message.media {
for media in self.message.effectiveMedia {
if let file = media as? TelegramMediaFile {
node.setFile(context: context, fileReference: .message(message: MessageReference(self.message), media: file))
break

View file

@ -29,7 +29,7 @@ class ChatExternalFileGalleryItem: GalleryItem {
func node(synchronous: Bool) -> GalleryItemNode {
let node = ChatExternalFileGalleryItemNode(context: self.context, presentationData: self.presentationData)
for media in self.message.media {
for media in self.message.effectiveMedia {
if let file = media as? TelegramMediaFile {
node.setFile(context: context, fileReference: .message(message: MessageReference(self.message), media: file))
break

View file

@ -1439,7 +1439,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
if let content = item.content as? NativeVideoContent {
isAnimated = content.fileReference.media.isAnimated
self.videoFramePreview = MediaPlayerFramePreview(postbox: item.context.account.postbox, userLocation: content.userLocation, userContentType: .video, fileReference: content.fileReference)
if case let .message(message, _) = item.contentInfo, let _ = message.media.first(where: { $0 is TelegramMediaImage }) {
if case let .message(message, _) = item.contentInfo, let _ = message.effectiveMedia.first(where: { $0 is TelegramMediaImage }) {
self.isLivePhoto = true
disablePlayerControls = true
isAnimated = false
@ -1635,7 +1635,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
var file: TelegramMediaFile?
var isWebpage = false
for m in message.media {
for m in message.effectiveMedia {
if let m = m as? TelegramMediaFile, m.isVideo {
file = m
break
@ -1885,7 +1885,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.zoomableContent = (videoSize, videoNode)
if case let .message(message, _) = item.contentInfo, let content = item.content as? NativeVideoContent, let image = message.media.first(where: { $0 is TelegramMediaImage }), let imageReference = content.fileReference.abstract.withUpdatedMedia(image).concrete(TelegramMediaImage.self) {
if case let .message(message, _) = item.contentInfo, let content = item.content as? NativeVideoContent, let image = message.effectiveMedia.first(where: { $0 is TelegramMediaImage }), let imageReference = content.fileReference.abstract.withUpdatedMedia(image).concrete(TelegramMediaImage.self) {
let imageNode = TransformImageNode()
imageNode.alpha = 1.0
imageNode.isUserInteractionEnabled = false
@ -3134,7 +3134,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
var hiddenMedia: (MessageId, Media)? = nil
switch item.contentInfo {
case let .message(message, _):
for media in message.media {
for media in message.effectiveMedia {
if let media = media as? TelegramMediaImage {
hiddenMedia = (message.id, media)
} else if let media = media as? TelegramMediaFile, media.isVideo {
@ -3858,7 +3858,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
})))
}
if let (message, _, _) = strongSelf.contentInfo(), let image = message.media.first(where: { $0 is TelegramMediaImage }) as? TelegramMediaImage, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil {
if let (message, _, _) = strongSelf.contentInfo(), let image = message.effectiveMedia.first(where: { $0 is TelegramMediaImage }) as? TelegramMediaImage, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil {
let context = strongSelf.context
var videoReference: AnyMediaReference?
if let video = image.video {

View file

@ -30,7 +30,7 @@ private func galleryMediaForMedia(media: Media) -> Media? {
}
private func mediaForMessage(message: Message) -> Media? {
for media in message.media {
for media in message.effectiveMedia {
if let result = galleryMediaForMedia(media: media) {
return result
} else if let webpage = media as? TelegramMediaWebpage {

View file

@ -682,7 +682,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
var selectedMedia: EngineRawMedia?
if let message = message {
var effectiveMessageMedia = message.media
var effectiveMessageMedia = message.effectiveMedia
for media in message.media {
if let storyMedia = media as? TelegramMediaStory {
if let story = message.associatedStories[storyMedia.storyId], !story.data.isEmpty, case let .item(storyItem) = story.get(Stories.StoredItem.self), let media = selectStoryMedia(item: storyItem, preferredHighQuality: item.interaction.preferredStoryHighQuality) {

View file

@ -145,7 +145,7 @@ public final class ListMessageItem: ListViewItem, ItemListItem {
if !self.hintIsLink {
if let message = self.message {
for media in message.media {
for media in message.effectiveMedia {
if let _ = media as? TelegramMediaFile {
viewClassName = ListMessageFileItemNode.self
break

View file

@ -2400,7 +2400,7 @@ public final class ShareController: ViewController {
let context = accountContext.context
let signals: [Signal<Float, NoError>] = messages.compactMap { message -> Signal<Float, NoError>? in
if let media = message.media.first {
if let media = message.effectiveMedia.first {
return SaveToCameraRoll.saveToCameraRoll(context: context, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: media))
} else {
return nil

View file

@ -240,7 +240,7 @@ private func findMediaResource(media: Media, previousMedia: Media?, resource: Me
}
public func findMediaResourceById(message: EngineMessage, resourceId: MediaResourceId) -> TelegramMediaResource? {
for media in message.media {
for media in message.effectiveMedia {
if let result = findMediaResourceById(media: media, resourceId: resourceId) {
return result
}

View file

@ -136,7 +136,7 @@ public func recentDownloadItems(postbox: Postbox) -> Signal<[RenderedRecentDownl
}
var size: Int64?
for media in message.media {
for media in message.effectiveMedia {
if let result = findMediaResourceById(media: media, resourceId: MediaResourceId(item.resourceId)) {
size = result.size
break

View file

@ -17,7 +17,7 @@ func addMessageMediaResourceIdsToRemove(media: Media, resourceIds: inout [MediaR
}
func addMessageMediaResourceIdsToRemove(message: Message, resourceIds: inout [MediaResourceId]) {
for media in message.media {
for media in message.effectiveMedia {
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
}
}

View file

@ -736,7 +736,7 @@ func _internal_collectCacheUsageStats(account: Account, peerId: PeerId? = nil, a
continue
}
if let message = transaction.getMessage(MessageId(peerId: PeerId(reference.peerId), namespace: MessageId.Namespace(reference.messageNamespace), id: reference.messageId)) {
for mediaItem in message.media {
for mediaItem in message.effectiveMedia {
guard let mediaId = mediaItem.id else {
continue
}

View file

@ -663,6 +663,28 @@ public extension Message {
}
}
public extension Message {
/// The media that should drive gallery / shared-media / preview surfaces for this message.
/// For a normal message this is exactly `self.media`. For a rich message (`text == ""`,
/// empty `media`, carrying a `RichTextMessageAttribute`) the media lives inside the
/// instant page, so fall back to it. Scope note: callers take the FIRST media for now.
var effectiveMedia: [Media] {
if !self.media.isEmpty {
return self.media
}
if let richText = self.richText {
return richText.instantPage.allMedia()
}
return self.media
}
}
public extension EngineMessage {
var effectiveMedia: [Media] {
return self._asMessage().effectiveMedia
}
}
public func _internal_parseMediaAttachment(data: Data) -> Media? {
guard let object = Api.parse(Buffer(buffer: MemoryBuffer(data: data))) else {
return nil

View file

@ -34,7 +34,7 @@ struct MessageMediaPlaylistItemStableId: Hashable {
private func extractFileMedia(_ message: EngineRawMessage) -> TelegramMediaFile? {
var file: TelegramMediaFile?
for media in message.media {
for media in message.effectiveMedia {
if let media = media as? TelegramMediaFile {
file = media
break

View file

@ -118,7 +118,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
if case .tap = gesture {
if let (item, _, _, _) = self.item {
var media: EngineRawMedia?
for value in item.message.media {
for value in item.message.effectiveMedia {
if let image = value as? TelegramMediaImage {
media = image
break
@ -151,7 +151,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
}
var media: EngineRawMedia?
for value in message.media {
for value in message.effectiveMedia {
if let image = value as? TelegramMediaImage {
media = image
break
@ -183,7 +183,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
}
self.theme = theme
var media: EngineRawMedia?
for value in item.message.media {
for value in item.message.effectiveMedia {
if let image = value as? TelegramMediaImage {
media = image
break
@ -425,7 +425,7 @@ private final class VisualMediaItem {
var aspectRatio: CGFloat = 1.0
var dimensions = CGSize(width: 100.0, height: 100.0)
for media in message.media {
for media in message.effectiveMedia {
if let file = media as? TelegramMediaFile {
if let dimensionsValue = file.dimensions, dimensions.height > 1 {
dimensions = dimensionsValue.cgSize

View file

@ -899,7 +899,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme
layer.updateHasSpoiler(hasSpoiler: hasSpoiler)
var selectedMedia: Media?
for media in message.media {
for media in message.effectiveMedia {
if let image = media as? TelegramMediaImage {
selectedMedia = image
break

View file

@ -2270,7 +2270,7 @@ extension ChatControllerImpl {
if text.length == 0 {
if strongSelf.presentationInterfaceState.editMessageState?.mediaReference != nil {
} else if message.media.contains(where: { media in
} else if message.effectiveMedia.contains(where: { media in
switch media {
case _ as TelegramMediaImage, _ as TelegramMediaFile, _ as TelegramMediaMap:
return true

View file

@ -4848,7 +4848,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
mediaReference = mediaReferenceValue
} else {
if let message = self.historyNode.messageInCurrentHistoryView(editingOriginalMessageId)?._asMessage() {
for media in message.media {
for media in message.effectiveMedia {
if media is TelegramMediaFile || media is TelegramMediaImage {
mediaReference = .message(message: MessageReference(message), media: media)
}

View file

@ -2962,7 +2962,7 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH
}
}
for media in message.media {
for media in message.effectiveMedia {
if let _ = media as? TelegramMediaUnsupported {
contentRequiredValidation = true
} else if message.flags.contains(.Incoming), let media = media as? TelegramMediaMap, let liveBroadcastingTimeout = media.liveBroadcastingTimeout {

View file

@ -708,7 +708,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
var isGiveawayServiceMessage = false
var diceEmoji: String?
if messages.count == 1 {
for media in messages[0].media {
for media in messages[0].effectiveMedia {
if let file = media as? TelegramMediaFile {
if file.isSticker {
loadStickerSaveStatus = file.fileId
@ -1088,7 +1088,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}
if !hasRateTranscription && message.minAutoremoveOrClearTimeout == nil {
for media in message.media {
for media in message.effectiveMedia {
if let file = media as? TelegramMediaFile, let size = file.size, size < 1 * 1024 * 1024, let duration = file.duration, duration < 60, (["audio/mpeg", "audio/mp3", "audio/mpeg3", "audio/ogg"] as [String]).contains(file.mimeType.lowercased()) {
let fileName = file.fileName ?? "Tone"
@ -1143,7 +1143,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
if !isPremium && isDownloading {
var isLargeFile = false
for media in message.media {
for media in message.effectiveMedia {
if let file = media as? TelegramMediaFile {
if let size = file.size, size >= 150 * 1024 * 1024 {
isLargeFile = true
@ -1298,7 +1298,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}
var isExpired = false
var isImage = false
for media in message.media {
for media in message.effectiveMedia {
if let _ = media as? TelegramMediaExpiredContent {
isExpired = true
}
@ -1364,7 +1364,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
})
}
if resourceAvailable {
for media in message.media {
for media in message.effectiveMedia {
if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
let _ = (context.engine.resources.data(resource: EngineMediaResource(largest.resource), incremental: true)
|> take(1)
@ -1454,7 +1454,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
if resourceAvailable, !message.containsSecretMedia && !isCopyProtected {
var mediaReference: AnyMediaReference?
var isVideo = false
for media in message.media {
for media in message.effectiveMedia {
if let image = media as? TelegramMediaImage, let _ = largestImageRepresentation(image.representations) {
mediaReference = ImageMediaReference.standalone(media: image).abstract
break
@ -1481,7 +1481,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}
var downloadableMediaResourceInfos: [String] = []
for media in message.media {
for media in message.effectiveMedia {
if let file = media as? TelegramMediaFile {
if let info = extractMediaResourceDebugInfo(resource: file.resource) {
downloadableMediaResourceInfos.append(info)
@ -1496,7 +1496,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}
if !isCopyProtected {
for media in message.media {
for media in message.effectiveMedia {
if let file = media as? TelegramMediaFile {
if file.isMusic {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_SaveToFiles, icon: { theme in

View file

@ -1366,7 +1366,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}
private func openMessageContextMenu(message: EngineRawMessage, node: ASDisplayNode, frame: CGRect, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, location: CGPoint? = nil) {
guard let node = node as? ContextExtractedContentContainingNode, let peer = message.peers[message.id.peerId].flatMap({ PeerReference($0) }), let file = message.media.first(where: { $0 is TelegramMediaFile}) as? TelegramMediaFile else {
guard let node = node as? ContextExtractedContentContainingNode, let peer = message.peers[message.id.peerId].flatMap({ PeerReference($0) }), let file = message.effectiveMedia.first(where: { $0 is TelegramMediaFile}) as? TelegramMediaFile else {
return
}
let context = self.context

View file

@ -302,7 +302,7 @@ final class DownloadedMediaStoreManagerImpl: DownloadedMediaStoreManager {
return
}
for item in items {
for media in item.message.media {
for media in item.message.effectiveMedia {
if let id = media.id, id == item.mediaId {
self.store(.standalone(media: media), timestamp: item.message.timestamp, peerId: item.message.id.peerId)
break