Files
extendedMediaForwards/docs/media-viewer-notes.md
T
lyuksovannyyandClaude Opus 5.5 1f0a9a1a7a
Publish plugin / publish (push) Successful in 3s
Send forwarded media to the forum topic picked in the share sheet
The picker only read the selected dialog ids, so media always landed in
a forum's General topic. Read ShareAlert's selectedDialogTopics as well
and address each target the way ShareAlert.sendInternal() does: reply to
the topic's start message in forums, or set monoForumPeer in a channel's
direct-messages chat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:50:03 +02:00

5.7 KiB

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.

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 pageBlockPhotos. 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:

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.

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.

Forum topics in the share sheet

When a forum is picked in ShareAlert, the topic lands in the protected selectedDialogTopics map. That map is keyed by the TLRPC.Dialog values of selectedDialogs, not by dialog id. ShareAlert.sendInternal() then addresses each chat in one of two ways:

  • Forum topic: wraps topic.topicStartMessage in a MessageObject, sets isTopicMainMessage = true, and passes it as both replyToMsg and replyToTopMsg. SendMessagesHelper sends its id as top_msg_id. A topic's id is its start message's id.
  • Mono forum (a channel's direct-messages chat, MessagesController.isMonoForum): no reply message. Instead monoForumPeer = DialogObject.getPeerDialogId(topic.from_id).

A plugin that only reads the selectedDialogs keys sends to the chat's General topic. picker._selected_targets reproduces the logic above inside didShare(), which runs before the sheet clears its selection.

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.