diff --git a/components/escalated-users-list/index.tsx b/components/escalated-users-list/index.tsx index 91f01b0..3e886cc 100644 --- a/components/escalated-users-list/index.tsx +++ b/components/escalated-users-list/index.tsx @@ -10,9 +10,10 @@ import { EscalatedItem } from '@/lib/types/moderation'; interface EscalatedUsersListProps { onUserSelect: (item: EscalatedItem) => void; + dismissedKeys?: Set; } -export const EscalatedUsersList = ({ onUserSelect }: EscalatedUsersListProps) => { +export const EscalatedUsersList = ({ onUserSelect, dismissedKeys }: EscalatedUsersListProps) => { const [state, setState] = useState({ items: [] as EscalatedItem[], loading: true, @@ -92,7 +93,14 @@ export const EscalatedUsersList = ({ onUserSelect }: EscalatedUsersListProps) => ); } - if (state.items.length === 0) { + const visibleItems = dismissedKeys && dismissedKeys.size > 0 + ? state.items.filter((item) => { + const key = item.type === 'post' ? item.postUri : item.did; + return !dismissedKeys.has(key); + }) + : state.items; + + if (visibleItems.length === 0) { return (

@@ -108,10 +116,10 @@ export const EscalatedUsersList = ({ onUserSelect }: EscalatedUsersListProps) => return (

- Escalated Reports ({state.items.length}) + Escalated Reports ({visibleItems.length})

- {state.items.map((item) => ( + {visibleItems.map((item) => ( void; + onEscalationHandled?: () => void; } export const UserModerationModal = ({ user, onClose, + onEscalationHandled, }: UserModerationModalProps) => { const toastContext = useContext(ToastContext); @@ -49,6 +56,10 @@ export const UserModerationModal = ({ const [moderationData, setModerationData] = useState(null); const [loadingModerationData, setLoadingModerationData] = useState(false); + const [postData, setPostData] = useState(null); + const [loadingPost, setLoadingPost] = useState(false); + const [postError, setPostError] = useState(null); + const [banForm, setBanForm] = useState({ showForm: false, reason: '', @@ -128,6 +139,31 @@ export const UserModerationModal = ({ console.error('Failed to check mute status:', error); setMuteStatus({ isMuted: false, error: 'Failed to check mute status. Try refreshing.' }); }); + + // Fetch post data for escalated posts + if (user && 'type' in user && user.type === 'post' && 'postUri' in user) { + setLoadingPost(true); + setPostError(null); + setPostData(null); + fetch(`/api/post?uri=${encodeURIComponent((user as EscalatedPostItem).postUri)}`) + .then((res) => { + if (!res.ok) throw new Error('Failed to fetch post'); + return res.json(); + }) + .then((data) => { + setPostData(data); + }) + .catch((error) => { + console.error('Failed to fetch post:', error); + setPostError(error instanceof Error ? error.message : 'Failed to load post'); + }) + .finally(() => { + setLoadingPost(false); + }); + } else { + setPostData(null); + setPostError(null); + } } }, [user?.did]); @@ -369,8 +405,12 @@ export const UserModerationModal = ({ intent: VisualIntent.Success, }); + const actionType = ozoneAction.type; resetOzoneAction(); await refreshModerationData(); + if (actionType === 'acknowledge' || actionType === 'takedown') { + onEscalationHandled?.(); + } } catch (error) { console.error('Failed to emit moderation event:', error); toastContext?.toast({ @@ -433,6 +473,7 @@ export const UserModerationModal = ({ resetOzoneAction(); await refreshModerationData(); + onEscalationHandled?.(); } catch (error) { console.error('Failed to acknowledge:', error); toastContext?.toast({ @@ -463,6 +504,7 @@ export const UserModerationModal = ({ resetOzoneAction(); await refreshModerationData(); + onEscalationHandled?.(); } catch (error) { console.error('Failed to takedown:', error); toastContext?.toast({ @@ -524,6 +566,47 @@ export const UserModerationModal = ({
+ {/* Inline Post Preview for Escalated Posts */} + {'type' in user && user.type === 'post' && ( +
+ {loadingPost && ( +
+
+ Loading post content... +
+ )} + {postError && ( +
+

Could not load post preview: {postError}

+
+ )} + {postData?.post && ( +
+ + ['porn', 'sexual', 'nudity', 'graphic-media'].includes(l.val) + )} + postIndexedAt={postData.post.indexedAt} + /> + {AppBskyFeedPost.isRecord(postData.post.record) && ( + + )} + {postData.post.embed && ( + + )} +
+ )} +
+ )} + {/* Two Column Layout */}
{/* Left Column - Actions */} diff --git a/components/user-management-panel/index.tsx b/components/user-management-panel/index.tsx index 0d20318..a162ed8 100644 --- a/components/user-management-panel/index.tsx +++ b/components/user-management-panel/index.tsx @@ -1,16 +1,17 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { BSUserSearch } from '@/components/bs-user-search/bs-user-search'; import { useModal } from '@/contexts/modal-context'; import { MODAL_INSTANCE_IDS } from '@/enums/modals'; import { ProfileViewBasic } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; import { UserModerationModal } from '@/components/modals/user-moderation-modal'; import { EscalatedUsersList } from '@/components/escalated-users-list'; -import { EscalatedItem } from '@/lib/types/moderation'; +import { EscalatedItem, EscalatedPostItem } from '@/lib/types/moderation'; export const UserManagementPanel = () => { const [selectedUser, setSelectedUser] = useState(null); + const [dismissedKeys, setDismissedKeys] = useState>(new Set()); const { openModalInstance, closeModalInstance } = useModal(); const handleUserSelect = (user: ProfileViewBasic | EscalatedItem) => { @@ -22,6 +23,14 @@ export const UserManagementPanel = () => { closeModalInstance(MODAL_INSTANCE_IDS.USER_MODERATION); }; + const handleEscalationHandled = useCallback(() => { + if (!selectedUser || !('type' in selectedUser)) return; + const key = selectedUser.type === 'post' + ? (selectedUser as EscalatedPostItem).postUri + : selectedUser.did; + setDismissedKeys(prev => new Set(prev).add(key)); + }, [selectedUser]); + return (
{/* Search Section */} @@ -36,12 +45,13 @@ export const UserManagementPanel = () => {
{/* Escalated Reports */} - + {/* User Moderation Modal */}
);