rspace-online/modules/rsocials/mod.ts

200 lines
6.2 KiB
TypeScript

/**
* 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 } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
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,
});
});
// ── Page route ──
routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
return c.html(
renderShell({
title: `${space} — Socials | rSpace`,
moduleId: "rsocials",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
body: `
<div class="rsocials-app" data-space="${space}">
<div class="rsocials-header">
<h2>Community Feed</h2>
<p class="rsocials-subtitle">Social activity across your community</p>
</div>
<div id="rsocials-feed" class="rsocials-feed">
<div class="rsocials-loading">Loading feed…</div>
</div>
</div>
<script type="module">
const space = document.querySelector('.rsocials-app')?.dataset.space || 'demo';
const feedEl = document.getElementById('rsocials-feed');
try {
const res = await fetch('api/feed');
const data = await res.json();
if (!data.items?.length) {
feedEl.innerHTML = '<p class="rsocials-empty">No posts yet. Share something with your community!</p>';
return;
}
feedEl.innerHTML = data.items.map(item => {
const time = new Date(item.timestamp);
const ago = Math.round((Date.now() - time.getTime()) / 60000);
const timeStr = ago < 60 ? ago + 'm ago' : Math.round(ago / 60) + 'h ago';
const sourceTag = '<span class="rsocials-source">' + item.source + '</span>';
return '<article class="rsocials-item">' +
'<div class="rsocials-item-header">' +
'<strong>' + item.author + '</strong> ' + sourceTag +
'<time>' + timeStr + '</time>' +
'</div>' +
'<p class="rsocials-item-content">' + item.content + '</p>' +
(item.url ? '<a class="rsocials-item-link" href="' + item.url + '" target="_blank" rel="noopener">' + item.url + '</a>' : '') +
'<div class="rsocials-item-actions">' +
'<span>♥ ' + (item.likes || 0) + '</span>' +
'<span>💬 ' + (item.replies || 0) + '</span>' +
'</div>' +
'</article>';
}).join('');
if (data.demo) {
feedEl.insertAdjacentHTML('beforeend',
'<p class="rsocials-demo-notice">This is demo data. Connect ActivityPub or RSS feeds in your own space.</p>'
);
}
} catch(e) {
feedEl.innerHTML = '<p class="rsocials-empty">Failed to load feed.</p>';
}
</script>`,
styles: `<style>
.rsocials-app { max-width: 640px; margin: 0 auto; padding: 2rem 1rem; }
.rsocials-header { margin-bottom: 1.5rem; }
.rsocials-header h2 {
font-size: 1.5rem; margin: 0 0 0.25rem;
background: linear-gradient(135deg, #7dd3fc, #c4b5fd);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.rsocials-subtitle { color: #64748b; font-size: 0.85rem; margin: 0; }
.rsocials-feed { display: flex; flex-direction: column; gap: 1px; }
.rsocials-loading { color: #64748b; padding: 2rem 0; text-align: center; }
.rsocials-empty { color: #64748b; padding: 2rem 0; text-align: center; }
.rsocials-item {
padding: 1rem; border-radius: 8px;
background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06);
margin-bottom: 0.5rem;
}
.rsocials-item-header {
display: flex; align-items: center; gap: 0.5rem;
font-size: 0.85rem; color: #94a3b8; margin-bottom: 0.5rem;
}
.rsocials-item-header strong { color: #e2e8f0; }
.rsocials-item-header time { margin-left: auto; font-size: 0.75rem; }
.rsocials-source {
font-size: 0.65rem; padding: 1px 6px; border-radius: 4px;
background: rgba(124,58,237,0.15); color: #c4b5fd;
text-transform: uppercase; letter-spacing: 0.05em;
}
.rsocials-item-content { margin: 0 0 0.5rem; color: #cbd5e1; line-height: 1.5; font-size: 0.9rem; }
.rsocials-item-link {
display: block; font-size: 0.8rem; color: #7dd3fc;
text-decoration: none; margin-bottom: 0.5rem; word-break: break-all;
}
.rsocials-item-link:hover { text-decoration: underline; }
.rsocials-item-actions {
display: flex; gap: 1rem; font-size: 0.8rem; color: #64748b;
}
.rsocials-demo-notice {
text-align: center; font-size: 0.75rem; color: #475569;
padding: 1rem 0; border-top: 1px solid rgba(255,255,255,0.05); margin-top: 0.5rem;
}
</style>`,
}),
);
});
export const socialsModule: RSpaceModule = {
id: "rsocials",
name: "rSocials",
icon: "\u{1F4E2}",
description: "Federated social feed aggregator for communities",
routes,
standaloneDomain: "rsocials.online",
feeds: [
{
id: "social-feed",
name: "Social Feed",
kind: "data",
description: "Community social timeline — posts, links, and activity from connected platforms",
},
],
acceptsFeeds: ["data", "trust"],
};