Updates
This commit is contained in:
@@ -39,15 +39,17 @@ async function handleDomainsRoutes(req, res) {
|
||||
for (const domain of domains) {
|
||||
const hash = await getHashForDomain(domain) || 'none';
|
||||
const isLocal = localWriter ? domainClaimants.get(domain)?.has(localWriter) || false : false;
|
||||
// Check if local writer is the resolved claimant (owner)
|
||||
|
||||
let isOwner = false;
|
||||
let consensusState = null;
|
||||
try {
|
||||
const consensusState = await getConsensusState(domain);
|
||||
consensusState = await getConsensusState(domain);
|
||||
isOwner = localWriter ? consensusState.resolvedClaimant === localWriter : false;
|
||||
} catch (err) {
|
||||
logDebug('Admin', `Error checking ownership for ${domain}: ${err.message}`);
|
||||
logDebug('Admin', `Error checking ownership/consensus for ${domain}: ${err.message}`);
|
||||
}
|
||||
resolved.push({ domain, hash, isLocal, isOwner });
|
||||
|
||||
resolved.push({ domain, hash, isLocal, isOwner, consensusState });
|
||||
}
|
||||
let internalDomains = ['p2ns.admin'];
|
||||
try {
|
||||
@@ -59,7 +61,7 @@ async function handleDomainsRoutes(req, res) {
|
||||
// Only add internal domains that aren't already in the resolved list
|
||||
for (const d of internalDomains) {
|
||||
if (!resolved.some(r => r.domain === d)) {
|
||||
resolved.push({ domain: d, hash: 'internal', isLocal: true, isOwner: true });
|
||||
resolved.push({ domain: d, hash: 'internal', isLocal: true, isOwner: true, consensusStatus: 'internal' });
|
||||
}
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
|
||||
@@ -107,24 +107,17 @@ window.tabs = {
|
||||
<td class="p-3">${item.isLocal && item.hash !== 'internal' ? `<button onclick="removeDomain('${item.domain}')" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Remove</button>` : ''}</td>`;
|
||||
return tr;
|
||||
},
|
||||
postFetch: async (data) => {
|
||||
// Fetch consensus states for all domains
|
||||
const domainsWithConsensus = await Promise.all(data.map(async (item) => {
|
||||
if (item.hash === 'internal' || item.hash === 'none') {
|
||||
postFetch: (data) => {
|
||||
// Data already contains consensusState from backend
|
||||
return data.map(item => {
|
||||
if (item.consensusStatus === 'internal' || item.hash === 'internal' || item.hash === 'none') {
|
||||
return { ...item, consensusStatus: 'internal' };
|
||||
}
|
||||
try {
|
||||
const consensusRes = await fetch(`/api/consensus/${encodeURIComponent(item.domain)}`);
|
||||
if (consensusRes.ok) {
|
||||
const consensusState = await consensusRes.json();
|
||||
return { ...item, consensusStatus: consensusState.status, consensusState };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch consensus for ${item.domain}:`, err);
|
||||
}
|
||||
return { ...item, consensusStatus: 'unknown' };
|
||||
}));
|
||||
return domainsWithConsensus;
|
||||
return {
|
||||
...item,
|
||||
consensusStatus: item.consensusState?.status || 'unknown'
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
entries: {
|
||||
|
||||
@@ -25,7 +25,7 @@ async function genericFetch(tabId, shouldRender = true) {
|
||||
window[config.dataKey] = data;
|
||||
|
||||
// Reset infinite scroll state when fetching fresh data
|
||||
if (window.infiniteScrollState && window.infiniteScrollState[tabId]) {
|
||||
if (shouldRender && window.infiniteScrollState && window.infiniteScrollState[tabId]) {
|
||||
window.infiniteScrollState[tabId].loadedCount = 0;
|
||||
window.infiniteScrollState[tabId].lastQuery = '';
|
||||
// Disconnect existing observer
|
||||
|
||||
@@ -148,8 +148,9 @@ function connectWebSocket() {
|
||||
window.updateMap[data.type]();
|
||||
} else {
|
||||
window.updateMap[data.type].forEach(tab => {
|
||||
if (window.activeTab === 'host' || window.activeTab === tab) {
|
||||
if (window.genericFetch) window.genericFetch(tab, true);
|
||||
// Always fetch data in background, but only render if tab is active
|
||||
if (window.genericFetch) {
|
||||
window.genericFetch(tab, window.activeTab === 'host' || window.activeTab === tab);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -39,7 +39,17 @@ async function handleDomainsRoutes(req, res) {
|
||||
for (const domain of domains) {
|
||||
const hash = await getHashForDomain(domain) || 'none';
|
||||
const isLocal = localWriter ? domainClaimants.get(domain)?.has(localWriter) || false : false;
|
||||
resolved.push({ domain, hash, isLocal });
|
||||
|
||||
let isOwner = false;
|
||||
let consensusState = null;
|
||||
try {
|
||||
consensusState = await getConsensusState(domain);
|
||||
isOwner = localWriter ? consensusState.resolvedClaimant === localWriter : false;
|
||||
} catch (err) {
|
||||
logDebug('Admin', `Error checking ownership/consensus for ${domain}: ${err.message}`);
|
||||
}
|
||||
|
||||
resolved.push({ domain, hash, isLocal, isOwner, consensusState });
|
||||
}
|
||||
let internalDomains = ['p2ns.admin'];
|
||||
try {
|
||||
@@ -51,7 +61,7 @@ async function handleDomainsRoutes(req, res) {
|
||||
// Only add internal domains that aren't already in the resolved list
|
||||
for (const d of internalDomains) {
|
||||
if (!resolved.some(r => r.domain === d)) {
|
||||
resolved.push({ domain: d, hash: 'internal', isLocal: true });
|
||||
resolved.push({ domain: d, hash: 'internal', isLocal: true, isOwner: true, consensusStatus: 'internal' });
|
||||
}
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
|
||||
@@ -22,9 +22,26 @@ window.paginationState = {
|
||||
'dns-conflicts': { current: 1, size: 10 },
|
||||
'host-servers': { current: 1, size: 10 },
|
||||
'host-clients': { current: 1, size: 10 },
|
||||
settings: { current: 1, size: 20 }
|
||||
plugins: { current: 1, size: 10 }
|
||||
};
|
||||
|
||||
// Helper function to get consensus status badge
|
||||
function getConsensusStatusBadge(status) {
|
||||
const badges = {
|
||||
'resolved': '<span class="px-2 py-1 bg-green-500 text-white rounded text-xs">Resolved</span>',
|
||||
'tie': '<span class="px-2 py-1 bg-yellow-500 text-white rounded text-xs">Tie</span>',
|
||||
'insufficient_quorum': '<span class="px-2 py-1 bg-orange-500 text-white rounded text-xs">No Quorum</span>',
|
||||
'no_claims': '<span class="px-2 py-1 bg-gray-500 text-white rounded text-xs">No Claims</span>',
|
||||
'error': '<span class="px-2 py-1 bg-red-500 text-white rounded text-xs">Error</span>',
|
||||
'internal': '<span class="px-2 py-1 bg-blue-500 text-white rounded text-xs">Internal</span>',
|
||||
'unknown': '<span class="px-2 py-1 bg-gray-400 text-white rounded text-xs">Unknown</span>'
|
||||
};
|
||||
return badges[status] || badges['unknown'];
|
||||
}
|
||||
|
||||
// Make function globally available
|
||||
window.getConsensusStatusBadge = getConsensusStatusBadge;
|
||||
|
||||
window.chartColors = {
|
||||
primary: 'rgb(59, 130, 246)',
|
||||
success: 'rgb(34, 197, 94)',
|
||||
@@ -59,10 +76,33 @@ window.tabs = {
|
||||
renderItem: (item) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'border-b hover:bg-gray-50 dark:hover:bg-gray-700';
|
||||
|
||||
let consensusInfo = '';
|
||||
if (item.consensusState) {
|
||||
const statusBadge = getConsensusStatusBadge(item.consensusStatus);
|
||||
consensusInfo = `<td class="p-3">${statusBadge}</td>`;
|
||||
} else if (item.consensusStatus === 'internal') {
|
||||
consensusInfo = `<td class="p-3">${getConsensusStatusBadge('internal')}</td>`;
|
||||
} else {
|
||||
consensusInfo = `<td class="p-3">${getConsensusStatusBadge('unknown')}</td>`;
|
||||
}
|
||||
|
||||
tr.innerHTML = `<td class="p-3">${item.domain}${item.isLocal ? '🏠' : ''}</td>
|
||||
<td class="p-3 break-all">${item.hash}</td>
|
||||
${consensusInfo}
|
||||
<td class="p-3">${item.isLocal && item.hash !== 'internal' ? `<button onclick="removeDomain('${item.domain}')" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Remove</button>` : ''}</td>`;
|
||||
return tr;
|
||||
},
|
||||
postFetch: (data) => {
|
||||
return data.map(item => {
|
||||
if (item.consensusStatus === 'internal' || item.hash === 'internal' || item.hash === 'none') {
|
||||
return { ...item, consensusStatus: 'internal' };
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
consensusStatus: item.consensusState?.status || 'unknown'
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
entries: {
|
||||
|
||||
@@ -148,8 +148,9 @@ function connectWebSocket() {
|
||||
window.updateMap[data.type]();
|
||||
} else {
|
||||
window.updateMap[data.type].forEach(tab => {
|
||||
if (window.activeTab === 'host' || window.activeTab === tab) {
|
||||
if (window.genericFetch) window.genericFetch(tab, true);
|
||||
// Always fetch data in background, but only render if tab is active
|
||||
if (window.genericFetch) {
|
||||
window.genericFetch(tab, window.activeTab === 'host' || window.activeTab === tab);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -288,26 +288,15 @@ function handleWebSocketUpdate(data) {
|
||||
window.utils.updateSidebarStats();
|
||||
}
|
||||
|
||||
// Notify current view of update (only if view is active)
|
||||
const activeView = document.querySelector('.view-content.active');
|
||||
if (activeView) {
|
||||
switch (currentView) {
|
||||
case 'overview':
|
||||
if (window.overviewView) {
|
||||
window.overviewView.handleUpdate(data);
|
||||
}
|
||||
break;
|
||||
case 'domain-list':
|
||||
if (window.domainListView) {
|
||||
window.domainListView.handleUpdate(data);
|
||||
}
|
||||
break;
|
||||
case 'domain-detail':
|
||||
if (window.domainDetailView) {
|
||||
window.domainDetailView.handleUpdate(data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Notify all views of update (they handle their own background refresh logic)
|
||||
if (window.overviewView) {
|
||||
window.overviewView.handleUpdate(data);
|
||||
}
|
||||
if (window.domainListView) {
|
||||
window.domainListView.handleUpdate(data);
|
||||
}
|
||||
if (window.domainDetailView) {
|
||||
window.domainDetailView.handleUpdate(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
# Read.it - Decentralized Reddit Clone
|
||||
|
||||
> ⚠️ **Note**: This plugin is currently in **active development**. Features may change, and you may encounter bugs while we work on stabilizing the P2P synchronization and real-time updates.
|
||||
|
||||
A fully-featured, decentralized Reddit-like platform built on the P2NS (Peer-to-Peer Name System) plugin architecture. Read.it provides all the community-driven content features you'd expect from a modern social platform, but with decentralized data storage and peer-to-peer communication.
|
||||
|
||||
## 🌟 Features Overview
|
||||
|
||||
### Core Content Features
|
||||
|
||||
#### Posts
|
||||
- **Multiple Post Types**: Text, Link, Image, and Poll posts
|
||||
- **Rich Media Embeds**: Automatic embedding for YouTube, Vimeo, SoundCloud, Spotify, Twitter/X, Twitch, and Imgur
|
||||
- **Markdown Support**: Full markdown rendering with syntax highlighting for code blocks
|
||||
- **Post Flairs**: Customizable post categorization
|
||||
- **Awards System**: Give and receive awards (Gold, Silver, Platinum, Helpful, Wholesome)
|
||||
- **Reactions**: Multiple reaction types (❤️ Like, 😂 Funny, 😮 Wow, 😢 Sad, 😡 Angry) with ability to see who reacted
|
||||
- **Crossposting**: Share posts across different communities
|
||||
- **Post Scheduling**: Schedule posts for future publication
|
||||
- **Draft System**: Auto-save and manual draft management
|
||||
|
||||
#### Comments
|
||||
- **Threaded Comments**: Nested comment threads with collapsible replies
|
||||
- **Markdown Support**: Full markdown in comments
|
||||
- **Voting**: Upvote/downvote system
|
||||
- **Editing & Deletion**: Authors can edit or delete their comments
|
||||
|
||||
#### Communities (Subreadits)
|
||||
- **Create Communities**: Anyone can create a new community
|
||||
- **Customization**: Description, rules, and display settings
|
||||
- **Subscription System**: Join/leave communities
|
||||
- **Member Counts**: Track subscriber numbers
|
||||
- **Wiki Pages**: Community-editable wiki documentation
|
||||
- **Community Events**: Schedule and manage community events
|
||||
- **Real-time Chat**: Per-community chat rooms
|
||||
|
||||
### User Features
|
||||
|
||||
#### Profiles
|
||||
- **Global Profile Integration**: Integrated with P2NS Global Profile SDK
|
||||
- **Custom Avatars**: User avatars with fallback to identicons
|
||||
- **Cover Photos**: Customizable profile cover images
|
||||
- **Pinned Posts**: Pin your best posts to your profile
|
||||
- **Custom Themes**: Per-user profile themes
|
||||
- **Karma System**: Post and comment karma tracking
|
||||
- **Badges & Trophies**: Achievement system with multiple tiers
|
||||
- **User Flairs**: Per-subreddit user flairs
|
||||
|
||||
#### Social Features
|
||||
- **Following**: Follow other users to see their content
|
||||
- **Friends System**: Mutual follows create friendships
|
||||
- **Blocking**: Block users you don't want to interact with
|
||||
- **Private Messaging**: Direct message other users
|
||||
- **Mentions**: @mention users in posts and comments
|
||||
- **Notifications**: Real-time notifications for replies, mentions, upvotes, and more
|
||||
|
||||
#### Content Management
|
||||
- **Saved Posts/Comments**: Bookmark content for later
|
||||
- **Read Later Queue**: Separate queue for posts to read later
|
||||
- **Bookmark Folders**: Organize saved items into folders
|
||||
- **Reading Progress**: Track which posts you've read
|
||||
- **Multi-Reddits**: Create custom feeds combining multiple communities
|
||||
|
||||
### Discovery & Navigation
|
||||
|
||||
#### Sorting Algorithms
|
||||
- **Hot**: Time-decay algorithm balancing recency and popularity
|
||||
- **New**: Chronological, newest first
|
||||
- **Top**: Highest voted (with time filters: hour, day, week, month, year, all)
|
||||
- **Controversial**: Posts with mixed voting patterns
|
||||
|
||||
#### Search
|
||||
- **Full-text Search**: Search posts and comments
|
||||
- **Subreddit Search**: Find communities by name
|
||||
- **User Search**: Find users by ID
|
||||
|
||||
#### Discovery
|
||||
- **Trending Topics**: Algorithm-detected trending keywords
|
||||
- **Related Posts**: Keyword-based post suggestions
|
||||
- **Community Recommendations**: Suggestions based on subscriptions
|
||||
- **Random Subreddit**: Discover random communities
|
||||
- **Leaderboards**: Top contributors globally and per-subreddit
|
||||
|
||||
### Moderation Tools
|
||||
|
||||
#### Basic Moderation
|
||||
- **Remove Posts/Comments**: Remove rule-breaking content
|
||||
- **Lock Posts**: Prevent new comments
|
||||
- **Pin Posts**: Sticky important posts
|
||||
- **Post Flairs**: Moderator-assigned flairs
|
||||
|
||||
#### Advanced Moderation
|
||||
- **Ban System**: Ban users from communities (temporary or permanent)
|
||||
- **Ban Appeals**: Users can appeal bans
|
||||
- **Mod Roles**: Granular permissions (can ban, can flair, can remove, etc.)
|
||||
- **Mod Mail**: Private messaging system for mod teams
|
||||
- **Mod Queue**: Centralized view of reported content
|
||||
- **Mod Activity Log**: Public transparency log of mod actions
|
||||
- **User Notes**: Private moderator notes on users
|
||||
|
||||
#### AutoMod
|
||||
- **Custom Rules**: Create automated moderation rules
|
||||
- **Keyword Filters**: Auto-remove content with specific keywords
|
||||
- **Domain Blacklists**: Block links from specific domains
|
||||
- **Regex Patterns**: Advanced pattern matching
|
||||
- **Spam Detection**: Automatic spam filtering
|
||||
|
||||
#### Community Moderation
|
||||
- **Report System**: Users can report content
|
||||
- **Crowd-sourced Moderation**: Community voting on reports
|
||||
- **Content Filters**: Keyword/regex/domain blacklists
|
||||
|
||||
### Super Admin
|
||||
|
||||
A special super admin system exists for platform-wide moderation:
|
||||
- **Hardcoded Public Key**: `a6a6d7ebcc1df8f33410067bf96ad2cc30d5276516f758bb0f34195e914c451f`
|
||||
- **Global Mod Powers**: Can moderate any community
|
||||
- **Platform Management**: Access to all moderation tools
|
||||
|
||||
### API & Integration
|
||||
|
||||
#### REST API
|
||||
- `GET /api/v1/r/:sub/posts` - Get posts from a subreddit
|
||||
- `GET /api/v1/post/:id` - Get a specific post
|
||||
- `GET /api/v1/r/:sub` - Get subreddit info
|
||||
- `GET /api/v1/user/:id` - Get user info
|
||||
|
||||
API authentication via `X-API-Key` header.
|
||||
|
||||
#### RSS Feeds
|
||||
- `/r/:sub/rss` - RSS feed for a subreddit
|
||||
- `/rss` or `/feed.xml` - Front page RSS feed
|
||||
|
||||
#### Webhooks
|
||||
- Configure webhooks to notify external services on events
|
||||
- Supports: new posts, new comments, reports
|
||||
|
||||
### PWA Support
|
||||
|
||||
Read.it is a Progressive Web App:
|
||||
- **Installable**: Install as a native-like app
|
||||
- **Offline Support**: Basic offline functionality via service worker
|
||||
- **Push Notifications**: (when supported by browser)
|
||||
|
||||
## 🛠 Technical Architecture
|
||||
|
||||
### Backend (`index.js`)
|
||||
|
||||
The backend is a P2NS plugin that handles:
|
||||
|
||||
#### WebSocket Message Handlers
|
||||
All real-time communication happens over WebSocket. Key message types:
|
||||
|
||||
**Subscriptions**
|
||||
- `subscribe` - Subscribe to a channel (feed, post, subreadits)
|
||||
- `unsubscribe` - Unsubscribe from a channel
|
||||
|
||||
**Posts**
|
||||
- `post:create` - Create a new post
|
||||
- `post:edit` - Edit an existing post
|
||||
- `post:delete` - Delete a post
|
||||
- `post:crosspost` - Crosspost to another community
|
||||
|
||||
**Comments**
|
||||
- `comment:create` - Create a comment
|
||||
- `comment:edit` - Edit a comment
|
||||
- `comment:delete` - Delete a comment
|
||||
|
||||
**Voting**
|
||||
- `vote` - Vote on a post or comment
|
||||
|
||||
**Communities**
|
||||
- `subreadit:create` - Create a community
|
||||
- `subreadit:subscribe` - Subscribe to a community
|
||||
|
||||
**Moderation**
|
||||
- `mod:removePost` - Remove a post
|
||||
- `mod:removeComment` - Remove a comment
|
||||
- `mod:lockPost` - Lock/unlock a post
|
||||
- `mod:pinPost` - Pin/unpin a post
|
||||
- `mod:ban` - Ban a user
|
||||
- `mod:unban` - Unban a user
|
||||
|
||||
**And many more...**
|
||||
|
||||
#### Database Collections (HyperDB)
|
||||
|
||||
| Collection | Description |
|
||||
|------------|-------------|
|
||||
| `@readit/posts` | All posts |
|
||||
| `@readit/comments` | All comments |
|
||||
| `@readit/subreadits` | Communities |
|
||||
| `@readit/votes` | User votes |
|
||||
| `@readit/saved` | Saved items |
|
||||
| `@readit/flairs` | Post flairs |
|
||||
| `@readit/notifications` | User notifications |
|
||||
| `@readit/bans` | User bans |
|
||||
| `@readit/modlogs` | Moderation logs |
|
||||
| `@readit/media` | Uploaded media |
|
||||
| `@readit/drafts` | Post drafts |
|
||||
| `@readit/multis` | Multi-reddit feeds |
|
||||
| `@readit/awards` | Given awards |
|
||||
| `@readit/userflairs` | User flairs |
|
||||
| `@readit/pollvotes` | Poll votes |
|
||||
| `@readit/wikis` | Wiki pages |
|
||||
| `@readit/messages` | Private messages |
|
||||
| `@readit/chatmessages` | Chat messages |
|
||||
| `@readit/follows` | User follows |
|
||||
| `@readit/blocks` | User blocks |
|
||||
| `@readit/reports` | Content reports |
|
||||
| `@readit/modmail` | Mod mail |
|
||||
| `@readit/automod` | AutoMod rules |
|
||||
| `@readit/badges` | User badges |
|
||||
| `@readit/events` | Community events |
|
||||
| `@readit/templates` | Post templates |
|
||||
| `@readit/reactions` | Post/comment reactions |
|
||||
| `@readit/readlater` | Read later queue |
|
||||
| `@readit/usernotes` | Moderator user notes |
|
||||
| `@readit/apikeys` | API keys |
|
||||
| `@readit/webhooks` | Webhook configurations |
|
||||
| `@readit/banappeals` | Ban appeals |
|
||||
| `@readit/modroles` | Moderator roles |
|
||||
| `@readit/contentfilters` | Content filters |
|
||||
| `@readit/reportvotes` | Community report votes |
|
||||
| `@readit/userprofiles` | Enhanced user profiles |
|
||||
| `@readit/trophies` | User trophies |
|
||||
|
||||
### Frontend
|
||||
|
||||
#### Technology Stack
|
||||
- **Alpine.js**: Reactive UI framework
|
||||
- **Tailwind CSS**: Utility-first CSS
|
||||
- **DaisyUI**: Component library
|
||||
- **marked.js**: Markdown rendering
|
||||
- **highlight.js**: Code syntax highlighting
|
||||
|
||||
#### Key Files
|
||||
- `www/index.html` - Main HTML with Alpine.js templates
|
||||
- `www/js/app.js` - Alpine.js application logic
|
||||
- `www/manifest.json` - PWA manifest
|
||||
- `www/sw.js` - Service worker
|
||||
|
||||
#### Client-Side Routing
|
||||
|
||||
The app uses client-side routing with `history.pushState`:
|
||||
|
||||
| Route | View |
|
||||
|-------|------|
|
||||
| `/` | Home feed |
|
||||
| `/r/:name` | Subreddit view |
|
||||
| `/r/:name/post/:id` | Post detail |
|
||||
| `/u/:id` | User profile |
|
||||
| `/submit` | Create post |
|
||||
| `/saved` | Saved items |
|
||||
| `/subscriptions` | Subscribed communities |
|
||||
| `/settings` | User settings |
|
||||
| `/inbox` | Private messages |
|
||||
| `/notifications` | Notifications |
|
||||
| `/drafts` | Post drafts |
|
||||
| `/feeds` | Multi-reddit management |
|
||||
| `/feeds/:name` | Multi-reddit view |
|
||||
| `/search` | Search results |
|
||||
| `/trending` | Trending topics |
|
||||
| `/events` | Community events |
|
||||
| `/modqueue` | Mod queue |
|
||||
| `/analytics` | Community analytics |
|
||||
| `/leaderboard` | Top contributors |
|
||||
|
||||
### Theming
|
||||
|
||||
The app supports light and dark modes via CSS variables:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--readit-dark: #1a1a1b;
|
||||
--readit-darker: #030303;
|
||||
--readit-border: #343536;
|
||||
--readit-hover: #2d2d2e;
|
||||
--readit-orange: #ff4500;
|
||||
--readit-blue: #0079d3;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--readit-dark: #ffffff;
|
||||
--readit-darker: #f6f7f8;
|
||||
--readit-border: #ccc;
|
||||
--readit-hover: #f0f0f0;
|
||||
}
|
||||
```
|
||||
|
||||
## 📁 File Structure
|
||||
|
||||
```
|
||||
plugin-sites/read.it/
|
||||
├── config.json # Plugin configuration & DB schemas
|
||||
├── index.js # Backend WebSocket & HTTP handlers
|
||||
├── README.md # This file
|
||||
└── www/
|
||||
├── index.html # Main HTML & Alpine.js templates
|
||||
├── manifest.json # PWA manifest
|
||||
├── sw.js # Service worker
|
||||
└── js/
|
||||
└── app.js # Alpine.js application logic
|
||||
```
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Prerequisites
|
||||
- P2NS server running
|
||||
- Plugin system enabled
|
||||
|
||||
### Installation
|
||||
|
||||
1. Place the `read.it` folder in your `plugin-sites` directory
|
||||
2. The plugin will be automatically loaded by P2NS
|
||||
3. Access via `https://read.it` (or your configured domain)
|
||||
|
||||
### Configuration
|
||||
|
||||
The `config.json` file defines:
|
||||
- Plugin metadata (name, version, description)
|
||||
- Database schemas and indexes
|
||||
- Collection definitions
|
||||
|
||||
## 🔐 Authentication
|
||||
|
||||
Read.it uses the P2NS authentication system:
|
||||
- Users authenticate via their P2NS identity
|
||||
- Auth tokens are passed with WebSocket messages
|
||||
- The `peerId` (public key) serves as the user identifier
|
||||
|
||||
## 🎨 UI Features
|
||||
|
||||
### Context Menu
|
||||
Right-click on posts to access:
|
||||
- Award
|
||||
- Crosspost
|
||||
- Read Later
|
||||
- Edit (author only)
|
||||
- Delete (author only)
|
||||
- Report
|
||||
- Mod actions (moderators only)
|
||||
|
||||
### Keyboard Shortcuts
|
||||
- `j` / `k` - Navigate posts
|
||||
- `a` / `z` - Upvote / Downvote
|
||||
- `Enter` - Open post
|
||||
- `Escape` - Close modals
|
||||
- `c` - Focus comment box
|
||||
- `s` - Save post
|
||||
- `/` - Focus search
|
||||
|
||||
### View Modes
|
||||
- Card view (default)
|
||||
- Compact view
|
||||
|
||||
## 📊 Algorithms
|
||||
|
||||
### Hot Score
|
||||
```javascript
|
||||
const age = (Date.now() - post.createdAt) / 3600000; // hours
|
||||
const score = post.upvotes - post.downvotes;
|
||||
const order = Math.log10(Math.max(Math.abs(score), 1));
|
||||
const sign = score > 0 ? 1 : score < 0 ? -1 : 0;
|
||||
const hotScore = sign * order - age / 12;
|
||||
```
|
||||
|
||||
### Controversial Score
|
||||
```javascript
|
||||
if (post.upvotes <= 0 || post.downvotes <= 0) return 0;
|
||||
const magnitude = post.upvotes + post.downvotes;
|
||||
const balance = post.downvotes / post.upvotes;
|
||||
return magnitude * Math.min(balance, 1 / balance);
|
||||
```
|
||||
|
||||
## 🔔 Notifications
|
||||
|
||||
Users receive notifications for:
|
||||
- Replies to their posts
|
||||
- Replies to their comments
|
||||
- Mentions (@username)
|
||||
- Upvote milestones (10, 50, 100, 500, 1000)
|
||||
- Awards received
|
||||
- New followers
|
||||
- Mod actions on their content
|
||||
|
||||
## 🏆 Gamification
|
||||
|
||||
### Badges
|
||||
Awarded automatically for milestones:
|
||||
- First Post
|
||||
- Prolific Poster (50+ posts)
|
||||
- Conversation Starter (100+ comments)
|
||||
- Popular (1000+ karma)
|
||||
- And more...
|
||||
|
||||
### Trophies
|
||||
Tiered achievements:
|
||||
- Bronze, Silver, Gold, Platinum tiers
|
||||
- Categories: Posts, Comments, Karma, Awards, etc.
|
||||
|
||||
### Leaderboards
|
||||
- Global top contributors
|
||||
- Per-subreddit top contributors
|
||||
- Metrics: karma, posts, comments, awards
|
||||
|
||||
## 🛡️ Security
|
||||
|
||||
- All mod actions require authentication
|
||||
- Super admin verified by public key
|
||||
- API endpoints require API key authentication
|
||||
- Content filters prevent malicious content
|
||||
- Spam detection on post/comment creation
|
||||
|
||||
## 📝 License
|
||||
|
||||
Part of the P2NS project. See main repository for license information.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions welcome! Key areas:
|
||||
- Bug fixes
|
||||
- New features
|
||||
- UI/UX improvements
|
||||
- Documentation
|
||||
- Performance optimization
|
||||
|
||||
---
|
||||
|
||||
Built with ❤️ on P2NS - The decentralized web awaits.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user