From 8535bde5ae416c8800892061430e44209c5946b0 Mon Sep 17 00:00:00 2001 From: lyuksovannyy Date: Sun, 11 Oct 2026 02:12:59 +0200 Subject: [PATCH] Message Logger: trash icon under the profile menu on logged photos The expanded profile gallery shows a red trash icon under the menu button while it shows a logged photo the chat no longer has. Co-Authored-By: Claude Opus 5.5 --- plugin.json | 2 +- src/hooks/avatars.py | 117 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/plugin.json b/plugin.json index ef08dc3..fb38d5d 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.4", + "version": "0.1.5", "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 cd37fb0..a892ce7 100644 --- a/src/hooks/avatars.py +++ b/src/hooks/avatars.py @@ -29,6 +29,9 @@ from views.history import _epoch CONTROLLER = "org.telegram.messenger.MessagesController" THUMBS = "org.telegram.ui.Components.GroupedPhotosListView" VIEWER = "org.telegram.ui.PhotoViewer" +GALLERY = "org.telegram.ui.Components.ProfileGalleryView" +OVERLAYS = "org.telegram.ui.ProfileActivity$OverlaysView" +_MARK_TAG = "message_logger_trash" NOTIFICATIONS = "org.telegram.messenger.NotificationCenter" # A logged photo whose id isn't Telegram's (logged by a bot) is the chat's # own when one of the chat's photos was set this close to its post. @@ -87,6 +90,8 @@ class Avatars: self._appending = False self._updates = {} # (account, dialog) -> args of its last dialogPhotosUpdate self._icon = None + self._watched = {} # gallery hash -> its page listener (kept alive) + self._position = None # ProfileGalleryView.getRealPosition self._shown = None # PhotoViewer.isVisible() def install(self): @@ -99,6 +104,20 @@ class Avatars: except Exception as exc: diag.error("avatars.photos", exc) + class AfterSetData(MethodHook): + def after_hooked_method(self, param): + try: + avatars.watch_gallery(param.thisObject) + except Exception as exc: + diag.error("avatars.gallery", exc) + + class AfterOverlays(MethodHook): + def after_hooked_method(self, param): + try: + avatars.update_profile(field(param.thisObject, "this$0")) + except Exception as exc: + diag.error("avatars.profile", exc) + class AfterThumbs(MethodHook): def after_hooked_method(self, param): try: @@ -115,6 +134,9 @@ 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()) + for name in ("onDraw", "setAlphaValue"): + self.handles += hook_methods(self.plugin, OVERLAYS, name, AfterOverlays()) self.handles += hook_methods( self.plugin, NOTIFICATIONS, "postNotificationName", BeforeNotify() ) @@ -344,6 +366,101 @@ class Avatars: icon.setBounds(right - size, bottom - size, right, bottom) icon.draw(canvas) + # the profile's trash icon (under the menu button) + + def watch_gallery(self, gallery): + """Redraws the icon as the profile's gallery changes page.""" + key = int(gallery.hashCode()) + if key in self._watched: + return + from java import dynamic_proxy, jclass + + listener_class = jclass("androidx.viewpager.widget.ViewPager$OnPageChangeListener") + avatars = self + + class Pages(dynamic_proxy(listener_class)): + def onPageScrolled(self, position, offset, pixels): + pass + + def onPageSelected(self, position): + on_ui(lambda: avatars.update_profile(field(gallery, "this$0"))) + + def onPageScrollStateChanged(self, state): + pass + + listener = Pages() + gallery.addOnPageChangeListener(listener) + self._watched[key] = listener + + def _current_photo(self, gallery): + if self._position is None: + method = java_class(GALLERY).getDeclaredMethod("getRealPosition") + method.setAccessible(True) + self._position = method + photos = field(gallery, "photos") + index = int(self._position.invoke(gallery)) + if photos is None or not 0 <= index < photos.size(): + return None + return photos.get(index) + + def update_profile(self, profile): + """Shows the icon while the expanded gallery shows a logged photo.""" + if profile is None or not self._ids: + return + gallery = field(profile, "avatarsViewPager") + if gallery is None: + return + photo = self._current_photo(gallery) + shown = ( + photo is not None + and int(photo.id) in self._ids + and bool(field(profile, "isPulledDown")) + ) + mark = self._profile_mark(profile, create=shown) + if mark is None: + return + from android.view import View + + mark.setVisibility(View.VISIBLE if shown else View.GONE) + mark.setAlpha(float(field(profile, "currentExpandAnimatorValue") or 1.0)) + + def _profile_mark(self, profile, create): + root = field(profile, "fragmentView") + if root is None or not hasattr(root, "addView"): + return None + for index in range(root.getChildCount()): + child = root.getChildAt(index) + if str(child.getTag()) == _MARK_TAG: + return child + if not create: + return None + from android.view import Gravity + from android.widget import FrameLayout, ImageView + from java import jint + from org.telegram.messenger import AndroidUtilities + from org.telegram.ui.ActionBar import ActionBar, Theme + + from android.graphics import PorterDuff, PorterDuffColorFilter + from org.telegram.messenger import R + + from hooks.render import _trash_drawable + + dp = AndroidUtilities.dp + icon = _trash_drawable(R.drawable.msg_delete) + red = jint(Theme.getColor(Theme.key_text_RedBold)) + icon.setColorFilter(PorterDuffColorFilter(red, PorterDuff.Mode.SRC_IN)) + mark = ImageView(root.getContext()) + mark.setTag(_MARK_TAG) + mark.setImageDrawable(icon) + mark.setPadding(dp(8), dp(8), dp(8), dp(8)) + mark.setBackground(Theme.createCircleDrawable(dp(36), jint(0x66000000))) + params = FrameLayout.LayoutParams(dp(36), dp(36)) + params.gravity = Gravity.TOP | Gravity.RIGHT + params.topMargin = AndroidUtilities.statusBarHeight + ActionBar.getCurrentActionBarHeight() + params.rightMargin = dp(10) + root.addView(mark, params) + return mark + def _trash(self): if self._icon is None: from android.graphics import Paint, PorterDuff, PorterDuffColorFilter