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>
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.addAllMediaFromBlockflattens everypageBlockSlideshow/pageBlockCollageitem intophotoBlocks. Tapping a slideshow item therefore goes throughRealPageBlocksAdapterlike any other article photo, and needed no special handling. - Rich messages:
RichMessageLayout.collectMediaBlocksflattens single media and slideshow items into one list forChatArticlePageBlocksAdapter. Swiping in the viewer walks through all of them, andupdateSlideshowCellkeeps 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 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.
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.
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.topicStartMessagein aMessageObject, setsisTopicMainMessage = true, and passes it as bothreplyToMsgandreplyToTopMsg.SendMessagesHelpersends its id astop_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. InsteadmonoForumPeer = 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.