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:
Juanjo M
2026-02-15 23:50:00 +00:00
parent abf4cb0df1
commit 6e56d94448
11 changed files with 919 additions and 35 deletions
+30 -31
View File
@@ -11,6 +11,7 @@ list controls. They follow the TWBlue compose function pattern:
import logging
import arrow
import languageHandler
from sessions.blueski import utils
log = logging.getLogger("sessions.blueski.compose")
@@ -76,6 +77,13 @@ def compose_post(post, db, settings, relative_times, show_screen_names=False, sa
else:
text = original_text
reply_to_handle = utils.extract_reply_to_handle(post)
if reply_to_handle:
if text:
text = _("Replying to @{}: {}").format(reply_to_handle, text)
else:
text = _("Replying to @{}").format(reply_to_handle)
# Check facets for links not visible in text and append them
facets = g(record, "facets", []) or []
hidden_urls = []
@@ -117,7 +125,7 @@ def compose_post(post, db, settings, relative_times, show_screen_names=False, sa
if cw_text:
text = f"CW: {cw_text}\n\n{text}"
# Embeds (Images, Quotes, Links)
# Embeds (Images, Links)
embed = g(actual_post, "embed", None)
if embed:
etype = g(embed, "$type") or g(embed, "py_type")
@@ -128,12 +136,7 @@ def compose_post(post, db, settings, relative_times, show_screen_names=False, sa
if images:
text += f" [{len(images)} {_('images')}]"
# Quote posts
quote_rec = None
if etype and ("recordWithMedia" in etype):
rec_embed = g(embed, "record", {})
if rec_embed:
quote_rec = g(rec_embed, "record", None) or rec_embed
media = g(embed, "media", {})
mtype = g(media, "$type") or g(media, "py_type")
if mtype and "images" in mtype:
@@ -145,37 +148,33 @@ def compose_post(post, db, settings, relative_times, show_screen_names=False, sa
title = g(ext, "title", "")
if title:
text += f" [{_('Link')}: {title}]"
elif etype and ("record" in etype):
quote_rec = g(embed, "record", {})
if isinstance(quote_rec, dict):
quote_rec = quote_rec.get("record") or quote_rec
if quote_rec:
qtype = g(quote_rec, "$type") or g(quote_rec, "py_type")
if qtype and "viewNotFound" in qtype:
text += f" [{_('Quoted post not found')}]"
elif qtype and "viewBlocked" in qtype:
text += f" [{_('Quoted post blocked')}]"
elif qtype and "generatorView" in qtype:
gen = g(quote_rec, "displayName", "Feed")
text += f" [{_('Quoting Feed')}: {gen}]"
else:
q_author = g(quote_rec, "author", {})
q_handle = g(q_author, "handle", "unknown")
q_val = g(quote_rec, "value", {})
q_text = g(q_val, "text", "")
if q_text:
text += " " + _("Quoting @{}: {}").format(q_handle, q_text)
else:
text += " " + _("Quoting @{}").format(q_handle)
elif etype and ("external" in etype):
ext = g(embed, "external", {})
title = g(ext, "title", "")
if title:
text += f" [{_('Link')}: {title}]"
quote_info = utils.extract_quoted_post_info(post)
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 @{}: {}").format(q_handle, q_text)
else:
text += " " + _("Quoting @{}").format(q_handle)
# Add full URLs from quoted content when they are not visible in text.
for uri in quote_info.get("urls", []):
if uri and uri not in text:
text += f" [{uri}]"
# Date
indexed_at = g(actual_post, "indexed_at", "") or g(actual_post, "indexedAt", "")
ts_str = ""