/**
* 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.content}
${post.likes}
${post.replies}
`,
).join("\n");
return `
${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 `
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 `
${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()
: `
`;
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" },
],
};