Rename to Extended Media Forwards and forward rich message media
Publish plugin / publish (push) Successful in 5s

Rename the plugin (id extended_media_forwards) and add the Forward button
to media opened from rich messages, both single media and carousels.

Rich message media opens PhotoViewer through ChatActivity's
ChatArticlePageBlocksAdapter, whose parent object is the MessageObject
rather than a WebPage, so the article-only check hid the button. The
re-sent media links back to the message when it lives in a channel or
supergroup; elsewhere it is sent without a caption. FileLoader now gets
the parent object so expired file references can be refreshed.

Instant View carousels already worked: slideshow items are flattened
into the article's photo list. Findings are in docs/media-viewer-notes.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lyuksovannyyandClaude Opus 5.5 committed 2026-10-07 00:51:58 +02:00
1 parent 05c5212a8a
commit 46503f24a0
9 files changed
+317 -36

No files matched your search

+100
View File
@@ -0,0 +1,100 @@
# Media viewer internals — findings
Notes on how exteraGram opens page-block media, gathered by decompiling the
12.10.6 APK (versionCode 71129) with jadx and testing on a device. Class and
field names are from that build. They are app internals, not SDK API, so check
them again after app updates.
## Two adapters, one PhotoViewer
Page-block media never opens with a `MessageObject` list. PhotoViewer runs on a
`PhotoViewer.PageBlocksAdapter` instead, and `onPhotoShow` fires the same way
for both callers below, so one hook covers them.
| Opened from | Adapter | `getParentObject()` |
|---|---|---|
| Instant View article (`ArticleViewer.openPhoto`) | `ArticleViewer.RealPageBlocksAdapter` | `TLRPC.WebPage` |
| Rich message in a chat (`ChatActivity.openArticlePhoto`) | `ChatActivity.ChatArticlePageBlocksAdapter` | the message's `MessageObject`, or the bare `TL_iv.RichMessage` when constructed without one |
Both adapters expose the same `getMedia(i)` / `getFile(i)` / `isVideo(i)`. Those
methods resolve through `ArticleViewer.WebPageUtils`, which has overloads for
`WebPage`, `TL_iv.Page` and `TL_iv.RichMessage`. That is why `ivmedia.media_at`
works unchanged for both. Only the parent object tells them apart.
A rich message is recognised by `MessageObject.messageOwner.rich_message != null`.
## Carousels (slideshows)
- **Articles:** `WebpageAdapter.addAllMediaFromBlock` flattens every
`pageBlockSlideshow` / `pageBlockCollage` item into `photoBlocks`. Tapping a
slideshow item therefore goes through `RealPageBlocksAdapter` like any other
article photo, and needed no special handling.
- **Rich messages:** `RichMessageLayout.collectMediaBlocks` flattens single
media and slideshow items into one list for `ChatArticlePageBlocksAdapter`.
Swiping in the viewer walks through all of them, and
`updateSlideshowCell` keeps the in-chat carousel in sync.
## telegra.ph cannot produce a carousel
Telegraph has no gallery/slideshow node. The editor has no such block, and
`createPage` keeps only the first `<img>` of a `<figure>` holding several, so
consecutive images render as separate `pageBlockPhoto`s. Carousels only reach
the viewer from other Instant View templates (sites other than telegra.ph,
covered by the `any_article` setting) and from rich messages.
`telegra.ph/upload` no longer accepts files ("Unknown error"). Test pages have
to reference external image URLs, e.g. on `upload.wikimedia.org`.
## Rich messages need Premium to send media
The rich composer is the attach menu's "Article" tab (`ChatAttachAlertRichLayout`).
Sending is locked unless `MessagesController.richEditorAllowed()` or
`UserConfig.isPremium()`, whenever the content is "lossy"
(`RichMessageConvert.isLossy`). Lossy means anything a plain message can't
represent, and every media block is lossy. A non-Premium account therefore
cannot send a test rich message with media.
To test without one, build the message locally and open it the same way
`ChatActivity.openArticlePhoto` does:
```python
rm = TL_iv.RichMessage() # fill rm.photos and rm.blocks
msg = TLRPC.TL_message() # peer_id/id/dialog_id of any chat
msg.rich_message = rm
mo = MessageObject(account, msg, False, False)
adapter = ChatActivity.ChatArticlePageBlocksAdapter(rm, flat_blocks, mo)
viewer = PhotoViewer.getInstance()
viewer.setParentActivity(chat_fragment)
viewer.openPhoto(index, adapter, PhotoViewer.EmptyPhotoViewerProvider())
```
Real `TLRPC.Photo` objects with server locations can be borrowed from the
`cached_page.photos` of any loaded article. Pointing `peer_id` at a channel
exercises the message-link caption.
`ArticleViewer.open(TL_webPage, anchor)` re-requests `messages.getWebPage` and
replaces the page with the server copy. Copy the original `hash` onto the
injected `TL_webPage` to keep a locally modified `cached_page`.
## Message links
Only channels and supergroups (`ChatObject.isChannel`) have message links:
`https://t.me/<username>/<id>` when public, `https://t.me/c/<chat id>/<id>`
otherwise. On Android the dialog id of a channel is `-chat.id`, with no `-100`
prefix. DMs, basic groups and Saved Messages have no link, so media forwarded
from rich messages there is sent without a caption.
## File references
Pass the adapter's parent object (WebPage or MessageObject) as `parentObject`
to `FileLoader.loadFile`. `FileRefController` uses it to refresh an expired
file reference, and it handles rich messages explicitly.
## Device debugging
Logcat can be completely silent when the device's log buffer is disabled in
developer options, and `scripts/logs.sh` then shows nothing. A plugin can
still write diagnostics to
`ApplicationLoader.applicationContext.getExternalFilesDir(None)`, which maps
to `/sdcard/Android/data/com.exteragram.messenger/files/` and is readable with
`adb shell cat` without `run-as`.