mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
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:
parent
c95e014681
commit
0050cc7a08
30 changed files with 130 additions and 64 deletions
|
|
@ -62,7 +62,7 @@ Rare exceptions: top-level view-controller views integrating with the system's f
|
||||||
|
|
||||||
## InstantPage V2 & rich-text messages
|
## 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)
|
## Postbox → TelegramEngine refactor (in progress)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
- **`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.
|
- **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.
|
- **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).
|
||||||
|
|
|
||||||
|
|
@ -1416,7 +1416,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||||
|
|
||||||
var resourceIds = Set<EngineMediaResource.Id>()
|
var resourceIds = Set<EngineMediaResource.Id>()
|
||||||
for message in messages {
|
for message in messages {
|
||||||
for media in message.media {
|
for media in message.effectiveMedia {
|
||||||
if let file = media as? TelegramMediaFile {
|
if let file = media as? TelegramMediaFile {
|
||||||
resourceIds.insert(EngineMediaResource.Id(file.resource.id))
|
resourceIds.insert(EngineMediaResource.Id(file.resource.id))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2106,7 +2106,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||||
let queryTokens = stringIndexTokens(query ?? "", transliteration: .combined)
|
let queryTokens = stringIndexTokens(query ?? "", transliteration: .combined)
|
||||||
|
|
||||||
func messageMatchesTokens(message: EngineMessage, tokens: [ValueBoxKey]) -> Bool {
|
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 file = media as? TelegramMediaFile {
|
||||||
if let fileName = file.fileName {
|
if let fileName = file.fileName {
|
||||||
if matchStringIndexTokens(stringIndexTokens(fileName, transliteration: .none), with: tokens) {
|
if matchStringIndexTokens(stringIndexTokens(fileName, transliteration: .none), with: tokens) {
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||||
if case .tap = gesture {
|
if case .tap = gesture {
|
||||||
if let _ = self.item {
|
if let _ = self.item {
|
||||||
var media: EngineRawMedia?
|
var media: EngineRawMedia?
|
||||||
for value in message.media {
|
for value in message.effectiveMedia {
|
||||||
if let image = value as? TelegramMediaImage {
|
if let image = value as? TelegramMediaImage {
|
||||||
media = image
|
media = image
|
||||||
break
|
break
|
||||||
|
|
@ -126,7 +126,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let media = media {
|
if let media = media {
|
||||||
if let file = media as? TelegramMediaFile {
|
if let file = media as? TelegramMediaFile {
|
||||||
if isMediaStreamable(message: EngineMessage(message), media: file) {
|
if isMediaStreamable(message: EngineMessage(message), media: file) {
|
||||||
|
|
@ -150,7 +150,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
var media: EngineRawMedia?
|
var media: EngineRawMedia?
|
||||||
for value in message.media {
|
for value in message.effectiveMedia {
|
||||||
if let image = value as? TelegramMediaImage {
|
if let image = value as? TelegramMediaImage {
|
||||||
media = image
|
media = image
|
||||||
break
|
break
|
||||||
|
|
@ -159,7 +159,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let resourceStatus = self.resourceStatus, let file = media as? TelegramMediaFile {
|
if let resourceStatus = self.resourceStatus, let file = media as? TelegramMediaFile {
|
||||||
switch resourceStatus {
|
switch resourceStatus {
|
||||||
case .Fetching:
|
case .Fetching:
|
||||||
|
|
@ -186,7 +186,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
var media: EngineRawMedia?
|
var media: EngineRawMedia?
|
||||||
if let message = item.message {
|
if let message = item.message {
|
||||||
for value in message.media {
|
for value in message.effectiveMedia {
|
||||||
if let image = value as? TelegramMediaImage {
|
if let image = value as? TelegramMediaImage {
|
||||||
media = image
|
media = image
|
||||||
break
|
break
|
||||||
|
|
@ -413,7 +413,7 @@ private final class VisualMediaItem {
|
||||||
|
|
||||||
var aspectRatio: CGFloat = 1.0
|
var aspectRatio: CGFloat = 1.0
|
||||||
var dimensions = CGSize(width: 100.0, height: 100.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 file = media as? TelegramMediaFile {
|
||||||
if let dimensionsValue = file.dimensions, dimensions.height > 1 {
|
if let dimensionsValue = file.dimensions, dimensions.height > 1 {
|
||||||
dimensions = dimensionsValue.cgSize
|
dimensions = dimensionsValue.cgSize
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ public func chatMessageGalleryControllerData(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for media in message.media {
|
for media in message.effectiveMedia {
|
||||||
if let poll = media as? TelegramMediaPoll {
|
if let poll = media as? TelegramMediaPoll {
|
||||||
standalone = true
|
standalone = true
|
||||||
galleryMedia = poll
|
galleryMedia = poll
|
||||||
|
|
|
||||||
|
|
@ -870,7 +870,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
|
||||||
var canEdit = false
|
var canEdit = false
|
||||||
var isImage = false
|
var isImage = false
|
||||||
var isVideo = false
|
var isVideo = false
|
||||||
for media in message.media {
|
for media in message.effectiveMedia {
|
||||||
if media is TelegramMediaImage {
|
if media is TelegramMediaImage {
|
||||||
canEdit = true
|
canEdit = true
|
||||||
isImage = true
|
isImage = true
|
||||||
|
|
|
||||||
|
|
@ -18,26 +18,27 @@ import UndoUI
|
||||||
import TranslateUI
|
import TranslateUI
|
||||||
|
|
||||||
private func tagsForMessage(_ message: Message) -> MessageTags? {
|
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 {
|
switch media {
|
||||||
case _ as TelegramMediaImage:
|
case _ as TelegramMediaImage:
|
||||||
return .photoOrVideo
|
return .photoOrVideo
|
||||||
case let file as TelegramMediaFile:
|
case let file as TelegramMediaFile:
|
||||||
if file.isVideo {
|
if file.isVideo {
|
||||||
if file.isAnimated {
|
if file.isAnimated {
|
||||||
return .gif
|
return .gif
|
||||||
} else {
|
|
||||||
return .photoOrVideo
|
|
||||||
}
|
|
||||||
} else if file.isVoice {
|
|
||||||
return .voiceOrInstantVideo
|
|
||||||
} else if file.isSticker {
|
|
||||||
return nil
|
|
||||||
} else {
|
} else {
|
||||||
return .file
|
return .photoOrVideo
|
||||||
}
|
}
|
||||||
default:
|
} else if file.isVoice {
|
||||||
break
|
return .voiceOrInstantVideo
|
||||||
|
} else if file.isSticker {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return .file
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -61,7 +62,8 @@ private func galleryMediaForMedia(media: Media) -> Media? {
|
||||||
}
|
}
|
||||||
|
|
||||||
func mediaForMessage(message: Message, mediaSubject: GalleryMediaSubject? = nil) -> [(Media, TelegramMediaImage?)] {
|
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) {
|
if let result = galleryMediaForMedia(media: media) {
|
||||||
return [(result, nil)]
|
return [(result, nil)]
|
||||||
} else if let poll = media as? TelegramMediaPoll {
|
} else if let poll = media as? TelegramMediaPoll {
|
||||||
|
|
@ -115,6 +117,7 @@ func mediaForMessage(message: Message, mediaSubject: GalleryMediaSubject? = nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class ChatAnimationGalleryItem: GalleryItem {
|
||||||
func node(synchronous: Bool) -> GalleryItemNode {
|
func node(synchronous: Bool) -> GalleryItemNode {
|
||||||
let node = ChatAnimationGalleryItemNode(context: self.context, presentationData: self.presentationData)
|
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 {
|
if let file = media as? TelegramMediaFile {
|
||||||
node.setFile(context: self.context, fileReference: .message(message: MessageReference(self.message), media: file))
|
node.setFile(context: self.context, fileReference: .message(message: MessageReference(self.message), media: file))
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ class ChatDocumentGalleryItem: GalleryItem {
|
||||||
func node(synchronous: Bool) -> GalleryItemNode {
|
func node(synchronous: Bool) -> GalleryItemNode {
|
||||||
let node = ChatDocumentGalleryItemNode(context: self.context, presentationData: self.presentationData)
|
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 {
|
if let file = media as? TelegramMediaFile {
|
||||||
node.setFile(context: context, fileReference: .message(message: MessageReference(self.message), media: file))
|
node.setFile(context: context, fileReference: .message(message: MessageReference(self.message), media: file))
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ class ChatExternalFileGalleryItem: GalleryItem {
|
||||||
func node(synchronous: Bool) -> GalleryItemNode {
|
func node(synchronous: Bool) -> GalleryItemNode {
|
||||||
let node = ChatExternalFileGalleryItemNode(context: self.context, presentationData: self.presentationData)
|
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 {
|
if let file = media as? TelegramMediaFile {
|
||||||
node.setFile(context: context, fileReference: .message(message: MessageReference(self.message), media: file))
|
node.setFile(context: context, fileReference: .message(message: MessageReference(self.message), media: file))
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -1439,7 +1439,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||||
if let content = item.content as? NativeVideoContent {
|
if let content = item.content as? NativeVideoContent {
|
||||||
isAnimated = content.fileReference.media.isAnimated
|
isAnimated = content.fileReference.media.isAnimated
|
||||||
self.videoFramePreview = MediaPlayerFramePreview(postbox: item.context.account.postbox, userLocation: content.userLocation, userContentType: .video, fileReference: content.fileReference)
|
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
|
self.isLivePhoto = true
|
||||||
disablePlayerControls = true
|
disablePlayerControls = true
|
||||||
isAnimated = false
|
isAnimated = false
|
||||||
|
|
@ -1635,7 +1635,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||||
|
|
||||||
var file: TelegramMediaFile?
|
var file: TelegramMediaFile?
|
||||||
var isWebpage = false
|
var isWebpage = false
|
||||||
for m in message.media {
|
for m in message.effectiveMedia {
|
||||||
if let m = m as? TelegramMediaFile, m.isVideo {
|
if let m = m as? TelegramMediaFile, m.isVideo {
|
||||||
file = m
|
file = m
|
||||||
break
|
break
|
||||||
|
|
@ -1885,7 +1885,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||||
self.zoomableContent = (videoSize, videoNode)
|
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()
|
let imageNode = TransformImageNode()
|
||||||
imageNode.alpha = 1.0
|
imageNode.alpha = 1.0
|
||||||
imageNode.isUserInteractionEnabled = false
|
imageNode.isUserInteractionEnabled = false
|
||||||
|
|
@ -3134,7 +3134,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||||
var hiddenMedia: (MessageId, Media)? = nil
|
var hiddenMedia: (MessageId, Media)? = nil
|
||||||
switch item.contentInfo {
|
switch item.contentInfo {
|
||||||
case let .message(message, _):
|
case let .message(message, _):
|
||||||
for media in message.media {
|
for media in message.effectiveMedia {
|
||||||
if let media = media as? TelegramMediaImage {
|
if let media = media as? TelegramMediaImage {
|
||||||
hiddenMedia = (message.id, media)
|
hiddenMedia = (message.id, media)
|
||||||
} else if let media = media as? TelegramMediaFile, media.isVideo {
|
} 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
|
let context = strongSelf.context
|
||||||
var videoReference: AnyMediaReference?
|
var videoReference: AnyMediaReference?
|
||||||
if let video = image.video {
|
if let video = image.video {
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ private func galleryMediaForMedia(media: Media) -> Media? {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func mediaForMessage(message: Message) -> Media? {
|
private func mediaForMessage(message: Message) -> Media? {
|
||||||
for media in message.media {
|
for media in message.effectiveMedia {
|
||||||
if let result = galleryMediaForMedia(media: media) {
|
if let result = galleryMediaForMedia(media: media) {
|
||||||
return result
|
return result
|
||||||
} else if let webpage = media as? TelegramMediaWebpage {
|
} else if let webpage = media as? TelegramMediaWebpage {
|
||||||
|
|
|
||||||
|
|
@ -682,7 +682,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||||
|
|
||||||
var selectedMedia: EngineRawMedia?
|
var selectedMedia: EngineRawMedia?
|
||||||
if let message = message {
|
if let message = message {
|
||||||
var effectiveMessageMedia = message.media
|
var effectiveMessageMedia = message.effectiveMedia
|
||||||
for media in message.media {
|
for media in message.media {
|
||||||
if let storyMedia = media as? TelegramMediaStory {
|
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) {
|
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) {
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ public final class ListMessageItem: ListViewItem, ItemListItem {
|
||||||
|
|
||||||
if !self.hintIsLink {
|
if !self.hintIsLink {
|
||||||
if let message = self.message {
|
if let message = self.message {
|
||||||
for media in message.media {
|
for media in message.effectiveMedia {
|
||||||
if let _ = media as? TelegramMediaFile {
|
if let _ = media as? TelegramMediaFile {
|
||||||
viewClassName = ListMessageFileItemNode.self
|
viewClassName = ListMessageFileItemNode.self
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -2400,7 +2400,7 @@ public final class ShareController: ViewController {
|
||||||
let context = accountContext.context
|
let context = accountContext.context
|
||||||
|
|
||||||
let signals: [Signal<Float, NoError>] = messages.compactMap { message -> Signal<Float, NoError>? in
|
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))
|
return SaveToCameraRoll.saveToCameraRoll(context: context, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: media))
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,7 @@ private func findMediaResource(media: Media, previousMedia: Media?, resource: Me
|
||||||
}
|
}
|
||||||
|
|
||||||
public func findMediaResourceById(message: EngineMessage, resourceId: MediaResourceId) -> TelegramMediaResource? {
|
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) {
|
if let result = findMediaResourceById(media: media, resourceId: resourceId) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ public func recentDownloadItems(postbox: Postbox) -> Signal<[RenderedRecentDownl
|
||||||
}
|
}
|
||||||
|
|
||||||
var size: Int64?
|
var size: Int64?
|
||||||
for media in message.media {
|
for media in message.effectiveMedia {
|
||||||
if let result = findMediaResourceById(media: media, resourceId: MediaResourceId(item.resourceId)) {
|
if let result = findMediaResourceById(media: media, resourceId: MediaResourceId(item.resourceId)) {
|
||||||
size = result.size
|
size = result.size
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ func addMessageMediaResourceIdsToRemove(media: Media, resourceIds: inout [MediaR
|
||||||
}
|
}
|
||||||
|
|
||||||
func addMessageMediaResourceIdsToRemove(message: Message, resourceIds: inout [MediaResourceId]) {
|
func addMessageMediaResourceIdsToRemove(message: Message, resourceIds: inout [MediaResourceId]) {
|
||||||
for media in message.media {
|
for media in message.effectiveMedia {
|
||||||
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
|
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -736,7 +736,7 @@ func _internal_collectCacheUsageStats(account: Account, peerId: PeerId? = nil, a
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if let message = transaction.getMessage(MessageId(peerId: PeerId(reference.peerId), namespace: MessageId.Namespace(reference.messageNamespace), id: reference.messageId)) {
|
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 {
|
guard let mediaId = mediaItem.id else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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? {
|
public func _internal_parseMediaAttachment(data: Data) -> Media? {
|
||||||
guard let object = Api.parse(Buffer(buffer: MemoryBuffer(data: data))) else {
|
guard let object = Api.parse(Buffer(buffer: MemoryBuffer(data: data))) else {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ struct MessageMediaPlaylistItemStableId: Hashable {
|
||||||
|
|
||||||
private func extractFileMedia(_ message: EngineRawMessage) -> TelegramMediaFile? {
|
private func extractFileMedia(_ message: EngineRawMessage) -> TelegramMediaFile? {
|
||||||
var file: TelegramMediaFile?
|
var file: TelegramMediaFile?
|
||||||
for media in message.media {
|
for media in message.effectiveMedia {
|
||||||
if let media = media as? TelegramMediaFile {
|
if let media = media as? TelegramMediaFile {
|
||||||
file = media
|
file = media
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||||
if case .tap = gesture {
|
if case .tap = gesture {
|
||||||
if let (item, _, _, _) = self.item {
|
if let (item, _, _, _) = self.item {
|
||||||
var media: EngineRawMedia?
|
var media: EngineRawMedia?
|
||||||
for value in item.message.media {
|
for value in item.message.effectiveMedia {
|
||||||
if let image = value as? TelegramMediaImage {
|
if let image = value as? TelegramMediaImage {
|
||||||
media = image
|
media = image
|
||||||
break
|
break
|
||||||
|
|
@ -151,7 +151,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
var media: EngineRawMedia?
|
var media: EngineRawMedia?
|
||||||
for value in message.media {
|
for value in message.effectiveMedia {
|
||||||
if let image = value as? TelegramMediaImage {
|
if let image = value as? TelegramMediaImage {
|
||||||
media = image
|
media = image
|
||||||
break
|
break
|
||||||
|
|
@ -183,7 +183,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||||
}
|
}
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
var media: EngineRawMedia?
|
var media: EngineRawMedia?
|
||||||
for value in item.message.media {
|
for value in item.message.effectiveMedia {
|
||||||
if let image = value as? TelegramMediaImage {
|
if let image = value as? TelegramMediaImage {
|
||||||
media = image
|
media = image
|
||||||
break
|
break
|
||||||
|
|
@ -425,7 +425,7 @@ private final class VisualMediaItem {
|
||||||
|
|
||||||
var aspectRatio: CGFloat = 1.0
|
var aspectRatio: CGFloat = 1.0
|
||||||
var dimensions = CGSize(width: 100.0, height: 100.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 file = media as? TelegramMediaFile {
|
||||||
if let dimensionsValue = file.dimensions, dimensions.height > 1 {
|
if let dimensionsValue = file.dimensions, dimensions.height > 1 {
|
||||||
dimensions = dimensionsValue.cgSize
|
dimensions = dimensionsValue.cgSize
|
||||||
|
|
|
||||||
|
|
@ -899,7 +899,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme
|
||||||
layer.updateHasSpoiler(hasSpoiler: hasSpoiler)
|
layer.updateHasSpoiler(hasSpoiler: hasSpoiler)
|
||||||
|
|
||||||
var selectedMedia: Media?
|
var selectedMedia: Media?
|
||||||
for media in message.media {
|
for media in message.effectiveMedia {
|
||||||
if let image = media as? TelegramMediaImage {
|
if let image = media as? TelegramMediaImage {
|
||||||
selectedMedia = image
|
selectedMedia = image
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -2270,7 +2270,7 @@ extension ChatControllerImpl {
|
||||||
|
|
||||||
if text.length == 0 {
|
if text.length == 0 {
|
||||||
if strongSelf.presentationInterfaceState.editMessageState?.mediaReference != nil {
|
if strongSelf.presentationInterfaceState.editMessageState?.mediaReference != nil {
|
||||||
} else if message.media.contains(where: { media in
|
} else if message.effectiveMedia.contains(where: { media in
|
||||||
switch media {
|
switch media {
|
||||||
case _ as TelegramMediaImage, _ as TelegramMediaFile, _ as TelegramMediaMap:
|
case _ as TelegramMediaImage, _ as TelegramMediaFile, _ as TelegramMediaMap:
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -4848,7 +4848,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||||
mediaReference = mediaReferenceValue
|
mediaReference = mediaReferenceValue
|
||||||
} else {
|
} else {
|
||||||
if let message = self.historyNode.messageInCurrentHistoryView(editingOriginalMessageId)?._asMessage() {
|
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 {
|
if media is TelegramMediaFile || media is TelegramMediaImage {
|
||||||
mediaReference = .message(message: MessageReference(message), media: media)
|
mediaReference = .message(message: MessageReference(message), media: media)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
if let _ = media as? TelegramMediaUnsupported {
|
||||||
contentRequiredValidation = true
|
contentRequiredValidation = true
|
||||||
} else if message.flags.contains(.Incoming), let media = media as? TelegramMediaMap, let liveBroadcastingTimeout = media.liveBroadcastingTimeout {
|
} else if message.flags.contains(.Incoming), let media = media as? TelegramMediaMap, let liveBroadcastingTimeout = media.liveBroadcastingTimeout {
|
||||||
|
|
|
||||||
|
|
@ -708,7 +708,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||||
var isGiveawayServiceMessage = false
|
var isGiveawayServiceMessage = false
|
||||||
var diceEmoji: String?
|
var diceEmoji: String?
|
||||||
if messages.count == 1 {
|
if messages.count == 1 {
|
||||||
for media in messages[0].media {
|
for media in messages[0].effectiveMedia {
|
||||||
if let file = media as? TelegramMediaFile {
|
if let file = media as? TelegramMediaFile {
|
||||||
if file.isSticker {
|
if file.isSticker {
|
||||||
loadStickerSaveStatus = file.fileId
|
loadStickerSaveStatus = file.fileId
|
||||||
|
|
@ -1088,7 +1088,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasRateTranscription && message.minAutoremoveOrClearTimeout == nil {
|
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()) {
|
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"
|
let fileName = file.fileName ?? "Tone"
|
||||||
|
|
||||||
|
|
@ -1143,7 +1143,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||||
|
|
||||||
if !isPremium && isDownloading {
|
if !isPremium && isDownloading {
|
||||||
var isLargeFile = false
|
var isLargeFile = false
|
||||||
for media in message.media {
|
for media in message.effectiveMedia {
|
||||||
if let file = media as? TelegramMediaFile {
|
if let file = media as? TelegramMediaFile {
|
||||||
if let size = file.size, size >= 150 * 1024 * 1024 {
|
if let size = file.size, size >= 150 * 1024 * 1024 {
|
||||||
isLargeFile = true
|
isLargeFile = true
|
||||||
|
|
@ -1298,7 +1298,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||||
}
|
}
|
||||||
var isExpired = false
|
var isExpired = false
|
||||||
var isImage = false
|
var isImage = false
|
||||||
for media in message.media {
|
for media in message.effectiveMedia {
|
||||||
if let _ = media as? TelegramMediaExpiredContent {
|
if let _ = media as? TelegramMediaExpiredContent {
|
||||||
isExpired = true
|
isExpired = true
|
||||||
}
|
}
|
||||||
|
|
@ -1364,7 +1364,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if resourceAvailable {
|
if resourceAvailable {
|
||||||
for media in message.media {
|
for media in message.effectiveMedia {
|
||||||
if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
|
if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
|
||||||
let _ = (context.engine.resources.data(resource: EngineMediaResource(largest.resource), incremental: true)
|
let _ = (context.engine.resources.data(resource: EngineMediaResource(largest.resource), incremental: true)
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|
|
@ -1454,7 +1454,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||||
if resourceAvailable, !message.containsSecretMedia && !isCopyProtected {
|
if resourceAvailable, !message.containsSecretMedia && !isCopyProtected {
|
||||||
var mediaReference: AnyMediaReference?
|
var mediaReference: AnyMediaReference?
|
||||||
var isVideo = false
|
var isVideo = false
|
||||||
for media in message.media {
|
for media in message.effectiveMedia {
|
||||||
if let image = media as? TelegramMediaImage, let _ = largestImageRepresentation(image.representations) {
|
if let image = media as? TelegramMediaImage, let _ = largestImageRepresentation(image.representations) {
|
||||||
mediaReference = ImageMediaReference.standalone(media: image).abstract
|
mediaReference = ImageMediaReference.standalone(media: image).abstract
|
||||||
break
|
break
|
||||||
|
|
@ -1481,7 +1481,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloadableMediaResourceInfos: [String] = []
|
var downloadableMediaResourceInfos: [String] = []
|
||||||
for media in message.media {
|
for media in message.effectiveMedia {
|
||||||
if let file = media as? TelegramMediaFile {
|
if let file = media as? TelegramMediaFile {
|
||||||
if let info = extractMediaResourceDebugInfo(resource: file.resource) {
|
if let info = extractMediaResourceDebugInfo(resource: file.resource) {
|
||||||
downloadableMediaResourceInfos.append(info)
|
downloadableMediaResourceInfos.append(info)
|
||||||
|
|
@ -1496,7 +1496,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isCopyProtected {
|
if !isCopyProtected {
|
||||||
for media in message.media {
|
for media in message.effectiveMedia {
|
||||||
if let file = media as? TelegramMediaFile {
|
if let file = media as? TelegramMediaFile {
|
||||||
if file.isMusic {
|
if file.isMusic {
|
||||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_SaveToFiles, icon: { theme in
|
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_SaveToFiles, icon: { theme in
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
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
|
return
|
||||||
}
|
}
|
||||||
let context = self.context
|
let context = self.context
|
||||||
|
|
|
||||||
|
|
@ -302,7 +302,7 @@ final class DownloadedMediaStoreManagerImpl: DownloadedMediaStoreManager {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for item in items {
|
for item in items {
|
||||||
for media in item.message.media {
|
for media in item.message.effectiveMedia {
|
||||||
if let id = media.id, id == item.mediaId {
|
if let id = media.id, id == item.mediaId {
|
||||||
self.store(.standalone(media: media), timestamp: item.message.timestamp, peerId: item.message.id.peerId)
|
self.store(.standalone(media: media), timestamp: item.message.timestamp, peerId: item.message.id.peerId)
|
||||||
break
|
break
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue