generated from exteraGramPlugins/pluginBase
Keep the status card visible and the unread prefill stable
Two bugs visible in a device log. "injector: anchor 0 not loaded, skipping insert" was a regression from the previous commit. _newest_visible_message_id returns 0 when the offset reaches past the loaded window, and the strict anchor check then refused to place the card, so pressing Summarize appeared to do nothing at all. Before that commit anchor 0 silently matched a date separator, which is why it went unnoticed. A live card is the user's only feedback that the press registered, so it must always appear: try the exact anchor, then id ordering, then the bottom of the chat, and let reanchor() move it once the analyzed range is known. Restoring saved summaries stays strict, since nothing corrects their position later. _newest_visible_message_id also falls back to the oldest loaded message rather than returning 0. The unread button prefilled a smaller range on each press -- 1000, then 15, then 75, with ~1100 unread. Viewing a chat calls MessagesController.markDialogAsRead, so dialog.unread_count decays while the chat is open and no longer describes the backlog the user wants summarized. Resolve the count from the unread divider instead, which is what the app's own button uses (onPageDownClicked branches on createUnreadMessageAfterId) and which does not decay while the chat stays open, and keep the largest value seen per chat so a later press never prefills less than an earlier one. Both are covered by tests that fail against the previous behaviour: the strict anchor path reports inserted=True while leaving no card in the list, and the unread resolver reproduces exactly 1100, 15, 75.
This commit is contained in:
1 parent
b05d1a274c
commit
785618233b
4 files changed
+162
-13
No files matched your search
@@ -102,6 +102,8 @@ class UnreadHook:
|
||||
self.plugin = plugin
|
||||
self._unhooks: List[Any] = []
|
||||
self._hooked_view_ids: set = set()
|
||||
# dialog_id -> largest unread count seen while this chat stayed open.
|
||||
self._peak_unread: dict = {}
|
||||
|
||||
def install(self) -> None:
|
||||
try:
|
||||
@@ -307,14 +309,13 @@ class UnreadHook:
|
||||
diagnostics.record_error("unread_hook._trigger", RuntimeError("could not resolve dialog_id"))
|
||||
return
|
||||
|
||||
raw_unread = 0
|
||||
try:
|
||||
controller = get_messages_controller()
|
||||
dialog = controller.dialogs_dict.get(dialog_id) if controller else None
|
||||
if dialog is not None:
|
||||
raw_unread = int(getattr(dialog, "unread_count", 0) or 0)
|
||||
except Exception:
|
||||
raw_unread = 0
|
||||
# unread_count decays while the chat is open: viewing messages calls
|
||||
# MessagesController.markDialogAsRead, so by a second press it can
|
||||
# report 15 where the first press saw ~1100. Prefer the unread
|
||||
# divider, which is what the app's own button uses
|
||||
# (onPageDownClicked branches on createUnreadMessageAfterId) and
|
||||
# which does not decay while the chat stays open.
|
||||
raw_unread = self._resolve_unread_count(fragment, dialog_id)
|
||||
|
||||
if raw_unread > 0:
|
||||
count = min(MAX_MSG_COUNT, max(1, raw_unread))
|
||||
@@ -330,6 +331,67 @@ class UnreadHook:
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("unread_hook._trigger", exc)
|
||||
|
||||
def _resolve_unread_count(self, fragment: Any, dialog_id: int) -> int:
|
||||
"""Unread message count that stays stable while the chat is open.
|
||||
|
||||
Three sources, best first:
|
||||
|
||||
1. The unread divider (`createUnreadMessageAfterId`): the id the app's
|
||||
own pagedown button scrolls to. Counting loaded messages newer than
|
||||
it gives the real backlog and does not shrink as messages are read.
|
||||
2. `dialog.unread_count`: accurate on first open, but
|
||||
MessagesController.markDialogAsRead decrements it while the chat is
|
||||
viewed, so a second press saw 15 instead of ~1100.
|
||||
3. The largest value seen earlier for this chat, so a later press never
|
||||
prefills less than an earlier one.
|
||||
"""
|
||||
from client_utils import get_messages_controller
|
||||
|
||||
candidates = []
|
||||
|
||||
divider_id = 0
|
||||
try:
|
||||
divider_id = int(getattr(fragment, "createUnreadMessageAfterId", 0) or 0)
|
||||
except Exception:
|
||||
divider_id = 0
|
||||
if divider_id > 0:
|
||||
try:
|
||||
from services.rich_injector import get_messages_list
|
||||
|
||||
messages_list = get_messages_list(fragment)
|
||||
if messages_list is not None:
|
||||
newer = 0
|
||||
for i in range(messages_list.size()):
|
||||
owner = getattr(messages_list.get(i), "messageOwner", None)
|
||||
msg_id = int(getattr(owner, "id", 0) or 0) if owner is not None else 0
|
||||
# Injected cards are negative and date separators are 0.
|
||||
if msg_id > 0 and msg_id >= divider_id:
|
||||
newer += 1
|
||||
if newer > 0:
|
||||
candidates.append(newer)
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("unread_hook.divider_count", exc)
|
||||
|
||||
try:
|
||||
controller = get_messages_controller()
|
||||
dialog = controller.dialogs_dict.get(dialog_id) if controller else None
|
||||
if dialog is not None:
|
||||
candidates.append(int(getattr(dialog, "unread_count", 0) or 0))
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("unread_hook.dialog_unread", exc)
|
||||
|
||||
previous = int(self._peak_unread.get(dialog_id, 0) or 0)
|
||||
resolved = max(candidates + [previous]) if candidates or previous else 0
|
||||
if resolved > 0:
|
||||
if len(self._peak_unread) > 64:
|
||||
self._peak_unread.clear()
|
||||
self._peak_unread[dialog_id] = resolved
|
||||
diagnostics.note(
|
||||
f"unread_hook: unread divider={divider_id} candidates={candidates} "
|
||||
f"previous={previous} resolved={resolved}"
|
||||
)
|
||||
return resolved
|
||||
|
||||
@staticmethod
|
||||
def _resolve_dialog_id(fragment: Any) -> Optional[int]:
|
||||
try:
|
||||
|
||||
+10
@@ -298,6 +298,13 @@ class ChatSummariesPlugin(BasePlugin):
|
||||
`skip` mirrors the summarize offset, so with an offset the guess lands
|
||||
where the analyzed range actually ends instead of at the bottom of the
|
||||
chat. Injected cards carry negative ids and are ignored.
|
||||
|
||||
When the offset reaches past the loaded window there is no message to
|
||||
skip to, so fall back to the oldest loaded message rather than 0: the
|
||||
card only needs somewhere valid to appear while "Fetching messages…" is
|
||||
shown, and reanchor() moves it to the exact spot once the range is
|
||||
known. Returning 0 would make the insert fail and the user would click
|
||||
Summarize and see nothing happen.
|
||||
"""
|
||||
try:
|
||||
from services.rich_injector import get_messages_list
|
||||
@@ -306,15 +313,18 @@ class ChatSummariesPlugin(BasePlugin):
|
||||
if messages_list is None:
|
||||
return 0
|
||||
remaining = max(0, int(skip))
|
||||
oldest = 0
|
||||
for i in range(messages_list.size()):
|
||||
owner = getattr(messages_list.get(i), "messageOwner", None)
|
||||
msg_id = int(getattr(owner, "id", 0) or 0) if owner is not None else 0
|
||||
if msg_id <= 0:
|
||||
continue
|
||||
oldest = msg_id
|
||||
if remaining:
|
||||
remaining -= 1
|
||||
continue
|
||||
return msg_id
|
||||
return oldest
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("_newest_visible_message_id", exc)
|
||||
return 0
|
||||
|
||||
@@ -316,14 +316,29 @@ class SummaryInjection:
|
||||
if existing >= 0:
|
||||
messages_list_now.set(existing, message_object)
|
||||
else:
|
||||
# A live card is a direct response to the user pressing
|
||||
# Summarize, so it must become visible even when the
|
||||
# provisional anchor is not loaded (a large offset reaches
|
||||
# past the loaded window). Fall back to id ordering, which
|
||||
# still keeps every newer message below the card, and let
|
||||
# reanchor() move it to the exact spot once the analyzed
|
||||
# range is known. Restoring saved summaries stays strict --
|
||||
# there is no follow-up correction there.
|
||||
index = find_index_for_anchor(messages_list_now, self.anchor_msg_id)
|
||||
if index < 0:
|
||||
# Anchor not loaded in this view: placing the card by id
|
||||
# order alone would put it next to unrelated messages.
|
||||
diagnostics.note(
|
||||
f"injector: anchor {self.anchor_msg_id} not loaded, skipping insert"
|
||||
index = find_index_for_anchor(
|
||||
messages_list_now, self.anchor_msg_id, require_anchor=False
|
||||
)
|
||||
return
|
||||
if index < 0:
|
||||
# No anchor at all (nothing loaded yet, or the offset
|
||||
# reached past everything). Show the card at the bottom,
|
||||
# where the user is looking after pressing Summarize;
|
||||
# reanchor() relocates it once the range is known.
|
||||
diagnostics.note(
|
||||
f"injector: anchor {self.anchor_msg_id} unresolved, "
|
||||
"showing status card at the bottom"
|
||||
)
|
||||
index = 0
|
||||
messages_list_now.add(index, message_object)
|
||||
adapter = get_chat_adapter(self.chat_activity)
|
||||
if adapter is not None:
|
||||
|
||||
@@ -794,6 +794,50 @@ def main(argv=None) -> int:
|
||||
f"installed hooks: {len(plugin.unread_hook._unhooks)} (expected 3)",
|
||||
)
|
||||
|
||||
# unread_count decays while a chat is open (markDialogAsRead runs as
|
||||
# messages are viewed), so pressing the unread button a second time used
|
||||
# to prefill a far smaller range than the first press: 1100 then 15 then
|
||||
# 75. The resolved count must not shrink while the chat stays open.
|
||||
class _UnreadDialog:
|
||||
def __init__(self, count):
|
||||
self.unread_count = count
|
||||
|
||||
class _UnreadFragment:
|
||||
def __init__(self, divider, ids):
|
||||
self.createUnreadMessageAfterId = divider
|
||||
self.messages = _JavaList()
|
||||
for mid in ids:
|
||||
owner_obj = _TLObj("TL_message")
|
||||
owner_obj.id = mid
|
||||
self.messages.add(_FakeMessageObject(0, owner_obj))
|
||||
|
||||
unread_dialog = _UnreadDialog(1100)
|
||||
cu_mod = sys.modules["client_utils"]
|
||||
real_controller = cu_mod.get_messages_controller
|
||||
|
||||
class _UnreadController:
|
||||
class dialogs_dict:
|
||||
@staticmethod
|
||||
def get(_dialog_id):
|
||||
return unread_dialog
|
||||
|
||||
cu_mod.get_messages_controller = lambda: _UnreadController
|
||||
try:
|
||||
unread_fragment = _UnreadFragment(171000, list(range(172700, 172670, -1)))
|
||||
resolved = []
|
||||
for decayed in (1100, 15, 75):
|
||||
unread_dialog.unread_count = decayed
|
||||
resolved.append(
|
||||
plugin.unread_hook._resolve_unread_count(unread_fragment, -1978105175)
|
||||
)
|
||||
finally:
|
||||
cu_mod.get_messages_controller = real_controller
|
||||
check(
|
||||
"the unread prefill does not shrink as messages are marked read",
|
||||
resolved == [1100, 1100, 1100],
|
||||
f"resolved={resolved}",
|
||||
)
|
||||
|
||||
settings_rows = plugin.create_settings()
|
||||
settings_keys = {row.key for row in settings_rows if hasattr(row, "key")}
|
||||
expected_keys = {
|
||||
@@ -1971,6 +2015,24 @@ def main(argv=None) -> int:
|
||||
f"dispatched={sep_n} ids={sep_ids}",
|
||||
)
|
||||
|
||||
# A live status card is the only feedback that pressing Summarize did
|
||||
# anything, so it must appear even when the provisional anchor cannot be
|
||||
# placed: a large offset reaches past the loaded window, and an
|
||||
# unresolved anchor arrives as 0. Requiring a loaded anchor here left the
|
||||
# user staring at an unchanged chat.
|
||||
for label, anchor in (("unresolved anchor 0", 0), ("anchor past the window", 4242)):
|
||||
live = _FakeChatActivity([700, 690, 680])
|
||||
live_injection = injector.SummaryInjection(
|
||||
live, 0, -4242, 0, anchor, injector.virtual_id_for(0, anchor or 1)
|
||||
)
|
||||
shown = live_injection.insert(status="Fetching messages…", streaming=True)
|
||||
live_ids = [m.messageOwner.id for m in live.messages]
|
||||
check(
|
||||
f"the status card still appears with an {label}",
|
||||
shown and len([i for i in live_ids if i < 0]) == 1,
|
||||
f"inserted={shown} ids={live_ids}",
|
||||
)
|
||||
|
||||
check(
|
||||
"sys.modules gained no entry for bundled plugin module names",
|
||||
not ({"main", "config", "services", "providers", "hooks", "ui.summarize_dialog"} & new_modules),
|
||||
|
||||
Reference in new issue
Block a user