diff --git a/frontend/src/app/messages/page.tsx b/frontend/src/app/messages/page.tsx
index 50d13b7f..31bd6032 100644
--- a/frontend/src/app/messages/page.tsx
+++ b/frontend/src/app/messages/page.tsx
@@ -5,7 +5,7 @@ import { AppShell } from "../../components/AppShell";
import { apiClient } from "@/lib/api";
import { resolveMediaUrl } from "@/lib/url";
import type { Chat } from "@/types/api";
-import { Search, MessageCircle } from "lucide-react";
+import { Search, MessageCircle, Pin, BellOff } from "lucide-react";
import Link from "next/link";
import Image from "next/image";
import { useUser } from "@/contexts/UserContext";
@@ -236,7 +236,17 @@ export default function MessagesPage() {
const filteredChats = useMemo(() => {
const q = search.trim().toLowerCase();
- const sorted = [...chats].sort((a, b) => getChatSortTimestamp(b) - getChatSortTimestamp(a));
+
+ // Сортировка: закрепленные сверху, потом по времени последнего сообщения
+ const sorted = [...chats].sort((a, b) => {
+ // Сначала сортируем по закрепленности
+ const aPinned = a.is_pinned ? 1 : 0;
+ const bPinned = b.is_pinned ? 1 : 0;
+ if (aPinned !== bPinned) return bPinned - aPinned;
+
+ // Затем по времени последнего сообщения
+ return getChatSortTimestamp(b) - getChatSortTimestamp(a);
+ });
if (!q) return sorted;
@@ -284,7 +294,11 @@ export default function MessagesPage() {
0
+ ? "border-sky-100 bg-sky-50/30"
+ : "border-transparent"
+ }`}
>
@@ -305,13 +319,30 @@ export default function MessagesPage() {
{isChatOnline(chat, user?.id) ? (
) : null}
+ {(chat.unread_count ?? 0) > 0 ? (
+
+ {chat.unread_count! > 99 ? '99+' : chat.unread_count}
+
+ ) : null}
-
{chatTitle}
+
+
0 ? "font-bold text-gray-900" : "font-semibold text-gray-900"
+ }`}>{chatTitle}
+ {chat.is_pinned ? (
+
+ ) : null}
+ {chat.notifications_enabled === false ? (
+
+ ) : null}
+
{formatTime(chat.last_message?.created_at)}
-
+
0 ? "font-medium text-gray-700" : "text-gray-500"
+ }`}>
{chat.last_message?.content || "Нет сообщений"}
diff --git a/frontend/src/app/notifications/page.tsx b/frontend/src/app/notifications/page.tsx
index 395f5157..276dfe45 100644
--- a/frontend/src/app/notifications/page.tsx
+++ b/frontend/src/app/notifications/page.tsx
@@ -1,23 +1,80 @@
'use client';
import { AppShell } from '@/components/AppShell';
-import { useNotifications } from '@/hooks/useApi';
+import { apiClient } from '@/lib/api';
import { Bell, Check, CheckCheck, Filter, Search, Trash2, Settings } from 'lucide-react';
-import { useState, useMemo } from 'react';
+import { useState, useMemo, useEffect } from 'react';
import { formatDistanceToNow } from 'date-fns';
import { ru } from 'date-fns/locale/ru';
import { getVerbCategory, getVerbName } from '@/lib/verbTranslations';
import Link from 'next/link';
export default function NotificationsPage() {
- const { notifications: notificationsData, loading, markAsRead, markAllAsRead, deleteNotification, deleteAllRead } = useNotifications();
- const notifications = Array.isArray(notificationsData) ? notificationsData : [];
+ // Локальное состояние для ВСЕХ уведомлений (не только непрочитанных)
+ const [notifications, setNotifications] = useState
([]);
+ const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [filterRead, setFilterRead] = useState<'all' | 'unread' | 'read'>('all');
const [filterCategory, setFilterCategory] = useState('');
const [filtersOpen, setFiltersOpen] = useState(false);
+ // Загрузка всех уведомлений (100 последних)
+ useEffect(() => {
+ async function fetchAllNotifications() {
+ try {
+ const data = await apiClient.getNotifications({ page_size: 100 });
+ const notifs = data.notifications || data.results || data;
+ setNotifications(Array.isArray(notifs) ? notifs : []);
+ } catch (err) {
+ console.error('Ошибка загрузки уведомлений:', err);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ fetchAllNotifications();
+ }, []);
+
+ // Функции для операций с уведомлениями
+ const markAsRead = async (id: number) => {
+ try {
+ await apiClient.markNotificationAsRead(id);
+ setNotifications(prev =>
+ prev.map(n => (n.id === id ? { ...n, is_read: true } : n))
+ );
+ } catch (err) {
+ console.error('Ошибка отметки уведомления:', err);
+ }
+ };
+
+ const markAllAsRead = async () => {
+ try {
+ await apiClient.markAllNotificationsAsRead();
+ setNotifications(prev => prev.map(n => ({ ...n, is_read: true })));
+ } catch (err) {
+ console.error('Ошибка отметки всех уведомлений:', err);
+ }
+ };
+
+ const deleteNotification = async (id: number) => {
+ try {
+ await apiClient.deleteNotification(id);
+ setNotifications(prev => prev.filter(n => n.id !== id));
+ } catch (err) {
+ console.error('Ошибка удаления уведомления:', err);
+ }
+ };
+
+ const deleteAllRead = async () => {
+ try {
+ await apiClient.deleteAllReadNotifications();
+ setNotifications(prev => prev.filter(n => !n.is_read));
+ } catch (err) {
+ console.error('Ошибка удаления прочитанных:', err);
+ }
+ };
+
// Уникальные категории
const categories = useMemo(() => {
const cats = new Set();
diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx
index ad075928..f02b691f 100644
--- a/frontend/src/components/AppShell.tsx
+++ b/frontend/src/components/AppShell.tsx
@@ -250,7 +250,8 @@ function LeftNavContent({ onNavigate }: LeftNavContentProps) {
const counts: Record = {};
notifications.forEach((n: any) => {
- if (!n.category || n.is_read) return; // Только непрочитанные
+ // Пропускаем прочитанные
+ if (!n.category || n.is_read) return;
const category = getVerbCategory(n.category);
counts[category] = (counts[category] || 0) + 1;
});
diff --git a/frontend/src/components/NotificationCenter.tsx b/frontend/src/components/NotificationCenter.tsx
index aa1af224..753dd6af 100644
--- a/frontend/src/components/NotificationCenter.tsx
+++ b/frontend/src/components/NotificationCenter.tsx
@@ -53,10 +53,7 @@ export function NotificationCenter({ variant = 'default', isOpen: externalIsOpen
}, [isOpen]);
const handleNotificationClick = async (notification: any) => {
- // v2 API использует unread вместо is_read (инвертированное)
- const isUnread = notification.unread ?? !notification.is_read;
-
- if (isUnread) {
+ if (!notification.is_read) {
await markAsRead(notification.id);
}
@@ -135,8 +132,7 @@ export function NotificationCenter({ variant = 'default', isOpen: externalIsOpen
) : (
{notifications.slice(0, 20).map((notification) => {
- // v2 API: unread вместо is_read (инвертированное)
- const isUnread = notification.unread ?? !notification.is_read;
+ const isUnread = !notification.is_read;
// v2 API: timestamp вместо created_at
const timestamp = notification.timestamp || notification.created_at;
// v2 API: verb + description вместо title + message
@@ -212,10 +208,7 @@ export function NotificationPanel({ onClose }: { onClose?: () => void }) {
const notifications = Array.isArray(notificationsData) ? notificationsData : [];
const handleNotificationClick = async (notification: any) => {
- // v2 API использует unread вместо is_read (инвертированное)
- const isUnread = notification.unread ?? !notification.is_read;
-
- if (isUnread) {
+ if (!notification.is_read) {
await markAsRead(notification.id);
}
if (notification.action_url) {
@@ -262,8 +255,7 @@ export function NotificationPanel({ onClose }: { onClose?: () => void }) {
) : (
{notifications.slice(0, 20).map((notification) => {
- // v2 API: unread вместо is_read (инвертированное)
- const isUnread = notification.unread ?? !notification.is_read;
+ const isUnread = !notification.is_read;
// v2 API: timestamp вместо created_at
const timestamp = notification.timestamp || notification.created_at;
// v2 API: verb + description вместо title + message
diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts
index 57fa2553..553f76f3 100644
--- a/frontend/src/hooks/useApi.ts
+++ b/frontend/src/hooks/useApi.ts
@@ -179,13 +179,18 @@ export function useNotifications() {
useEffect(() => {
async function fetchNotifications() {
try {
- const data = await apiClient.getNotifications();
- // Бэкенд возвращает { notifications: [...] }
+ // Для бейджей загружаем только непрочитанные (первые 50)
+ const data = await apiClient.getNotifications({
+ page_size: 50,
+ unread_only: true
+ });
+ // Бэкенд возвращает { notifications: [...], unread_count: N }
const notifs = data.notifications || data.results || data;
// Убеждаемся что notifs это массив
const notificationsArray = Array.isArray(notifs) ? notifs : [];
setNotifications(notificationsArray);
- setUnreadCount(notificationsArray.filter((n: any) => !n.is_read).length);
+ // Используем unread_count из API или считаем сами
+ setUnreadCount(data.unread_count ?? notificationsArray.length);
} catch (err) {
setError(err as Error);
setNotifications([]); // В случае ошибки устанавливаем пустой массив
@@ -256,7 +261,7 @@ export function useNotifications() {
});
// Обновляем счетчик если непрочитанное
- if (data.notification.unread) {
+ if (!data.notification.is_read) {
setUnreadCount(prev => prev + 1);
}
}
@@ -311,7 +316,7 @@ export function useNotifications() {
try {
await apiClient.markNotificationAsRead(id);
setNotifications(prev =>
- prev.map(n => (n.id === id ? { ...n, is_read: true, unread: false } : n))
+ prev.map(n => (n.id === id ? { ...n, is_read: true } : n))
);
setUnreadCount(prev => Math.max(0, prev - 1));
} catch (err) {
@@ -322,7 +327,7 @@ export function useNotifications() {
const markAllAsRead = async () => {
try {
await apiClient.markAllNotificationsAsRead();
- setNotifications(prev => prev.map(n => ({ ...n, is_read: true, unread: false })));
+ setNotifications(prev => prev.map(n => ({ ...n, is_read: true })));
setUnreadCount(0);
} catch (err) {
console.error('Failed to mark all notifications as read:', err);
@@ -335,7 +340,7 @@ export function useNotifications() {
setNotifications(prev => prev.filter(n => n.id !== id));
// Если удаленное было непрочитанным, уменьшаем счетчик
const deletedNotif = notifications.find(n => n.id === id);
- if (deletedNotif && (deletedNotif.unread ?? !deletedNotif.is_read)) {
+ if (deletedNotif && !deletedNotif.is_read) {
setUnreadCount(prev => Math.max(0, prev - 1));
}
} catch (err) {
@@ -346,7 +351,7 @@ export function useNotifications() {
const deleteAllRead = async () => {
try {
const result = await apiClient.deleteAllReadNotifications();
- setNotifications(prev => prev.filter(n => n.unread ?? !n.is_read));
+ setNotifications(prev => prev.filter(n => !n.is_read));
return result.count;
} catch (err) {
console.error('Failed to delete all read notifications:', err);
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 8505bf29..98610dbe 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -787,8 +787,20 @@ class ApiClient {
}
// Уведомления
- async getNotifications(): Promise {
- return this.request('/api/v1/notifications/');
+ async getNotifications(params?: { page?: number; page_size?: number; unread_only?: boolean }): Promise {
+ const queryParams = new URLSearchParams();
+ if (params?.page) queryParams.append('page', String(params.page));
+ if (params?.page_size) queryParams.append('page_size', String(params.page_size));
+ if (params?.unread_only) queryParams.append('unread_only', 'true');
+
+ const url = queryParams.toString()
+ ? `/api/v1/notifications/?${queryParams.toString()}`
+ : '/api/v1/notifications/';
+ return this.request(url);
+ }
+
+ async getUnreadNotificationsCount(): Promise<{ count: number }> {
+ return this.request('/api/v1/notifications/count/');
}
async markNotificationAsRead(id: number): Promise {