mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-03-07 01:47:32 +01:00
Improve Bluesky reply/quote accessibility and split test bootstrap workflow
- Fix Bluesky quoted post rendering across list output, screen-reader speech, and View Post by centralizing quote extraction. - Add robust quote URL extraction (facets/embed/text), include quoted URLs in URL shortcuts, and append full quoted URLs when hidden/truncated. - Improve reply context handling: - add and use `$reply_to` template variable, - hydrate missing reply target handles in home/feed items, - keep backward compatibility for templates that do not include `$reply_to`. - Align Bluesky default/fallback post templates to include reply context (`$reply_to`). - Add/extend focused Bluesky tests for quote text, quote URLs, reply context, and template fallback behavior. - Refactor scripts: - add bootstrap-dev.ps1 for environment setup (submodules, venv, deps), - keep run-tests.ps1 focused on running tests only, - add PowerShell comment-based help in English. - Update README with the new bootstrap/test workflow and examples.
This commit is contained in:
@@ -357,7 +357,7 @@ class Handler:
|
||||
buffer.session.settings["templates"] = {}
|
||||
templates_cfg = buffer.session.settings.get("templates", {})
|
||||
template_state = {
|
||||
"post": templates_cfg.get("post", "$display_name, $safe_text $date."),
|
||||
"post": templates_cfg.get("post", "$display_name, $reply_to$safe_text $date."),
|
||||
"person": templates_cfg.get("person", "$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at."),
|
||||
"notification": templates_cfg.get("notification", "$display_name $text, $date"),
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import languageHandler
|
||||
import output
|
||||
import widgetUtils
|
||||
from controller import messages as base_messages
|
||||
from sessions.blueski import utils as bluesky_utils
|
||||
from wxUI.dialogs.blueski import postDialogs
|
||||
from extra.autocompletionUsers import completion
|
||||
|
||||
@@ -258,6 +259,29 @@ def _extract_post_view_data(session: Any, item: Any) -> dict[str, Any] | None:
|
||||
author_label = display_name
|
||||
|
||||
text = _g(record, "text", "") or ""
|
||||
reply_to_handle = bluesky_utils.extract_reply_to_handle(item)
|
||||
if reply_to_handle:
|
||||
if text:
|
||||
text = _("Replying to @{handle}: {text}").format(handle=reply_to_handle, text=text)
|
||||
else:
|
||||
text = _("Replying to @{handle}").format(handle=reply_to_handle)
|
||||
|
||||
quote_info = bluesky_utils.extract_quoted_post_info(item)
|
||||
if quote_info:
|
||||
if quote_info["kind"] == "not_found":
|
||||
text += f" [{_('Quoted post not found')}]"
|
||||
elif quote_info["kind"] == "blocked":
|
||||
text += f" [{_('Quoted post blocked')}]"
|
||||
elif quote_info["kind"] == "feed":
|
||||
text += f" [{_('Quoting Feed')}: {quote_info.get('feed_name', 'Feed')}]"
|
||||
else:
|
||||
q_handle = quote_info.get("handle", "unknown")
|
||||
q_text = quote_info.get("text", "")
|
||||
if q_text:
|
||||
text += " " + _("Quoting @{handle}: {text}").format(handle=q_handle, text=q_text)
|
||||
else:
|
||||
text += " " + _("Quoting @{handle}").format(handle=q_handle)
|
||||
|
||||
cw_text = _extract_cw_text(post, record)
|
||||
if cw_text:
|
||||
text = f"CW: {cw_text}\n\n{text}" if text else f"CW: {cw_text}"
|
||||
|
||||
@@ -805,12 +805,12 @@ class BaseBuffer(base.Buffer):
|
||||
try:
|
||||
if self.type == "notifications":
|
||||
template = template_settings.get("notification", "$display_name $text, $date")
|
||||
post_template = template_settings.get("post", "$display_name, $safe_text $date.")
|
||||
post_template = template_settings.get("post", "$display_name, $reply_to$safe_text $date.")
|
||||
return templates.render_notification(item, template, post_template, self.session.settings, relative_times, offset_hours)
|
||||
if self.type in ("user", "post_user_list"):
|
||||
template = template_settings.get("person", "$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at.")
|
||||
return templates.render_user(item, template, self.session.settings, relative_times, offset_hours)
|
||||
template = template_settings.get("post", "$display_name, $safe_text $date.")
|
||||
template = template_settings.get("post", "$display_name, $reply_to$safe_text $date.")
|
||||
return templates.render_post(item, template, self.session.settings, relative_times, offset_hours)
|
||||
except Exception:
|
||||
# Fallback to compose if any template render fails.
|
||||
@@ -934,6 +934,85 @@ class BaseBuffer(base.Buffer):
|
||||
"did": did,
|
||||
"handle": handle,
|
||||
}
|
||||
|
||||
def _hydrate_reply_handles(self, items):
|
||||
"""Populate _reply_to_handle on items when reply metadata lacks hydrated author."""
|
||||
if not items:
|
||||
return
|
||||
|
||||
def g(obj, key, default=None):
|
||||
if isinstance(obj, dict):
|
||||
return obj.get(key, default)
|
||||
return getattr(obj, key, default)
|
||||
|
||||
def set_handle(obj, handle):
|
||||
if not obj or not handle:
|
||||
return
|
||||
if isinstance(obj, dict):
|
||||
obj["_reply_to_handle"] = handle
|
||||
else:
|
||||
try:
|
||||
setattr(obj, "_reply_to_handle", handle)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_parent_uri(item):
|
||||
actual_post = g(item, "post", item)
|
||||
record = g(actual_post, "record", {}) or {}
|
||||
reply = g(record, "reply", None)
|
||||
if not reply:
|
||||
return None
|
||||
parent = g(reply, "parent", None) or reply
|
||||
return g(parent, "uri", None)
|
||||
|
||||
unresolved_by_parent_uri = {}
|
||||
for item in items:
|
||||
if utils.extract_reply_to_handle(item):
|
||||
continue
|
||||
parent_uri = get_parent_uri(item)
|
||||
if not parent_uri:
|
||||
continue
|
||||
unresolved_by_parent_uri.setdefault(parent_uri, []).append(item)
|
||||
|
||||
if not unresolved_by_parent_uri:
|
||||
return
|
||||
|
||||
api = self.session._ensure_client()
|
||||
if not api:
|
||||
return
|
||||
|
||||
uris = list(unresolved_by_parent_uri.keys())
|
||||
uri_to_handle = {}
|
||||
chunk_size = 25
|
||||
|
||||
for start in range(0, len(uris), chunk_size):
|
||||
chunk = uris[start:start + chunk_size]
|
||||
try:
|
||||
try:
|
||||
res = api.app.bsky.feed.get_posts({"uris": chunk})
|
||||
except Exception:
|
||||
res = api.app.bsky.feed.get_posts(uris=chunk)
|
||||
posts = list(getattr(res, "posts", None) or [])
|
||||
for parent_post in posts:
|
||||
uri = g(parent_post, "uri", None)
|
||||
author = g(parent_post, "author", None) or {}
|
||||
handle = g(author, "handle", None)
|
||||
if uri and handle:
|
||||
uri_to_handle[uri] = handle
|
||||
except Exception as e:
|
||||
log.debug("Could not hydrate reply handles for chunk: %s", e)
|
||||
|
||||
if not uri_to_handle:
|
||||
return
|
||||
|
||||
for parent_uri, item_list in unresolved_by_parent_uri.items():
|
||||
handle = uri_to_handle.get(parent_uri)
|
||||
if not handle:
|
||||
continue
|
||||
for item in item_list:
|
||||
set_handle(item, handle)
|
||||
actual_post = g(item, "post", item)
|
||||
set_handle(actual_post, handle)
|
||||
|
||||
def process_items(self, items, play_sound=True, avoid_autoreading=False):
|
||||
"""
|
||||
@@ -1016,6 +1095,8 @@ class BaseBuffer(base.Buffer):
|
||||
|
||||
if not new_items:
|
||||
return 0
|
||||
|
||||
self._hydrate_reply_handles(new_items)
|
||||
|
||||
# Add to DB
|
||||
# Reverse timeline setting
|
||||
@@ -1064,6 +1145,7 @@ class BaseBuffer(base.Buffer):
|
||||
|
||||
def add_new_item(self, item):
|
||||
"""Add a single new item from streaming."""
|
||||
self._hydrate_reply_handles([item])
|
||||
safe = True
|
||||
relative_times = self.session.settings["general"].get("relative_times", False)
|
||||
show_screen_names = self.session.settings["general"].get("show_screen_names", False)
|
||||
|
||||
Reference in New Issue
Block a user