/** * Socials module — federated social feed aggregator. * * Aggregates and displays social media activity across community members. * Supports ActivityPub, RSS, and manual link sharing. */ import { Hono } from "hono"; import { renderShell, renderExternalAppShell, escapeHtml } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; import { renderLanding } from "./landing"; import { MYCOFI_CAMPAIGN, PLATFORM_ICONS, PLATFORM_COLORS } from "./campaign-data"; const routes = new Hono(); // ── API: Health ── routes.get("/api/health", (c) => { return c.json({ ok: true, module: "rsocials" }); }); // ── API: Info ── routes.get("/api/info", (c) => { return c.json({ module: "rsocials", description: "Federated social feed aggregator for communities", features: [ "ActivityPub integration", "RSS feed aggregation", "Link sharing", "Community timeline", ], }); }); // ── API: Feed — community social timeline ── routes.get("/api/feed", (c) => { // Demo feed items return c.json({ items: [ { id: "demo-1", type: "post", author: "Alice", content: "Just published our community governance proposal!", source: "fediverse", timestamp: new Date(Date.now() - 3600_000).toISOString(), likes: 12, replies: 3, }, { id: "demo-2", type: "link", author: "Bob", content: "Great article on local-first collaboration", url: "https://example.com/local-first", source: "shared", timestamp: new Date(Date.now() - 7200_000).toISOString(), likes: 8, replies: 1, }, { id: "demo-3", type: "post", author: "Carol", content: "Welcome new members! Check out rSpace's tools in the app switcher above.", source: "local", timestamp: new Date(Date.now() - 14400_000).toISOString(), likes: 24, replies: 7, }, ], demo: true, }); }); // ── API: Campaigns list ── routes.get("/api/campaigns", (c) => { const space = c.req.param("space") || "demo"; const campaign = MYCOFI_CAMPAIGN; return c.json({ campaigns: [ { id: campaign.id, title: campaign.title, description: campaign.description, duration: campaign.duration, platforms: campaign.platforms, postCount: campaign.posts.length, updated_at: "2026-02-21T09:00:00Z", url: `/${space}/rsocials/campaign`, }, ], }); }); // ── Demo feed data (server-rendered, no API calls) ── const DEMO_FEED = [ { username: "@alice", initial: "A", color: "#6366f1", content: "Just deployed the new rFunds river view! The enoughness score is such a powerful concept. \u{1F30A}", timeAgo: "2 hours ago", likes: 5, replies: 2, }, { username: "@bob", initial: "B", color: "#f59e0b", content: "Workshop recording is up on rTube: 'Introduction to Local-First Data'. Check it out!", timeAgo: "5 hours ago", likes: 8, replies: 4, }, { username: "@carol", initial: "C", color: "#10b981", content: "The cosmolocal print network now has 6 providers across 4 countries. Design global, manufacture local! \u{1F30D}", timeAgo: "1 day ago", likes: 12, replies: 3, }, { username: "@diana", initial: "D", color: "#ec4899", content: "Reading Elinor Ostrom's 'Governing the Commons' \u2014 so many parallels to what we're building with rSpace governance.", timeAgo: "1 day ago", likes: 7, replies: 5, }, { username: "@eve", initial: "E", color: "#14b8a6", content: "New community garden plot assignments are up on rChoices. Vote for your preferred plot by Friday!", timeAgo: "2 days ago", likes: 3, replies: 1, }, { username: "@frank", initial: "F", color: "#8b5cf6", content: "Mesh network node #42 is online! Coverage now extends to the community center. \u{1F4E1}", timeAgo: "3 days ago", likes: 15, replies: 6, }, ]; function renderDemoFeedHTML(): string { const cards = DEMO_FEED.map( (post) => `
${post.initial}
${post.username}

${post.content}

${post.likes} ${post.replies}
`, ).join("\n"); return `

Social Feed DEMO

Open Full App

A preview of your community's social timeline

${cards}

This is demo data. Connect ActivityPub or RSS feeds in your own space.

`; } // ── Campaign page route ── function renderCampaignPage(space: string): string { const c = MYCOFI_CAMPAIGN; const phases = [1, 2, 3]; const phaseIcons = ["📣", "🚀", "📡"]; const phaseHTML = phases.map((phaseNum, i) => { const phasePosts = c.posts.filter((p) => p.phase === phaseNum); const phaseInfo = c.phases[i]; const postsHTML = phasePosts.map((post) => { const icon = PLATFORM_ICONS[post.platform] || post.platform; const color = PLATFORM_COLORS[post.platform] || "#64748b"; const statusClass = post.status === "scheduled" ? "campaign-status--scheduled" : "campaign-status--draft"; const date = new Date(post.scheduledAt); const dateStr = date.toLocaleDateString("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }); const contentPreview = escapeHtml(post.content.length > 180 ? post.content.substring(0, 180) + "..." : post.content); const tags = post.hashtags.map((h) => `#${escapeHtml(h)}`).join(" "); return `
${icon} ${escapeHtml(post.status)}
Step ${post.stepNumber}

