Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions components/escalated-users-list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import { EscalatedItem } from '@/lib/types/moderation';

interface EscalatedUsersListProps {
onUserSelect: (item: EscalatedItem) => void;
dismissedKeys?: Set<string>;
}

export const EscalatedUsersList = ({ onUserSelect }: EscalatedUsersListProps) => {
export const EscalatedUsersList = ({ onUserSelect, dismissedKeys }: EscalatedUsersListProps) => {
const [state, setState] = useState({
items: [] as EscalatedItem[],
loading: true,
Expand Down Expand Up @@ -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 (
<div className='bg-app-background border border-app-border rounded-lg p-6'>
<h3 className='text-lg font-semibold text-app mb-4'>
Expand All @@ -108,10 +116,10 @@ export const EscalatedUsersList = ({ onUserSelect }: EscalatedUsersListProps) =>
return (
<div className='bg-app-background border border-app-border rounded-lg p-6'>
<h3 className='text-lg font-semibold text-app mb-4'>
Escalated Reports ({state.items.length})
Escalated Reports ({visibleItems.length})
</h3>
<div className='space-y-3'>
{state.items.map((item) => (
{visibleItems.map((item) => (
<UserCard
key={item.type === 'post' ? item.postUri : item.did}
user={item}
Expand Down
83 changes: 83 additions & 0 deletions components/modals/user-moderation-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
import { ProfileModerationResponse, ModerationEventType, EscalatedItem, EscalatedPostItem } from '@/lib/types/moderation';
import { BLACKSKY_PDS_URL } from '@/lib/constants/moderation';
import { getPostUrl } from '@/components/post/utils';
import { PostHeader } from '@/components/post/components/post-header';
import { PostText } from '@/components/post/components/post-text';
import { EmbedRenderer } from '@/components/post/components/embed-renderer';
import { AppBskyFeedPost } from '@atproto/api';
import { ThreadViewPost } from '@atproto/api/dist/client/types/app/bsky/feed/defs';

type UserLike = {
did: string;
Expand All @@ -23,11 +28,13 @@
interface UserModerationModalProps {
user: ProfileViewBasic | EscalatedItem | UserLike | null;
onClose: () => void;
onEscalationHandled?: () => void;
}

export const UserModerationModal = ({
user,
onClose,
onEscalationHandled,
}: UserModerationModalProps) => {
const toastContext = useContext(ToastContext);

Expand All @@ -49,6 +56,10 @@
const [moderationData, setModerationData] = useState<ProfileModerationResponse | null>(null);
const [loadingModerationData, setLoadingModerationData] = useState(false);

const [postData, setPostData] = useState<ThreadViewPost | null>(null);
const [loadingPost, setLoadingPost] = useState(false);
const [postError, setPostError] = useState<string | null>(null);

const [banForm, setBanForm] = useState({
showForm: false,
reason: '',
Expand Down Expand Up @@ -128,8 +139,33 @@
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]);

Check warning on line 168 in components/modals/user-moderation-modal.tsx

View workflow job for this annotation

GitHub Actions / validate

React Hook useEffect has a missing dependency: 'user'. Either include it or remove the dependency array

if (!user) {
return null;
Expand Down Expand Up @@ -369,8 +405,12 @@
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({
Expand Down Expand Up @@ -433,6 +473,7 @@

resetOzoneAction();
await refreshModerationData();
onEscalationHandled?.();
} catch (error) {
console.error('Failed to acknowledge:', error);
toastContext?.toast({
Expand Down Expand Up @@ -463,6 +504,7 @@

resetOzoneAction();
await refreshModerationData();
onEscalationHandled?.();
} catch (error) {
console.error('Failed to takedown:', error);
toastContext?.toast({
Expand Down Expand Up @@ -524,6 +566,47 @@
</div>
</div>

{/* Inline Post Preview for Escalated Posts */}
{'type' in user && user.type === 'post' && (
<div className='mt-4 border border-app-border rounded-lg overflow-hidden'>
{loadingPost && (
<div className='flex items-center space-x-2 p-4'>
<div className='animate-spin h-4 w-4 border-2 border-blue-500 rounded-full border-t-transparent'></div>
<span className='text-sm text-app-secondary'>Loading post content...</span>
</div>
)}
{postError && (
<div className='p-4 bg-yellow-50 border-b border-yellow-200'>
<p className='text-sm text-yellow-700'>Could not load post preview: {postError}</p>
</div>
)}
{postData?.post && (
<div className='p-4'>
<PostHeader
author={postData.post.author}
isAuthorLabeled={postData.post.author?.labels?.some((l: { val: string }) =>
['porn', 'sexual', 'nudity', 'graphic-media'].includes(l.val)
)}
postIndexedAt={postData.post.indexedAt}
/>
{AppBskyFeedPost.isRecord(postData.post.record) && (
<PostText
text={postData.post.record.text}
facets={postData.post.record.facets}
/>
)}
{postData.post.embed && (
<EmbedRenderer
content={postData.post.embed}
labels={postData.post.labels}
isSignedIn={true}
/>
)}
</div>
)}
</div>
)}

{/* Two Column Layout */}
<div className='flex flex-row gap-6 flex-1 min-h-0 mt-6 overflow-hidden'>
{/* Left Column - Actions */}
Expand Down
16 changes: 13 additions & 3 deletions components/user-management-panel/index.tsx
Original file line number Diff line number Diff line change
@@ -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<ProfileViewBasic | EscalatedItem | null>(null);
const [dismissedKeys, setDismissedKeys] = useState<Set<string>>(new Set());
const { openModalInstance, closeModalInstance } = useModal();

const handleUserSelect = (user: ProfileViewBasic | EscalatedItem) => {
Expand All @@ -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 (
<div className='w-full max-w-4xl space-y-6'>
{/* Search Section */}
Expand All @@ -36,12 +45,13 @@ export const UserManagementPanel = () => {
</div>

{/* Escalated Reports */}
<EscalatedUsersList onUserSelect={handleUserSelect} />
<EscalatedUsersList onUserSelect={handleUserSelect} dismissedKeys={dismissedKeys} />

{/* User Moderation Modal */}
<UserModerationModal
user={selectedUser}
onClose={handleCloseModal}
onEscalationHandled={handleEscalationHandled}
/>
</div>
);
Expand Down