Skip to content

Commit 3a9bbe5

Browse files
committed
feat: Add report notifications tab to header dropdown
- Add Reports tab alongside existing Moderation and Payments tabs - Integrate useReportNotifications hook for fetching and managing reports - Add report counter and unread notification tracking - Import ReportNotificationsOverlay for the dropdown display - Register report-notifications route in AppRouter
1 parent 4ca3c33 commit 3a9bbe5

9 files changed

Lines changed: 952 additions & 8 deletions

File tree

src/components/header/components/notificationsDropdown/NotificationsDropdown.tsx

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { BaseButton } from '@app/components/common/BaseButton/BaseButton';
44
import { BaseBadge } from '@app/components/common/BaseBadge/BaseBadge';
55
import { NotificationsOverlay } from '@app/components/header/components/notificationsDropdown/NotificationsOverlay/NotificationsOverlay';
66
import { PaymentNotificationsOverlay } from '@app/components/header/components/notificationsDropdown/PaymentNotificationsOverlay';
7+
import ReportNotificationsOverlay from '@app/components/header/components/notificationsDropdown/ReportNotificationsOverlay';
78
import { notifications as fetchedNotifications, Notification, Message } from '@app/api/notifications.api';
89
import { useModerationNotifications } from '@app/hooks/useModerationNotifications';
910
import { usePaymentNotifications } from '@app/hooks/usePaymentNotifications';
11+
import { useReportNotifications } from '@app/hooks/useReportNotifications';
1012
import { HeaderActionWrapper } from '@app/components/header/Header.styles';
1113
import { BasePopover } from '@app/components/common/BasePopover/BasePopover';
1214
import { useTranslation } from 'react-i18next';
@@ -40,18 +42,28 @@ export const NotificationsDropdown: React.FC = () => {
4042
fetchNotifications: refreshModerationNotifications
4143
} = useModerationNotifications();
4244

43-
// Filter to only show unread moderation notifications in the dropdown
44-
const moderationNotifications = allModerationNotifications.filter(notification => !notification.is_read);
45-
4645
const {
4746
notifications: allPaymentNotifications,
4847
markAsRead: markPaymentAsRead,
4948
markAllAsRead: markAllPaymentsAsRead,
5049
fetchNotifications: refreshPaymentNotifications
5150
} = usePaymentNotifications();
51+
52+
const {
53+
notifications: allReportNotifications,
54+
markAsRead: markReportAsRead,
55+
markAllAsRead: markAllReportsAsRead,
56+
fetchNotifications: refreshReportNotifications
57+
} = useReportNotifications();
58+
59+
// Filter to only show unread moderation notifications in the dropdown
60+
const moderationNotifications = allModerationNotifications.filter(notification => !notification.is_read);
5261

5362
// Filter to only show unread notifications in the dropdown
5463
const paymentNotifications = allPaymentNotifications.filter(notification => !notification.is_read);
64+
65+
// Filter to only show unread report notifications in the dropdown
66+
const reportNotifications = allReportNotifications.filter(notification => !notification.is_read);
5567

5668
// Use ref to track if we've processed these notifications before
5769
const processedModerationIdsRef = useRef<Set<number>>(new Set());
@@ -80,17 +92,19 @@ export const NotificationsDropdown: React.FC = () => {
8092
}
8193
}, [allModerationNotifications]);
8294

83-
// Initialize both notification types
95+
// Initialize all notification types
8496
useEffect(() => {
8597
refreshModerationNotifications({ filter: 'unread' });
8698
refreshPaymentNotifications({ filter: 'unread' });
87-
}, [refreshModerationNotifications, refreshPaymentNotifications]);
99+
refreshReportNotifications({ filter: 'unread' });
100+
}, [refreshModerationNotifications, refreshPaymentNotifications, refreshReportNotifications]);
88101

89102
// Refresh all notifications, only showing unread ones
90103
const handleRefresh = useCallback(() => {
91104
refreshModerationNotifications({ filter: 'unread' });
92105
refreshPaymentNotifications({ filter: 'unread' });
93-
}, [refreshModerationNotifications, refreshPaymentNotifications]);
106+
refreshReportNotifications({ filter: 'unread' });
107+
}, [refreshModerationNotifications, refreshPaymentNotifications, refreshReportNotifications]);
94108

