aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/features/messages.js40
-rw-r--r--src/i18n.js1
-rw-r--r--src/style.css2
3 files changed, 38 insertions, 5 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 836966f..79526b8 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -587,7 +587,10 @@ export function messagesFeature(ctx) {
let repostBy = null; // set while noteRow draws a repost's note
// A profile shows its articles (NIP-23) among the notes, as Amethyst does.
// Their boosts too, under "<name> reposted", as in a feed.
- const PROFILE_KINDS = [...FEED_KINDS, 30023, REPOST];
+ // ...and their NIP-22 comments (kind 1111): the Replies tab is where
+ // those go, and a comment scoped to a web address (DaddyTube's, on a
+ // film's IMDb page) was never asked for at all
+ const PROFILE_KINDS = [...FEED_KINDS, 30023, REPOST, 1111];
// An edited article is a new event at the same address: keep the newest.
const newestArticles = (evs) => {
const best = new Map();
@@ -5011,6 +5014,17 @@ export function messagesFeature(ctx) {
if (!ref.author) ref.authors = [...new Set((ev.tags || []).filter((x) => x[0] === 'p').map((x) => hex(x[1])).filter((p) => p && p !== ev.pubkey))].slice(0, 3);
return ref;
}
+ // The web page a NIP-22 comment is on (its I tag, an http(s) URL), with
+ // the label its alt tag gives it — DaddyTube: "Comment on Sheep
+ // Detectives (2025) in DaddyTube" — or else the address, host and path.
+ function webScopeOf(ev) {
+ const url = ((ev.tags || []).find((x) => x[0] === 'I' && /^https?:\/\/\S+$/i.test(x[1] || '')) || [])[1];
+ if (!url) return null;
+ let where = url;
+ try { const u = new URL(url); where = (u.host + u.pathname).replace(/^www\./, '').replace(/\/$/, ''); } catch {}
+ const alt = ((ev.tags || []).find((x) => x[0] === 'alt' && String(x[1] || '').trim()) || [])[1];
+ return { url, label: alt ? String(alt).trim().slice(0, 140) : t('noteCommentOn', { where }) };
+ }
// A note already in hand — in a feed, a thread, someone's page — before
// asking any relay for it: the post a reply answers is usually right
// there in the Following feed.
@@ -6240,13 +6254,17 @@ export function messagesFeature(ctx) {
}
try {
const orT = orTopicsOf(def);
+ // the feed's own kinds: a Conversations view asks for replies and
+ // NIP-22 comments (asked for posts, it painted no comment at all
+ // until a refresh half a minute on; the pass below holds for them)
+ const kinds = feedKinds(def);
await Promise.all([
// people's reposts with their posts (a topic or firehose ask: posts only);
// every author, a chunk of them per ask
...(authors.length ? authorChunks(authors) : [null]).map((chunk) =>
- queryFeed(feedRelaysOr(def, def && def.all ? FIREHOSE_RELAYS : NOTE_RELAYS), { kinds: chunk && !topics.length ? [...FEED_KINDS, REPOST] : FEED_KINDS, limit: FEED_PAGE,
+ queryFeed(feedRelaysOr(def, def && def.all ? FIREHOSE_RELAYS : NOTE_RELAYS), { kinds: chunk && !topics.length && !def.replies ? [...kinds, REPOST] : kinds, limit: FEED_PAGE,
...(chunk ? { authors: chunk } : {}), ...(topics.length ? { '#t': topics } : {}) }, merge, c, 3000)),
- orT.length ? queryFeed(feedRelaysOr(def, TOPIC_RELAYS), { kinds: FEED_KINDS, limit: FEED_PAGE, '#t': orT }, merge, c, 3000) : null]);
+ orT.length ? queryFeed(feedRelaysOr(def, TOPIC_RELAYS), { kinds, limit: FEED_PAGE, '#t': orT }, merge, c, 3000) : null]);
} catch {} finally {
if (!c.stopped) {
c.status = 'ready';
@@ -11278,10 +11296,14 @@ export function messagesFeature(ctx) {
function noteReport(ev) { indexReport(reportsNow(), ev, myPubkeys().includes(ev.pubkey)); }
function indexReport(r, ev, mine) {
const hex = (v) => /^[0-9a-f]{64}$/.test(v || '');
+ // every post named counts toward es, the 'other' ones too: a report
+ // of a post as 'other' with its author's p tag beside it used to have
+ // es = 0 and land on the AUTHOR as a person report
let es = 0;
for (const x of ev.tags || []) {
- if (x[0] !== 'e' || !hex(x[1]) || x[2] === 'other') continue;
+ if (x[0] !== 'e' || !hex(x[1])) continue;
es++;
+ if (x[2] === 'other') continue;
(r.ev.get(x[1]) || r.ev.set(x[1], new Set()).get(x[1])).add(ev.pubkey);
if (mine) r.mine.add(x[1]);
}
@@ -12102,6 +12124,11 @@ export function messagesFeature(ctx) {
const altText = foreign ? ((ev.tags.find((x) => x[0] === 'alt') || [])[1] || t('noteForeignKind', { kind: ev.kind })) : null;
const isReply = ev.kind === COMMENT || (!foreign && !zapRec && ev.tags.some((x) => x[0] === 'e'));
const parent = context && !foreign && !zapRec ? parentRefOf(ev) : null;
+ // A NIP-22 comment on something that isn't an event — a web page (I
+ // tag: DaddyTube comments on a film's IMDb page) — has no post to show
+ // above it; the page it is on goes there instead, named by the
+ // comment's own alt tag when it carries one, else by the address.
+ const scope = ev.kind === COMMENT && !parent && !(ev.tags || []).some((x) => (x[0] === 'e' || x[0] === 'E') && x[1]) ? webScopeOf(ev) : null;
const canZap = canZapPk(pk);
// an optimistic post mid-publish: visible but not yet a real event —
// dimmed, and no thread/reply/zap until its signed self takes over
@@ -12144,6 +12171,9 @@ export function messagesFeature(ctx) {
parent ? h('div', { class: 'note-context', 'data-context-of': parent.id },
h('div', { class: 'small faint note-context-label' }, '\u21a9 ' + t('noteReplyingTo')),
quoteCard(parent, 0)) : null,
+ scope ? h('div', { class: 'note-context note-scope' },
+ h('a', { class: 'small faint note-context-label', href: scope.url, target: '_blank', rel: 'noopener noreferrer', onClick: (e) => e.stopPropagation() },
+ '\u{1F4AC} ' + scope.label)) : null,
h('div', { class: 'row between note-header', style: 'align-items:center;gap:8px' },
h('div', { class: 'row', style: 'gap:7px;align-items:baseline;min-width:0' },
h('span', {
@@ -12153,7 +12183,7 @@ export function messagesFeature(ctx) {
onClick: (e) => { e.stopPropagation(); openProfile(pk); },
}, name),
h('span', { class: 'small faint', style: 'white-space:nowrap' },
- (isReply && !parent ? '↩ ' + t('profReplyTag') + ' · ' : '') + timeLabel(ev.created_at * 1000)
+ (isReply && !parent && !scope ? '↩ ' + t('profReplyTag') + ' · ' : '') + timeLabel(ev.created_at * 1000)
+ (edited ? ' · ' + t('postEditedTag') : ''))),
pending ? null : h('button', {
// the overflow stays up here; reply, boost, quote, react and zap
diff --git a/src/i18n.js b/src/i18n.js
index fdcd1e1..b944f96 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -329,6 +329,7 @@ const STR = {
feedTitle: 'Feed',
feedFollowingName: 'Following',
noteReplyingTo: 'Replying to',
+ noteCommentOn: 'Comment on {where}',
feedNew: 'New feed',
feedNewName: 'New feed',
feedEdit: 'Edit feed',
diff --git a/src/style.css b/src/style.css
index cef3663..b65f632 100644
--- a/src/style.css
+++ b/src/style.css
@@ -1949,6 +1949,8 @@ body::after {
/* a reply shown with the post it answers above it (Conversations, a profile's Replies) */
.note-context { margin-bottom: 6px; }
.note-context-label { margin-bottom: 2px; }
+.note-scope .note-context-label { display: inline-block; text-decoration: none; }
+.note-scope .note-context-label:hover { text-decoration: underline; }
.note-context .quote-card { margin-top: 0; }
.note-context .quote-card .note-text { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4; overflow: hidden; }
/* attach + GIF inside the composer's field, bottom right; the field keeps