|
|
|
|
@ -28,6 +28,19 @@ from imwald.ui.theme import ACCENT_SOFT, BG_CARD, BG_CODE, BORDER, FEED_DOC_CSS,
@@ -28,6 +28,19 @@ from imwald.ui.theme import ACCENT_SOFT, BG_CARD, BG_CODE, BORDER, FEED_DOC_CSS,
|
|
|
|
|
# Notes to list under “Recent in local DB” (feed-shaped kinds). |
|
|
|
|
_PROFILE_NOTE_KINDS: tuple[int, ...] = (1, 6, 20, 21, 30023, 9802, 11) |
|
|
|
|
|
|
|
|
|
_ACTIVITY_KIND_LABEL: dict[int, str] = { |
|
|
|
|
1: "Reply", |
|
|
|
|
6: "Repost", |
|
|
|
|
7: "Reaction", |
|
|
|
|
16: "Repost", |
|
|
|
|
20: "Channel", |
|
|
|
|
21: "Channel msg", |
|
|
|
|
42: "File", |
|
|
|
|
1111: "Comment", |
|
|
|
|
9802: "Highlight", |
|
|
|
|
30023: "Article", |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
PROFILE_HERO_BANNER_H = 240 |
|
|
|
|
PROFILE_HERO_AVATAR = 136 |
|
|
|
|
PROFILE_HERO_OVERLAP = 0.48 # fraction of avatar height drawn above the banner bottom edge |
|
|
|
|
@ -307,7 +320,7 @@ class _ProfileLnurlRunnable(QRunnable):
@@ -307,7 +320,7 @@ class _ProfileLnurlRunnable(QRunnable):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ProfilePage(QWidget): |
|
|
|
|
"""One pubkey: metadata, NIP-65 relays, follows (kind 3), emoji inventory, raw JSON, recent notes.""" |
|
|
|
|
"""One pubkey: compact profile column, posts, and outgoing activity (replies/reactions to others).""" |
|
|
|
|
|
|
|
|
|
open_note_new_tab = Signal(str) |
|
|
|
|
open_profile = Signal(str) |
|
|
|
|
@ -338,6 +351,13 @@ class ProfilePage(QWidget):
@@ -338,6 +351,13 @@ class ProfilePage(QWidget):
|
|
|
|
|
self._feed_body.setOpenExternalLinks(False) |
|
|
|
|
self._feed_body.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) |
|
|
|
|
self._feed_body.anchorClicked.connect(self._dispatch_profile_anchor) |
|
|
|
|
self._interact_body = NoteTextBrowser() |
|
|
|
|
self._interact_body.setObjectName("ProfileBodyInteract") |
|
|
|
|
self._interact_body.setOpenLinks(False) |
|
|
|
|
self._interact_body.setOpenExternalLinks(False) |
|
|
|
|
self._interact_body.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) |
|
|
|
|
self._interact_body.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) |
|
|
|
|
self._interact_body.anchorClicked.connect(self._dispatch_profile_anchor) |
|
|
|
|
self._left_column = QWidget(self) |
|
|
|
|
left_lay = QVBoxLayout(self._left_column) |
|
|
|
|
left_lay.setContentsMargins(0, 0, 0, 0) |
|
|
|
|
@ -349,12 +369,15 @@ class ProfilePage(QWidget):
@@ -349,12 +369,15 @@ class ProfilePage(QWidget):
|
|
|
|
|
split.setChildrenCollapsible(False) |
|
|
|
|
split.addWidget(self._left_column) |
|
|
|
|
split.addWidget(self._feed_body) |
|
|
|
|
# Narrower metadata column, wider posts — inverse of FeedPage (thread wide, OP narrower). |
|
|
|
|
split.addWidget(self._interact_body) |
|
|
|
|
# Metadata (narrow) · posts (widest) · activity (medium). |
|
|
|
|
split.setStretchFactor(0, 2) |
|
|
|
|
split.setStretchFactor(1, 3) |
|
|
|
|
split.setSizes([400, 780]) |
|
|
|
|
self._left_column.setMinimumWidth(300) |
|
|
|
|
self._feed_body.setMinimumWidth(260) |
|
|
|
|
split.setStretchFactor(1, 4) |
|
|
|
|
split.setStretchFactor(2, 2) |
|
|
|
|
split.setSizes([300, 640, 280]) |
|
|
|
|
self._left_column.setMinimumWidth(260) |
|
|
|
|
self._feed_body.setMinimumWidth(240) |
|
|
|
|
self._interact_body.setMinimumWidth(220) |
|
|
|
|
lay = QVBoxLayout(self) |
|
|
|
|
lay.setContentsMargins(0, 0, 0, 0) |
|
|
|
|
lay.addWidget(split) |
|
|
|
|
@ -374,6 +397,9 @@ class ProfilePage(QWidget):
@@ -374,6 +397,9 @@ class ProfilePage(QWidget):
|
|
|
|
|
# resetting placeholders + bumping fetch generations on every refresh() while ingest runs. |
|
|
|
|
self._cover_banner_http_key: str | None = None |
|
|
|
|
self._cover_avatar_http_key: str | None = None |
|
|
|
|
self._left_panel_sig: object | None = None |
|
|
|
|
self._feed_panel_sig: object | None = None |
|
|
|
|
self._interact_panel_sig: object | None = None |
|
|
|
|
self.refresh() |
|
|
|
|
|
|
|
|
|
def tab_title(self) -> str: |
|
|
|
|
@ -416,18 +442,6 @@ class ProfilePage(QWidget):
@@ -416,18 +442,6 @@ class ProfilePage(QWidget):
|
|
|
|
|
lud06_s = lud06_raw.strip() if isinstance(lud06_raw, str) else "" |
|
|
|
|
lud16_s = lud16_raw.strip() if isinstance(lud16_raw, str) else "" |
|
|
|
|
lnurls = collect_unique_lnurlp_urls(lud06_s or None, lud16_s or None) |
|
|
|
|
pay_rows: list[str] = [] |
|
|
|
|
if lud06_s: |
|
|
|
|
pay_rows.append( |
|
|
|
|
f"<p style='margin:6px 0;color:{TEXT_DIM}'><b>lud06</b> (LNURL / NIP-57): " |
|
|
|
|
f"<code style='color:{TEXT_MUTED}'>{html.escape(lud06_s[:200])}</code></p>" |
|
|
|
|
) |
|
|
|
|
if lud16_s: |
|
|
|
|
pay_rows.append( |
|
|
|
|
f"<p style='margin:6px 0;color:{TEXT_DIM}'><b>lud16</b> (Lightning address or HTTPS LNURL): " |
|
|
|
|
f"<code style='color:{TEXT_MUTED}'>{html.escape(lud16_s[:200])}</code></p>" |
|
|
|
|
) |
|
|
|
|
pay_static = "".join(pay_rows) |
|
|
|
|
live_lnurl = "" |
|
|
|
|
if from_lnurl and lnurl_html is not None: |
|
|
|
|
live_lnurl = lnurl_html |
|
|
|
|
@ -435,10 +449,43 @@ class ProfilePage(QWidget):
@@ -435,10 +449,43 @@ class ProfilePage(QWidget):
|
|
|
|
|
self._lnurl_gen += 1 |
|
|
|
|
gen = self._lnurl_gen |
|
|
|
|
self._lnurl_pool.start(_ProfileLnurlRunnable(lnurls, gen, self._lnurl_sigs)) |
|
|
|
|
live_lnurl = f"<p style='color:{TEXT_DIM}'><i>Fetching LNURL-pay metadata…</i></p>" |
|
|
|
|
live_lnurl = f"<p style='color:{TEXT_DIM};font-size:14px;margin:8px 0 0 0'><i>Loading tip details…</i></p>" |
|
|
|
|
tech_dl: list[str] = [] |
|
|
|
|
if lud06_s: |
|
|
|
|
tech_dl.append( |
|
|
|
|
f"<dt style='color:{TEXT_MUTED};margin-top:6px'>lud06</dt>" |
|
|
|
|
f"<dd style='margin:0'><code style='color:{TEXT_DIM};font-size:12px;word-break:break-all'>" |
|
|
|
|
f"{html.escape(lud06_s[:400])}</code></dd>" |
|
|
|
|
) |
|
|
|
|
if lud16_s: |
|
|
|
|
tech_dl.append( |
|
|
|
|
f"<dt style='color:{TEXT_MUTED};margin-top:6px'>lud16</dt>" |
|
|
|
|
f"<dd style='margin:0'><code style='color:{TEXT_DIM};font-size:12px;word-break:break-all'>" |
|
|
|
|
f"{html.escape(lud16_s[:400])}</code></dd>" |
|
|
|
|
) |
|
|
|
|
tech_block = "" |
|
|
|
|
if tech_dl: |
|
|
|
|
tech_block = ( |
|
|
|
|
f"<details style='margin-top:10px'><summary style='cursor:pointer;color:{TEXT_DIM};font-size:13px'>" |
|
|
|
|
f"Raw LNURL fields</summary><dl style='margin:8px 0 0 0'>{''.join(tech_dl)}</dl></details>" |
|
|
|
|
) |
|
|
|
|
tips_head = "" |
|
|
|
|
if lud16_s: |
|
|
|
|
if "@" in lud16_s and not lud16_s.lower().startswith("http"): |
|
|
|
|
tips_head = ( |
|
|
|
|
f"<p style='margin:0;color:{TEXT};font-size:17px;font-weight:700'>{html.escape(lud16_s)}</p>" |
|
|
|
|
f"<p style='margin:4px 0 0 0;color:{TEXT_DIM};font-size:13px'>Lightning tips</p>" |
|
|
|
|
) |
|
|
|
|
elif lud16_s.lower().startswith("https://"): |
|
|
|
|
tips_head = f"<p style='margin:0;color:{TEXT_DIM};font-size:14px'>LNURL-pay (HTTPS) on file.</p>" |
|
|
|
|
elif lud06_s: |
|
|
|
|
tips_head = ( |
|
|
|
|
f"<p style='margin:0;color:{TEXT_DIM};font-size:14px'>LNURL (bech32) on file — " |
|
|
|
|
f"expand <i>Raw LNURL fields</i> if you need the decoded URL.</p>" |
|
|
|
|
) |
|
|
|
|
pay_card = "" |
|
|
|
|
if pay_static or live_lnurl: |
|
|
|
|
pay_card = f"{_sec_title('Lightning · NIP-57')}<div>{pay_static}{live_lnurl}</div>" |
|
|
|
|
if tips_head or live_lnurl or tech_block: |
|
|
|
|
pay_card = f"{_sec_title('Tips')}<div>{tips_head}{live_lnurl}{tech_block}</div>" |
|
|
|
|
|
|
|
|
|
disp_plain = display_name_from_profile_or_hex(parsed, pk) |
|
|
|
|
nip05_plain = (parsed.get("nip05") or "").strip() if parsed.get("nip05") else "" |
|
|
|
|
@ -517,26 +564,29 @@ class ProfilePage(QWidget):
@@ -517,26 +564,29 @@ class ProfilePage(QWidget):
|
|
|
|
|
raw_json = content or "" |
|
|
|
|
raw_esc = html.escape(raw_json[:12000] + ("…" if len(raw_json) > 12000 else ""), quote=False) |
|
|
|
|
json_inner = ( |
|
|
|
|
f"{_sec_title('Kind 0 JSON')}" |
|
|
|
|
f"<pre style='color:{TEXT_DIM};font-size:13px;white-space:pre-wrap;word-break:break-all;" |
|
|
|
|
f"background:{BG_CODE};padding:12px;border-radius:8px;border:1px solid {BORDER};margin:0'>{raw_esc}</pre>" |
|
|
|
|
f"<details><summary style='cursor:pointer;color:{TEXT_DIM};font-size:13px'>" |
|
|
|
|
f"Developer · raw kind 0 JSON</summary>" |
|
|
|
|
f"<pre style='color:{TEXT_DIM};font-size:12px;white-space:pre-wrap;word-break:break-all;" |
|
|
|
|
f"background:{BG_CODE};padding:10px;border-radius:8px;border:1px solid {BORDER};margin:8px 0 0 0'>" |
|
|
|
|
f"{raw_esc}</pre></details>" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
k10002 = self._db.get_latest_kind10002_event(pk) |
|
|
|
|
relay_inner = ( |
|
|
|
|
f"<p style='color:{TEXT_DIM};font-size:15px;margin:0'>" |
|
|
|
|
f"<i>No NIP-65 relay list (kind 10002) in local DB yet.</i></p>" |
|
|
|
|
f"<p style='color:{TEXT_DIM};font-size:14px;margin:0'>" |
|
|
|
|
f"<i>No relay list stored locally yet (NIP-65).</i></p>" |
|
|
|
|
) |
|
|
|
|
if k10002: |
|
|
|
|
reads, writes = parse_kind10002_tags(k10002.get("tags") or []) |
|
|
|
|
r_esc = "<br>".join(html.escape(u) for u in reads[:40]) |
|
|
|
|
w_esc = "<br>".join(html.escape(u) for u in writes[:40]) |
|
|
|
|
r_esc = "<br>".join(html.escape(u) for u in reads[:24]) |
|
|
|
|
w_esc = "<br>".join(html.escape(u) for u in writes[:24]) |
|
|
|
|
relay_inner = ( |
|
|
|
|
f"{_sec_title('Relays · NIP-65 (kind 10002)')}" |
|
|
|
|
f"<p style='color:{TEXT_MUTED};font-size:14px;margin:0 0 6px 0'><b>Read</b></p>" |
|
|
|
|
f"<div style='color:{TEXT_DIM};font-size:14px;line-height:1.45'>{r_esc or '—'}</div>" |
|
|
|
|
f"<p style='color:{TEXT_MUTED};font-size:14px;margin:12px 0 6px 0'><b>Write</b></p>" |
|
|
|
|
f"<div style='color:{TEXT_DIM};font-size:14px;line-height:1.45'>{w_esc or '—'}</div>" |
|
|
|
|
f"{_sec_title('Relays')}" |
|
|
|
|
f"<p style='color:{TEXT_MUTED};font-size:12px;margin:0 0 6px 0'>Outbox (read / write)</p>" |
|
|
|
|
f"<p style='color:{TEXT_MUTED};font-size:13px;margin:0 0 4px 0'><b>Read</b></p>" |
|
|
|
|
f"<div style='color:{TEXT_DIM};font-size:13px;line-height:1.45'>{r_esc or '—'}</div>" |
|
|
|
|
f"<p style='color:{TEXT_MUTED};font-size:13px;margin:10px 0 4px 0'><b>Write</b></p>" |
|
|
|
|
f"<div style='color:{TEXT_DIM};font-size:13px;line-height:1.45'>{w_esc or '—'}</div>" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
follows = self._db.get_latest_kind3_contact_pubkeys(pk, limit=400) |
|
|
|
|
@ -547,23 +597,29 @@ class ProfilePage(QWidget):
@@ -547,23 +597,29 @@ class ProfilePage(QWidget):
|
|
|
|
|
follow_lines.append( |
|
|
|
|
f'<div style="margin:4px 0"><a href="{html.escape(href, quote=True)}" ' |
|
|
|
|
f'style="color:{TEXT};text-decoration:none">{html.escape(np)}</a>' |
|
|
|
|
f'<span style="color:{TEXT_DIM};font-size:13px"> · {html.escape(fp[:16])}…</span></div>' |
|
|
|
|
f'<span style="color:{TEXT_DIM};font-size:12px"> · {html.escape(fp[:16])}…</span></div>' |
|
|
|
|
) |
|
|
|
|
_no_follow = f"<i style='color:{TEXT_DIM}'>No kind 3 in local DB.</i>" |
|
|
|
|
follow_inner = ( |
|
|
|
|
f"{_sec_title('Following · kind 3 (local)')}" |
|
|
|
|
f"<div style='font-size:14px;line-height:1.45'>{''.join(follow_lines) or _no_follow}</div>" |
|
|
|
|
) |
|
|
|
|
_no_follow = f"<i style='color:{TEXT_DIM}'>No contact list in local DB yet.</i>" |
|
|
|
|
follow_inner = f"{_sec_title('Following')}<div style='font-size:13px;line-height:1.45'>{''.join(follow_lines) or _no_follow}</div>" |
|
|
|
|
|
|
|
|
|
nip30 = self._db.get_author_nip30_emoji_urls(pk) |
|
|
|
|
em_lines: list[str] = [] |
|
|
|
|
for short, url in sorted(nip30.items(), key=lambda x: x[0])[:48]: |
|
|
|
|
em_lines.append( |
|
|
|
|
f"<div style='margin:3px 0'><code style='color:{TEXT_MUTED}'>:{html.escape(short)}:</code> " |
|
|
|
|
f'<a href="{html.escape(url, quote=True)}" style="color:{TEXT}">{html.escape(url[:48])}…</a></div>' |
|
|
|
|
f'<a href="{html.escape(url, quote=True)}" style="color:{TEXT};font-size:12px">' |
|
|
|
|
f"{html.escape(url[:40])}…</a></div>" |
|
|
|
|
) |
|
|
|
|
_no_emoji = f"<i style='color:{TEXT_DIM}'>No emoji packs indexed yet.</i>" |
|
|
|
|
emoji_inner = f"{_sec_title('Custom emoji · NIP-30')}" f"<div>{''.join(em_lines) or _no_emoji}</div>" |
|
|
|
|
_no_emoji = f"<i style='color:{TEXT_DIM}'>No custom emoji indexed yet.</i>" |
|
|
|
|
if em_lines: |
|
|
|
|
emoji_inner = ( |
|
|
|
|
f"{_sec_title('Emoji')}" |
|
|
|
|
f"<p style='color:{TEXT_DIM};font-size:13px;margin:0 0 6px 0'>{len(nip30)} shortcodes (local).</p>" |
|
|
|
|
f"<details><summary style='cursor:pointer;color:{TEXT_DIM};font-size:13px'>Show list</summary>" |
|
|
|
|
f"<div style='margin-top:8px'>{''.join(em_lines)}</div></details>" |
|
|
|
|
) |
|
|
|
|
else: |
|
|
|
|
emoji_inner = f"{_sec_title('Emoji')}<div>{_no_emoji}</div>" |
|
|
|
|
|
|
|
|
|
notes = self._db.list_events_by_pubkey(pk, kinds=_PROFILE_NOTE_KINDS, limit=40) |
|
|
|
|
note_lines: list[str] = [] |
|
|
|
|
@ -595,8 +651,8 @@ class ProfilePage(QWidget):
@@ -595,8 +651,8 @@ class ProfilePage(QWidget):
|
|
|
|
|
_no_notes = "No matching notes stored yet." |
|
|
|
|
|
|
|
|
|
card_style = ( |
|
|
|
|
f"background:{BG_CARD};border:1px solid {BORDER};border-radius:14px;" |
|
|
|
|
f"padding:16px 18px;margin-bottom:14px" |
|
|
|
|
f"background:{BG_CARD};border:1px solid {BORDER};border-radius:12px;" |
|
|
|
|
f"padding:12px 14px;margin-bottom:12px" |
|
|
|
|
) |
|
|
|
|
left_parts: list[str] = [] |
|
|
|
|
if about_inner: |
|
|
|
|
@ -610,7 +666,7 @@ class ProfilePage(QWidget):
@@ -610,7 +666,7 @@ class ProfilePage(QWidget):
|
|
|
|
|
|
|
|
|
|
left_doc = ( |
|
|
|
|
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">" |
|
|
|
|
f"{FEED_DOC_CSS}</head><body style='padding:14px 16px 28px 18px'>" |
|
|
|
|
f"{FEED_DOC_CSS}</head><body style='padding:12px 12px 22px 14px'>" |
|
|
|
|
f"{''.join(left_parts)}</body></html>" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
@ -631,8 +687,93 @@ class ProfilePage(QWidget):
@@ -631,8 +687,93 @@ class ProfilePage(QWidget):
|
|
|
|
|
f"{FEED_DOC_CSS}</head><body style='padding:14px 14px 28px 12px'>" |
|
|
|
|
f"{feed_intro}{feed_inner}</body></html>" |
|
|
|
|
) |
|
|
|
|
self._left_body.setHtml(left_doc) |
|
|
|
|
self._feed_body.setHtml(feed_doc) |
|
|
|
|
|
|
|
|
|
interact = self._db.list_pubkey_outgoing_interactions(pk, limit=45) |
|
|
|
|
it_pks = {str(x.get("target_pubkey") or "").lower() for x in interact if x.get("target_pubkey")} |
|
|
|
|
it_profiles = self._db.get_latest_kind0_profiles(sorted(it_pks)) if it_pks else {} |
|
|
|
|
act_lines: list[str] = [] |
|
|
|
|
for evi in interact: |
|
|
|
|
eid_i = str(evi["id"]) |
|
|
|
|
href_self = f"imwald://note/{eid_i}" |
|
|
|
|
k_i = int(evi["kind"]) |
|
|
|
|
k_lab = _ACTIVITY_KIND_LABEL.get(k_i, f"Kind {k_i}") |
|
|
|
|
nip_i = cast(list[list[str]], evi["tags"]) if isinstance(evi.get("tags"), list) else None |
|
|
|
|
snip_i = markdown_plain_summary( |
|
|
|
|
evi.get("content") or "", |
|
|
|
|
max_len=72, |
|
|
|
|
db=self._db, |
|
|
|
|
nip30_tags=nip_i, |
|
|
|
|
nip30_author_pubkey=str(evi.get("pubkey") or "") or None, |
|
|
|
|
) |
|
|
|
|
t_hi = _fmt_event_time(evi.get("created_at")) |
|
|
|
|
tpk = str(evi.get("target_pubkey") or "").lower() |
|
|
|
|
row_pr = it_profiles.get(tpk) |
|
|
|
|
t_nm = ( |
|
|
|
|
display_name_from_profile_or_hex(parse_kind0_profile(row_pr["content"]), tpk) |
|
|
|
|
if row_pr |
|
|
|
|
else encode_npub(tpk)[:18] + "…" |
|
|
|
|
) |
|
|
|
|
parent_href = "" |
|
|
|
|
tid = evi.get("target_event_id") |
|
|
|
|
if tid: |
|
|
|
|
note_h = html.escape(f"imwald://note/{str(tid).lower()}", quote=True) |
|
|
|
|
pub_h = html.escape(f"imwald://pub/{tpk}", quote=True) |
|
|
|
|
parent_href = ( |
|
|
|
|
f"<div style='margin-top:6px;font-size:12px;color:{TEXT_MUTED}'>On " |
|
|
|
|
f'<a href="{note_h}" style="color:{ACCENT_SOFT};text-decoration:none">parent note</a>' |
|
|
|
|
f' · <a href="{pub_h}" style="color:{ACCENT_SOFT};text-decoration:none">' |
|
|
|
|
f"{html.escape(t_nm)}</a></div>" |
|
|
|
|
) |
|
|
|
|
esc_h = html.escape(href_self, quote=True) |
|
|
|
|
act_lines.append( |
|
|
|
|
f'<a href="{esc_h}" style="display:block;text-decoration:none;color:{TEXT};' |
|
|
|
|
f"background:{BG_CARD};border:1px solid {BORDER};border-left:3px solid {ACCENT_SOFT};" |
|
|
|
|
f'border-radius:10px;padding:10px 12px;margin-bottom:10px">' |
|
|
|
|
f"<div style='color:{TEXT_MUTED};font-size:11px;font-weight:700;text-transform:uppercase'>" |
|
|
|
|
f"{html.escape(k_lab)} · {html.escape(t_hi)}</div>" |
|
|
|
|
f"<div style='color:{TEXT_DIM};margin-top:6px;font-size:14px;line-height:1.45'>" |
|
|
|
|
f"{html.escape(snip_i)}</div>{parent_href}" |
|
|
|
|
f"<div style='color:{ACCENT_SOFT};font-size:11px;margin-top:6px;font-weight:600'>" |
|
|
|
|
f"Open thread →</div></a>" |
|
|
|
|
) |
|
|
|
|
_no_act = "No replies or reactions to other people's notes in the local database yet." |
|
|
|
|
act_intro = ( |
|
|
|
|
f'<div style="margin-bottom:14px;padding-bottom:12px;border-bottom:1px solid {BORDER}">' |
|
|
|
|
f'<div style="font-size:18px;font-weight:700;color:{TEXT};letter-spacing:-0.02em">Activity</div>' |
|
|
|
|
f'<div style="color:{TEXT_DIM};font-size:13px;margin-top:6px;line-height:1.4">' |
|
|
|
|
f"Replies, boosts, and reactions that <b style='color:{TEXT}'>link to someone else's</b> events " |
|
|
|
|
f"(from this device’s index).</div></div>" |
|
|
|
|
) |
|
|
|
|
act_inner = "".join(act_lines) if act_lines else f"<p style='color:{TEXT_DIM};font-size:14px'><i>{html.escape(_no_act)}</i></p>" |
|
|
|
|
interact_doc = ( |
|
|
|
|
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">" |
|
|
|
|
f"{FEED_DOC_CSS}</head><body style='padding:12px 12px 24px 10px'>" |
|
|
|
|
f"{act_intro}{act_inner}</body></html>" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
left_sig = ( |
|
|
|
|
content, |
|
|
|
|
created0, |
|
|
|
|
str(k10002["id"]) if k10002 else "", |
|
|
|
|
tuple(follows[:80]), |
|
|
|
|
tuple(sorted(nip30.items())), |
|
|
|
|
about_raw, |
|
|
|
|
lud06_s, |
|
|
|
|
lud16_s, |
|
|
|
|
live_lnurl, |
|
|
|
|
) |
|
|
|
|
feed_sig = tuple((str(ev["id"]), int(ev["created_at"])) for ev in notes) |
|
|
|
|
interact_sig = tuple((str(x["id"]), int(x["created_at"])) for x in interact) |
|
|
|
|
|
|
|
|
|
if self._left_panel_sig != left_sig: |
|
|
|
|
self._left_panel_sig = left_sig |
|
|
|
|
self._left_body.setHtml(left_doc) |
|
|
|
|
if self._feed_panel_sig != feed_sig: |
|
|
|
|
self._feed_panel_sig = feed_sig |
|
|
|
|
self._feed_body.setHtml(feed_doc) |
|
|
|
|
if self._interact_panel_sig != interact_sig: |
|
|
|
|
self._interact_panel_sig = interact_sig |
|
|
|
|
self._interact_body.setHtml(interact_doc) |
|
|
|
|
tw = self.parentWidget() |
|
|
|
|
if isinstance(tw, QTabWidget): |
|
|
|
|
i = tw.indexOf(self) |
|
|
|
|
|