generated from exteraGramPlugins/pluginBase
Inject the summary card only once the answer starts streaming
Publish plugin / publish (push) Successful in 3s
Publish plugin / publish (push) Successful in 3s
Pressing Summarize used to inject a status card immediately, at a guessed anchor, and move it once the fetch returned. Any failure before the model answered (fetch error, empty history, unreachable host, bad key, HTTP error) then left an error bubble in the chat and had already scrolled the user away from where they were reading. The pipeline is now fetch -> stream -> card. A bulletin acknowledges the press; every failure before the first chunk is a bulletin only and leaves the chat untouched. On the first chunk the card is created with its final anchor and virtual id and placed strictly after the newest analyzed message. If that message is not loaded, the chat is asked to load it and placement retries; the card never falls back to the chat bottom. Placement then hard-jumps the layout manager to the card's row: scrollToMessageId only smooth-scrolls from cell positions that are stale right after an insert. The provisional anchor and reanchor() machinery is gone. The row is reserved with INSERT .. DO NOTHING, so re-summarizing a saved range no longer blanks its text before the new answer arrives; a fresh row whose stream breaks is deleted instead of left empty. Also: - Finished summaries end with #aisummaries as a native TL_iv.textHashtag (rendered by RichMessageLayout like a hashtag entity) and carry a plain-text body with a TL_messageEntityHashtag. - Deleting a card (MessagesController.deleteMessages, all overloads) removes its saved row and the card, with a "Summary deleted" bulletin. - Settings show the saved summary count and approximate size, and a confirmed "Clear all summaries" action. - Version 1.1.0. Verified on device: no card before streaming, correct placement and jump, restore on reopen, deletion, and the hashtag rendering. Telegram's search is server-side, so local cards do not appear in its results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b2105ca753
commit
16bf8de84c
10 files changed
+1123
-382
No files matched your search
+233
-174
@@ -16,7 +16,8 @@ Verified against exteraGram 12.5.x (dex inspection):
|
||||
from typing import Any, Optional
|
||||
|
||||
import diagnostics
|
||||
from services.rich_markdown import build_rich_message
|
||||
from config import 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.
|
||||
MOCK_SENDER_ID = 8000000000000001
|
||||
@@ -108,8 +109,15 @@ def build_summary_message(
|
||||
thinking: str = "",
|
||||
streaming: bool = False,
|
||||
status: str = "",
|
||||
tagged: bool = False,
|
||||
) -> Optional[Any]:
|
||||
"""Builds the local MessageObject for a summary, or None on failure."""
|
||||
"""Builds the local MessageObject for a summary, or None on failure.
|
||||
|
||||
`tagged` marks a finished summary: SUMMARY_HASHTAG is appended to the
|
||||
card as a native hashtag node (TL_iv.textHashtag), and to the message's
|
||||
plain-text body with a TL_messageEntityHashtag. The tag is added here rather than stored, so every
|
||||
summary restored from the DB carries it too.
|
||||
"""
|
||||
try:
|
||||
import time
|
||||
|
||||
@@ -124,7 +132,6 @@ def build_summary_message(
|
||||
msg.unread = False
|
||||
msg.post = False
|
||||
msg.flags = 0
|
||||
msg.message = ""
|
||||
msg.peer_id = peer_for_dialog(dialog_id)
|
||||
|
||||
sender = TLRPC.TL_peerUser()
|
||||
@@ -143,7 +150,12 @@ def build_summary_message(
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("injector.reply_header", exc)
|
||||
|
||||
tagged = tagged and bool((answer_markdown or "").strip())
|
||||
_set_plain_body(msg, answer_markdown, tagged, TLRPC)
|
||||
|
||||
msg.rich_message = build_rich_message(answer_markdown, thinking, streaming, status)
|
||||
if tagged:
|
||||
msg.rich_message.blocks.add(hashtag_block(SUMMARY_HASHTAG))
|
||||
msg.flags2 = int(getattr(msg, "flags2", 0) or 0) | FLAGS2_RICH_MESSAGE
|
||||
|
||||
message_object = MessageObject(account, msg, True, True)
|
||||
@@ -156,6 +168,33 @@ def build_summary_message(
|
||||
return None
|
||||
|
||||
|
||||
def _utf16_len(text: str) -> int:
|
||||
"""Length in UTF-16 code units, which is what Telegram entity offsets count."""
|
||||
return len(text.encode("utf-16-le")) // 2
|
||||
|
||||
|
||||
def _set_plain_body(msg: Any, answer_markdown: str, tagged: bool, TLRPC: Any) -> None:
|
||||
"""Fills `message` with the summary as plain text.
|
||||
|
||||
The card renders from rich_message, but copy, translate and any text
|
||||
matching in the app read `message`. Left empty, a summary is invisible to
|
||||
all of them.
|
||||
"""
|
||||
text = markdown_to_plain(answer_markdown)
|
||||
if tagged:
|
||||
text = (text + "\n\n" + SUMMARY_HASHTAG) if text else SUMMARY_HASHTAG
|
||||
msg.message = text
|
||||
if not tagged:
|
||||
return
|
||||
try:
|
||||
entity = TLRPC.TL_messageEntityHashtag()
|
||||
entity.offset = _utf16_len(text) - _utf16_len(SUMMARY_HASHTAG)
|
||||
entity.length = _utf16_len(SUMMARY_HASHTAG)
|
||||
msg.entities.add(entity)
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("injector.hashtag_entity", exc)
|
||||
|
||||
|
||||
def get_messages_list(chat_activity: Any) -> Optional[Any]:
|
||||
"""Returns the ChatActivity's live ArrayList<MessageObject>, or None."""
|
||||
try:
|
||||
@@ -260,7 +299,12 @@ def notify_adapter(chat_activity: Any) -> None:
|
||||
|
||||
|
||||
class SummaryInjection:
|
||||
"""A single injected summary message, updatable in place while streaming."""
|
||||
"""A single injected summary message, updatable in place while streaming.
|
||||
|
||||
Created only once the summary has started arriving, with its final anchor
|
||||
and virtual id already known: nothing about it is provisional, so it is
|
||||
placed exactly once, directly after the newest analyzed message.
|
||||
"""
|
||||
|
||||
def __init__(self, chat_activity: Any, account: int, dialog_id: int, topic_id: int, anchor_msg_id: int, virtual_id: int):
|
||||
self.chat_activity = chat_activity
|
||||
@@ -270,25 +314,13 @@ class SummaryInjection:
|
||||
self.anchor_msg_id = int(anchor_msg_id)
|
||||
self.virtual_id = int(virtual_id)
|
||||
self.message_object: Optional[Any] = None
|
||||
# Every id this injection has ever placed in the list. reanchor()
|
||||
# changes the id mid-flight, so an update must be able to find and
|
||||
# remove the card under its previous id instead of adding a second one.
|
||||
self._used_ids = {int(virtual_id)}
|
||||
# True once the card is in the list. Until then renders only replace
|
||||
# message_object, which placement reads when it finally lands.
|
||||
self.placed = False
|
||||
self._placing = False
|
||||
|
||||
def insert(
|
||||
self,
|
||||
answer_markdown: str = "",
|
||||
thinking: str = "",
|
||||
streaming: bool = True,
|
||||
status: str = "",
|
||||
) -> bool:
|
||||
messages_list = get_messages_list(self.chat_activity)
|
||||
if messages_list is None:
|
||||
diagnostics.note("injector: ChatActivity messages list not found")
|
||||
return False
|
||||
|
||||
register_mock_sender(self.account)
|
||||
message_object = build_summary_message(
|
||||
def _build(self, answer_markdown: str, thinking: str, streaming: bool, status: str, tagged: bool) -> Optional[Any]:
|
||||
return build_summary_message(
|
||||
self.account,
|
||||
self.dialog_id,
|
||||
self.topic_id,
|
||||
@@ -298,81 +330,143 @@ class SummaryInjection:
|
||||
thinking,
|
||||
streaming,
|
||||
status,
|
||||
tagged,
|
||||
)
|
||||
|
||||
def insert(
|
||||
self,
|
||||
answer_markdown: str = "",
|
||||
thinking: str = "",
|
||||
streaming: bool = True,
|
||||
status: str = "",
|
||||
tagged: bool = False,
|
||||
) -> bool:
|
||||
"""Places the card after its anchor and jumps the chat to it.
|
||||
|
||||
Returns False only when the chat cannot take an injected card at all.
|
||||
An anchor that is not loaded yet is not a failure: the chat is asked to
|
||||
load it and placement retries until it arrives.
|
||||
"""
|
||||
if get_messages_list(self.chat_activity) is None:
|
||||
diagnostics.note("injector: ChatActivity messages list not found")
|
||||
return False
|
||||
|
||||
register_mock_sender(self.account)
|
||||
message_object = self._build(answer_markdown, thinking, streaming, status, tagged)
|
||||
if message_object is None:
|
||||
return False
|
||||
self.message_object = message_object
|
||||
|
||||
# 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_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:
|
||||
index = find_index_for_anchor(
|
||||
messages_list_now, self.anchor_msg_id, require_anchor=False
|
||||
)
|
||||
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:
|
||||
adapter.notifyDataSetChanged()
|
||||
# The analyzed range can end far up the history, so the card
|
||||
# may be injected off-screen. Bring it into view once, on the
|
||||
# first insert only -- scrolling on every streaming update
|
||||
# would fight the user for control of the list.
|
||||
if existing < 0:
|
||||
self.scroll_into_view()
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("injector.insert", exc)
|
||||
|
||||
_on_ui(run)
|
||||
self._placing = True
|
||||
_on_ui(lambda: self._place(PLACEMENT_RETRIES))
|
||||
return True
|
||||
|
||||
def scroll_into_view(self) -> bool:
|
||||
"""Scrolls the chat so the summary card is visible.
|
||||
|
||||
Targets the anchor, not the card: the anchor is a real message the app
|
||||
can resolve and load, and the card sits directly above it, so both end
|
||||
up on screen. ChatActivity.scrollToMessageId(id, fromMessageId,
|
||||
select, loadIndex, forceScroll, unreadCount) -- arity verified in the
|
||||
APK.
|
||||
def _place(self, retries_left: int) -> None:
|
||||
"""Runs on the UI thread. The index is computed immediately before the
|
||||
mutation that uses it: computed earlier, paging or an incoming message
|
||||
could shift the list and land the card after the wrong message.
|
||||
"""
|
||||
if not self.anchor_msg_id:
|
||||
return False
|
||||
try:
|
||||
self.chat_activity.scrollToMessageId(
|
||||
int(self.anchor_msg_id), 0, False, 0, True, 0
|
||||
messages_list = get_messages_list(self.chat_activity)
|
||||
if messages_list is None:
|
||||
self._placing = False
|
||||
return
|
||||
message_object = self.message_object
|
||||
index = find_existing(messages_list, self.virtual_id)
|
||||
if index >= 0:
|
||||
# A card for this summary is already on screen -- the restore
|
||||
# hook re-injected the saved row being re-summarized. Claim it.
|
||||
messages_list.set(index, message_object)
|
||||
else:
|
||||
index = find_index_for_anchor(messages_list, self.anchor_msg_id)
|
||||
if index < 0:
|
||||
if retries_left == PLACEMENT_RETRIES:
|
||||
# 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.
|
||||
self._load_anchor()
|
||||
if retries_left > 0:
|
||||
_on_ui(lambda: self._place(retries_left - 1), PLACEMENT_RETRY_DELAY_MS)
|
||||
else:
|
||||
self._placing = False
|
||||
diagnostics.note(
|
||||
f"injector: anchor {self.anchor_msg_id} never loaded, card not placed"
|
||||
)
|
||||
return
|
||||
messages_list.add(index, message_object)
|
||||
# A render may have landed while this was queued; show the newest.
|
||||
if self.message_object is not message_object:
|
||||
messages_list.set(index, self.message_object)
|
||||
self.placed = True
|
||||
self._placing = False
|
||||
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()
|
||||
except Exception as exc:
|
||||
self._placing = False
|
||||
diagnostics.record_error("injector.place", exc)
|
||||
|
||||
def _load_anchor(self) -> None:
|
||||
"""Asks the chat to load the anchor and scroll to it.
|
||||
|
||||
ChatActivity.scrollToMessageId(id, fromMessageId, select, loadIndex,
|
||||
forceScroll, unreadCount) -- arity verified in the APK. For an
|
||||
unloaded id it requests history around it and jumps there on arrival.
|
||||
"""
|
||||
try:
|
||||
self.chat_activity.scrollToMessageId(int(self.anchor_msg_id), 0, False, 0, True, 0)
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("injector.load_anchor", exc)
|
||||
|
||||
def scroll_into_view(self) -> bool:
|
||||
"""Jumps the chat so the card sits a third of the way down the screen,
|
||||
with its anchor directly above it.
|
||||
|
||||
A hard jump, not scrollToMessageId: for a message already in the list
|
||||
that only does smoothScrollBy() from the current cell positions, which
|
||||
are stale right after an insert (the layout pass has not run yet), so
|
||||
it undershoots or does nothing. Setting the layout manager's pending
|
||||
position is applied on the next layout, after the insert, and is what
|
||||
ChatActivity itself does when it must land on a specific row.
|
||||
"""
|
||||
if self._jump_to_card():
|
||||
return True
|
||||
self._load_anchor()
|
||||
return False
|
||||
|
||||
def _jump_to_card(self) -> bool:
|
||||
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"
|
||||
)
|
||||
list_view = _chat_list_view(self.chat_activity)
|
||||
if messages_list is None or adapter is None or layout_manager is None:
|
||||
return False
|
||||
index = find_existing(messages_list, self.virtual_id)
|
||||
if index < 0:
|
||||
return False
|
||||
# Public on ChatActivityAdapter; already current, because
|
||||
# notifyDataSetChanged() runs updateRowsInternal() before returning.
|
||||
position = int(adapter.messagesStartRow) + index
|
||||
offset = 0
|
||||
if list_view is not None:
|
||||
try:
|
||||
list_view.stopScroll()
|
||||
except Exception:
|
||||
pass
|
||||
offset = int(list_view.getMeasuredHeight()) // 3
|
||||
# bottom=False: offset counts from the top edge, as in
|
||||
# ChatActivity.scrollToMessageId's own scrollToPosition call.
|
||||
layout_manager.scrollToPositionWithOffset(position, offset, False)
|
||||
return True
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("injector.scroll_into_view", exc)
|
||||
diagnostics.record_error("injector.jump_to_card", exc)
|
||||
return False
|
||||
|
||||
def update(
|
||||
@@ -381,6 +475,7 @@ class SummaryInjection:
|
||||
thinking: str = "",
|
||||
streaming: bool = True,
|
||||
status: str = "",
|
||||
tagged: bool = False,
|
||||
) -> None:
|
||||
"""Re-renders the card in place, without rebuilding the whole list.
|
||||
|
||||
@@ -397,32 +492,28 @@ class SummaryInjection:
|
||||
a fallback when the live cell cannot be found.
|
||||
"""
|
||||
if self.message_object is None:
|
||||
self.insert(answer_markdown, thinking, streaming, status)
|
||||
self.insert(answer_markdown, thinking, streaming, status, tagged)
|
||||
return
|
||||
|
||||
replacement = build_summary_message(
|
||||
self.account,
|
||||
self.dialog_id,
|
||||
self.topic_id,
|
||||
self.anchor_msg_id,
|
||||
self.virtual_id,
|
||||
answer_markdown,
|
||||
thinking,
|
||||
streaming,
|
||||
status,
|
||||
)
|
||||
replacement = self._build(answer_markdown, thinking, streaming, status, tagged)
|
||||
if replacement is None:
|
||||
return
|
||||
self.message_object = replacement
|
||||
|
||||
if not self.placed:
|
||||
# Placement is still waiting for the anchor and will show this
|
||||
# render. If it gave up, give it one more look now: the user may
|
||||
# have scrolled the anchor in since.
|
||||
if not self._placing:
|
||||
self._placing = True
|
||||
_on_ui(lambda: self._place(0))
|
||||
return
|
||||
|
||||
def run():
|
||||
try:
|
||||
messages_list = get_messages_list(self.chat_activity)
|
||||
inserted = False
|
||||
if messages_list is not None:
|
||||
# Replace at the current id; drop any card left behind
|
||||
# under an id this injection used earlier, so a reanchor
|
||||
# mid-stream cannot leave two cards on screen.
|
||||
index = find_existing(messages_list, self.virtual_id)
|
||||
if index >= 0:
|
||||
messages_list.set(index, replacement)
|
||||
@@ -434,16 +525,9 @@ class SummaryInjection:
|
||||
return
|
||||
messages_list.add(index, replacement)
|
||||
inserted = True
|
||||
for stale_id in self._used_ids:
|
||||
if stale_id == self.virtual_id:
|
||||
continue
|
||||
stale = find_existing(messages_list, stale_id)
|
||||
if stale >= 0:
|
||||
messages_list.remove(stale)
|
||||
inserted = True
|
||||
|
||||
# A row was added or removed, so the adapter's item count
|
||||
# changed and only a full rebind is correct.
|
||||
# A row was added, so the adapter's item count changed and only
|
||||
# a full rebind is correct.
|
||||
if inserted or not self._force_update_cell(replacement):
|
||||
adapter = get_chat_adapter(self.chat_activity)
|
||||
if adapter is not None:
|
||||
@@ -467,9 +551,7 @@ class SummaryInjection:
|
||||
delegate = getattr(self.chat_activity, "chatMessageCellDelegate", None) or get_private_field(
|
||||
self.chat_activity, "chatMessageCellDelegate"
|
||||
)
|
||||
list_view = getattr(self.chat_activity, "chatListView", None) or get_private_field(
|
||||
self.chat_activity, "chatListView"
|
||||
)
|
||||
list_view = _chat_list_view(self.chat_activity)
|
||||
if delegate is None or list_view is None:
|
||||
return False
|
||||
|
||||
@@ -491,80 +573,56 @@ class SummaryInjection:
|
||||
diagnostics.record_error("injector._force_update_cell", exc)
|
||||
return False
|
||||
|
||||
def reanchor(self, anchor_msg_id: int, virtual_id: int) -> None:
|
||||
"""Re-targets an already-visible card once the analyzed range is known.
|
||||
|
||||
The card is injected on click, before any messages are fetched, so both
|
||||
the anchor and the DB row id only become known later. Moving it keeps
|
||||
the promise that a summary sits directly after its newest analyzed
|
||||
message.
|
||||
"""
|
||||
anchor_msg_id = int(anchor_msg_id)
|
||||
virtual_id = int(virtual_id)
|
||||
if anchor_msg_id == self.anchor_msg_id and virtual_id == self.virtual_id:
|
||||
return
|
||||
def _chat_list_view(chat_activity: Any) -> Optional[Any]:
|
||||
try:
|
||||
from hook_utils import get_private_field
|
||||
|
||||
message_object = self.message_object
|
||||
old_virtual_id = self.virtual_id
|
||||
self.anchor_msg_id = anchor_msg_id
|
||||
self.virtual_id = virtual_id
|
||||
self._used_ids.add(virtual_id)
|
||||
if message_object is None:
|
||||
return
|
||||
return getattr(chat_activity, "chatListView", None) or get_private_field(chat_activity, "chatListView")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def run():
|
||||
try:
|
||||
owner = message_object.messageOwner
|
||||
messages_list = get_messages_list(self.chat_activity)
|
||||
# Locate the card by its OLD id before rewriting it, otherwise
|
||||
# the lookup misses and the card is never moved.
|
||||
current = find_existing(messages_list, old_virtual_id) if messages_list is not None else -1
|
||||
|
||||
owner.id = virtual_id
|
||||
try:
|
||||
header = _tlrpc().TL_messageReplyHeader()
|
||||
header.reply_to_msg_id = anchor_msg_id
|
||||
if self.topic_id:
|
||||
header.reply_to_top_id = int(self.topic_id)
|
||||
owner.reply_to = header
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("injector.reanchor.reply_header", exc)
|
||||
def remove_cards(chat_activity: Any, virtual_ids) -> None:
|
||||
"""Drops injected cards from an open chat's list.
|
||||
|
||||
if messages_list is not None and current >= 0:
|
||||
messages_list.remove(current)
|
||||
# A card may already sit under the new id -- e.g. the
|
||||
# restore hook re-injected this summary's saved row. Claim
|
||||
# that id rather than ending up with two cards.
|
||||
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)
|
||||
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)
|
||||
ChatActivity removes deleted messages by looking them up in messagesDict,
|
||||
which injected cards are never added to, so a deleted card would otherwise
|
||||
stay on screen.
|
||||
"""
|
||||
wanted = {int(v) for v in virtual_ids}
|
||||
if chat_activity is None or not wanted:
|
||||
return
|
||||
|
||||
def run():
|
||||
try:
|
||||
messages_list = get_messages_list(chat_activity)
|
||||
if messages_list is None:
|
||||
return
|
||||
removed = 0
|
||||
for i in range(messages_list.size() - 1, -1, -1):
|
||||
owner = getattr(messages_list.get(i), "messageOwner", None)
|
||||
if owner is not None and int(getattr(owner, "id", 0) or 0) in wanted:
|
||||
messages_list.remove(i)
|
||||
removed += 1
|
||||
if removed:
|
||||
adapter = get_chat_adapter(chat_activity)
|
||||
if adapter is not None:
|
||||
adapter.notifyDataSetChanged()
|
||||
# The card just moved to the real end of the analyzed range,
|
||||
# which may be somewhere else entirely, so follow it there.
|
||||
self.scroll_into_view()
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("injector.reanchor", exc)
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("injector.remove_cards", exc)
|
||||
|
||||
_on_ui(run)
|
||||
_on_ui(run)
|
||||
|
||||
|
||||
def _on_ui(fn) -> None:
|
||||
def _on_ui(fn, delay_ms: int = 0) -> None:
|
||||
try:
|
||||
from android_utils import run_on_ui_thread
|
||||
|
||||
run_on_ui_thread(fn)
|
||||
if delay_ms:
|
||||
run_on_ui_thread(fn, delay_ms)
|
||||
else:
|
||||
run_on_ui_thread(fn)
|
||||
except Exception:
|
||||
fn()
|
||||
|
||||
@@ -621,6 +679,7 @@ def restore_saved_summaries(chat_activity: Any, account: int, dialog_id: int, to
|
||||
summary.text,
|
||||
summary.thinking,
|
||||
False,
|
||||
tagged=True,
|
||||
)
|
||||
if message_object is not None:
|
||||
pending.append((summary.anchor_msg_id, virtual_id, message_object))
|
||||
|
||||
Reference in new issue
Block a user