diff --git a/src/imwald/core/author_html.py b/src/imwald/core/author_html.py index 5d40f35..f97bbb8 100644 --- a/src/imwald/core/author_html.py +++ b/src/imwald/core/author_html.py @@ -21,7 +21,12 @@ def avatar_img_or_placeholder( size_px: int, *, border_hex: str = "#2a3d34", + profile_href: str | None = None, ) -> str: + """ + ``profile_href``: if set (e.g. ``imwald://pub/…``), the avatar is clickable for that profile. + If unset but a picture URL exists, the avatar links to the full-size image (same URL as ``src``). + """ pic = safe_http_url(parsed.get("picture")) r = max(6, size_px // 5) if pic: @@ -29,6 +34,11 @@ def avatar_img_or_placeholder( f'' ) + if profile_href: + return ( + f'{img}' + ) return ( f'{img}' @@ -55,11 +65,11 @@ def feed_op_author_block_html( ) -> str: """Top-of-note author row: picture, display name, npub, optional nip05/about lines (links to profile tab).""" disp = html.escape(display_name_from_profile(parsed)) - av = avatar_img_or_placeholder(parsed, 52, border_hex=border) - npub_e = html.escape(npub_bech) - pk_s = html.escape(pk_short) pk_l = pubkey_hex.strip().lower() href = html.escape(f"imwald://pub/{pk_l}", quote=True) + av = avatar_img_or_placeholder(parsed, 52, border_hex=border, profile_href=href) + npub_e = html.escape(npub_bech) + pk_s = html.escape(pk_short) inner = ( f'
' f"{av}" @@ -86,11 +96,11 @@ def thread_reply_author_row_html( border: str, ) -> str: """Single-row author badge for thread replies (avatar + kind + name + npub).""" - av = avatar_img_or_placeholder(parsed, 40, border_hex=border) - name = html.escape(display_name_from_profile_or_hex(parsed, pubkey_hex)) - npub_e = html.escape(npub_bech) pk_l = pubkey_hex.strip().lower() href = html.escape(f"imwald://pub/{pk_l}", quote=True) + av = avatar_img_or_placeholder(parsed, 40, border_hex=border, profile_href=href) + name = html.escape(display_name_from_profile_or_hex(parsed, pubkey_hex)) + npub_e = html.escape(npub_bech) inner = ( '
' f"{av}" @@ -111,9 +121,20 @@ def inline_profile_badge_html(parsed: dict[str, str | None], pubkey_hex: str, np tip = html.escape(npub_tooltip) pic = safe_http_url(parsed.get("picture")) img = "" + pk_l = pubkey_hex.strip().lower() + prof_href = ( + html.escape(f"imwald://pub/{pk_l}", quote=True) + if len(pk_l) == 64 and all(c in "0123456789abcdef" for c in pk_l) + else None + ) if pic: + wrap = ( + f'' + if prof_href + else f'' + ) img = ( - f'' + f"{wrap}" f'' "" @@ -122,9 +143,20 @@ def inline_profile_badge_html(parsed: dict[str, str | None], pubkey_hex: str, np return f'{inner}' -def embed_author_row_html(parsed: dict[str, str | None], head_line_html: str) -> str: +def embed_author_row_html( + parsed: dict[str, str | None], + head_line_html: str, + *, + pubkey_hex: str | None = None, +) -> str: """Avatar + headline row for embedded events.""" - av = avatar_img_or_placeholder(parsed, 36, border_hex="#2a3d34") + pk_l = (pubkey_hex or "").strip().lower() + prof = ( + html.escape(f"imwald://pub/{pk_l}", quote=True) + if len(pk_l) == 64 and all(c in "0123456789abcdef" for c in pk_l) + else None + ) + av = avatar_img_or_placeholder(parsed, 36, border_hex="#2a3d34", profile_href=prof) return ( '
' f"{av}" diff --git a/src/imwald/core/md_render.py b/src/imwald/core/md_render.py index ee3f017..3c60b1d 100644 --- a/src/imwald/core/md_render.py +++ b/src/imwald/core/md_render.py @@ -23,6 +23,8 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) +_NH3_URL_SCHEMES = frozenset({"http", "https", "mailto", "imwald"}) + # Bare HTTPS image URLs in notes → Markdown image (so renderer emits ````). # Exclude ``<`` so ``![]()`` / HTML ``src="https://…"`` are not double-wrapped. _STANDALONE_IMAGE_URL = re.compile( @@ -96,6 +98,7 @@ def _nh3_clean(html: str) -> str: html, attributes=_nh3_attributes(), filter_style_properties=_NH3_STYLE_FILTER, + url_schemes=_NH3_URL_SCHEMES, ) @@ -145,6 +148,9 @@ def _wrap_http_images_for_fullsize(fragment: str) -> str: if re.search(r']*class="[^"\']*imwald-fullimg[^"\']*"[^>]*>\s*$', pre, re.I): out.append(full_tag) continue + if re.search(r']*href=["\']imwald://[^"\']*["\'][^>]*>\s*$', pre, re.I): + out.append(full_tag) + continue w, h = _parse_img_width_height(attr_blob) if w is not None and h is not None and w <= 64 and h <= 64: out.append(full_tag) diff --git a/src/imwald/core/nostr_entity_render.py b/src/imwald/core/nostr_entity_render.py index 3fe6985..31b06b8 100644 --- a/src/imwald/core/nostr_entity_render.py +++ b/src/imwald/core/nostr_entity_render.py @@ -62,7 +62,8 @@ def _event_embed_html(db: Database, event_id_hex: str) -> str: if len(raw) > 400: body += "…" head = html.escape(head_plain) - head_row = embed_author_row_html(p, head) + pk_hex = str(pk).strip().lower() + head_row = embed_author_row_html(p, head, pubkey_hex=pk_hex) return ( "\n\n" f'
' diff --git a/src/imwald/ui/profile_page.py b/src/imwald/ui/profile_page.py index a19b207..e135f99 100644 --- a/src/imwald/ui/profile_page.py +++ b/src/imwald/ui/profile_page.py @@ -137,7 +137,8 @@ class ProfilePage(QWidget): ) disp = html.escape(display_name_from_profile_or_hex(parsed, pk)) - av = avatar_img_or_placeholder(parsed, 72, border_hex=BORDER) + prof_href = html.escape(f"imwald://pub/{pk}", quote=True) + av = avatar_img_or_placeholder(parsed, 72, border_hex=BORDER, profile_href=prof_href) nip05 = html.escape((parsed.get("nip05") or "").strip()) if parsed.get("nip05") else "" nip05_html = ( f"
{nip05}
" if nip05 else "" diff --git a/tests/test_md_render.py b/tests/test_md_render.py index c226712..cb6642b 100644 --- a/tests/test_md_render.py +++ b/tests/test_md_render.py @@ -28,3 +28,9 @@ def test_markdown_image_wrapped_for_fullsize_link() -> None: assert "imwald-fullimg" in html assert "https://example.com/wide.png" in html assert " None: + pk = "c" * 64 + html = markdown_html_fragment(f"[profile](imwald://pub/{pk})") + assert f'href="imwald://pub/{pk}"' in html