diff --git a/backend/api/v1/communications/serializers.py b/backend/api/v1/communications/serializers.py
index 9b86288d..258011dc 100644
--- a/backend/api/v1/communications/serializers.py
+++ b/backend/api/v1/communications/serializers.py
@@ -110,6 +110,8 @@ class ChatDetailSerializer(serializers.ModelSerializer):
participant_details = serializers.SerializerMethodField()
memberships = ChatMembershipSerializer(many=True, read_only=True, source='chatmembership_set')
user_settings = serializers.SerializerMethodField()
+ is_pinned = serializers.SerializerMethodField()
+ notifications_enabled = serializers.SerializerMethodField()
last_read_message_id = serializers.SerializerMethodField()
class Meta:
@@ -119,7 +121,8 @@ class ChatDetailSerializer(serializers.ModelSerializer):
'created_by', 'created_at', 'is_main',
'include_all_employees', 'department',
'participants', 'participant_details',
- 'memberships', 'user_settings', 'last_read_message_id'
+ 'memberships', 'user_settings', 'is_pinned',
+ 'notifications_enabled', 'last_read_message_id'
]
read_only_fields = ['created_at', 'created_by']
@@ -141,6 +144,22 @@ class ChatDetailSerializer(serializers.ModelSerializer):
return ChatUserSettingsSerializer(settings).data
return {'is_pinned': False, 'notifications_enabled': True}
+ def get_is_pinned(self, obj):
+ """Закреплен ли чат для текущего пользователя"""
+ request = self.context.get('request')
+ if request and request.user.is_authenticated:
+ settings = obj.user_settings.filter(user=request.user).first()
+ return settings.is_pinned if settings else False
+ return False
+
+ def get_notifications_enabled(self, obj):
+ """Включены ли уведомления для текущего пользователя"""
+ request = self.context.get('request')
+ if request and request.user.is_authenticated:
+ settings = obj.user_settings.filter(user=request.user).first()
+ return settings.notifications_enabled if settings else True
+ return True
+
def get_last_read_message_id(self, obj):
"""ID последнего прочитанного сообщения для текущего пользователя"""
from communications.models import ChatReadState
diff --git a/backend/communications/notifications/config.py b/backend/communications/notifications/config.py
index e359038a..a1a07cc9 100644
--- a/backend/communications/notifications/config.py
+++ b/backend/communications/notifications/config.py
@@ -69,19 +69,17 @@ class ActionURLs:
def chat_detail(chat_id: int) -> str:
"""
Возвращает URL-адрес чата.
-
- TODO: реализовать прямые ссылки на конкретные чаты
"""
- return f'/messages?chat={chat_id}'
+ return f'/messages/{chat_id}'
@staticmethod
def message_detail(chat_id: int, message_id: int) -> str:
"""
Возвращает URL-адрес сообщения.
- TODO: реализовать прямые ссылки на конкретные сообщения
+ В будущем можно добавить прокрутку к конкретному сообщению.
"""
- return f'/messages?chat={chat_id}&message={message_id}'
+ return f'/messages/{chat_id}?message={message_id}'
# ===== Вспомогательные функции =====
diff --git a/backend/communications/notifications/handlers.py b/backend/communications/notifications/handlers.py
index cae8f8b9..11a4e586 100644
--- a/backend/communications/notifications/handlers.py
+++ b/backend/communications/notifications/handlers.py
@@ -101,7 +101,7 @@ def notify_new_message(message):
verb=NotificationVerbs.MENTION,
action_object=message,
description=truncate_message(content, 100),
- action_url=ActionURLs.MESSAGES,
+ action_url=ActionURLs.chat_detail(chat.id),
data={
'title': MessageTemplates.mention(author_name),
'chat_id': chat.id,
@@ -126,7 +126,7 @@ def notify_new_message(message):
verb=NotificationVerbs.REPLY,
action_object=message,
description=truncate_message(content, 100),
- action_url=ActionURLs.MESSAGES,
+ action_url=ActionURLs.chat_detail(chat.id),
data={
'title': MessageTemplates.reply(author_name),
'chat_id': chat.id,
@@ -177,7 +177,7 @@ def notify_new_message(message):
verb=notification_verb,
action_object=message,
description=truncate_message(content, max_length),
- action_url=ActionURLs.MESSAGES,
+ action_url=ActionURLs.chat_detail(chat.id),
data={**metadata, 'title': title},
)
@@ -210,7 +210,7 @@ def notify_chat_added(chat, new_users, added_by=None):
verb=NotificationVerbs.ADDED_TO_CHAT,
action_object=chat,
description=MessageTemplates.added_to_chat(get_chat_name(chat)),
- action_url=ActionURLs.MESSAGES,
+ action_url=ActionURLs.chat_detail(chat.id),
data={
'title': MessageTemplates.added_to_chat_title(),
'chat_id': chat.id,
diff --git a/backend/eusrr_backend/urls.py b/backend/eusrr_backend/urls.py
index 858924f3..a958e7b8 100644
--- a/backend/eusrr_backend/urls.py
+++ b/backend/eusrr_backend/urls.py
@@ -3,18 +3,17 @@ from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
-# Импорт Service Worker view из приложения notifications
-from notifications.views import serve_service_worker
-
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include(("api.urls", "api"), namespace="api")),
- # Service Worker должен быть в корне для правильного scope
- path("sw.js", serve_service_worker, name="sw"),
- # django-filer URLs для обработки приватных файлов (с проверкой прав доступа)
+ # django-filer URLs для обработки приватных файлов
+ # (с проверкой прав доступа)
path("", include("filer.server.urls")),
]
-# Публичные медиа файлы (без проверки прав) - только в режиме разработки
+# Публичные медиа файлы (без проверки прав)
+# только в режиме разработки
if settings.DEBUG:
- urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+ urlpatterns += static(
+ settings.MEDIA_URL, document_root=settings.MEDIA_ROOT
+ )
diff --git a/frontend/src/app/messages/[chatId]/page.tsx b/frontend/src/app/messages/[chatId]/page.tsx
index 1a7589ee..d78f53fe 100644
--- a/frontend/src/app/messages/[chatId]/page.tsx
+++ b/frontend/src/app/messages/[chatId]/page.tsx
@@ -3,7 +3,7 @@
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState, useCallback } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
-import { MessageCircle, Send, ArrowLeft, Paperclip, X, FileText, ChevronRight, ChevronDown, Reply, Pencil, Trash2, Smile } from "lucide-react";
+import { MessageCircle, Send, ArrowLeft, Paperclip, X, FileText, ChevronRight, ChevronDown, Reply, Pencil, Trash2, Smile, Pin, Bell, BellOff } from "lucide-react";
import { AppShell } from "../../../components/AppShell";
import { apiClient } from "@/lib/api";
import type { Chat, Message, MessageAttachment } from "@/types/api";
@@ -286,11 +286,23 @@ export default function MessageDialogPage() {
const [anchorRetryTick, setAnchorRetryTick] = useState(0);
const hasMoreNewerRef = useRef(false);
+ // Локальное состояние для настроек чата
+ const [isPinned, setIsPinned] = useState(false);
+ const [notificationsEnabled, setNotificationsEnabled] = useState(true);
+
// Синхронизируем ref с state
useEffect(() => {
hasMoreNewerRef.current = hasMoreNewer;
}, [hasMoreNewer]);
+ // Синхронизация настроек чата
+ useEffect(() => {
+ if (chat) {
+ setIsPinned(chat.is_pinned ?? false);
+ setNotificationsEnabled(chat.notifications_enabled ?? true);
+ }
+ }, [chat]);
+
const messagesById = useMemo(() => new Map(messages.map((m) => [m.id, m])), [messages]);
const selectedActionMessage = expandedReplyActionForId ? messagesById.get(expandedReplyActionForId) || null : null;
const selectedActionCanManage = Boolean(
@@ -999,6 +1011,40 @@ export default function MessageDialogPage() {
}
};
+ const handleTogglePin = async () => {
+ if (!chatId) return;
+
+ try {
+ const response = await apiClient.togglePinChat(chatId);
+ const newIsPinned = response.is_pinned ?? !isPinned;
+ setIsPinned(newIsPinned);
+
+ // Обновляем chat объект
+ if (chat) {
+ setChat({ ...chat, is_pinned: newIsPinned });
+ }
+ } catch (e) {
+ console.error("Ошибка переключения закрепления:", e);
+ }
+ };
+
+ const handleToggleNotifications = async () => {
+ if (!chatId) return;
+
+ try {
+ const response = await apiClient.toggleChatNotifications(chatId);
+ const newNotificationsEnabled = response.notifications_enabled ?? !notificationsEnabled;
+ setNotificationsEnabled(newNotificationsEnabled);
+
+ // Обновляем chat объект
+ if (chat) {
+ setChat({ ...chat, notifications_enabled: newNotificationsEnabled });
+ }
+ } catch (e) {
+ console.error("Ошибка переключения уведомлений:", e);
+ }
+ };
+
useEffect(() => {
const onWindowClick = (e: MouseEvent) => {
const target = e.target as HTMLElement | null;
@@ -1138,14 +1184,47 @@ export default function MessageDialogPage() {
-
-