"""Deleted messages removed from view ("hidden"), marked on the server so every client skips them; a chat can reveal its hidden messages again. - `flush` sends the marks waiting in the store to the server; - the chat menu toggles revealing (per chat, until the app restarts): the hidden deleted messages come back from the logger, then go again; - a revealed hidden message has "Unhide" in its menu (hooks/menu.py), and while revealing, the selection bar gets an Unhide button for those selected (ChatActivity.createActionMode adds the bar's items, updateActionModeTitle runs on every selection change; probed on 12.10.6). """ import threading from base_plugin import MethodHook import diag import peers import store from api import ApiError from jutil import field, hook_methods, in_background, on_ui CHAT = "org.telegram.ui.ChatActivity" _UNHIDE_ID = 0x4D4C01 # the selection bar item def _bulletin(text, error=False): def show(): from ui.bulletin import BulletinHelper (BulletinHelper.show_error if error else BulletinHelper.show_info)(text) on_ui(show) def _selected(chat): """MessageObjects selected in the chat (both halves of a merged chat).""" out = [] for array in field(chat, "selectedMessagesIds") or []: if array is None: continue for index in range(array.size()): message = array.valueAt(index) if message is not None: out.append(message) return out def is_revealed_hidden(message): """A hidden deleted message shown because its chat reveals them.""" if message is None or int(message.getId()) <= 0: return False account, dialog_id = int(message.currentAccount), int(message.getDialogId()) message_id = int(message.getId()) return ( store.is_revealed(account, dialog_id) and store.is_deleted(account, dialog_id, message_id) and store.is_hidden(account, dialog_id, message_id) ) class Hidden: def __init__(self, plugin, api, chats): self.plugin = plugin self.api = api self.chats = chats # hooks/chat.ChatSync self.handles = [] self._flushing = threading.Lock() self._items = {} # ChatActivity classGuid -> the bar's Unhide item def install(self): hidden = self class AfterActionMode(MethodHook): def after_hooked_method(self, param): try: hidden.add_bar_item(param.thisObject) except Exception as exc: diag.error("hidden.bar", exc) class AfterTitle(MethodHook): def after_hooked_method(self, param): try: hidden.update_bar_item(param.thisObject) except Exception as exc: diag.error("hidden.title", exc) self.handles += hook_methods(self.plugin, CHAT, "createActionMode", AfterActionMode()) self.handles += hook_methods(self.plugin, CHAT, "updateActionModeTitle", AfterTitle()) in_background(self.flush) def uninstall(self): for handle in self.handles: try: self.plugin.unhook_method(handle) except Exception: pass self.handles = [] # the server def flush(self): """Sends hidden marks waiting in the store (background).""" if not self.api.configured or not self._flushing.acquire(blocking=False): return try: for (account, dialog_id, hidden), ids in store.pending_hidden().items(): me = peers.own_user_id(account) chat_id = peers.marked_chat_id(account, dialog_id) if not me or chat_id is None: continue try: self.api.hidden(me, chat_id, ids, hidden) except ApiError as exc: diag.log(f"hidden {chat_id}: {exc}") continue store.hidden_sent(account, dialog_id, ids, hidden) diag.log(f"{'hid' if hidden else 'unhid'} {chat_id}: {ids}") finally: self._flushing.release() def hide(self, account, dialog_id, ids, hidden=True): store.set_hidden(account, dialog_id, ids, hidden) in_background(self.flush) # revealing (chat menu) def toggle_reveal(self, fragment): account = int(fragment.getCurrentAccount()) dialog_id = int(fragment.getDialogId()) on = not store.is_revealed(account, dialog_id) store.reveal(account, dialog_id, on) if on: count = self.chats.restore_hidden(fragment) # And the server's answer, for those not cached. store.expire_coverage(account, dialog_id) self.chats.schedule(fragment) _bulletin( f"Message Logger: revealing hidden deleted messages ({count} loaded)" if count else "Message Logger: revealing hidden deleted messages" ) else: ids = store.hidden_deleted(account, dialog_id) self.chats.drop_from_view(account, dialog_id, ids) _bulletin("Message Logger: hidden deleted messages are hidden again") # unhiding def unhide(self, messages): by_chat = {} for message in messages: if is_revealed_hidden(message): key = (int(message.currentAccount), int(message.getDialogId())) by_chat.setdefault(key, []).append(int(message.getId())) for (account, dialog_id), ids in by_chat.items(): self.hide(account, dialog_id, ids, hidden=False) self.chats.refresh_dialog(account, dialog_id, ids) count = sum(len(ids) for ids in by_chat.values()) if count: _bulletin(f"Message Logger: {count} message{'s' if count > 1 else ''} unhidden") # the selection bar def add_bar_item(self, chat): """The bar's Unhide item (also for a chat opened before the plugin loaded: its bar exists already).""" bar = chat.getActionBar() menu = bar.createActionMode() if bar is not None else None if menu is None: return None from android.view import View from android_utils import OnClickListener from org.telegram.messenger import AndroidUtilities, R item = menu.getItem(_UNHIDE_ID) if item is None: item = menu.addItemWithWidth( _UNHIDE_ID, R.drawable.msg_views, AndroidUtilities.dp(54) ) item.setContentDescription("Unhide") item.setVisibility(View.GONE) def chosen(_view): self.unhide(_selected(chat)) chat.clearSelectionMode() item.setOnClickListener(OnClickListener(chosen)) self._items[int(chat.getClassGuid())] = item return item def update_bar_item(self, chat): item = self._items.get(int(chat.getClassGuid())) or self.add_bar_item(chat) if item is None: return from android.view import View account, dialog_id = int(chat.getCurrentAccount()), int(chat.getDialogId()) show = store.is_revealed(account, dialog_id) and any( is_revealed_hidden(m) for m in _selected(chat) ) item.setVisibility(View.VISIBLE if show else View.GONE)