generated from exteraGramPlugins/pluginBase
Keep the chat on a far-back summary card after its anchor loads
Publish plugin / publish (push) Successful in 3s
Publish plugin / publish (push) Successful in 3s
With an offset the newest analyzed message is usually outside the loaded window, so placement asks the chat to load it first. That path could leave the view away from the card while offset 0 always worked: - Placement gave up after ~3 s (25 x 120 ms) and asked for the anchor only once. scrollToMessageId drops the request while waitingForGetDifference, and a load can be superseded. It now waits ~10 s and re-requests every ~2 s. - Loading a far anchor makes ChatActivity run its own postponed scroll, and a cache-then-server reload can re-scroll or replace the list. Either can land after the jump. Settle checks at 0.25/0.7/1.5/3 s re-jump if the card row is outside the visible range, and re-place the card if a reload dropped it. They stop as soon as the user drags the list. Also logs each run's count/offset, the fetched id range, pre-stream failures, load requests, placement and jumps to the diagnostics file, since bulletin-only errors previously left no trace. Verified on device with count 1000, offset 1000: anchor loaded on the first retry, card placed after it and the chat jumped there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
16bf8de84c
commit
af23d2e682
5 files changed
+219
-13
No files matched your search
@@ -16,7 +16,13 @@ Verified against exteraGram 12.5.x (dex inspection):
|
||||
from typing import Any, Optional
|
||||
|
||||
import diagnostics
|
||||
from config import PLACEMENT_RETRIES, PLACEMENT_RETRY_DELAY_MS, SUMMARY_HASHTAG
|
||||
from config import (
|
||||
JUMP_SETTLE_DELAYS_MS,
|
||||
PLACEMENT_RELOAD_EVERY,
|
||||
PLACEMENT_RETRIES,
|
||||
PLACEMENT_RETRY_DELAY_MS,
|
||||
SUMMARY_HASHTAG,
|
||||
)
|
||||
from services.rich_markdown import build_rich_message, hashtag_block, markdown_to_plain
|
||||
|
||||
# Above Telegram's issued id range, so this user exists only locally.
|
||||
@@ -379,10 +385,18 @@ class SummaryInjection:
|
||||
else:
|
||||
index = find_index_for_anchor(messages_list, self.anchor_msg_id)
|
||||
if index < 0:
|
||||
if retries_left == PLACEMENT_RETRIES:
|
||||
attempt = PLACEMENT_RETRIES - retries_left
|
||||
if retries_left > 0 and attempt % PLACEMENT_RELOAD_EVERY == 0:
|
||||
# The analyzed range ends outside the loaded window
|
||||
# (an offset, or history scrolled away). Have the chat
|
||||
# load and show it, then place once it is in the list.
|
||||
# Asked again periodically: scrollToMessageId drops the
|
||||
# request outright while waitingForGetDifference, and
|
||||
# a load can be superseded by another one.
|
||||
diagnostics.note(
|
||||
f"injector: anchor {self.anchor_msg_id} not loaded "
|
||||
f"({_id_span(messages_list)}), requesting it (attempt {attempt})"
|
||||
)
|
||||
self._load_anchor()
|
||||
if retries_left > 0:
|
||||
_on_ui(lambda: self._place(retries_left - 1), PLACEMENT_RETRY_DELAY_MS)
|
||||
@@ -398,16 +412,87 @@ class SummaryInjection:
|
||||
messages_list.set(index, self.message_object)
|
||||
self.placed = True
|
||||
self._placing = False
|
||||
diagnostics.note(
|
||||
f"injector: card placed after anchor {self.anchor_msg_id} at index {index} "
|
||||
f"(retries left {retries_left})"
|
||||
)
|
||||
adapter = get_chat_adapter(self.chat_activity)
|
||||
if adapter is not None:
|
||||
adapter.notifyDataSetChanged()
|
||||
# Once, on placement only -- scrolling on every streaming update
|
||||
# would fight the user for control of the list.
|
||||
self.scroll_into_view()
|
||||
# On placement only -- scrolling on every streaming update would
|
||||
# fight the user for control of the list. The settle checks right
|
||||
# after only undo scrolls the app itself makes in that window.
|
||||
if self.scroll_into_view():
|
||||
self._schedule_settle(0)
|
||||
except Exception as exc:
|
||||
self._placing = False
|
||||
diagnostics.record_error("injector.place", exc)
|
||||
|
||||
def _schedule_settle(self, step: int) -> None:
|
||||
if step >= len(JUMP_SETTLE_DELAYS_MS):
|
||||
return
|
||||
previous = JUMP_SETTLE_DELAYS_MS[step - 1] if step else 0
|
||||
_on_ui(lambda: self._settle(step), JUMP_SETTLE_DELAYS_MS[step] - previous)
|
||||
|
||||
def _settle(self, step: int) -> None:
|
||||
"""Re-jumps if something scrolled the fresh card off screen.
|
||||
|
||||
Stops for good as soon as the user touches the list: from then on
|
||||
where the chat is scrolled is theirs to decide.
|
||||
"""
|
||||
try:
|
||||
list_view = _chat_list_view(self.chat_activity)
|
||||
if list_view is not None and int(list_view.getScrollState()) == 1: # SCROLL_STATE_DRAGGING
|
||||
diagnostics.note("injector: user is scrolling, jump settled")
|
||||
return
|
||||
span = self._visible_span()
|
||||
if span is not None:
|
||||
position, first, last = span
|
||||
if not first <= position <= last:
|
||||
diagnostics.note(
|
||||
f"injector: card row {position} scrolled away (visible {first}..{last}), "
|
||||
f"jumping back (check {step})"
|
||||
)
|
||||
self._jump_to_card()
|
||||
elif self.placed and find_existing(get_messages_list(self.chat_activity), self.virtual_id) < 0:
|
||||
# The list was reloaded under the card. Put it back.
|
||||
messages_list = get_messages_list(self.chat_activity)
|
||||
index = find_index_for_anchor(messages_list, self.anchor_msg_id)
|
||||
if index >= 0:
|
||||
diagnostics.note(f"injector: list reloaded, re-placing card (check {step})")
|
||||
messages_list.add(index, self.message_object)
|
||||
adapter = get_chat_adapter(self.chat_activity)
|
||||
if adapter is not None:
|
||||
adapter.notifyDataSetChanged()
|
||||
self._jump_to_card()
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("injector.settle", exc)
|
||||
self._schedule_settle(step + 1)
|
||||
|
||||
def _visible_span(self) -> Optional[tuple]:
|
||||
"""(card adapter position, first visible, last visible), or None."""
|
||||
try:
|
||||
from hook_utils import get_private_field
|
||||
|
||||
messages_list = get_messages_list(self.chat_activity)
|
||||
adapter = get_chat_adapter(self.chat_activity)
|
||||
layout_manager = getattr(self.chat_activity, "chatLayoutManager", None) or get_private_field(
|
||||
self.chat_activity, "chatLayoutManager"
|
||||
)
|
||||
if messages_list is None or adapter is None or layout_manager is None:
|
||||
return None
|
||||
index = find_existing(messages_list, self.virtual_id)
|
||||
if index < 0:
|
||||
return None
|
||||
first = int(layout_manager.findFirstVisibleItemPosition())
|
||||
last = int(layout_manager.findLastVisibleItemPosition())
|
||||
if first < 0 or last < 0:
|
||||
return None
|
||||
return int(adapter.messagesStartRow) + index, min(first, last), max(first, last)
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("injector.visible_span", exc)
|
||||
return None
|
||||
|
||||
def _load_anchor(self) -> None:
|
||||
"""Asks the chat to load the anchor and scroll to it.
|
||||
|
||||
@@ -464,6 +549,7 @@ class SummaryInjection:
|
||||
# bottom=False: offset counts from the top edge, as in
|
||||
# ChatActivity.scrollToMessageId's own scrollToPosition call.
|
||||
layout_manager.scrollToPositionWithOffset(position, offset, False)
|
||||
diagnostics.note(f"injector: jump to row {position} offset {offset}")
|
||||
return True
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("injector.jump_to_card", exc)
|
||||
@@ -583,6 +669,21 @@ def _chat_list_view(chat_activity: Any) -> Optional[Any]:
|
||||
return None
|
||||
|
||||
|
||||
def _id_span(messages_list: Any) -> str:
|
||||
"""'loaded ids A..B, N rows' for diagnostics."""
|
||||
try:
|
||||
ids = [
|
||||
int(getattr(getattr(messages_list.get(i), "messageOwner", None), "id", 0) or 0)
|
||||
for i in range(messages_list.size())
|
||||
]
|
||||
real = [i for i in ids if i > 0]
|
||||
if not real:
|
||||
return f"no messages, {len(ids)} rows"
|
||||
return f"loaded ids {min(real)}..{max(real)}, {len(ids)} rows"
|
||||
except Exception:
|
||||
return "loaded ids unknown"
|
||||
|
||||
|
||||
def remove_cards(chat_activity: Any, virtual_ids) -> None:
|
||||
"""Drops injected cards from an open chat's list.
|
||||
|
||||
|
||||
Reference in new issue
Block a user