Enhance content management and user interaction features

- Introduced a new content source toggle in the profile and app header to switch between IndeeHub and TopDoc films.
- Updated the content fetching logic to dynamically load content based on the selected source.
- Enhanced the seeding process to include a combined catalog of IndeeHub and TopDoc films, ensuring diverse content availability.
- Improved user interaction by preventing duplicate reactions and ensuring a smoother voting experience across comments and content.
- Added support for Amber login (NIP-55) for Android users, integrating it into the existing authentication flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Dorian
2026-02-12 14:24:52 +00:00
parent ab0560de00
commit 35bc78b890
38 changed files with 1107 additions and 185 deletions

View File

@@ -21,7 +21,7 @@ const RELAY_URL = process.env.RELAY_URL || 'ws://localhost:7777'
const ORIGIN = process.env.ORIGIN || 'http://localhost:5174'
// ── Content catalog (matching src/data/indeeHubFilms.ts) ──────────
const CONTENT = [
const INDEEHUB_CONTENT = [
{ id: 'god-bless-bitcoin', title: 'God Bless Bitcoin' },
{ id: 'thethingswecarry', title: 'The Things We Carry' },
{ id: 'duel', title: 'Duel' },
@@ -46,6 +46,36 @@ const CONTENT = [
{ id: '34f042bd-23d6-40f4-9707-4b3bb62fdd58', title: 'Little Billy' },
]
// ── TopDocumentaryFilms catalog (matching src/data/topDocFilms.ts) ──
const TOPDOC_CONTENT = [
{ id: 'tdf-god-bless-bitcoin', title: 'God Bless Bitcoin' },
{ id: 'tdf-bitcoin-end-of-money', title: 'Bitcoin: The End of Money as We Know It' },
{ id: 'tdf-bitcoin-beyond-bubble', title: 'Bitcoin: Beyond the Bubble' },
{ id: 'tdf-bitcoin-gospel', title: 'The Bitcoin Gospel' },
{ id: 'tdf-bitcoin-psyop', title: 'The Bitcoin Psyop' },
{ id: 'tdf-missing-cryptoqueen', title: 'The Missing Cryptoqueen' },
{ id: 'tdf-billion-dollar-scam', title: 'The Billion Dollar Scam' },
{ id: 'tdf-money-banking-fed', title: 'Money, Banking, and The Federal Reserve' },
{ id: 'tdf-american-dream', title: 'The American Dream' },
{ id: 'tdf-century-enslavement', title: 'Century of Enslavement' },
{ id: 'tdf-money-power-wall-street', title: 'Money, Power and Wall Street' },
{ id: 'tdf-gold-6000-year', title: "Gold: Man's 6000 Year Obsession" },
{ id: 'tdf-debtasized', title: 'Debtasized' },
{ id: 'tdf-crash-next-crisis', title: 'Crash: Are We Ready?' },
{ id: 'tdf-pension-gamble', title: 'The Pension Gamble' },
{ id: 'tdf-why-americans-poor', title: 'Why Americans Feel So Poor?' },
{ id: 'tdf-chain-reaction', title: 'Chain Reaction' },
{ id: 'tdf-usa-on-brink', title: 'USA on the Brink' },
{ id: 'tdf-big-four', title: 'The Big Four' },
{ id: 'tdf-congo-millionaires', title: 'Congo: Millionaires of Chaos' },
{ id: 'tdf-economics-of', title: 'The Economics Of' },
{ id: 'tdf-big-business-food', title: 'Big Business: Food Empires' },
{ id: 'tdf-so-long-superstores', title: 'So Long, Superstores?' },
]
// Combined catalog — seeder covers both sources
const CONTENT = [...INDEEHUB_CONTENT, ...TOPDOC_CONTENT]
// ── helpers ──────────────────────────────────────────────────────
type Persona = { name: string; nsec: string; pubkey: string }
@@ -74,10 +104,23 @@ const ONE_DAY = 86400
const ONE_WEEK = 7 * ONE_DAY
// Content subsets for different activity patterns
const topContent = CONTENT.slice(0, 8)
const midContent = CONTENT.slice(8, 16)
const trendingContent = pick(CONTENT.slice(0, 12), 5)
const tastemakerFaves = pick(CONTENT.slice(0, 10), 6)
// Draw from BOTH catalogs to ensure TopDoc films get activity
const topContent = [
...INDEEHUB_CONTENT.slice(0, 5),
...TOPDOC_CONTENT.slice(0, 5),
]
const midContent = [
...INDEEHUB_CONTENT.slice(5, 11),
...TOPDOC_CONTENT.slice(5, 11),
]
const trendingContent = [
...pick(INDEEHUB_CONTENT.slice(0, 8), 3),
...pick(TOPDOC_CONTENT.slice(0, 8), 3),
]
const tastemakerFaves = [
...pick(INDEEHUB_CONTENT.slice(0, 8), 3),
...pick(TOPDOC_CONTENT.slice(0, 8), 3),
]
// ── sample comments ─────────────────────────────────────────────
const POSITIVE_COMMENTS = [
@@ -93,6 +136,12 @@ const POSITIVE_COMMENTS = [
'Rewatched it last night — still holds up beautifully.',
'Such an important documentary. Everyone should see this.',
'The storytelling here is on another level.',
'Required viewing. This opened my eyes to things I never considered.',
'The depth of research in this is incredible. Really well done.',
'Finally a documentary that treats the subject seriously.',
'Shared this with my whole family. Everyone needs to see this.',
'This is the kind of content that changes how you see the world.',
'Brilliant piece of journalism. Respect to the filmmakers.',
]
const MIXED_COMMENTS = [
@@ -104,12 +153,44 @@ const MIXED_COMMENTS = [
'Some great moments, but also some really slow stretches.',
'I can see why people love it, just not my cup of tea.',
'Better than I expected, worse than the reviews suggest.',
'Interesting topic but the production quality could be better.',
'They barely scratched the surface on this topic. Wanted more depth.',
'Decent intro to the topic but experts won\'t learn anything new.',
]
const NEGATIVE_COMMENTS = [
'I really don\'t understand the hype around this one.',
'Couldn\'t finish it. Way too slow for my taste.',
'Overrated. There are much better films in this genre.',
'Felt more like a sales pitch than a documentary.',
]
// Documentary-specific comments for TopDoc films
const DOC_POSITIVE_COMMENTS = [
'One of the best Bitcoin documentaries out there. Really explains the fundamentals.',
'Everyone who thinks they understand money should watch this.',
'This completely changed how I think about the financial system.',
'Incredible deep dive into a topic most people don\'t understand.',
'The interviews in this are absolutely fascinating.',
'Been orange-pilled for years but this doc still taught me new things.',
'Sent this to my dad and now he finally gets it.',
'The production quality for a documentary like this is outstanding.',
'This should be required viewing in economics classes.',
'The parallels they draw to historical events are eye-opening.',
'Watched this with my skeptical friends. They were impressed.',
'Finally a balanced take on cryptocurrency. Well researched.',
'This doc captures the movement perfectly. A time capsule for future generations.',
'The personal stories in this really humanize what can feel like a dry topic.',
'Phenomenal. I\'ve recommended this to at least 20 people.',
]
const DOC_MIXED_COMMENTS = [
'Good overview but a bit surface-level for people already in the space.',
'Some parts felt dated already given how fast things move in crypto.',
'Wish they had interviewed more diverse perspectives.',
'Interesting but could have been 30 minutes shorter.',
'The first half is excellent, second half loses steam.',
'Fair attempt but misses some key nuances about the technology.',
]
// ── publishing helper ───────────────────────────────────────────
@@ -213,9 +294,55 @@ async function seedReactions(relay: Relay) {
}
}
// ── Ensure EVERY TopDoc film has at least some reactions ───────
const alreadyReacted = new Set([
...topContent.map(c => c.id),
...midContent.map(c => c.id),
...trendingContent.map(c => c.id),
...tastemakerFaves.map(c => c.id),
])
const unreactedTopDoc = TOPDOC_CONTENT.filter(c => !alreadyReacted.has(c.id))
for (const item of unreactedTopDoc) {
const voters = pick(allPersonas, randomInt(3, 7))
for (const persona of voters) {
const signer = PrivateKeySigner.fromKey(persona.nsec)
const emoji = Math.random() < 0.75 ? '+' : '-'
const age = randomInt(1 * ONE_DAY, 35 * ONE_DAY)
const ok = await publishEvent(relay, signer, {
kind: 17,
content: emoji,
tags: [
['i', contentUrl(item.id)],
['k', 'web'],
],
created_at: now - age,
}, `catch-all-reaction ${persona.name}->${item.title}`)
if (ok) count++
}
}
console.log(`${count} reactions seeded`)
}
// ── comment picker (uses doc-specific comments for TopDoc films) ─
function pickComment(itemId: string, sentiment: 'positive' | 'mixed' | 'negative'): string {
const isTopDoc = itemId.startsWith('tdf-')
if (sentiment === 'positive') {
const pool = isTopDoc
? [...DOC_POSITIVE_COMMENTS, ...POSITIVE_COMMENTS]
: POSITIVE_COMMENTS
return pool[randomInt(0, pool.length - 1)]
}
if (sentiment === 'mixed') {
const pool = isTopDoc
? [...DOC_MIXED_COMMENTS, ...MIXED_COMMENTS]
: MIXED_COMMENTS
return pool[randomInt(0, pool.length - 1)]
}
return NEGATIVE_COMMENTS[randomInt(0, NEGATIVE_COMMENTS.length - 1)]
}
// ── seed comments (kind 1111) ───────────────────────────────────
async function seedComments(relay: Relay) {
console.log('\n💬 Seeding comments (kind 1111)...')
@@ -224,13 +351,12 @@ async function seedComments(relay: Relay) {
// Top content: several comments
for (const item of topContent) {
const url = contentUrl(item.id)
const commenters = pick(allPersonas, randomInt(2, 5))
const commenters = pick(allPersonas, randomInt(3, 6))
for (const persona of commenters) {
const signer = PrivateKeySigner.fromKey(persona.nsec)
const comments =
Math.random() < 0.7 ? POSITIVE_COMMENTS : MIXED_COMMENTS
const content = comments[randomInt(0, comments.length - 1)]
const sentiment = Math.random() < 0.7 ? 'positive' : 'mixed'
const content = pickComment(item.id, sentiment)
const age = randomInt(1 * ONE_DAY, 30 * ONE_DAY)
const ok = await publishEvent(relay, signer, {
@@ -249,14 +375,15 @@ async function seedComments(relay: Relay) {
}
// Mid content: occasional comments
for (const item of pick(midContent, 4)) {
for (const item of pick(midContent, 8)) {
const url = contentUrl(item.id)
const commenters = pick(allPersonas, randomInt(1, 2))
const commenters = pick(allPersonas, randomInt(1, 3))
for (const persona of commenters) {
const signer = PrivateKeySigner.fromKey(persona.nsec)
const pool = [...MIXED_COMMENTS, ...NEGATIVE_COMMENTS]
const content = pool[randomInt(0, pool.length - 1)]
const r = Math.random()
const sentiment = r < 0.4 ? 'positive' : r < 0.8 ? 'mixed' : 'negative'
const content = pickComment(item.id, sentiment)
const age = randomInt(3 * ONE_DAY, 45 * ONE_DAY)
const ok = await publishEvent(relay, signer, {
@@ -275,13 +402,13 @@ async function seedComments(relay: Relay) {
}
// Tastemaker reviews on their faves
for (const item of tastemakerFaves.slice(0, 4)) {
for (const item of tastemakerFaves) {
const url = contentUrl(item.id)
const reviewers = pick(tastemakers, randomInt(1, 3))
for (const persona of reviewers) {
const signer = PrivateKeySigner.fromKey(persona.nsec)
const content = POSITIVE_COMMENTS[randomInt(0, POSITIVE_COMMENTS.length - 1)]
const content = pickComment(item.id, 'positive')
const age = randomInt(0, 10 * ONE_DAY)
const ok = await publishEvent(relay, signer, {
@@ -300,13 +427,13 @@ async function seedComments(relay: Relay) {
}
// Trending content: recent comments
for (const item of trendingContent.slice(0, 3)) {
for (const item of trendingContent) {
const url = contentUrl(item.id)
const commenters = pick(allPersonas, randomInt(2, 4))
for (const persona of commenters) {
const signer = PrivateKeySigner.fromKey(persona.nsec)
const content = POSITIVE_COMMENTS[randomInt(0, POSITIVE_COMMENTS.length - 1)]
const content = pickComment(item.id, 'positive')
const age = randomInt(0, 3 * ONE_DAY)
const ok = await publishEvent(relay, signer, {
@@ -324,6 +451,42 @@ async function seedComments(relay: Relay) {
}
}
// ── Ensure EVERY TopDoc film has at least some comments ────────
// This catches any TopDoc films not already in top/mid/trending subsets
const alreadyCommented = new Set([
...topContent.map(c => c.id),
...midContent.map(c => c.id),
...trendingContent.map(c => c.id),
...tastemakerFaves.map(c => c.id),
])
const uncommentedTopDoc = TOPDOC_CONTENT.filter(c => !alreadyCommented.has(c.id))
for (const item of uncommentedTopDoc) {
const url = contentUrl(item.id)
const commenters = pick(allPersonas, randomInt(2, 4))
for (const persona of commenters) {
const signer = PrivateKeySigner.fromKey(persona.nsec)
const r = Math.random()
const sentiment = r < 0.6 ? 'positive' : r < 0.9 ? 'mixed' : 'negative'
const content = pickComment(item.id, sentiment)
const age = randomInt(2 * ONE_DAY, 40 * ONE_DAY)
const ok = await publishEvent(relay, signer, {
kind: 1111,
content,
tags: [
['I', url],
['K', 'web'],
['i', url],
['k', 'web'],
],
created_at: now - age,
}, `catch-all-comment ${persona.name}->${item.title}`)
if (ok) count++
}
}
console.log(`${count} comments seeded`)
}