1 Commits
Author SHA1 Message Date
lyuksovannyyandClaude Opus 5.5 5e4c2f2a6f Message Logger: deleted dates of logged photos, open them without a photo
Publish plugin / publish (push) Successful in 4s
- The photo viewer's subtitle of a logged photo the chat no longer has
  says when it was deleted and set.
- Tapping the avatar of a chat without a photo opens the viewer on its
  photos, the logged ones included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:16:42 +02:00
2 changed files with 98 additions and 3 deletions

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Message Logger", "name": "Message Logger",
"description": "Shows deleted and edited messages and their edit history from your telegram-panel message logger.", "description": "Shows deleted and edited messages and their edit history from your telegram-panel message logger.",
"author": "@lyuksovannyy", "author": "@lyuksovannyy",
"version": "0.1.5", "version": "0.1.6",
"icon": "exteraGramPluginIcons/4", "icon": "exteraGramPluginIcons/4",
"app_version": ">=12.5.1", "app_version": ">=12.5.1",
"sdk_version": ">=1.4.4.3", "sdk_version": ">=1.4.4.3",
+97 -2
View File
@@ -90,6 +90,7 @@ class Avatars:
self._appending = False self._appending = False
self._updates = {} # (account, dialog) -> args of its last dialogPhotosUpdate self._updates = {} # (account, dialog) -> args of its last dialogPhotosUpdate
self._icon = None self._icon = None
self._dates = {} # logged photo id -> (set at, deleted at), ISO strings
self._watched = {} # gallery hash -> its page listener (kept alive) self._watched = {} # gallery hash -> its page listener (kept alive)
self._position = None # ProfileGalleryView.getRealPosition self._position = None # ProfileGalleryView.getRealPosition
self._shown = None # PhotoViewer.isVisible() self._shown = None # PhotoViewer.isVisible()
@@ -118,6 +119,21 @@ class Avatars:
except Exception as exc: except Exception as exc:
diag.error("avatars.profile", exc) diag.error("avatars.profile", exc)
class AfterSwitch(MethodHook):
def after_hooked_method(self, param):
try:
avatars.viewer_dates(param.thisObject, int(param.args[0]))
except Exception as exc:
diag.error("avatars.dates", exc)
class BeforeOpen(MethodHook):
def before_hooked_method(self, param):
try:
if avatars.open_without_photo(param.thisObject):
param.setResult(None)
except Exception as exc:
diag.error("avatars.open", exc)
class AfterThumbs(MethodHook): class AfterThumbs(MethodHook):
def after_hooked_method(self, param): def after_hooked_method(self, param):
try: try:
@@ -135,6 +151,12 @@ class Avatars:
self.handles += hook_methods(self.plugin, CONTROLLER, "getDialogPhotos", AfterPhotos()) self.handles += hook_methods(self.plugin, CONTROLLER, "getDialogPhotos", AfterPhotos())
self.handles += hook_methods(self.plugin, THUMBS, "onDraw", AfterThumbs()) self.handles += hook_methods(self.plugin, THUMBS, "onDraw", AfterThumbs())
self.handles += hook_methods(self.plugin, GALLERY, "setData", AfterSetData()) self.handles += hook_methods(self.plugin, GALLERY, "setData", AfterSetData())
self.handles += hook_methods(
self.plugin, VIEWER, "setIsAboutToSwitchToIndex", AfterSwitch()
)
self.handles += hook_methods(
self.plugin, "org.telegram.ui.ProfileActivity", "openAvatar", BeforeOpen()
)
for name in ("onDraw", "setAlphaValue"): for name in ("onDraw", "setAlphaValue"):
self.handles += hook_methods(self.plugin, OVERLAYS, name, AfterOverlays()) self.handles += hook_methods(self.plugin, OVERLAYS, name, AfterOverlays())
self.handles += hook_methods( self.handles += hook_methods(
@@ -193,6 +215,7 @@ class Avatars:
recorded.append( recorded.append(
(photo, int(original) if original else None, _epoch(avatar.get("date"))) (photo, int(original) if original else None, _epoch(avatar.get("date")))
) )
self._dates[int(photo.id)] = (avatar.get("date"), avatar.get("deleted_at"))
with self._lock: with self._lock:
self._recorded[(account, dialog_id)] = recorded self._recorded[(account, dialog_id)] = recorded
self._loaded_at[(account, dialog_id)] = time.monotonic() self._loaded_at[(account, dialog_id)] = time.monotonic()
@@ -249,14 +272,14 @@ class Avatars:
field(viewer, "avatarsDialogId") or 0 field(viewer, "avatarsDialogId") or 0
) == int(dialog_id) ) == int(dialog_id)
def _append(self, account, dialog_id): def _append(self, account, dialog_id, force=False):
"""Puts the logged photos into the chat's photos and tells the """Puts the logged photos into the chat's photos and tells the
gallery, at most every few seconds (it reads the photos again).""" gallery, at most every few seconds (it reads the photos again)."""
from org.telegram.messenger import MessagesController, NotificationCenter from org.telegram.messenger import MessagesController, NotificationCenter
if not self.handles: if not self.handles:
return # unloaded return # unloaded
if self._viewing(dialog_id): if not force and self._viewing(dialog_id):
on_ui(lambda: self._append(account, dialog_id), 1000) # once it closed on_ui(lambda: self._append(account, dialog_id), 1000) # once it closed
return return
self._appending = True # getDialogPhotos calls back into photos_wanted self._appending = True # getDialogPhotos calls back into photos_wanted
@@ -461,6 +484,78 @@ class Avatars:
root.addView(mark, params) root.addView(mark, params)
return mark return mark
# dates in the photo viewer
def viewer_dates(self, viewer, index):
"""A logged photo's subtitle: when it was deleted and set (instead of
its upload to the log group)."""
photos = field(viewer, "avatarsArr")
if photos is None or not 0 <= index < photos.size():
return
photo = photos.get(index)
dates = self._dates.get(int(photo.id)) if photo is not None else None
container = field(viewer, "actionBarContainer")
if dates is None or container is None:
return
from views.history import _when
set_at, deleted_at = dates
parts = [f"deleted {_when(deleted_at)}" if deleted_at else "deleted"]
if set_at:
parts.append(f"set {_when(set_at)}")
text = " · ".join(parts)
container.setSubtitle(text[:1].upper() + text[1:])
# a chat without a photo
def open_without_photo(self, profile):
"""openAvatar of a chat that has no photo but logged ones: the photo
viewer opens on the newest logged photo (with the rest)."""
from org.telegram.messenger import ImageLocation, MessagesController
from org.telegram.tgnet import TLRPC
from org.telegram.ui import PhotoViewer
account = int(profile.getCurrentAccount())
controller = MessagesController.getInstance(account)
user_id, chat_id = int(field(profile, "userId") or 0), int(field(profile, "chatId") or 0)
if user_id:
peer, dialog_id = controller.getUser(user_id), user_id
empty = TLRPC.TL_userProfilePhotoEmpty
else:
peer, dialog_id = controller.getChat(chat_id), -chat_id
empty = TLRPC.TL_chatPhotoEmpty
photo = getattr(peer, "photo", None) if peer is not None else None
if photo is not None and not isinstance(photo, empty):
return False # the app opens it
recorded = self._recorded.get((account, dialog_id))
if not recorded:
controller.getDialogPhotos(dialog_id) # loads the logged ones
return False
# The newest of the chat's photos (its own, then the logged ones).
holder = controller.getDialogPhotos(dialog_id)
if holder is not None:
self._add_to(account, dialog_id, holder)
listed = java_list(field(holder, "photos")) if holder is not None else []
first = next((p for p in listed if p is not None), recorded[0][0])
size = media_share.photo_size(first, 800)
if size is None:
return False
viewer = PhotoViewer.getInstance()
viewer.setParentActivity(profile)
viewer.openPhoto(
size.location, ImageLocation.getForPhoto(size, first), field(profile, "provider")
)
# The profile names no chat for a photo it doesn't show itself: the
# viewer is told, and gets the chat's photos (the logged ones).
from hook_utils import set_private_field
from java import jlong
set_private_field(viewer, "avatarsDialogId", jlong(dialog_id))
self._told.pop((account, dialog_id), None)
self._append(account, dialog_id, force=True)
return True
def _trash(self): def _trash(self):
if self._icon is None: if self._icon is None:
from android.graphics import Paint, PorterDuff, PorterDuffColorFilter from android.graphics import Paint, PorterDuff, PorterDuffColorFilter