Skip to content

Commit db1776f

Browse files
committed
Implement image moderation unblocking functionality and scroll reset
This commit includes: - New API endpoints for viewing and unblocking moderated events - Enhanced ModerationNotifications component with event details modal - Unblock button for false positives in moderation system - Scroll position reset after event deletion - Updated BaseModal to use 'open' prop instead of deprecated 'visible' prop
1 parent e32c922 commit db1776f

5 files changed

Lines changed: 801 additions & 228 deletions

File tree

src/api/moderationNotifications.api.ts

Lines changed: 139 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { httpApi } from './http.api';
1+
import config from '@app/config/config';
2+
import { readToken } from '@app/services/localStorage.service';
3+
4+
export interface BlockedEventResponse {
5+
event: any; // Full Nostr event details
6+
moderation_details: ModerationNotification;
7+
}
28

39
export interface ModerationNotificationParams {
410
page?: number;
@@ -45,22 +51,148 @@ export interface ModerationStats {
4551
export const getModerationNotifications = async (
4652
params: ModerationNotificationParams = {},
4753
): Promise<ModerationNotificationsResponse> => {
48-
const response = await httpApi.get<ModerationNotificationsResponse>('/api/moderation/notifications', { params });
49-
return response.data;
54+
const token = readToken();
55+
56+
// Construct query parameters
57+
const queryParams = new URLSearchParams();
58+
if (params.page) queryParams.append('page', params.page.toString());
59+
if (params.limit) queryParams.append('limit', params.limit.toString());
60+
if (params.filter) queryParams.append('filter', params.filter);
61+
if (params.pubkey) queryParams.append('pubkey', params.pubkey);
62+
63+
const response = await fetch(`${config.baseURL}/api/moderation/notifications?${queryParams}`, {
64+
headers: {
65+
'Content-Type': 'application/json',
66+
'Authorization': `Bearer ${token}`,
67+
},
68+
});
69+
70+
if (response.status === 204) {
71+
// 204 No Content means no notifications, return empty arrays
72+
return {
73+
notifications: [],
74+
pagination: {
75+
currentPage: 1,
76+
pageSize: params.limit || 10,
77+
totalItems: 0,
78+
totalPages: 0,
79+
hasNext: false,
80+
hasPrevious: false
81+
}
82+
};
83+
} else if (!response.ok) {
84+
throw new Error(`Request failed: ${response.status}`);
85+
}
86+
87+
return await response.json();
5088
};
5189

5290
// Mark a specific notification as read
5391
export const markNotificationAsRead = async (id: number): Promise<void> => {
54-
await httpApi.post('/api/moderation/notifications/read', { id: [id] });
92+
const token = readToken();
93+
94+
const response = await fetch(`${config.baseURL}/api/moderation/notifications/read`, {
95+
method: 'POST',
96+
headers: {
97+
'Content-Type': 'application/json',
98+
'Authorization': `Bearer ${token}`,
99+
},
100+
body: JSON.stringify({ id: [id] }),
101+
});
102+
103+
if (!response.ok) {
104+
throw new Error(`Request failed: ${response.status}`);
105+
}
55106
};
56107

57108
// Mark all notifications as read
58109
export const markAllNotificationsAsRead = async (): Promise<void> => {
59-
await httpApi.post('/api/moderation/notifications/read-all');
110+
const token = readToken();
111+
112+
const response = await fetch(`${config.baseURL}/api/moderation/notifications/read-all`, {
113+
method: 'POST',
114+
headers: {
115+
'Content-Type': 'application/json',
116+
'Authorization': `Bearer ${token}`,
117+
},
118+
});
119+
120+
if (!response.ok) {
121+
throw new Error(`Request failed: ${response.status}`);
122+
}
60123
};
61124

62125
// Get moderation statistics
63126
export const getModerationStats = async (): Promise<ModerationStats> => {
64-
const response = await httpApi.get<ModerationStats>('/api/moderation/stats');
65-
return response.data;
127+
const token = readToken();
128+
129+
const response = await fetch(`${config.baseURL}/api/moderation/stats`, {
130+
headers: {
131+
'Content-Type': 'application/json',
132+
'Authorization': `Bearer ${token}`,
133+
},
134+
});
135+
136+
if (!response.ok) {
137+
throw new Error(`Request failed: ${response.status}`);
138+
}
139+
140+
return await response.json();
141+
};
142+
143+
// Get details of a blocked event
144+
export const getBlockedEvent = async (eventId: string): Promise<BlockedEventResponse> => {
145+
const token = readToken();
146+
147+
const response = await fetch(`${config.baseURL}/api/moderation/blocked-event/${eventId}`, {
148+
headers: {
149+
'Content-Type': 'application/json',
150+
'Authorization': `Bearer ${token}`,
151+
},
152+
});
153+
154+
if (!response.ok) {
155+
throw new Error(`Request failed: ${response.status}`);
156+
}
157+
158+
return await response.json();
159+
};
160+
161+
// Unblock an incorrectly flagged event
162+
export const unblockEvent = async (eventId: string): Promise<{ success: boolean; message: string; event_id: string }> => {
163+
const token = readToken();
164+
165+
const response = await fetch(`${config.baseURL}/api/moderation/unblock`, {
166+
method: 'POST',
167+
headers: {
168+
'Content-Type': 'application/json',
169+
'Authorization': `Bearer ${token}`,
170+
},
171+
body: JSON.stringify({ event_id: eventId }),
172+
});
173+
174+
if (!response.ok) {
175+
throw new Error(`Request failed: ${response.status}`);
176+
}
177+
178+
return await response.json();
179+
};
180+
181+
// Permanently delete a moderated event
182+
export const deleteModeratedEvent = async (eventId: string): Promise<{ success: boolean; message: string; event_id: string }> => {
183+
const token = readToken();
184+
185+
const response = await fetch(`${config.baseURL}/api/moderation/event/${eventId}`, {
186+
method: 'DELETE',
187+
headers: {
188+
'Content-Type': 'application/json',
189+
'Authorization': `Bearer ${token}`,
190+
},
191+
});
192+
193+
if (!response.ok) {
194+
throw new Error(`Request failed: ${response.status}`);
195+
}
196+
197+
return await response.json();
66198
};

src/components/common/BaseModal/BaseModal.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import React from 'react';
22
import { Modal, ModalProps } from 'antd';
33
import { modalSizes } from 'constants/modalSizes';
44

5-
interface BaseModalProps extends ModalProps {
5+
// Extend ModalProps to include our custom props
6+
interface BaseModalProps extends Omit<ModalProps, 'visible'> {
67
size?: 'small' | 'medium' | 'large';
8+
visible?: boolean; // Deprecated prop
9+
open?: boolean; // New prop that will replace visible
710
}
811

912
interface BaseModalInterface extends React.FC<BaseModalProps> {
@@ -13,11 +16,33 @@ interface BaseModalInterface extends React.FC<BaseModalProps> {
1316
error: typeof Modal.error;
1417
}
1518

16-
export const BaseModal: BaseModalInterface = ({ size = 'medium', children, ...props }) => {
19+
export const BaseModal: BaseModalInterface = ({
20+
size = 'medium',
21+
visible,
22+
open,
23+
children,
24+
...props
25+
}) => {
1726
const modalSize = Object.entries(modalSizes).find((sz) => sz[0] === size)?.[1];
27+
28+
// If open is provided, use it. Otherwise, fall back to visible.
29+
// This ensures backward compatibility while supporting the new prop.
30+
const isOpen = open !== undefined ? open : visible;
31+
32+
// Show deprecation warning in development mode
33+
if (process.env.NODE_ENV === 'development' && visible !== undefined && open === undefined) {
34+
console.warn(
35+
'[antd: Modal] `visible` will be removed in next major version, please use `open` instead.'
36+
);
37+
}
1838

1939
return (
20-
<Modal getContainer={false} width={modalSize} {...props}>
40+
<Modal
41+
getContainer={false}
42+
width={modalSize}
43+
open={isOpen}
44+
{...props}
45+
>
2146
{children}
2247
</Modal>
2348
);

src/components/moderation/ModerationNotifications/ModerationNotifications.styles.ts

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,23 +105,27 @@ export const ModerationBanner = styled.div`
105105
export const MediaWrapper = styled.div`
106106
position: relative;
107107
width: 100%;
108-
max-width: 300px;
108+
max-width: 400px;
109109
border-radius: ${BORDER_RADIUS};
110110
overflow: hidden;
111111
border: 1px solid var(--border-color);
112+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
113+
margin-bottom: 1rem;
112114
`;
113115

114116
export const StyledImage = styled.img`
115117
max-width: 100%;
116-
max-height: 200px;
118+
max-height: 300px;
117119
object-fit: contain;
118120
display: block;
121+
background-color: #000;
119122
`;
120123

121124
export const StyledVideo = styled.video`
122125
max-width: 100%;
123-
max-height: 200px;
126+
max-height: 300px;
124127
display: block;
128+
background-color: #000;
125129
`;
126130

127131
export const StyledAudio = styled.audio`
@@ -196,3 +200,84 @@ export const ContentTypeTag = styled.span<{ $type: string }>`
196200
}
197201
}}
198202
`;
203+
204+
// Action buttons container
205+
export const ActionButtons = styled.div`
206+
display: flex;
207+
gap: 0.5rem;
208+
margin-top: 0.5rem;
209+
`;
210+
211+
// View event button
212+
export const ViewEventButton = styled(Button)`
213+
display: flex;
214+
align-items: center;
215+
`;
216+
217+
// Event details modal styles
218+
export const EventDetailsContainer = styled.div`
219+
display: flex;
220+
flex-direction: column;
221+
gap: 1.5rem;
222+
padding-bottom: 1rem;
223+
`;
224+
225+
export const EventSection = styled.div`
226+
display: flex;
227+
flex-direction: column;
228+
gap: 0.75rem;
229+
padding: 1.25rem;
230+
background-color: var(--background-color);
231+
border-radius: ${BORDER_RADIUS};
232+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
233+
`;
234+
235+
export const SectionTitle = styled.h3`
236+
font-size: ${FONT_SIZE.lg};
237+
font-weight: ${FONT_WEIGHT.semibold};
238+
margin-bottom: 0.5rem;
239+
color: var(--heading-color);
240+
`;
241+
242+
export const DetailItem = styled.div`
243+
display: flex;
244+
flex-direction: column;
245+
gap: 0.25rem;
246+
margin-bottom: 0.5rem;
247+
`;
248+
249+
export const DetailLabel = styled.span`
250+
font-weight: ${FONT_WEIGHT.semibold};
251+
color: var(--text-light-color);
252+
font-size: ${FONT_SIZE.xs};
253+
`;
254+
255+
export const DetailValue = styled.span`
256+
color: var(--text-main-color);
257+
font-size: ${FONT_SIZE.md};
258+
word-break: break-all;
259+
`;
260+
261+
export const EventContent = styled.pre`
262+
background-color: var(--secondary-background-color);
263+
padding: 1rem;
264+
border-radius: ${BORDER_RADIUS};
265+
overflow-x: auto;
266+
font-family: monospace;
267+
font-size: ${FONT_SIZE.xs};
268+
white-space: pre-wrap;
269+
word-break: break-word;
270+
max-height: 250px;
271+
overflow-y: auto;
272+
margin-bottom: 0.5rem;
273+
border: 1px solid var(--border-color);
274+
`;
275+
276+
export const MediaContainer = styled.div`
277+
margin-top: 1rem;
278+
width: 100%;
279+
display: flex;
280+
flex-direction: column;
281+
align-items: center;
282+
padding-bottom: 0.5rem;
283+
`;

0 commit comments

Comments
 (0)