[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

+186
View File
@@ -0,0 +1,186 @@
"""The plugin's settings screen.
main page global ghost mode switch, the option switches, the accounts
account page that account's choice per option (Global / On / Off) and
its chat exceptions
"""
from ui.settings import Divider, Header, Selector, Switch, Text
import features
import peers
from prefs import INHERIT, OFF, ON, account_key, chats_key, global_key
GHOST_KEY = "g_ghost"
GHOST_ICON = "ghost"
def _state(value):
return "On" if value else "Off"
class SettingsPage:
def __init__(self, plugin, prefs, menus):
self.plugin = plugin
self.prefs = prefs
self.menus = menus
# main page
def build(self):
prefs = self.prefs
# The master switch shows whether every essential is on (AyuGram's
# isGhostModeActive), whichever way they got there.
self.plugin.set_setting(GHOST_KEY, prefs.ghost_active(0))
rows = [
Header(text="Ghost mode"),
Switch(
key=GHOST_KEY,
text="Ghost mode",
default=False,
subtext="Turns all ghost essentials on or off",
icon=GHOST_ICON,
on_change=self._on_ghost_change,
),
Header(text="Ghost essentials"),
]
rows += [self._switch(f) for f in features.ESSENTIALS]
rows.append(Header(text="Options"))
rows += [self._switch(f) for f in features.EXTRAS]
rows.append(Divider(text="These are the defaults. Each account can override them, and each chat can override its account: open the chat's ⋮ menu → Ghost mode."))
rows.append(Header(text="Accounts"))
accounts = self._accounts()
for slot, uid in accounts:
rows.append(
Text(
text=peers.account_name(slot),
subtext=self._account_summary(uid),
icon="msg_contacts",
create_sub_fragment=lambda slot=slot, uid=uid: self.build_account(slot, uid),
)
)
if not accounts:
rows.append(Text(text="No signed-in accounts", icon="msg_info"))
rows.append(Divider(text="The side menu's Ghost mode item opens these options for the account in use."))
return rows
def _switch(self, feature):
return Switch(
key=global_key(feature),
text=feature.title,
default=feature.default,
subtext=feature.subtext,
icon=feature.icon,
on_change=self._on_essential_change if feature.essential else None,
)
def _on_ghost_change(self, enabled):
for feature in features.ESSENTIALS:
self.prefs.set_global(feature, enabled)
self.plugin.set_setting(GHOST_KEY, bool(enabled), True)
def _on_essential_change(self, _value):
active = self.prefs.ghost_active(0)
if bool(self.plugin.get_setting(GHOST_KEY, False)) != active:
self.plugin.set_setting(GHOST_KEY, active, True)
def _accounts(self):
try:
return peers.signed_in()
except Exception as exc:
self.plugin.log(f"accounts: {exc}")
return []
def _account_summary(self, uid):
overridden = len(self.prefs.account_overrides(uid))
chats = len(self.prefs.chat_overrides(uid))
parts = [f"Ghost mode {_state(self.prefs.ghost_active(uid)).lower()}"]
parts.append(f"{overridden} own option{'s' if overridden != 1 else ''}" if overridden else "global options")
if chats:
parts.append(f"{chats} chat exception{'s' if chats != 1 else ''}")
return " · ".join(parts)
# account page
def build_account(self, slot, uid):
prefs = self.prefs
ghost_key = f"a_{uid}_ghost"
self.plugin.set_setting(ghost_key, self._ghost_choice(uid))
rows = [
Header(text=peers.account_name(slot)),
Selector(
key=ghost_key,
text="Ghost mode",
default=INHERIT,
items=["Global", "On", "Off"],
icon=GHOST_ICON,
on_change=lambda choice: self._on_account_ghost_change(uid, choice),
),
Header(text="Options"),
]
for feature in features.ALL:
global_value = prefs.global_value(feature)
rows.append(
Selector(
key=account_key(uid, feature),
text=feature.title,
default=INHERIT,
items=[f"Global ({_state(global_value)})", "On", "Off"],
icon=feature.icon,
on_change=lambda _choice: self._refresh_account_ghost(uid),
)
)
rows.append(Divider(text="Global follows the plugin's main switches."))
rows.append(Header(text="Chat exceptions"))
chats = prefs.chat_overrides(uid)
for dialog, values in sorted(chats.items(), key=lambda item: peers.dialog_title(slot, item[0]).lower()):
rows.append(
Text(
text=peers.dialog_title(slot, dialog),
subtext=" · ".join(
f"{features.BY_KEY[k].title}: {_state(v)}" for k, v in values.items()
),
icon="msg_discussion",
on_click=lambda _view, dialog=dialog: self._edit_chat(slot, uid, dialog),
)
)
if chats:
rows.append(
Text(
text="Remove all chat exceptions",
icon="msg_delete",
red=True,
on_click=lambda _view: prefs.clear_chats(uid, True),
)
)
else:
rows.append(Text(text="No chat exceptions", icon="msg_info"))
rows.append(Divider(text="Add one from a chat's ⋮ menu → Ghost mode. Tap an exception to change it."))
return rows
def _ghost_choice(self, uid):
"""What the account's Ghost mode selector shows."""
choices = {self.prefs.account_choice(uid, f) for f in features.ESSENTIALS}
if choices == {INHERIT}:
return INHERIT
return ON if self.prefs.ghost_active(uid) else OFF
def _on_account_ghost_change(self, uid, choice):
for feature in features.ESSENTIALS:
self.prefs.set_account_choice(uid, feature, choice)
self.plugin.set_setting(f"a_{uid}_ghost", int(choice), True)
def _refresh_account_ghost(self, uid):
key = f"a_{uid}_ghost"
choice = self._ghost_choice(uid)
if self.plugin.get_setting(key, INHERIT) != choice:
self.plugin.set_setting(key, choice, True)
def _edit_chat(self, slot, uid, dialog):
# Reload the page once the dialog closes, so the list shows the result.
raw_key = chats_key(uid)
def closed():
self.plugin.set_setting(raw_key, self.plugin.get_setting(raw_key, ""), True)
self.menus.open_chat_dialog(slot, dialog, closed)