---------------------
This commit is contained in:
parent
b0e88e8e8b
commit
96e375cd6f
@ -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
|
||||
|
||||
@ -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}'
|
||||
|
||||
|
||||
# ===== Вспомогательные функции =====
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/messages"
|
||||
aria-label="К списку чатов"
|
||||
title="К списку чатов"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full border border-gray-200 bg-white text-gray-600 transition hover:bg-gray-50 hover:text-sky-700"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Кнопка закрепления чата */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTogglePin}
|
||||
className={`flex h-10 w-10 items-center justify-center rounded-full border transition ${
|
||||
isPinned
|
||||
? "border-sky-500 bg-sky-50 text-sky-700 hover:bg-sky-100"
|
||||
: "border-gray-200 bg-white text-gray-600 hover:bg-gray-50 hover:text-sky-700"
|
||||
}`}
|
||||
title={isPinned ? "Открепить чат" : "Закрепить чат"}
|
||||
aria-label={isPinned ? "Открепить чат" : "Закрепить чат"}
|
||||
>
|
||||
<Pin size={16} className={isPinned ? "fill-current" : ""} />
|
||||
</button>
|
||||
|
||||
{/* Кнопка управления уведомлениями */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleNotifications}
|
||||
className={`flex h-10 w-10 items-center justify-center rounded-full border transition ${
|
||||
notificationsEnabled
|
||||
? "border-gray-200 bg-white text-gray-600 hover:bg-gray-50 hover:text-sky-700"
|
||||
: "border-amber-500 bg-amber-50 text-amber-700 hover:bg-amber-100"
|
||||
}`}
|
||||
title={notificationsEnabled ? "Отключить уведомления" : "Включить уведомления"}
|
||||
aria-label={notificationsEnabled ? "Отключить уведомления" : "Включить уведомления"}
|
||||
>
|
||||
{notificationsEnabled ? <Bell size={16} /> : <BellOff size={16} />}
|
||||
</button>
|
||||
|
||||
{/* Кнопка возврата к списку */}
|
||||
<Link
|
||||
href="/messages"
|
||||
aria-label="К списку чатов"
|
||||
title="К списку чатов"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full border border-gray-200 bg-white text-gray-600 transition hover:bg-gray-50 hover:text-sky-700"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
|
||||
@ -41,6 +41,8 @@ export const VERB_CATEGORIES: Record<string, string> = {
|
||||
|
||||
// Новости
|
||||
feed_new_post: 'Новости',
|
||||
feed_post_comment: 'Новости',
|
||||
feed_post_reaction: 'Новости',
|
||||
|
||||
// Системные
|
||||
system_announcement: 'Система',
|
||||
@ -83,6 +85,8 @@ export const VERB_NAMES: Record<string, string> = {
|
||||
|
||||
// Новости
|
||||
feed_new_post: 'Новая публикация',
|
||||
feed_post_comment: 'Комментарий к публикации',
|
||||
feed_post_reaction: 'Реакция на публикацию',
|
||||
|
||||
// Системные
|
||||
system_announcement: 'Системное объявление',
|
||||
|
||||
@ -367,6 +367,8 @@ export interface Chat {
|
||||
last_message?: Message;
|
||||
unread_count?: number;
|
||||
created_at: string;
|
||||
is_pinned?: boolean;
|
||||
notifications_enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user