${contentPreview.replace(/\n/g, "
")}

${tags}
`; }).join("\n"); return `

${phaseIcons[i]} Phase ${phaseNum}: ${escapeHtml(phaseInfo.label)} ${escapeHtml(phaseInfo.days)}

${postsHTML}
`; }).join("\n"); return `
🍄

${escapeHtml(c.title)}

${escapeHtml(c.description)}

📅 ${escapeHtml(c.duration)} 📱 ${c.platforms.join(", ")} 📝 ${c.posts.length} posts across ${c.phases.length} phases
${phaseHTML}
`; } const CAMPAIGN_CSS = ` .campaign-page { max-width: 900px; margin: 0 auto; padding: 2rem 1rem; } .campaign-page__header { display: flex; align-items: flex-start; gap: 1rem; margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid #334155; } .campaign-page__icon { font-size: 3rem; } .campaign-page__title { margin: 0; font-size: 1.5rem; color: #f1f5f9; } .campaign-page__desc { margin: 0.25rem 0 0.5rem; color: #94a3b8; font-size: 0.9rem; line-height: 1.5; } .campaign-page__stats { display: flex; flex-wrap: wrap; gap: 1rem; font-size: 0.8rem; color: #64748b; } .campaign-phase { margin-bottom: 2rem; } .campaign-phase__title { font-size: 1.15rem; color: #e2e8f0; margin: 0 0 1rem; display: flex; align-items: center; gap: 0.5rem; } .campaign-phase__days { font-size: 0.8rem; color: #64748b; font-weight: 400; } .campaign-phase__posts { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 0.75rem; } .campaign-post { background: #1e293b; border: 1px solid #334155; border-radius: 0.75rem; padding: 1rem; transition: border-color 0.15s; } .campaign-post:hover { border-color: #6366f1; } .campaign-post__header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; } .campaign-post__platform { width: 28px; height: 28px; border-radius: 6px; display: flex; align-items: center; justify-content: center; color: white; font-size: 0.75rem; font-weight: 700; flex-shrink: 0; } .campaign-post__meta { flex: 1; min-width: 0; } .campaign-post__meta strong { display: block; font-size: 0.8rem; color: #e2e8f0; text-transform: capitalize; } .campaign-post__date { font-size: 0.7rem; color: #64748b; } .campaign-post__step { font-size: 0.65rem; color: #6366f1; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; } .campaign-status { font-size: 0.6rem; padding: 2px 6px; border-radius: 4px; text-transform: uppercase; font-weight: 600; letter-spacing: 0.05em; white-space: nowrap; } .campaign-status--scheduled { background: rgba(16,185,129,0.15); color: #34d399; } .campaign-status--draft { background: rgba(251,191,36,0.15); color: #fbbf24; } .campaign-post__content { font-size: 0.8rem; color: #94a3b8; line-height: 1.5; margin: 0 0 0.5rem; } .campaign-post__tags { display: flex; flex-wrap: wrap; gap: 0.25rem; } .campaign-tag { font-size: 0.65rem; color: #7dd3fc; } `; routes.get("/campaign", (c) => { const space = c.req.param("space") || "demo"; return c.html(renderShell({ title: `Campaign — rSocials | rSpace`, moduleId: "rsocials", spaceSlug: space, modules: getModuleInfoList(), theme: "dark", body: renderCampaignPage(space), styles: ``, })); }); // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; const view = c.req.query("view"); if (view === "app") { return c.html(renderExternalAppShell({ title: `${space} — Postiz | rSpace`, moduleId: "rsocials", spaceSlug: space, modules: getModuleInfoList(), appUrl: "https://social.jeffemmett.com", appName: "Postiz", theme: "dark", })); } const isDemo = space === "demo"; const body = isDemo ? renderDemoFeedHTML() : `

Community Feed

Social activity across your community

Loading feed…
`; return c.html( renderShell({ title: `${space} — Socials | rSpace`, moduleId: "rsocials", spaceSlug: space, modules: getModuleInfoList(), theme: "dark", body, styles: ``, }), ); }); export const socialsModule: RSpaceModule = { id: "rsocials", name: "rSocials", icon: "📢", description: "Federated social feed aggregator for communities", routes, standaloneDomain: "rsocials.online", landingPage: renderLanding, externalApp: { url: "https://social.jeffemmett.com", name: "Postiz" }, feeds: [ { id: "social-feed", name: "Social Feed", kind: "data", description: "Community social timeline — posts, links, and activity from connected platforms", }, ], acceptsFeeds: ["data", "trust"], outputPaths: [ { path: "campaigns", name: "Campaigns", icon: "📢", description: "Social media campaigns" }, { path: "posts", name: "Posts", icon: "📱", description: "Social feed posts across platforms" }, ], };