[no bump] Ghost Mode: AyuGram's ghost mode with per-account and per-chat settings
Publish plugin / publish (push) Successful in 5s

Read receipts, story views, typing and online status are held back per
the options; read on interact, scheduled and silent sending as in AyuGram.
Every option resolves chat -> account -> global; forum topics follow
their group. Settings live in the plugin page, a chat/profile menu popup,
and a drawer row for the current account.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lyuksovannyyandClaude Opus 5.5 committed 2026-10-09 15:54:41 +02:00
1 parent dfd1f659fe
commit 00553970b7
20 files changed
+3223 -178

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
"""Keeps the ghost icon at menu-icon size.
exteraGram's `ghost` drawable is 32dp (its icon-pack variants 30dp), while
menu and drawer icons are 24dp, and both views show icons at their intrinsic
size (ImageView.ScaleType.CENTER). The menus only take a resource id, so
after a view has set the ghost icon, the drawable it holds (already swapped
for the active icon pack by the app's resources) is wrapped in a LayerDrawable
sized to 24dp:
ActionBarMenuSubItem.setTextAndIcon(CharSequence, int, ...) chat/profile menus
DrawerMenuItemView.setMenuItem(int, int, int, CharSequence) drawer rows
"""
from base_plugin import MethodHook
from hooking import field, hook_all, unhook_all
GHOST = "ghost"
SIZE_DP = 24
SUB_ITEM = "org.telegram.ui.ActionBar.ActionBarMenuSubItem"
DRAWER_ITEM = "com.exteragram.messenger.drawer.DrawerMenuItemView"
_ids = {}
def drawable_id(name):
"""A drawable's resource id by name (the app's R isn't importable)."""
if name not in _ids:
from org.telegram.messenger import ApplicationLoader
context = ApplicationLoader.applicationContext
_ids[name] = int(context.getResources().getIdentifier(name, "drawable", context.getPackageName()))
return _ids[name]
def fit(drawable, size_dp=SIZE_DP):
"""`drawable` drawn into a size_dp square, or as is when it isn't bigger."""
if drawable is None:
return None
from android.graphics.drawable import LayerDrawable
from org.telegram.messenger import AndroidUtilities
size = int(AndroidUtilities.dp(size_dp))
if max(int(drawable.getIntrinsicWidth()), int(drawable.getIntrinsicHeight())) <= size:
return drawable
layer = LayerDrawable([drawable])
layer.setLayerSize(0, size, size)
return layer
def shrink(image_view):
if image_view is not None:
image_view.setImageDrawable(fit(image_view.getDrawable()))
class Icons:
def __init__(self, plugin):
self.plugin = plugin
self.handles = []
def install(self):
icons = self
class SubItemHook(MethodHook):
# setTextAndIcon(CharSequence text, int icon, ...)
def after_hooked_method(self, param):
icons._after(param, 1, "imageView")
class DrawerItemHook(MethodHook):
# setMenuItem(int layoutId, int account, int icon, CharSequence text)
def after_hooked_method(self, param):
icons._after(param, 2, "iconView")
self.handles += hook_all(self.plugin, SUB_ITEM, "setTextAndIcon", SubItemHook())
self.handles += hook_all(self.plugin, DRAWER_ITEM, "setMenuItem", DrawerItemHook())
def uninstall(self):
unhook_all(self.plugin, self.handles)
def _after(self, param, icon_arg, view_field):
try:
args = param.args
if len(args) <= icon_arg or not isinstance(args[icon_arg], int):
return
if int(args[icon_arg]) != drawable_id(GHOST):
return
shrink(field(param.thisObject, view_field))
except Exception as exc:
self.plugin.log(f"ghost icon size: {exc}")