mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-05 06:13:11 +00:00
feat: admin can view moderated content
This commit is contained in:
parent
cad1dcb5b4
commit
860b6a138f
27
src/App.tsx
27
src/App.tsx
@ -18,6 +18,7 @@ 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 { ModerationProvider } from '@/contexts/ModerationContext';
|
||||
import CellPage from './pages/CellPage';
|
||||
import PostPage from './pages/PostPage';
|
||||
import NotFound from './pages/NotFound';
|
||||
@ -39,18 +40,20 @@ const App = () => (
|
||||
<Router>
|
||||
<AuthProvider>
|
||||
<ForumProvider>
|
||||
<TooltipProvider>
|
||||
<Toaster />
|
||||
<Sonner />
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/cells" element={<Index />} />
|
||||
<Route path="/cell/:cellId" element={<CellPage />} />
|
||||
<Route path="/post/:postId" element={<PostPage />} />
|
||||
<Route path="/profile" element={<ProfilePage />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</TooltipProvider>
|
||||
<ModerationProvider>
|
||||
<TooltipProvider>
|
||||
<Toaster />
|
||||
<Sonner />
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/cells" element={<Index />} />
|
||||
<Route path="/cell/:cellId" element={<CellPage />} />
|
||||
<Route path="/post/:postId" element={<PostPage />} />
|
||||
<Route path="/profile" element={<ProfilePage />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</TooltipProvider>
|
||||
</ModerationProvider>
|
||||
</ForumProvider>
|
||||
</AuthProvider>
|
||||
</Router>
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { CypherImage } from './ui/CypherImage';
|
||||
import { RelevanceIndicator } from './ui/relevance-indicator';
|
||||
import { ModerationToggle } from './ui/moderation-toggle';
|
||||
import { sortCells, SortOption } from '@/lib/utils/sorting';
|
||||
import { Cell } from '@/types/forum';
|
||||
import { usePending } from '@/hooks/usePending';
|
||||
@ -121,6 +122,8 @@ const CellList = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<ModerationToggle />
|
||||
|
||||
<Select
|
||||
value={sortOption}
|
||||
onValueChange={(value: SortOption) => setSortOption(value)}
|
||||
|
||||
@ -4,7 +4,7 @@ import { TrendingUp, Users, Eye } from 'lucide-react';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useForumData, useForumSelectors, useAuth } from '@/hooks';
|
||||
import { useForumData, useAuth } from '@/hooks';
|
||||
import { EVerificationStatus } from '@/types/identity';
|
||||
import { CypherImage } from '@/components/ui/CypherImage';
|
||||
import { useUserDisplay } from '@/hooks';
|
||||
@ -12,7 +12,6 @@ import { useUserDisplay } from '@/hooks';
|
||||
const FeedSidebar: React.FC = () => {
|
||||
// ✅ Use reactive hooks for data
|
||||
const forumData = useForumData();
|
||||
const selectors = useForumSelectors(forumData);
|
||||
const { currentUser, verificationStatus } = useAuth();
|
||||
|
||||
// Get user display information using the hook
|
||||
@ -20,11 +19,29 @@ const FeedSidebar: React.FC = () => {
|
||||
currentUser?.address || ''
|
||||
);
|
||||
|
||||
// ✅ Get pre-computed stats and trending data from selectors
|
||||
const stats = selectors.selectStats();
|
||||
// Use cellsWithStats from forumData to get post counts
|
||||
const { cellsWithStats } = forumData;
|
||||
const trendingCells = cellsWithStats
|
||||
// ✅ Get stats from filtered data
|
||||
const {
|
||||
filteredPosts,
|
||||
filteredComments,
|
||||
filteredCellsWithStats,
|
||||
cells,
|
||||
userVerificationStatus,
|
||||
} = forumData;
|
||||
|
||||
const stats = {
|
||||
totalCells: cells.length,
|
||||
totalPosts: filteredPosts.length,
|
||||
totalComments: filteredComments.length,
|
||||
totalUsers: new Set([
|
||||
...filteredPosts.map(post => post.author),
|
||||
...filteredComments.map(comment => comment.author),
|
||||
]).size,
|
||||
verifiedUsers: Object.values(userVerificationStatus).filter(
|
||||
status => status.isVerified
|
||||
).length,
|
||||
};
|
||||
// Use filtered cells with stats for trending cells
|
||||
const trendingCells = filteredCellsWithStats
|
||||
.sort((a, b) => b.recentActivity - a.recentActivity)
|
||||
.slice(0, 5);
|
||||
|
||||
|
||||
@ -51,7 +51,7 @@ const PostDetail = () => {
|
||||
|
||||
// ✅ Use reactive hooks for data and actions
|
||||
const post = usePost(postId);
|
||||
const comments = usePostComments(postId, { includeModerated: false });
|
||||
const comments = usePostComments(postId);
|
||||
const {
|
||||
createComment,
|
||||
votePost,
|
||||
|
||||
46
src/components/ui/moderation-toggle.tsx
Normal file
46
src/components/ui/moderation-toggle.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
import { useModeration } from '@/contexts/ModerationContext';
|
||||
import { usePermissions } from '@/hooks/core/usePermissions';
|
||||
import { useForumData } from '@/hooks/core/useForumData';
|
||||
|
||||
export function ModerationToggle() {
|
||||
const { showModerated, toggleShowModerated } = useModeration();
|
||||
const { canModerate } = usePermissions();
|
||||
const { cellsWithStats } = useForumData();
|
||||
|
||||
// Check if user is admin of any cell
|
||||
const isAdminOfAnyCell = cellsWithStats.some(cell => canModerate(cell.id));
|
||||
|
||||
// Only show the toggle if user is admin of at least one cell
|
||||
if (!isAdminOfAnyCell) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="show-moderated"
|
||||
checked={showModerated}
|
||||
onCheckedChange={toggleShowModerated}
|
||||
className="data-[state=checked]:bg-cyber-accent"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="show-moderated"
|
||||
className="flex items-center gap-2 text-sm cursor-pointer"
|
||||
>
|
||||
{showModerated ? (
|
||||
<Eye className="w-4 h-4 text-cyber-accent" />
|
||||
) : (
|
||||
<EyeOff className="w-4 h-4 text-cyber-neutral" />
|
||||
)}
|
||||
<span
|
||||
className={showModerated ? 'text-cyber-accent' : 'text-cyber-neutral'}
|
||||
>
|
||||
Show Moderated
|
||||
</span>
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
83
src/contexts/ModerationContext.tsx
Normal file
83
src/contexts/ModerationContext.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { localDatabase } from '@/lib/database/LocalDatabase';
|
||||
|
||||
interface ModerationContextType {
|
||||
showModerated: boolean;
|
||||
setShowModerated: (show: boolean) => void;
|
||||
toggleShowModerated: () => void;
|
||||
}
|
||||
|
||||
const ModerationContext = createContext<ModerationContextType | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export function ModerationProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [showModerated, setShowModerated] = useState<boolean>(false);
|
||||
const [isInitialized, setIsInitialized] = useState<boolean>(false);
|
||||
|
||||
// Load initial state from IndexedDB
|
||||
useEffect(() => {
|
||||
const loadModerationPreference = async () => {
|
||||
try {
|
||||
const saved = await localDatabase.loadUIState('show-moderated');
|
||||
setShowModerated(saved === true);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Failed to load moderation preference from IndexedDB:',
|
||||
error
|
||||
);
|
||||
setShowModerated(false);
|
||||
} finally {
|
||||
setIsInitialized(true);
|
||||
}
|
||||
};
|
||||
|
||||
loadModerationPreference();
|
||||
}, []);
|
||||
|
||||
// Save to IndexedDB whenever the value changes (but only after initialization)
|
||||
useEffect(() => {
|
||||
if (!isInitialized) return;
|
||||
|
||||
const saveModerationPreference = async () => {
|
||||
try {
|
||||
await localDatabase.storeUIState('show-moderated', showModerated);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Failed to save moderation preference to IndexedDB:',
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
saveModerationPreference();
|
||||
}, [showModerated, isInitialized]);
|
||||
|
||||
const toggleShowModerated = () => {
|
||||
setShowModerated(prev => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<ModerationContext.Provider
|
||||
value={{
|
||||
showModerated,
|
||||
setShowModerated,
|
||||
toggleShowModerated,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ModerationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useModeration() {
|
||||
const context = useContext(ModerationContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useModeration must be used within a ModerationProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useForum } from '@/contexts/useForum';
|
||||
import { useAuth } from '@/contexts/useAuth';
|
||||
import { useModeration } from '@/contexts/ModerationContext';
|
||||
import { Cell, Post, Comment, UserVerificationStatus } from '@/types/forum';
|
||||
import { EVerificationStatus } from '@/types/identity';
|
||||
|
||||
@ -44,6 +45,12 @@ export interface ForumData {
|
||||
postsWithVoteStatus: PostWithVoteStatus[];
|
||||
commentsWithVoteStatus: CommentWithVoteStatus[];
|
||||
|
||||
// Filtered data based on moderation settings
|
||||
filteredPosts: PostWithVoteStatus[];
|
||||
filteredComments: CommentWithVoteStatus[];
|
||||
filteredCellsWithStats: CellWithStats[];
|
||||
filteredCommentsByPost: Record<string, CommentWithVoteStatus[]>;
|
||||
|
||||
// Organized data
|
||||
postsByCell: Record<string, PostWithVoteStatus[]>;
|
||||
commentsByPost: Record<string, CommentWithVoteStatus[]>;
|
||||
@ -72,6 +79,7 @@ export function useForumData(): ForumData {
|
||||
} = useForum();
|
||||
|
||||
const { currentUser } = useAuth();
|
||||
const { showModerated } = useModeration();
|
||||
|
||||
// Compute cells with statistics
|
||||
const cellsWithStats = useMemo((): CellWithStats[] => {
|
||||
@ -287,6 +295,55 @@ export function useForumData(): ForumData {
|
||||
return createdComments;
|
||||
}, [comments, currentUser]);
|
||||
|
||||
// Filtered data based on moderation settings
|
||||
const filteredPosts = useMemo(() => {
|
||||
return showModerated
|
||||
? postsWithVoteStatus
|
||||
: postsWithVoteStatus.filter(post => !post.moderated);
|
||||
}, [postsWithVoteStatus, showModerated]);
|
||||
|
||||
const filteredComments = useMemo(() => {
|
||||
return showModerated
|
||||
? commentsWithVoteStatus
|
||||
: commentsWithVoteStatus.filter(comment => !comment.moderated);
|
||||
}, [commentsWithVoteStatus, showModerated]);
|
||||
|
||||
// Filtered cells with stats based on filtered posts
|
||||
const filteredCellsWithStats = useMemo((): CellWithStats[] => {
|
||||
return cells.map(cell => {
|
||||
const cellPosts = filteredPosts.filter(post => post.cellId === cell.id);
|
||||
const recentPosts = cellPosts.filter(
|
||||
post => Date.now() - post.timestamp < 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||
);
|
||||
|
||||
const uniqueAuthors = new Set(cellPosts.map(post => post.author));
|
||||
|
||||
return {
|
||||
...cell,
|
||||
postCount: cellPosts.length,
|
||||
activeUsers: uniqueAuthors.size,
|
||||
recentActivity: recentPosts.length,
|
||||
};
|
||||
});
|
||||
}, [cells, filteredPosts]);
|
||||
|
||||
// Filtered comments organized by post
|
||||
const filteredCommentsByPost = useMemo((): Record<
|
||||
string,
|
||||
CommentWithVoteStatus[]
|
||||
> => {
|
||||
const organized: Record<string, CommentWithVoteStatus[]> = {};
|
||||
|
||||
filteredComments.forEach(comment => {
|
||||
if (!organized[comment.postId]) {
|
||||
organized[comment.postId] = [];
|
||||
}
|
||||
organized[comment.postId].push(comment);
|
||||
});
|
||||
|
||||
return organized;
|
||||
}, [filteredComments]);
|
||||
|
||||
return {
|
||||
// Raw data
|
||||
cells,
|
||||
@ -305,6 +362,12 @@ export function useForumData(): ForumData {
|
||||
postsWithVoteStatus,
|
||||
commentsWithVoteStatus,
|
||||
|
||||
// Filtered data based on moderation settings
|
||||
filteredPosts,
|
||||
filteredComments,
|
||||
filteredCellsWithStats,
|
||||
filteredCommentsByPost,
|
||||
|
||||
// Organized data
|
||||
postsByCell,
|
||||
commentsByPost,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useForumData, PostWithVoteStatus } from '@/hooks/core/useForumData';
|
||||
import { useAuth } from '@/hooks/core/useAuth';
|
||||
import { useModeration } from '@/contexts/ModerationContext';
|
||||
|
||||
export interface CellPostsOptions {
|
||||
includeModerated?: boolean;
|
||||
@ -24,8 +25,13 @@ export function useCellPosts(
|
||||
): CellPostsData {
|
||||
const { postsByCell, isInitialLoading, cellsWithStats } = useForumData();
|
||||
const { currentUser } = useAuth();
|
||||
const { showModerated } = useModeration();
|
||||
|
||||
const { includeModerated = false, sortBy = 'relevance', limit } = options;
|
||||
const {
|
||||
includeModerated = showModerated,
|
||||
sortBy = 'relevance',
|
||||
limit,
|
||||
} = options;
|
||||
|
||||
return useMemo(() => {
|
||||
if (!cellId) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useForumData, CommentWithVoteStatus } from '@/hooks/core/useForumData';
|
||||
import { useAuth } from '@/hooks/core/useAuth';
|
||||
import { useModeration } from '@/contexts/ModerationContext';
|
||||
|
||||
export interface PostCommentsOptions {
|
||||
includeModerated?: boolean;
|
||||
@ -29,8 +30,13 @@ export function usePostComments(
|
||||
cellsWithStats,
|
||||
} = useForumData();
|
||||
const { currentUser } = useAuth();
|
||||
const { showModerated } = useModeration();
|
||||
|
||||
const { includeModerated = false, sortBy = 'timestamp', limit } = options;
|
||||
const {
|
||||
includeModerated = showModerated,
|
||||
sortBy = 'timestamp',
|
||||
limit,
|
||||
} = options;
|
||||
|
||||
return useMemo(() => {
|
||||
if (!postId) {
|
||||
|
||||
@ -11,36 +11,29 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import PostCard from '@/components/PostCard';
|
||||
import FeedSidebar from '@/components/FeedSidebar';
|
||||
import { ModerationToggle } from '@/components/ui/moderation-toggle';
|
||||
import { useForumData, useAuth, useForumActions } from '@/hooks';
|
||||
import { EVerificationStatus } from '@/types/identity';
|
||||
import { sortPosts, SortOption } from '@/lib/utils/sorting';
|
||||
|
||||
const FeedPage: React.FC = () => {
|
||||
// ✅ Use reactive hooks
|
||||
const forumData = useForumData();
|
||||
// const selectors = useForumSelectors(forumData); // Available if needed
|
||||
const { verificationStatus } = useAuth();
|
||||
const { refreshData } = useForumActions();
|
||||
const [sortOption, setSortOption] = useState<SortOption>('relevance');
|
||||
|
||||
const {
|
||||
postsWithVoteStatus,
|
||||
commentsByPost,
|
||||
isInitialLoading,
|
||||
isRefreshing,
|
||||
} = forumData;
|
||||
const { filteredPosts, filteredCommentsByPost, isInitialLoading, isRefreshing } =
|
||||
forumData;
|
||||
|
||||
// ✅ Use pre-computed data and selectors
|
||||
// ✅ Use pre-computed filtered data
|
||||
const allPosts = useMemo(() => {
|
||||
const filteredPosts = postsWithVoteStatus.filter(post => !post.moderated);
|
||||
return sortPosts(filteredPosts, sortOption);
|
||||
}, [postsWithVoteStatus, sortOption]);
|
||||
}, [filteredPosts, sortOption]);
|
||||
|
||||
// ✅ Get comment count from organized data
|
||||
// ✅ Get comment count from filtered organized data
|
||||
const getCommentCount = (postId: string) => {
|
||||
return (
|
||||
commentsByPost[postId]?.filter(comment => !comment.moderated).length || 0
|
||||
);
|
||||
const comments = filteredCommentsByPost[postId] || [];
|
||||
return comments.length;
|
||||
};
|
||||
|
||||
// Loading skeleton
|
||||
@ -112,6 +105,8 @@ const FeedPage: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<ModerationToggle />
|
||||
|
||||
<Select
|
||||
value={sortOption}
|
||||
onValueChange={(value: SortOption) => setSortOption(value)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user