generated from exteraGramPlugins/pluginBase
Fix scroll lag and misplaced summary cards
Three defects in the restore/injection path. Scrolling stuttered in any chat that had been summarized. The restore gate keyed on id(chat_activity), but every Xposed callback hands Python a fresh Chaquopy wrapper around the same Java object, so the key never matched and each scroll frame opened SQLite and rescanned the message list. updateMessagesVisiblePart runs per frame, so that ran continuously. BaseFragment.classGuid is assigned once per fragment instance, so it identifies an open chat across calls; measured 1 query per 300 frames instead of 300. Saved rows are cached per scope, invalidated on save. The card could attach to the wrong message when many messages were included. insert() computed the index on the calling thread and applied it later on the UI thread; a large range takes long enough for paging or an incoming message to shift the list in between, so a stale index was used. The index is now computed on the UI thread immediately before the mutation that consumes it. Every saved summary appeared stacked together in a row. find_index_for_anchor fell back to nearest-by-id whenever the anchor was not loaded, and a chat opens with a short window, so summaries anchored far up all collapsed to adjacent slots at the bottom. Absence is now reported as -1 and those cards are skipped until paging brings their anchor in; the id-order fallback stays opt-in for reanchor, which must place a card it has already removed. Verified against the 6 real saved summaries for one chat: previously all 6 were injected consecutively on open, now none are, and each appears in its correct place once its range is scrolled in. Also makes restore linear in list length rather than rescanning per summary.
This commit is contained in:
1 parent
7f0fcfe72f
commit
0619c5fe68
4 files changed
+297
-50
No files matched your search
@@ -181,17 +181,18 @@ def get_chat_adapter(chat_activity: Any) -> Optional[Any]:
|
||||
return None
|
||||
|
||||
|
||||
def find_index_for_anchor(messages_list: Any, anchor_msg_id: int) -> int:
|
||||
def find_index_for_anchor(messages_list: Any, anchor_msg_id: int, require_anchor: bool = True) -> int:
|
||||
"""Index at which to insert so the summary sits directly after the anchor.
|
||||
|
||||
ChatActivity.messages is newest-first, so "directly after the anchor"
|
||||
chronologically means "immediately before it" in list order.
|
||||
|
||||
The anchor is the newest message that was actually analyzed, which is not
|
||||
the newest message in the chat whenever a non-zero offset was used. If the
|
||||
anchor itself is not loaded (paged out, or never fetched into this view),
|
||||
fall back to ordering by id rather than to index 0: index 0 is the bottom
|
||||
of the chat and would place the card after messages the summary never saw.
|
||||
Returns -1 when the anchor message is not loaded in this view and
|
||||
`require_anchor` is set. Positioning by id order alone is not safe: with a
|
||||
non-zero offset, or a range that has since paged out, the nearest-by-id slot
|
||||
can sit among messages the summary never analyzed, which is how cards ended
|
||||
up attached to the wrong message. Callers skip the insert instead and try
|
||||
again once the anchor is paged in.
|
||||
"""
|
||||
anchor_msg_id = int(anchor_msg_id)
|
||||
try:
|
||||
@@ -200,6 +201,8 @@ def find_index_for_anchor(messages_list: Any, anchor_msg_id: int) -> int:
|
||||
owner = getattr(messages_list.get(i), "messageOwner", None)
|
||||
if owner is not None and int(getattr(owner, "id", 0) or 0) == anchor_msg_id:
|
||||
return i
|
||||
if require_anchor:
|
||||
return -1
|
||||
|
||||
# Anchor absent: insert before the first real message whose id is at
|
||||
# most the anchor's, i.e. the newest message not newer than the
|
||||
@@ -215,7 +218,7 @@ def find_index_for_anchor(messages_list: Any, anchor_msg_id: int) -> int:
|
||||
return size
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("injector.find_index_for_anchor", exc)
|
||||
return 0
|
||||
return -1
|
||||
|
||||
|
||||
def find_existing(messages_list: Any, virtual_id: int) -> int:
|
||||
@@ -294,15 +297,28 @@ class SummaryInjection:
|
||||
return False
|
||||
self.message_object = message_object
|
||||
|
||||
existing = find_existing(messages_list, self.virtual_id)
|
||||
index = existing if existing >= 0 else find_index_for_anchor(messages_list, self.anchor_msg_id)
|
||||
|
||||
# The index must be computed on the UI thread, immediately before the
|
||||
# mutation that uses it. Computing it here would race: loading a large
|
||||
# range takes long enough for paging or an incoming message to shift the
|
||||
# list, and the stale index then lands the card after the wrong message.
|
||||
def run():
|
||||
try:
|
||||
messages_list_now = get_messages_list(self.chat_activity)
|
||||
if messages_list_now is None:
|
||||
return
|
||||
existing = find_existing(messages_list_now, self.virtual_id)
|
||||
if existing >= 0:
|
||||
messages_list.set(index, message_object)
|
||||
messages_list_now.set(existing, message_object)
|
||||
else:
|
||||
messages_list.add(index, message_object)
|
||||
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"
|
||||
)
|
||||
return
|
||||
messages_list_now.add(index, message_object)
|
||||
adapter = get_chat_adapter(self.chat_activity)
|
||||
if adapter is not None:
|
||||
adapter.notifyDataSetChanged()
|
||||
@@ -391,6 +407,10 @@ class SummaryInjection:
|
||||
messages_list.set(index, replacement)
|
||||
else:
|
||||
index = find_index_for_anchor(messages_list, self.anchor_msg_id)
|
||||
if index < 0:
|
||||
# Anchor paged out mid-stream. Keep the streamed
|
||||
# text; it is re-inserted once the anchor is back.
|
||||
return
|
||||
messages_list.add(index, replacement)
|
||||
inserted = True
|
||||
for stale_id in self._used_ids:
|
||||
@@ -497,8 +517,16 @@ class SummaryInjection:
|
||||
duplicate = find_existing(messages_list, virtual_id)
|
||||
if duplicate >= 0:
|
||||
messages_list.remove(duplicate)
|
||||
# The card was just removed, so it must go back somewhere:
|
||||
# the new anchor came from the loaded list and is normally
|
||||
# present, but if it paged out in between, fall back to id
|
||||
# ordering rather than dropping the card entirely.
|
||||
target = find_index_for_anchor(messages_list, anchor_msg_id)
|
||||
messages_list.add(target, message_object)
|
||||
if target < 0:
|
||||
target = find_index_for_anchor(
|
||||
messages_list, anchor_msg_id, require_anchor=False
|
||||
)
|
||||
messages_list.add(max(target, 0), message_object)
|
||||
adapter = get_chat_adapter(self.chat_activity)
|
||||
if adapter is not None:
|
||||
adapter.notifyDataSetChanged()
|
||||
@@ -520,17 +548,43 @@ def _on_ui(fn) -> None:
|
||||
fn()
|
||||
|
||||
|
||||
def loaded_message_ids(messages_list: Any) -> set:
|
||||
"""Ids currently loaded in the view, in one pass.
|
||||
|
||||
Restoring used to scan the whole list once per saved summary; a single set
|
||||
keeps it linear in the list length no matter how many summaries exist.
|
||||
"""
|
||||
ids = set()
|
||||
try:
|
||||
for i in range(messages_list.size()):
|
||||
owner = getattr(messages_list.get(i), "messageOwner", None)
|
||||
if owner is not None:
|
||||
ids.add(int(getattr(owner, "id", 0) or 0))
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("injector.loaded_message_ids", exc)
|
||||
return ids
|
||||
|
||||
|
||||
def restore_saved_summaries(chat_activity: Any, account: int, dialog_id: int, topic_id: int, summaries) -> int:
|
||||
"""Re-injects persisted summaries into a freshly opened chat. Returns count."""
|
||||
"""Re-injects persisted summaries into a freshly opened chat.
|
||||
|
||||
Only summaries whose anchor is currently loaded are injected: a card whose
|
||||
anchor is outside the loaded window has no correct position, and guessing
|
||||
one by id order is what stacked every saved card together at the bottom of
|
||||
the chat. The rest are picked up on a later call, once paging has brought
|
||||
their anchor in. Returns how many were dispatched for insertion.
|
||||
"""
|
||||
messages_list = get_messages_list(chat_activity)
|
||||
if messages_list is None or not summaries:
|
||||
return 0
|
||||
|
||||
register_mock_sender(account)
|
||||
loaded = loaded_message_ids(messages_list)
|
||||
pending = []
|
||||
for summary in summaries:
|
||||
virtual_id = virtual_id_for(summary.id, summary.anchor_msg_id)
|
||||
if find_existing(messages_list, virtual_id) >= 0:
|
||||
if virtual_id in loaded:
|
||||
continue
|
||||
if int(summary.anchor_msg_id) not in loaded:
|
||||
continue
|
||||
message_object = build_summary_message(
|
||||
account,
|
||||
@@ -543,11 +597,12 @@ def restore_saved_summaries(chat_activity: Any, account: int, dialog_id: int, to
|
||||
False,
|
||||
)
|
||||
if message_object is not None:
|
||||
pending.append((summary.anchor_msg_id, message_object))
|
||||
pending.append((summary.anchor_msg_id, virtual_id, message_object))
|
||||
|
||||
if not pending:
|
||||
return 0
|
||||
|
||||
register_mock_sender(account)
|
||||
# Newest anchor first: each insert shifts everything below it, so placing
|
||||
# the newest card first keeps the remaining lookups pointing at unmoved
|
||||
# rows. The index is recomputed per card against the live list anyway.
|
||||
@@ -555,11 +610,24 @@ def restore_saved_summaries(chat_activity: Any, account: int, dialog_id: int, to
|
||||
|
||||
def run():
|
||||
try:
|
||||
for anchor_msg_id, message_object in pending:
|
||||
messages_list.add(find_index_for_anchor(messages_list, anchor_msg_id), message_object)
|
||||
adapter = get_chat_adapter(chat_activity)
|
||||
if adapter is not None:
|
||||
adapter.notifyDataSetChanged()
|
||||
# Re-resolve on the UI thread: the list this was queued against may
|
||||
# have paged or been replaced in between.
|
||||
live = get_messages_list(chat_activity)
|
||||
if live is None:
|
||||
return
|
||||
added = 0
|
||||
for anchor_msg_id, virtual_id, message_object in pending:
|
||||
if find_existing(live, virtual_id) >= 0:
|
||||
continue
|
||||
index = find_index_for_anchor(live, anchor_msg_id)
|
||||
if index < 0:
|
||||
continue
|
||||
live.add(index, message_object)
|
||||
added += 1
|
||||
if added:
|
||||
adapter = get_chat_adapter(chat_activity)
|
||||
if adapter is not None:
|
||||
adapter.notifyDataSetChanged()
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("injector.restore", exc)
|
||||
|
||||
|
||||
Reference in new issue
Block a user