Skip to content

Commit d268d56

Browse files
ux changes (#34)
1 parent fb545f9 commit d268d56

3 files changed

Lines changed: 108 additions & 7 deletions

File tree

components/escalated-users-list/index.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import { EscalatedItem } from '@/lib/types/moderation';
1010

1111
interface EscalatedUsersListProps {
1212
onUserSelect: (item: EscalatedItem) => void;
13+
dismissedKeys?: Set<string>;
1314
}
1415

15-
export const EscalatedUsersList = ({ onUserSelect }: EscalatedUsersListProps) => {
16+
export const EscalatedUsersList = ({ onUserSelect, dismissedKeys }: EscalatedUsersListProps) => {
1617
const [state, setState] = useState({
1718
items: [] as EscalatedItem[],
1819
loading: true,
@@ -92,7 +93,14 @@ export const EscalatedUsersList = ({ onUserSelect }: EscalatedUsersListProps) =>
9293
);
9394
}
9495

95-
if (state.items.length === 0) {
96+
const visibleItems = dismissedKeys && dismissedKeys.size > 0
97+
? state.items.filter((item) => {
98+
const key = item.type === 'post' ? item.postUri : item.did;
99+
return !dismissedKeys.has(key);
100+
})
101+
: state.items;
102+
103+
if (visibleItems.length === 0) {
96104
return (
97105
<div className='bg-app-background border border-app-border rounded-lg p-6'>
98106
<h3 className='text-lg font-semibold text-app mb-4'>
@@ -108,10 +116,10 @@ export const EscalatedUsersList = ({ onUserSelect }: EscalatedUsersListProps) =>
108116
return (
109117
<div className='bg-app-background border border-app-border rounded-lg p-6'>
110118
<h3 className='text-lg font-semibold text-app mb-4'>
111-
Escalated Reports ({state.items.length})
119+
Escalated Reports ({visibleItems.length})
112120
</h3>
113121
<div className='space-y-3'>
114-
{state.items.map((item) => (
122+
{visibleItems.map((item) => (
115123
<UserCard
116124
key={item.type === 'post' ? item.postUri : item.did}
117125
user={item}

components/modals/user-moderation-modal.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import { ToastContext } from '@/contexts/toast-context';
1212
import { ProfileModerationResponse, ModerationEventType, EscalatedItem, EscalatedPostItem } from '@/lib/types/moderation';
1313
import { BLACKSKY_PDS_URL } from '@/lib/constants/moderation';
1414
import { getPostUrl } from '@/components/post/utils';
15+
import { PostHeader } from '@/components/post/components/post-header';
16+
import { PostText } from '@/components/post/components/post-text';
17+
import { EmbedRenderer } from '@/components/post/components/embed-renderer';
18+
import { AppBskyFeedPost } from '@atproto/api';
19+
import { ThreadViewPost } from '@atproto/api/dist/client/types/app/bsky/feed/defs';
1520

1621
type UserLike = {
1722
did: string;
@@ -23,11 +28,13 @@ type UserLike = {
2328
interface UserModerationModalProps {
2429
user: ProfileViewBasic | EscalatedItem | UserLike | null;
2530
onClose: () => void;
31+
onEscalationHandled?: () => void;
2632
}
2733

2834
export const UserModerationModal = ({
2935
user,
3036
onClose,
37+
onEscalationHandled,
3138
}: UserModerationModalProps) => {
3239
const toastContext = useContext(ToastContext);
3340

@@ -49,6 +56,10 @@ export const UserModerationModal = ({
4956
const [moderationData, setModerationData] = useState<ProfileModerationResponse | null>(null);
5057
const [loadingModerationData, setLoadingModerationData] = useState(false);
5158

59+
const [postData, setPostData] = useState<ThreadViewPost | null>(null);
60+
const [loadingPost, setLoadingPost] = useState(false);
61+
const [postError, setPostError] = useState<string | null>(null);
62+
5263
const [banForm, setBanForm] = useState({
5364
showForm: false,
5465
reason: '',
@@ -128,6 +139,31 @@ export const UserModerationModal = ({
128139
console.error('Failed to check mute status:', error);
129140
setMuteStatus({ isMuted: false, error: 'Failed to check mute status. Try refreshing.' });
130141
});
142+
143+
// Fetch post data for escalated posts
144+
if (user && 'type' in user && user.type === 'post' && 'postUri' in user) {
145+
setLoadingPost(true);
146+
setPostError(null);
147+
setPostData(null);
148+
fetch(`/api/post?uri=${encodeURIComponent((user as EscalatedPostItem).postUri)}`)
149+
.then((res) => {
150+
if (!res.ok) throw new Error('Failed to fetch post');
151+
return res.json();
152+
})
153+
.then((data) => {
154+
setPostData(data);
155+
})
156+
.catch((error) => {
157+
console.error('Failed to fetch post:', error);
158+
setPostError(error instanceof Error ? error.message : 'Failed to load post');
159+
})
160+
.finally(() => {
161+
setLoadingPost(false);
162+
});
163+
} else {
164+
setPostData(null);
165+
setPostError(null);
166+
}
131167
}
132168
}, [user?.did]);
133169

@@ -369,8 +405,12 @@ export const UserModerationModal = ({
369405
intent: VisualIntent.Success,
370406
});
371407

408+
const actionType = ozoneAction.type;
372409
resetOzoneAction();
373410
await refreshModerationData();
411+
if (actionType === 'acknowledge' || actionType === 'takedown') {
412+
onEscalationHandled?.();
413+
}
374414
} catch (error) {
375415
console.error('Failed to emit moderation event:', error);
376416
toastContext?.toast({
@@ -433,6 +473,7 @@ export const UserModerationModal = ({
433473

434474
resetOzoneAction();
435475
await refreshModerationData();
476+
onEscalationHandled?.();
436477
} catch (error) {
437478
console.error('Failed to acknowledge:', error);
438479
toastContext?.toast({
@@ -463,6 +504,7 @@ export const UserModerationModal = ({
463504

464505
resetOzoneAction();
465506
await refreshModerationData();
507+
onEscalationHandled?.();
466508
} catch (error) {
467509
console.error('Failed to takedown:', error);
468510
toastContext?.toast({
@@ -524,6 +566,47 @@ export const UserModerationModal = ({
524566
</div>
525567
</div>
526568

569+
{/* Inline Post Preview for Escalated Posts */}
570+
{'type' in user && user.type === 'post' && (
571+
<div className='mt-4 border border-app-border rounded-lg overflow-hidden'>
572+
{loadingPost && (
573+
<div className='flex items-center space-x-2 p-4'>
574+
<div className='animate-spin h-4 w-4 border-2 border-blue-500 rounded-full border-t-transparent'></div>
575+
<span className='text-sm text-app-secondary'>Loading post content...</span>
576+
</div>
577+
)}
578+
{postError && (
579+
<div className='p-4 bg-yellow-50 border-b border-yellow-200'>
580+
<p className='text-sm text-yellow-700'>Could not load post preview: {postError}</p>
581+
</div>
582+
)}
583+
{postData?.post && (
584+
<div className='p-4'>
585+
<PostHeader
586+
author={postData.post.author}
587+
isAuthorLabeled={postData.post.author?.labels?.some((l: { val: string }) =>
588+
['porn', 'sexual', 'nudity', 'graphic-media'].includes(l.val)
589+
)}
590+
postIndexedAt={postData.post.indexedAt}
591+
/>
592+
{AppBskyFeedPost.isRecord(postData.post.record) && (
593+
<PostText
594+
text={postData.post.record.text}
595+
facets={postData.post.record.facets}
596+
/>
597+
)}
598+
{postData.post.embed && (
599+
<EmbedRenderer
600+
content={postData.post.embed}
601+
labels={postData.post.labels}
602+
isSignedIn={true}
603+
/>
604+
)}
605+
</div>
606+
)}
607+
</div>
608+
)}
609+
527610
{/* Two Column Layout */}
528611
<div className='flex flex-row gap-6 flex-1 min-h-0 mt-6 overflow-hidden'>
529612
{/* Left Column - Actions */}

components/user-management-panel/index.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
'use client';
22

3-
import React, { useState } from 'react';
3+
import React, { useCallback, useState } from 'react';
44
import { BSUserSearch } from '@/components/bs-user-search/bs-user-search';
55
import { useModal } from '@/contexts/modal-context';
66
import { MODAL_INSTANCE_IDS } from '@/enums/modals';
77
import { ProfileViewBasic } from '@atproto/api/dist/client/types/app/bsky/actor/defs';
88
import { UserModerationModal } from '@/components/modals/user-moderation-modal';
99
import { EscalatedUsersList } from '@/components/escalated-users-list';
10-
import { EscalatedItem } from '@/lib/types/moderation';
10+
import { EscalatedItem, EscalatedPostItem } from '@/lib/types/moderation';
1111

1212
export const UserManagementPanel = () => {
1313
const [selectedUser, setSelectedUser] = useState<ProfileViewBasic | EscalatedItem | null>(null);
14+
const [dismissedKeys, setDismissedKeys] = useState<Set<string>>(new Set());
1415
const { openModalInstance, closeModalInstance } = useModal();
1516

1617
const handleUserSelect = (user: ProfileViewBasic | EscalatedItem) => {
@@ -22,6 +23,14 @@ export const UserManagementPanel = () => {
2223
closeModalInstance(MODAL_INSTANCE_IDS.USER_MODERATION);
2324
};
2425

26+
const handleEscalationHandled = useCallback(() => {
27+
if (!selectedUser || !('type' in selectedUser)) return;
28+
const key = selectedUser.type === 'post'
29+
? (selectedUser as EscalatedPostItem).postUri
30+
: selectedUser.did;
31+
setDismissedKeys(prev => new Set(prev).add(key));
32+
}, [selectedUser]);
33+
2534
return (
2635
<div className='w-full max-w-4xl space-y-6'>
2736
{/* Search Section */}
@@ -36,12 +45,13 @@ export const UserManagementPanel = () => {
3645
</div>
3746

3847
{/* Escalated Reports */}
39-
<EscalatedUsersList onUserSelect={handleUserSelect} />
48+
<EscalatedUsersList onUserSelect={handleUserSelect} dismissedKeys={dismissedKeys} />
4049

4150
{/* User Moderation Modal */}
4251
<UserModerationModal
4352
user={selectedUser}
4453
onClose={handleCloseModal}
54+
onEscalationHandled={handleEscalationHandled}
4555
/>
4656
</div>
4757
);

0 commit comments

Comments
 (0)