From 5e4c2f2a6f93195af1cdefd7519c11c40d3f6ae1 Mon Sep 17 00:00:00 2001 From: lyuksovannyy Date: Sun, 11 Oct 2026 04:16:42 +0200 Subject: [PATCH] Message Logger: deleted dates of logged photos, open them without a photo - 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 --- plugin.json | 2 +- src/hooks/avatars.py | 99 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/plugin.json b/plugin.json index fb38d5d..970bf59 100644 --- a/plugin.json +++ b/plugin.json @@ -3,7 +3,7 @@ "name": "Message Logger", "description": "Shows deleted and edited messages and their edit history from your telegram-panel message logger.", "author": "@lyuksovannyy", - "version": "0.1.5", + "version": "0.1.6", "icon": "exteraGramPluginIcons/4", "app_version": ">=12.5.1", "sdk_version": ">=1.4.4.3", diff --git a/src/hooks/avatars.py b/src/hooks/avatars.py index a892ce7..15bae55 100644 --- a/src/hooks/avatars.py +++ b/src/hooks/avatars.py @@ -90,6 +90,7 @@ class Avatars: self._appending = False self._updates = {} # (account, dialog) -> args of its last dialogPhotosUpdate self._icon = None + self._dates = {} # logged photo id -> (set at, deleted at), ISO strings self._watched = {} # gallery hash -> its page listener (kept alive) self._position = None # ProfileGalleryView.getRealPosition self._shown = None # PhotoViewer.isVisible() @@ -118,6 +119,21 @@ class Avatars: except Exception as 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): def after_hooked_method(self, param): try: @@ -135,6 +151,12 @@ class Avatars: self.handles += hook_methods(self.plugin, CONTROLLER, "getDialogPhotos", AfterPhotos()) self.handles += hook_methods(self.plugin, THUMBS, "onDraw", AfterThumbs()) 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"): self.handles += hook_methods(self.plugin, OVERLAYS, name, AfterOverlays()) self.handles += hook_methods( @@ -193,6 +215,7 @@ class Avatars: recorded.append( (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: self._recorded[(account, dialog_id)] = recorded self._loaded_at[(account, dialog_id)] = time.monotonic() @@ -249,14 +272,14 @@ class Avatars: field(viewer, "avatarsDialogId") or 0 ) == 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 gallery, at most every few seconds (it reads the photos again).""" from org.telegram.messenger import MessagesController, NotificationCenter if not self.handles: 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 return self._appending = True # getDialogPhotos calls back into photos_wanted @@ -461,6 +484,78 @@ class Avatars: root.addView(mark, params) 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): if self._icon is None: from android.graphics import Paint, PorterDuff, PorterDuffColorFilter