generated from exteraGramPlugins/pluginBase
Publish plugin / publish (push) Successful in 3s
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>
120 lines
5.7 KiB
Markdown
120 lines
5.7 KiB
Markdown
# 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.
|
|
|
|
## 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`.
|