95109
// Handle clearing all notifications, including moderation ones
96110
const handleClearAll = useCallback(() => {
@@ -108,21 +122,29 @@ export const NotificationsDropdown: React.FC = () => {
108122
// Check specifically for unread notifications
109123
const hasUnreadModerationNotifications = moderationNotifications.some(notification => !notification.is_read);
110124
const hasUnreadPaymentNotifications = paymentNotifications.some(notification => !notification.is_read);
111-
const hasUnreadNotifications = hasUnreadModerationNotifications || hasUnreadPaymentNotifications;
125+
const hasUnreadReportNotifications = reportNotifications.some(notification => !notification.is_read);
126+
const hasUnreadNotifications = hasUnreadModerationNotifications || hasUnreadPaymentNotifications || hasUnreadReportNotifications;
112127

113128
// Count unread notifications
114129
const unreadModerationCount = moderationNotifications.filter(notification => !notification.is_read).length;
115130
const unreadPaymentCount = paymentNotifications.filter(notification => !notification.is_read).length;
116-
const totalUnreadCount = unreadModerationCount + unreadPaymentCount;
131+
const unreadReportCount = reportNotifications.filter(notification => !notification.is_read).length;
132+
const totalUnreadCount = unreadModerationCount + unreadPaymentCount + unreadReportCount;
117133

118134
// Handle clearing all payment notifications
119135
const handleClearAllPayments = useCallback(() => {
120136
return markAllPaymentsAsRead();
121137
}, [markAllPaymentsAsRead]);
122138

139+
// Handle clearing all report notifications
140+
const handleClearAllReports = useCallback(() => {
141+
return markAllReportsAsRead();
142+
}, [markAllReportsAsRead]);
143+
123144
// Get translated tab names
124145
const moderationLabel = t('moderation.notifications.title', 'Moderation');
125146
const paymentsLabel = t('payment.notifications.title', 'Payments');
147+
const reportsLabel = t('report.notifications.title', 'Reports');
126148

127149
// Custom tab style to ensure content overflow is handled correctly
128150
const tabContentStyle = {
@@ -186,6 +208,30 @@ export const NotificationsDropdown: React.FC = () => {
186208
</div>
187209
),
188210
},
211+
{
212+
key: '3',
213+
label: (
214+
<span>
215+
{reportsLabel}
216+
{unreadReportCount > 0 && (
217+
<BaseBadge count={unreadReportCount} size="small" style={{ marginLeft: '5px' }} />
218+
)}
219+
</span>
220+
),
221+
children: (
222+
<div style={tabContentStyle}>
223+
<ReportNotificationsOverlay
224+
notifications={reportNotifications}
225+
markAsRead={markReportAsRead}
226+
markAllAsRead={handleClearAllReports}
227+
onRefresh={() => {
228+
refreshReportNotifications({ filter: 'unread' });
229+
return Promise.resolve();
230+
}}
231+
/>
232+
</div>
233+
),
234+
},
189235
];
190236

191237
return (
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import React, { useCallback } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { Link } from 'react-router-dom';
4+
import { BaseNotification } from '@app/components/common/BaseNotification/BaseNotification';
5+
import { BaseButton } from '@app/components/common/BaseButton/BaseButton';
6+
import { BaseSpace } from '@app/components/common/BaseSpace/BaseSpace';
7+
import { BaseRow } from '@app/components/common/BaseRow/BaseRow';
8+
import { BaseCol } from '@app/components/common/BaseCol/BaseCol';
9+
import { ReportNotification } from '@app/api/reportNotifications.api';
10+
import { WarningOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
11+
import * as S from '../NotificationsOverlay/NotificationsOverlay.styles';
12+
13+
interface ReportNotificationsOverlayProps {
14+
notifications: ReportNotification[];
15+
markAsRead: (id: number) => Promise<void>;
16+
markAllAsRead: () => Promise<void>;
17+
onRefresh: () => Promise<void>;
18+
}
19+
20+
export const ReportNotificationsOverlay: React.FC<ReportNotificationsOverlayProps> = ({
21+
notifications,
22+
markAsRead,
23+
markAllAsRead,
24+
onRefresh,
25+
...props
26+
}) => {
27+
const { t } = useTranslation();
28+
29+
const formatDate = (dateString: string): string => {
30+
const date = new Date(dateString);
31+
return date.toLocaleString();
32+
};
33+
34+
const getReportTypeIcon = (type: string) => {
35+
switch (type) {
36+
case 'nudity':
37+
case 'illegal':
38+
return <ExclamationCircleOutlined />;
39+
case 'spam':
40+
case 'profanity':
41+
case 'impersonation':
42+
return <WarningOutlined />;
43+
default:
44+
return <WarningOutlined />;
45+
}
46+
};
47+
48+
const handleMarkAllAsRead = useCallback(() => {
49+
markAllAsRead();
50+
}, [markAllAsRead]);
51+
52+
const noticesList = notifications.map((notification) => (
53+
<BaseNotification
54+
key={notification.id}
55+
type="warning"
56+
title={
57+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
58+
<span style={{
59+
fontSize: '0.7rem',
60+
padding: '2px 6px',
61+
background: 'rgba(var(--warning-rgb-color), 0.1)',
62+
color: 'var(--warning-color)',
63+
borderRadius: '10px',
64+
textTransform: 'uppercase'
65+
}}>
66+
{notification.report_type}
67+
</span>
68+
<span>
69+
{t('report.notifications.reportedContent', 'Reported Content')}
70+
</span>
71+
<span style={{
72+
fontSize: '0.7rem',
73+
padding: '2px 6px',
74+
background: 'rgba(var(--warning-rgb-color), 0.1)',
75+
color: 'var(--warning-color)',
76+
borderRadius: '10px'
77+
}}>
78+
{notification.report_count}
79+
</span>
80+
</div>
81+
}
82+
description={
83+
<div>
84+
<div style={{ fontSize: '0.85rem', color: 'var(--text-light-color)', marginBottom: '4px' }}>
85+
{formatDate(notification.created_at)}
86+
</div>
87+
88+
<div style={{ marginBottom: '8px' }}>
89+
<strong>{t('report.notifications.reason', 'Reason')}: </strong>
90+
{notification.report_content}
91+
</div>
92+
93+
<div style={{ marginBottom: '4px' }}>
94+
<strong>{t('report.notifications.reporter', 'Reporter')}: </strong>
95+
<span style={{ fontFamily: 'monospace' }}>
96+
{notification.reporter_pubkey.substring(0, 10)}...
97+
</span>
98+
</div>
99+
100+
<div style={{ marginBottom: '8px' }}>
101+
<strong>{t('report.notifications.author', 'Content Author')}: </strong>
102+
<span style={{ fontFamily: 'monospace' }}>
103+
{notification.pubkey.substring(0, 10)}...
104+
</span>
105+
</div>
106+
107+
{!notification.is_read && (
108+
<BaseButton
109+
type="link"
110+
size="small"
111+
onClick={() => markAsRead(notification.id)}
112+
style={{ padding: '4px 0', height: 'auto', marginTop: '4px' }}
113+
>
114+
{t('report.notifications.markAsRead', 'Mark as read')}
115+
</BaseButton>
116+
)}
117+
118+
<div style={{ marginTop: '4px' }}>
119+
<Link to="/report-notifications" style={{ fontSize: '0.85rem' }}>
120+
{t('report.notifications.viewDetails', 'View details')}
121+
</Link>
122+
</div>
123+
</div>
124+
}
125+
/>
126+
));
127+
128+
return (
129+
<S.NoticesOverlayMenu {...props}>
130+
<BaseRow gutter={[20, 20]}>
131+
<BaseCol span={24}>
132+
{notifications.length > 0 ? (
133+
<S.NotificationsList>
134+
<BaseSpace direction="vertical" size={10} split={<S.SplitDivider />}>
135+
{noticesList}
136+
</BaseSpace>
137+
</S.NotificationsList>
138+
) : (
139+
<div style={{ textAlign: 'center', padding: '20px 0' }}>
140+
<div style={{ fontSize: '24px', marginBottom: '8px' }}>🚨</div>
141+
<S.Text style={{ display: 'block', marginBottom: '12px', fontWeight: 500 }}>
142+
{t('report.notifications.noNotifications', 'No content reports')}
143+
</S.Text>
144+
<S.Text style={{ display: 'block', color: 'var(--text-light-color)', fontSize: '0.85rem' }}>
145+
{t('report.notifications.emptyDescription', 'Reports from users will appear here when content is flagged')}
146+
</S.Text>
147+
</div>
148+
)}
149+
</BaseCol>
150+
<BaseCol span={24}>
151+
<BaseRow gutter={[10, 10]}>
152+
{notifications.some(n => !n.is_read) && (
153+
<BaseCol span={24}>
154+
<S.Btn type="ghost" onClick={handleMarkAllAsRead}>
155+
{t('report.notifications.readAll', 'Mark all as read')}
156+
</S.Btn>
157+
</BaseCol>
158+
)}
159+
<BaseCol span={24}>
160+
<S.Btn type="ghost" onClick={onRefresh}>
161+
{t('report.notifications.refresh', 'Refresh')}
162+
</S.Btn>
163+
</BaseCol>
164+
<BaseCol span={24}>
165+
<S.Btn type="link">
166+
<Link to="/report-notifications">{t('report.notifications.viewAll', 'View all')}</Link>
167+
</S.Btn>
168+
</BaseCol>
169+
</BaseRow>
170+
</BaseCol>
171+
</BaseRow>
172+
</S.NoticesOverlayMenu>
173+
);
174+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { ReportNotificationsOverlay } from './ReportNotificationsOverlay';
2+
3+
export default ReportNotificationsOverlay;

0 commit comments

Comments
 (0)