diff --git a/.env.example b/.env.example index e74110b..589050a 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,2 @@ -VITE_REOWN_SECRETVITE_REOWN_SECRET -# Mock/bypass settings for development -VITE_OPCHAN_MOCK_ORDINAL_CHECK=false \ No newline at end of file +VITE_REOWN_SECRET= +VITE_OPCHAN_MOCK_ORDINAL_CHECK= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5eed1ff..bd6adef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,7 @@ .cursorrules -comparison.md .giga/ -furps.md +furps-comparison.md README-task-master.md .cursor diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..2ca0d98 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,14 @@ +node_modules +dist +build +.next +coverage +*.min.js +*.min.css +package-lock.json +yarn.lock +.env +.env.local +.env.development.local +.env.test.local +.env.production.local diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..de21e74 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "tabWidth": 2, + "useTabs": false, + "printWidth": 80, + "bracketSpacing": true, + "arrowParens": "avoid" +} diff --git a/README.md b/README.md index 130ac02..668e124 100644 --- a/README.md +++ b/README.md @@ -13,22 +13,26 @@ A decentralized forum application built as a Proof of Concept for a Waku-powered ### Installation 1. **Clone the repository** + ```bash git clone https://github.com/waku-org/OpChan.git cd OpChan ``` 2. **Install dependencies** + ```bash npm install ``` 3. **Setup environment variables** + ```bash cp .env.example .env ``` - + Edit `.env` to configure development settings: + ```env # Set to 'true' to bypass verification in development VITE_OPCHAN_MOCK_ORDINAL_CHECK=false @@ -101,6 +105,7 @@ OpChan uses a two-tier authentication system: 7. Open a Pull Request ## TODOs + - [x] replace mock wallet connection/disconnection - supports Phantom - [x] replace mock Ordinal verification (API) @@ -119,7 +124,6 @@ OpChan implements a decentralized architecture with these key components: - **Content Addressing**: Messages are cryptographically signed and verifiable - **Moderation Layer**: Cell-based moderation without global censorship - ## Support For questions, issues, or contributions: @@ -131,4 +135,4 @@ For questions, issues, or contributions: --- -**Note**: This is a Proof of Concept implementation. Use at your own risk in production environments. \ No newline at end of file +**Note**: This is a Proof of Concept implementation. Use at your own risk in production environments. diff --git a/components.json b/components.json index f29e3f1..62e1011 100644 --- a/components.json +++ b/components.json @@ -17,4 +17,4 @@ "lib": "@/lib", "hooks": "@/hooks" } -} \ No newline at end of file +} diff --git a/eslint.config.js b/eslint.config.js index 9c6c22e..c5b5079 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,32 +1,35 @@ -import js from "@eslint/js"; -import globals from "globals"; -import reactHooks from "eslint-plugin-react-hooks"; -import reactRefresh from "eslint-plugin-react-refresh"; -import tseslint from "typescript-eslint"; +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; export default tseslint.config( - { ignores: ["dist"] }, + { ignores: ['dist'] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ["**/*.{ts,tsx}"], + files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { - "react-hooks": reactHooks, - "react-refresh": reactRefresh, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, - "react-refresh/only-export-components": [ - "warn", + 'react-refresh/only-export-components': [ + 'warn', { allowConstantExport: true }, ], - "@typescript-eslint/no-unused-vars": ["error", { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_" - }], + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], }, } ); diff --git a/furps-report.md b/furps-report.md new file mode 100644 index 0000000..799d57a --- /dev/null +++ b/furps-report.md @@ -0,0 +1,233 @@ +# Waku Forum FURPS Compliance Report + +**Generated:** December 2024 +**Codebase Analysis Date:** Current HEAD + +Legend: ✅ **Fully Implemented** | 🟡 **Partially Implemented** | ❌ **Not Implemented** | ❔ **Unclear/Ambiguous** + +--- + +## Executive Summary + +This report provides a comprehensive analysis of the OpChan codebase against the specified FURPS requirements. The application shows **strong implementation** of core forum functionality, authentication systems, and Waku network integration. Key strengths include a sophisticated relevance scoring system, comprehensive moderation capabilities, and effective key delegation for improved UX. Major gaps exist in anonymous user interactions, user identity features, and some usability enhancements. + +**Overall Compliance: 72% (26/36 requirements fully implemented)** + +--- + +## Functionality Requirements + +| # | Requirement | Status | Implementation Evidence | File References | +| ------ | ------------------------------------------------------------------------------------------ | :----: | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| **1** | Users can identify themselves by signing with their Bitcoin key | ✅ | Full Bitcoin wallet integration via ReOwnWalletService with signing capabilities | `src/lib/identity/wallets/ReOwnWalletService.ts:169-188` | +| **2** | Only users owning Logos ordinal or an ENS can create a cell | 🟡 | ENS ownership checks implemented, but ordinal verification bypassed in development mode | `src/lib/forum/actions.ts:180-187`, `src/lib/identity/ordinal.ts:13-20` | +| **3** | Any user (authenticated or not) can see the content; basic encryption functionality | ✅ | All content viewing routes accessible without authentication | `src/pages/*.tsx`, `src/components/PostList.tsx` | +| **4** | Existing cells can be listed | ✅ | Comprehensive cell listing with sorting and filtering | `src/components/CellList.tsx:35-120` | +| **5** | Cell can be created with a name, description, icon; icon size restricted; creator is admin | 🟡 | Form validation for URL exists but no size restriction enforcement | `src/components/CreateCellDialog.tsx:64-75` | +| **6** | Post can be created in a cell with title and body; text only | ✅ | Full post creation with text validation | `src/lib/forum/actions.ts:24-96` | +| **7** | Comments can be made on posts; text only | ✅ | Complete comment system with threading | `src/lib/forum/actions.ts:98-168` | +| **8** | Posts can be upvoted | ✅ | Comprehensive voting system with upvote/downvote tracking | `src/lib/forum/actions.ts:233-304` | +| **9** | Users can setup call sign; ordinal used as avatar | ❌ | No nickname/call sign fields in User interface; only truncated addresses displayed | `src/types/forum.ts:5-24` | +| **10** | Cell admin can mark posts and comments as moderated | ✅ | Full moderation system with reason tracking | `src/lib/forum/actions.ts:310-414` | +| **11** | Cell admin can mark users as moderated | ✅ | User-level moderation with cell-scoped restrictions | `src/lib/forum/actions.ts:416-459` | +| **12** | Users can identify themselves by signing with Web3 key | ✅ | Ethereum wallet support with ENS resolution via Wagmi | `src/lib/identity/wallets/ReOwnWalletService.ts:222-251` | +| **13** | Posts, comments, cells have relevance index for ordering/hiding | ✅ | Sophisticated RelevanceCalculator with multiple scoring factors | `src/lib/forum/relevance.ts:4-340` | +| **14** | Relevance lowered for moderated content/users | ✅ | Moderation penalty of 50% reduction applied | `src/lib/forum/relevance.ts:62-63` | +| **15** | Relevance increased for ENS/Ordinal owners | ✅ | 25% bonus for verified owners, 10% for basic verification | `src/lib/forum/relevance.ts:16-17` | +| **16** | Relevance increased for verified upvoters | ✅ | Verified upvote bonus of 0.1 per verified vote | `src/lib/forum/relevance.ts:47-51` | +| **17** | Relevance increased for verified commenters | ✅ | Verified commenter bonus of 0.05 per verified commenter | `src/lib/forum/relevance.ts:53-57` | +| **18** | Anonymous users can upvote, comment, and post | ❌ | All actions require authentication and verification checks | `src/lib/forum/actions.ts:34-41, 242-249` | + +**Functionality Score: 14/18 (78%)** + +--- + +## Usability Requirements + +| # | Requirement | Status | Implementation Evidence | File References | +| ------ | ----------------------------------------------- | :----: | ------------------------------------------------------------------------- | ----------------------------------------------------------- | +| **1** | Users can see all topics through all cells | ✅ | Feed page aggregates posts from all cells | `src/pages/FeedPage.tsx:24-27` | +| **2** | Users can see active members per cell | 🟡 | Post count displayed, but active member calculation not fully implemented | `src/components/CellList.tsx:31-33` | +| **3** | Users can bookmark posts/topics (local only) | ❌ | No bookmarking functionality found in interfaces or components | _Not found_ | +| **4** | Users can sort topics by new or top | ✅ | Sorting controls with relevance and time options implemented | `src/pages/FeedPage.tsx:97-115`, `src/lib/forum/sorting.ts` | +| **5** | Ordinal picture and custom nickname for user ID | 🟡 | CypherImage generates avatars, but no nickname system | `src/components/ui/CypherImage.tsx` | +| **6** | Moderated content hidden from users | ✅ | Filtering logic hides moderated posts/comments from non-admins | `src/components/PostList.tsx:114-118` | +| **7** | Users don't need to sign every message | ✅ | Key delegation system with configurable duration | `src/lib/services/CryptoService.ts:250-274` | +| **8** | Only browser needed (no additional software) | ✅ | Web-based with optional wallet integration | _Architecture_ | +| **9** | Prototype UI for dogfooding | ✅ | Complete React UI with shadcn/ui components | `src/components/**` | +| **10** | Library with clear API for developers | ❌ | Internal services exist but no packaged library or external API | `src/lib/index.ts:8-249` | +| **11** | ENS holders can use ENS for display | ✅ | ENS names resolved and displayed throughout UI | `src/lib/identity/wallets/ReOwnWalletService.ts:232-236` | +| **12** | Relevance index used for content ranking | ✅ | Relevance-based sorting implemented as default option | `src/pages/FeedPage.tsx:21-27` | + +**Usability Score: 8/12 (67%)** + +--- + +## Reliability Requirements + +| Requirement | Status | Implementation Evidence | File References | +| ------------------------------------------------ | :----: | ---------------------------------------------------------------------------------------------- | -------------------------------------------- | +| **Data is ephemeral; will disappear after time** | ✅ | Waku network inherently ephemeral; no permanent storage attempted | `src/lib/waku/core/WakuNodeManager.ts` | +| **End-to-end reliability for missing messages** | 🟡 | Basic health monitoring and reconnection logic, but no comprehensive missing message detection | `src/lib/waku/core/WakuNodeManager.ts:25-45` | + +**Reliability Score: 1.5/2 (75%)** + +--- + +## Performance Requirements + +**No specific requirements defined** ✅ + +--- + +## Supportability Requirements + +| Requirement | Status | Implementation Evidence | File References | +| -------------------------------------------- | :----: | --------------------------------------------------------------- | ---------------------------------- | +| **Web app; wallets optional** | ✅ | Read-only functionality without wallet connection | `src/pages/Index.tsx` | +| **Centralized API for Bitcoin ordinal info** | ✅ | OrdinalAPI class queries Logos dashboard API | `src/lib/identity/ordinal.ts:3-46` | +| **Uses Waku Network** | ✅ | Complete Waku integration with LightNode and reliable messaging | `src/lib/waku/core/*.ts` | + +**Supportability Score: 3/3 (100%)** + +--- + +## Privacy, Anonymity, Deployments + +| Requirement | Status | Implementation Evidence | File References | +| --------------------------- | :----: | -------------------------------------------------- | ---------------------------------- | +| **Centralized ordinal API** | ✅ | Implemented with Logos dashboard integration | `src/lib/identity/ordinal.ts:3-46` | +| **Uses Waku Network** | ✅ | Decentralized messaging infrastructure implemented | `src/lib/waku/**` | + +**Privacy/Anonymity Score: 2/2 (100%)** + +--- + +## Key Implementation Strengths + +### 🎯 **Sophisticated Relevance System** + +- **Full scoring algorithm** with base scores, engagement metrics, verification bonuses +- **Time decay function** and moderation penalties properly implemented +- **Comprehensive test coverage** for relevance calculations +- **Integration** with sorting and UI display systems + +### 🔐 **Robust Authentication Architecture** + +- **Multi-wallet support** (Bitcoin via ReOwnWalletService, Ethereum via Wagmi) +- **Key delegation system** reducing wallet interaction friction +- **ENS integration** with proper resolution and display +- **Verification tiers** (unverified, basic, owner) properly implemented + +### 🛡️ **Complete Moderation System** + +- **Cell-level admin controls** for posts, comments, and users +- **Reason tracking** and timestamp recording +- **Visibility filtering** based on user roles +- **Integration** with relevance scoring for penalties + +### 📡 **Solid Waku Integration** + +- **Reliable messaging** with status callbacks +- **Health monitoring** and reconnection logic +- **Message caching** and transformation pipeline +- **Multi-channel support** for different message types + +--- + +## Critical Implementation Gaps + +### ❌ **Anonymous User Support (Requirement #18)** + +**Impact:** High - Violates key accessibility requirement +**Current:** All actions require authentication (`isAuthenticated` checks) +**Files:** `src/lib/forum/actions.ts:34-41, 107-114, 242-249` +**Fix Required:** Remove authentication requirements for voting, posting, commenting + +### ❌ **User Identity System (Requirement #9)** + +**Impact:** Medium - Core user experience feature missing +**Current:** No nickname/call sign fields in User interface +**Files:** `src/types/forum.ts:5-24` +**Fix Required:** Add nickname field and call sign setup UI + +### ❌ **Bookmarking System (Requirement #21)** + +**Impact:** Medium - Important usability feature +**Current:** No local storage or bookmark functionality found +**Files:** None found +**Fix Required:** Implement local bookmark storage and UI + +### ❌ **Developer Library (Requirement #28)** + +**Impact:** Low - External integration capability +**Current:** Internal services only, no packaged library +**Files:** `src/lib/index.ts` exists but not exported as library +**Fix Required:** Create npm package with clear API documentation + +--- + +## Technical Architecture Assessment + +### **Message Flow & State Management** ⭐⭐⭐⭐⭐ + +- Clean separation between Waku messaging and forum domain logic +- Proper message transformation pipeline with verification +- Effective caching strategy with optimistic updates + +### **Security & Cryptography** ⭐⭐⭐⭐⭐ + +- Proper message signing with delegation support +- Secure key management with expiration +- Protection against signature replay attacks + +### **Error Handling & UX** ⭐⭐⭐⭐ + +- Comprehensive error messages with user-friendly descriptions +- Loading states and network status indicators +- Toast notifications for user feedback + +### **Code Quality & Organization** ⭐⭐⭐⭐ + +- Well-structured TypeScript with proper type definitions +- Clear separation of concerns across modules +- Good test coverage for critical functions + +--- + +## Priority Recommendations + +### **P0 - Critical (Next Sprint)** + +1. **Enable Anonymous Interactions** - Remove authentication requirements for basic actions +2. **Fix Ordinal Verification** - Implement real ordinal ownership checks for cell creation +3. **Add Missing User Identity** - Implement nickname/call sign system + +### **P1 - High (Next Month)** + +4. **Implement Bookmarking** - Add local bookmark functionality +5. **Enhance Active Member Tracking** - Calculate and display real active member counts +6. **Complete Icon Size Validation** - Add proper size restrictions for cell icons + +### **P2 - Medium (Future Releases)** + +7. **Create Developer Library** - Package services as distributable npm module +8. **Enhance Reliability** - Implement comprehensive missing message detection +9. **Add Advanced Sorting** - Extend sorting options beyond time/relevance + +--- + +## Compliance Summary + +| Category | Score | Status | +| --------------------- | :---------: | ---------------------------------------------- | +| **Functionality** | 14/18 (78%) | 🟡 Strong core, missing anonymous access | +| **Usability** | 8/12 (67%) | 🟡 Good UX foundation, needs identity features | +| **Reliability** | 1.5/2 (75%) | 🟡 Basic reliability, can be enhanced | +| **Performance** | N/A | ✅ No requirements specified | +| **Supportability** | 3/3 (100%) | ✅ Full compliance | +| **Privacy/Anonymity** | 2/2 (100%) | ✅ Full compliance | + +**🎯 Overall FURPS Compliance: 72% (26/36 requirements fully implemented)** + +--- diff --git a/index.html b/index.html index c279823..0705ead 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + @@ -10,11 +10,17 @@ - + - + diff --git a/package-lock.json b/package-lock.json index a7566ac..89fb08f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -87,6 +87,7 @@ "jsdom": "^26.1.0", "lovable-tagger": "^1.1.7", "postcss": "^8.4.47", + "prettier": "^3.6.2", "tailwindcss": "^3.4.11", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", @@ -13664,6 +13665,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", diff --git a/package.json b/package.json index 94d5eef..188ddcf 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "dev": "vite", "build": "vite build", "build:dev": "vite build --mode development", - "check": "tsc --noEmit --strict && eslint . --fix", - "lint": "eslint .", + "check": "tsc --project tsconfig.app.json --noEmit && tsc --project tsconfig.node.json --noEmit && eslint . --fix && prettier --write .", + "fix": "prettier --write . && eslint . --fix", "preview": "vite preview", "test": "vitest", "test:ui": "vitest --ui" @@ -93,6 +93,7 @@ "jsdom": "^26.1.0", "lovable-tagger": "^1.1.7", "postcss": "^8.4.47", + "prettier": "^3.6.2", "tailwindcss": "^3.4.11", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", diff --git a/postcss.config.js b/postcss.config.js index 2e7af2b..2aa7205 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/src/App.tsx b/src/App.tsx index 2070ab8..e9205c1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,34 +4,33 @@ * Reference: * https://www.notion.so/Logos-Forum-PoC-Waku-Powered-Opchan-1968f96fb65c8078b343c43429d66d0a#1968f96fb65c8025a929c2c9255a57c4 * Also note that for UX purposes, **we should not ask a user to sign with their Bitcoin wallet for every action.** - * + * * Instead, a key delegation system should be developed. - * + * * - User sign an in-browser key with their wallet and broadcast it * - Browser uses in-browser key to sign messages moving forward */ -import { Toaster } from "@/components/ui/toaster"; -import { Toaster as Sonner } from "@/components/ui/sonner"; -import { TooltipProvider } from "@/components/ui/tooltip"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; -import { AuthProvider } from "@/contexts/AuthContext"; -import { ForumProvider } from "@/contexts/ForumContext"; -import CellPage from "./pages/CellPage"; -import PostPage from "./pages/PostPage"; -import NotFound from "./pages/NotFound"; -import Dashboard from "./pages/Dashboard"; -import Index from "./pages/Index"; -import { appkitConfig } from "./lib/identity/wallets/appkit"; -import { WagmiProvider } from "wagmi"; -import { config } from "./lib/identity/wallets/appkit"; -import { AppKitProvider } from "@reown/appkit/react"; +import { Toaster } from '@/components/ui/toaster'; +import { Toaster as Sonner } from '@/components/ui/sonner'; +import { TooltipProvider } from '@/components/ui/tooltip'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import { AuthProvider } from '@/contexts/AuthContext'; +import { ForumProvider } from '@/contexts/ForumContext'; +import CellPage from './pages/CellPage'; +import PostPage from './pages/PostPage'; +import NotFound from './pages/NotFound'; +import Dashboard from './pages/Dashboard'; +import Index from './pages/Index'; +import { appkitConfig } from './lib/identity/wallets/appkit'; +import { WagmiProvider } from 'wagmi'; +import { config } from './lib/identity/wallets/appkit'; +import { AppKitProvider } from '@reown/appkit/react'; // Create a client const queryClient = new QueryClient(); - const App = () => ( diff --git a/src/components/ActivityFeed.tsx b/src/components/ActivityFeed.tsx index ab1d03f..ec7ed6e 100644 --- a/src/components/ActivityFeed.tsx +++ b/src/components/ActivityFeed.tsx @@ -11,8 +11,8 @@ interface FeedItemBase { type: 'post' | 'comment'; timestamp: number; ownerAddress: string; - cellId?: string; - postId?: string; + cellId?: string; + postId?: string; } interface PostFeedItem extends FeedItemBase { @@ -20,70 +20,90 @@ interface PostFeedItem extends FeedItemBase { title: string; cellId: string; postId: string; - commentCount: number; - voteCount: number; + commentCount: number; + voteCount: number; } interface CommentFeedItem extends FeedItemBase { type: 'comment'; content: string; postId: string; - voteCount: number; + voteCount: number; } type FeedItem = PostFeedItem | CommentFeedItem; const ActivityFeed: React.FC = () => { - const { posts, comments, getCellById, isInitialLoading, userVerificationStatus } = useForum(); + const { + posts, + comments, + getCellById, + isInitialLoading, + userVerificationStatus, + } = useForum(); const combinedFeed: FeedItem[] = [ - ...posts.map((post): PostFeedItem => ({ - id: post.id, - type: 'post', - timestamp: post.timestamp, - ownerAddress: post.authorAddress, - title: post.title, - cellId: post.cellId, - postId: post.id, - commentCount: 0, - voteCount: post.upvotes.length - post.downvotes.length, - })), - ...comments.map((comment): CommentFeedItem | null => { - const parentPost = posts.find(p => p.id === comment.postId); - if (!parentPost) return null; - return { - id: comment.id, - type: 'comment', - timestamp: comment.timestamp, - ownerAddress: comment.authorAddress, - content: comment.content, - postId: comment.postId, - cellId: parentPost.cellId, - voteCount: comment.upvotes.length - comment.downvotes.length, - }; - }) - .filter((item): item is CommentFeedItem => item !== null), - ].sort((a, b) => b.timestamp - a.timestamp); + ...posts.map( + (post): PostFeedItem => ({ + id: post.id, + type: 'post', + timestamp: post.timestamp, + ownerAddress: post.authorAddress, + title: post.title, + cellId: post.cellId, + postId: post.id, + commentCount: 0, + voteCount: post.upvotes.length - post.downvotes.length, + }) + ), + ...comments + .map((comment): CommentFeedItem | null => { + const parentPost = posts.find(p => p.id === comment.postId); + if (!parentPost) return null; + return { + id: comment.id, + type: 'comment', + timestamp: comment.timestamp, + ownerAddress: comment.authorAddress, + content: comment.content, + postId: comment.postId, + cellId: parentPost.cellId, + voteCount: comment.upvotes.length - comment.downvotes.length, + }; + }) + .filter((item): item is CommentFeedItem => item !== null), + ].sort((a, b) => b.timestamp - a.timestamp); const renderFeedItem = (item: FeedItem) => { const cell = item.cellId ? getCellById(item.cellId) : undefined; - const timeAgo = formatDistanceToNow(new Date(item.timestamp), { addSuffix: true }); + const timeAgo = formatDistanceToNow(new Date(item.timestamp), { + addSuffix: true, + }); - const linkTarget = item.type === 'post' ? `/post/${item.postId}` : `/post/${item.postId}#comment-${item.id}`; + const linkTarget = + item.type === 'post' + ? `/post/${item.postId}` + : `/post/${item.postId}#comment-${item.id}`; return ( -
- {item.type === 'post' ? : } + {item.type === 'post' ? ( + + ) : ( + + )} - {item.type === 'post' ? item.title : `Comment on: ${posts.find(p => p.id === item.postId)?.title || 'post'}`} + {item.type === 'post' + ? item.title + : `Comment on: ${posts.find(p => p.id === item.postId)?.title || 'post'}`} - by - { {cell && ( <> in - /{cell.name} + + /{cell.name} + )} {timeAgo}
{item.type === 'comment' && ( -

+

{item.content}

)} @@ -109,7 +131,9 @@ const ActivityFeed: React.FC = () => { if (isInitialLoading) { return (
-

Latest Activity

+

+ Latest Activity +

{[...Array(5)].map((_, i) => (
@@ -122,9 +146,13 @@ const ActivityFeed: React.FC = () => { return (
-

Latest Activity

+

+ Latest Activity +

{combinedFeed.length === 0 ? ( -

No activity yet. Be the first to post!

+

+ No activity yet. Be the first to post! +

) : ( combinedFeed.map(renderFeedItem) )} @@ -132,4 +160,4 @@ const ActivityFeed: React.FC = () => { ); }; -export default ActivityFeed; \ No newline at end of file +export default ActivityFeed; diff --git a/src/components/CellList.tsx b/src/components/CellList.tsx index 85f46ec..cc545c3 100644 --- a/src/components/CellList.tsx +++ b/src/components/CellList.tsx @@ -1,16 +1,30 @@ -import React, { useState, useMemo } from 'react'; +import { useState, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { useForum } from '@/contexts/useForum'; -import { Layout, MessageSquare, RefreshCw, Loader2, TrendingUp, Clock } from 'lucide-react'; +import { + Layout, + MessageSquare, + RefreshCw, + Loader2, + TrendingUp, + Clock, +} from 'lucide-react'; import { CreateCellDialog } from './CreateCellDialog'; import { Button } from '@/components/ui/button'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { CypherImage } from './ui/CypherImage'; import { RelevanceIndicator } from './ui/relevance-indicator'; import { sortCells, SortOption } from '@/lib/forum/sorting'; const CellList = () => { - const { cells, isInitialLoading, posts, refreshData, isRefreshing } = useForum(); + const { cells, isInitialLoading, posts, refreshData, isRefreshing } = + useForum(); const [sortOption, setSortOption] = useState('relevance'); // Apply sorting to cells @@ -22,8 +36,12 @@ const CellList = () => { return (
-

Loading Cells...

-

Connecting to the network and fetching data...

+

+ Loading Cells... +

+

+ Connecting to the network and fetching data... +

); } @@ -40,40 +58,45 @@ const CellList = () => {

Cells

- setSortOption(value)} + > - - -
- - Relevance -
-
- -
- - Newest -
-
-
+ + +
+ + Relevance +
+
+ +
+ + Newest +
+
+
- -
- +
{cells.length === 0 ? (
@@ -82,26 +105,34 @@ const CellList = () => {
) : ( - sortedCells.map((cell) => ( - + sortedCells.map(cell => ( +
-
-

{cell.name}

-

{cell.description}

+

+ {cell.name} +

+

+ {cell.description} +

{getPostCount(cell.id)} threads
{cell.relevanceScore !== undefined && ( - ({ - cn: (...classes: (string | undefined | null)[]) => classes.filter(Boolean).join(' ') -})) + cn: (...classes: (string | undefined | null)[]) => + classes.filter(Boolean).join(' '), +})); describe('Create Cell Without Icon - CypherImage Fallback', () => { it('shows fallback identicon when src is empty (simulating cell without icon)', () => { render( - - ) + ); // Verify that the fallback identicon is rendered instead of an img tag - const identicon = screen.getByTitle('Test Cell') - expect(identicon).toBeInTheDocument() - + const identicon = screen.getByTitle('Test Cell'); + expect(identicon).toBeInTheDocument(); + // Check for the fallback identicon characteristics - expect(identicon).toHaveClass('flex', 'items-center', 'justify-center') - + expect(identicon).toHaveClass('flex', 'items-center', 'justify-center'); + // The fallback should contain the first letter of the alt text (cell name) - const firstLetter = screen.getByText('T') // First letter of "Test Cell" - expect(firstLetter).toBeInTheDocument() - expect(firstLetter).toHaveClass('font-bold') - + const firstLetter = screen.getByText('T'); // First letter of "Test Cell" + expect(firstLetter).toBeInTheDocument(); + expect(firstLetter).toHaveClass('font-bold'); + // Should not render an img element when src is empty - const imgElement = screen.queryByRole('img') - expect(imgElement).not.toBeInTheDocument() - }) + const imgElement = screen.queryByRole('img'); + expect(imgElement).not.toBeInTheDocument(); + }); it('shows fallback identicon when src is undefined (simulating cell without icon)', () => { render( - - ) + ); // Verify that the fallback identicon is rendered - const identicon = screen.getByTitle('Another Cell') - expect(identicon).toBeInTheDocument() - + const identicon = screen.getByTitle('Another Cell'); + expect(identicon).toBeInTheDocument(); + // The fallback should contain the first letter of the alt text - const firstLetter = screen.getByText('A') // First letter of "Another Cell" - expect(firstLetter).toBeInTheDocument() - + const firstLetter = screen.getByText('A'); // First letter of "Another Cell" + expect(firstLetter).toBeInTheDocument(); + // Should not render an img element when src is undefined - const imgElement = screen.queryByRole('img') - expect(imgElement).not.toBeInTheDocument() - }) + const imgElement = screen.queryByRole('img'); + expect(imgElement).not.toBeInTheDocument(); + }); it('shows fallback identicon with correct cyberpunk styling', () => { render( - - ) + ); + + const identicon = screen.getByTitle('Cyberpunk Cell'); - const identicon = screen.getByTitle('Cyberpunk Cell') - // Check for cyberpunk styling elements - expect(identicon).toHaveStyle({ backgroundColor: '#0a1119' }) - + expect(identicon).toHaveStyle({ backgroundColor: '#0a1119' }); + // Check that the first letter is rendered with appropriate styling - const firstLetter = screen.getByText('C') - expect(firstLetter).toHaveClass('relative', 'font-bold', 'cyberpunk-glow', 'z-10') - }) + const firstLetter = screen.getByText('C'); + expect(firstLetter).toHaveClass( + 'relative', + 'font-bold', + 'cyberpunk-glow', + 'z-10' + ); + }); it('renders normal img when src is provided (control test)', () => { render( - - ) + ); // Should render an img element when src is provided - const imgElement = screen.getByRole('img') - expect(imgElement).toBeInTheDocument() - expect(imgElement).toHaveAttribute('src', 'https://example.com/valid-image.jpg') - expect(imgElement).toHaveAttribute('alt', 'Valid Image Cell') - + const imgElement = screen.getByRole('img'); + expect(imgElement).toBeInTheDocument(); + expect(imgElement).toHaveAttribute( + 'src', + 'https://example.com/valid-image.jpg' + ); + expect(imgElement).toHaveAttribute('alt', 'Valid Image Cell'); + // Should not show fallback identicon when image src is provided - const identicon = screen.queryByTitle('Valid Image Cell') - expect(identicon).not.toBeInTheDocument() - }) + const identicon = screen.queryByTitle('Valid Image Cell'); + expect(identicon).not.toBeInTheDocument(); + }); it('generates unique fallbacks for different cell names', () => { const { rerender } = render( - - ) + + ); - const alphaLetter = screen.getByText('A') - expect(alphaLetter).toBeInTheDocument() + const alphaLetter = screen.getByText('A'); + expect(alphaLetter).toBeInTheDocument(); rerender( - - ) + + ); + + const betaLetter = screen.getByText('B'); + expect(betaLetter).toBeInTheDocument(); - const betaLetter = screen.getByText('B') - expect(betaLetter).toBeInTheDocument() - // Alpha should no longer be present - expect(screen.queryByText('A')).not.toBeInTheDocument() - }) -}) \ No newline at end of file + expect(screen.queryByText('A')).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/CreateCellDialog.tsx b/src/components/CreateCellDialog.tsx index a61c741..429e895 100644 --- a/src/components/CreateCellDialog.tsx +++ b/src/components/CreateCellDialog.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { Loader2 } from "lucide-react"; -import { useForum } from "@/contexts/useForum"; -import { useAuth } from "@/contexts/useAuth"; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { Loader2 } from 'lucide-react'; +import { useForum } from '@/contexts/useForum'; +import { useAuth } from '@/contexts/useAuth'; import { Form, FormControl, @@ -12,28 +12,34 @@ import { FormItem, FormLabel, FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { Button } from "@/components/ui/button"; +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, -} from "@/components/ui/dialog"; -import { useToast } from "@/hooks/use-toast"; -import { urlLoads } from "@/lib/utils/urlLoads"; +} from '@/components/ui/dialog'; +import { useToast } from '@/hooks/use-toast'; +import { urlLoads } from '@/lib/utils/urlLoads'; const formSchema = z.object({ - title: z.string().min(3, "Title must be at least 3 characters").max(50, "Title must be less than 50 characters"), - description: z.string().min(10, "Description must be at least 10 characters").max(200, "Description must be less than 200 characters"), + title: z + .string() + .min(3, 'Title must be at least 3 characters') + .max(50, 'Title must be less than 50 characters'), + description: z + .string() + .min(10, 'Description must be at least 10 characters') + .max(200, 'Description must be less than 200 characters'), icon: z .string() .optional() - .refine((val) => !val || val.length === 0 || URL.canParse(val), { - message: "Must be a valid URL" + .refine(val => !val || val.length === 0 || URL.canParse(val), { + message: 'Must be a valid URL', }), }); @@ -42,20 +48,23 @@ interface CreateCellDialogProps { onOpenChange?: (open: boolean) => void; } -export function CreateCellDialog({ open: externalOpen, onOpenChange }: CreateCellDialogProps = {}) { +export function CreateCellDialog({ + open: externalOpen, + onOpenChange, +}: CreateCellDialogProps = {}) { const { createCell, isPostingCell } = useForum(); const { isAuthenticated } = useAuth(); const { toast } = useToast(); const [internalOpen, setInternalOpen] = React.useState(false); - + const open = externalOpen ?? internalOpen; const setOpen = onOpenChange ?? setInternalOpen; - + const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - title: "", - description: "", + title: '', + description: '', icon: undefined, }, }); @@ -66,15 +75,20 @@ export function CreateCellDialog({ open: externalOpen, onOpenChange }: CreateCel const ok = await urlLoads(values.icon, 5000); if (!ok) { toast({ - title: "Icon URL Error", - description: "Icon URL could not be loaded. Please check the URL and try again.", - variant: "destructive", + title: 'Icon URL Error', + description: + 'Icon URL could not be loaded. Please check the URL and try again.', + variant: 'destructive', }); return; } } - const cell = await createCell(values.title, values.description, values.icon || undefined); + const cell = await createCell( + values.title, + values.description, + values.icon || undefined + ); if (cell) { setOpen(false); form.reset(); @@ -87,7 +101,9 @@ export function CreateCellDialog({ open: externalOpen, onOpenChange }: CreateCel {!onOpenChange && ( - + )} @@ -103,7 +119,11 @@ export function CreateCellDialog({ open: externalOpen, onOpenChange }: CreateCel Title - + @@ -116,7 +136,7 @@ export function CreateCellDialog({ open: externalOpen, onOpenChange }: CreateCel Description -