diff --git a/src/imwald/core/author_html.py b/src/imwald/core/author_html.py index 1c46999..88acf21 100644 --- a/src/imwald/core/author_html.py +++ b/src/imwald/core/author_html.py @@ -72,12 +72,15 @@ def format_nip05_chips_html( container_margin_top: str | None = None, ) -> str: """ - Horizontal row of NIP-05 chips: identifier with domain favicon in place of ``@``, plus verification mark. + NIP-05 lines for rich text (e.g. ``QTextBrowser``): domain favicon replaces ``@`` between local and domain. + + Uses block / inline-block and explicit margins — Qt's HTML subset ignores ``display:flex`` and ``gap``, + which is why a flex row collapses into an illegible run. ``bool | None``: ``True`` verified, ``False`` failed, ``None`` pending / not checked. """ parts: list[str] = [] - for ident, okv in identifiers: + for i, (ident, okv) in enumerate(identifiers): parsed = parse_nip05_identifier(ident) dom = parsed[1] if parsed else "" fav = favicon_url_for_domain(dom) if dom else "" @@ -92,47 +95,53 @@ def format_nip05_chips_html( esc_local = html.escape(local_raw, quote=False) esc_dom = html.escape(dom_raw, quote=False) id_body = ( - f'' - f'{esc_local}' - f'' - f'{esc_dom}' + f'' + f'{esc_local}' + f'' + f'{esc_dom}' f"" ) else: id_body = ( - f'{esc_full}' + f'{esc_full}' ) if okv is True: - mark = f'' + mark = ( + f'' + ) elif okv is False: - mark = f'' + mark = ( + f'' + ) else: - mark = f'' + mark = ( + f'' + ) + row_mt = "0" if i == 0 else "20px" if chip_bg: brd = chip_border or dim - wrap = ( - "display:inline-flex;align-items:center;gap:10px;padding:10px 18px;" - "margin:0 12px 10px 0;" - f"border-radius:999px;background:{chip_bg};border:1px solid {brd}" + row = ( + f'
' + f"{id_body}{mark}
" ) else: - wrap = ( - "display:inline-flex;align-items:center;gap:8px;padding:4px 4px 4px 0;" - "margin:0 16px 6px 0" + row = ( + f'
' + f"{id_body}{mark}
" ) - parts.append(f'{id_body}{mark}') + parts.append(row) if not parts: return "" if container_margin_top is not None: mt = container_margin_top else: mt = "14px" if chip_bg else "4px" - return ( - f'
{"".join(parts)}
' - ) + return f'
{"".join(parts)}
' def feed_op_author_compact_html( @@ -166,6 +175,7 @@ def feed_op_author_compact_html( f'
{event_meta_inner_html}
' ) row_mt = "12px" if nip38_block else "8px" + # ``QTextBrowser`` / ``QTextDocument`` ignore most flexbox; use a table so avatar + npub sit side‑by‑side. return ( f'
' @@ -174,16 +184,18 @@ def feed_op_author_compact_html( f"{name_e}
" f"" f"{nip38_block}" - f'
' - f"{av}" - f'
' + f'' + f'' + f'
{av}' f'' f'
{npub_line}
' + f'line-height:1.5;word-break:break-all;margin:0 0 14px 0" title="{npub_title}">{npub_line}' f"
" - f"{nip05_chips_html}" + f'
{nip05_chips_html}
' f"{meta_block}" - f"" + f"
" + f"
" ) diff --git a/src/imwald/core/database.py b/src/imwald/core/database.py index 409ffe2..ff261de 100644 --- a/src/imwald/core/database.py +++ b/src/imwald/core/database.py @@ -659,7 +659,7 @@ class Database: ) -> list[dict[str, Any]]: """ Feed-shaped rows newest-first. With ``exclude_viewed`` and ``viewer_pubkey``, - omits ids already in ``feed_views`` (the UI records an OP when it is shown). + omits ids already in ``feed_views`` (the feed pager records an OP when the user leaves it). """ kind_list = list(kinds) placeholders = ",".join("?" * len(kind_list)) @@ -695,7 +695,7 @@ class Database: return out def mark_feed_viewed(self, viewer_pubkey: str, event_id: str) -> None: - """Persist that this viewer was shown this OP (pager); used to prefer fresh notes in the feed.""" + """Persist that this viewer finished this OP (pager left / account switch); used for unseen feed ranking.""" with self.write_lock() as c: c.execute( """ diff --git a/src/imwald/ui/feed_page.py b/src/imwald/ui/feed_page.py index 5689173..44e5324 100644 --- a/src/imwald/ui/feed_page.py +++ b/src/imwald/ui/feed_page.py @@ -408,9 +408,16 @@ class FeedPage(QWidget): following: set[str], list30000_pubkeys: set[str] | None = None, ) -> None: + old_vk = (self._my_pubkey or "_anon").lower() self._my_pubkey = my_pubkey self._following = following self._list30000_pubkeys = list30000_pubkeys or set() + new_vk = (self._my_pubkey or "_anon").lower() + if old_vk != new_vk and self._rendered_op_id: + self._db.mark_feed_viewed(old_vk, self._rendered_op_id) + self._rendered_op_id = None + self._rendered_reply_sig = None + self._op_ev_snapshot = None def _feed_viewer_key(self) -> str: """Per-device feed history; logged-out users share `_anon`.""" @@ -600,6 +607,10 @@ class FeedPage(QWidget): self._engagement_label.setText("") return ev = self._queue[self._index % len(self._queue)] + root_id = str(ev["id"]) + prev_rid = self._rendered_op_id + if prev_rid and prev_rid != root_id: + self._db.mark_feed_viewed(self._feed_viewer_key(), prev_rid) if ev.get("deleted"): self._rendered_op_id = None self._rendered_reply_sig = None @@ -631,7 +642,6 @@ class FeedPage(QWidget): _format_engagement_html(stats, reaction_nip30_urls=author_nip30) ) - root_id = str(ev["id"]) replies = self._db.list_replies_to(ev) reply_sig = tuple(str(r["id"]) for r in replies) if root_id == self._rendered_op_id and reply_sig == self._rendered_reply_sig: @@ -644,7 +654,6 @@ class FeedPage(QWidget): self._op_ev_snapshot = ev body = self._build_op_html(ev, None) self._op.setHtml(body) - self._db.mark_feed_viewed(self._feed_viewer_key(), root_id) pk = op_pk prof_row2 = self._db.get_latest_kind0_profile(pk)