Corp/backend/communications/MESSENGER_IMPROVEMENTS.md
Игорь Надеин ddf22c47da fix: автоматическое обновление JWT токенов
- ApiClient теперь обрабатывает 401 и 403 ошибки для обновления токена
- Добавлен JWTRefreshMiddleware для проактивного обновления токенов за 5 минут до истечения
- Добавлена настройка JWT_REFRESH_THRESHOLD_MINUTES (по умолчанию 5)
- Исправлена проблема с периодическими 403 ошибками после истечения access token
2025-11-20 18:52:29 +03:00

32 KiB
Raw Blame History

Улучшения модели мессенджера

1. Добавить базовые поля в модель Chat

class Chat(models.Model):
    # Существующие поля...
    type = models.CharField(max_length=16, choices=CHAT_TYPE_CHOICES)
    
    # НОВЫЕ ПОЛЯ:
    name = models.CharField(
        max_length=255, 
        blank=True, 
        verbose_name="Название чата",
        help_text="Для групповых/канальных чатов"
    )
    description = models.TextField(
        blank=True, 
        verbose_name="Описание чата"
    )
    avatar = models.ImageField(
        upload_to='chat_avatars/%Y/%m/', 
        null=True, 
        blank=True,
        verbose_name="Аватар чата"
    )
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        on_delete=models.SET_NULL,
        related_name='created_chats',
        verbose_name="Создатель"
    )
    
    # Для гибкой настройки участников
    include_all_employees = models.BooleanField(
        default=False,
        verbose_name="Включить всех сотрудников",
        help_text="Для анонсов и общих чатов"
    )

2. Создать модель для закрепления чатов (персональная настройка)

class ChatUserSettings(models.Model):
    """Персональные настройки пользователя для чата"""
    chat = models.ForeignKey(Chat, on_delete=models.CASCADE, related_name='user_settings')
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    
    # Закрепление
    is_pinned = models.BooleanField(default=False, verbose_name="Закреплен")
    pinned_at = models.DateTimeField(null=True, blank=True, verbose_name="Время закрепления")
    pin_order = models.IntegerField(default=0, verbose_name="Порядок закрепленных")
    
    # Уведомления
    notifications_enabled = models.BooleanField(default=True, verbose_name="Уведомления")
    
    # Кастомное название (если пользователь переименовал)
    custom_name = models.CharField(max_length=255, blank=True, verbose_name="Свое название")
    
    # Скрыть чат
    is_hidden = models.BooleanField(default=False, verbose_name="Скрыт")
    
    class Meta:
        unique_together = [('chat', 'user')]
        ordering = ['-is_pinned', '-pinned_at']

3. Расширить типы чатов

class Chat(models.Model):
    CHAT_TYPE_CHOICES = [
        ('private', 'Личный'),           # 1-на-1
        ('group', 'Групповой'),          # Произвольная группа сотрудников
        ('department', 'Отдел'),         # Привязан к отделу
        ('channel', 'Канал'),            # Только админы пишут, все читают
        ('announcement', 'Объявления'),  # Системный канал
    ]

4. Добавить роли участников

class ChatMembership(models.Model):
    """Участие в чате с ролями"""
    ROLE_CHOICES = [
        ('owner', 'Владелец'),
        ('admin', 'Администратор'),
        ('moderator', 'Модератор'),
        ('member', 'Участник'),
        ('readonly', 'Только чтение'),
    ]
    
    chat = models.ForeignKey(Chat, on_delete=models.CASCADE, related_name='memberships')
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='member')
    joined_at = models.DateTimeField(auto_now_add=True)
    invited_by = models.ForeignKey(
        settings.AUTH_USER_MODEL, 
        null=True, 
        on_delete=models.SET_NULL,
        related_name='+'
    )
    
    # Разрешения
    can_send_messages = models.BooleanField(default=True)
    can_add_members = models.BooleanField(default=False)
    can_edit_chat = models.BooleanField(default=False)
    
    class Meta:
        unique_together = [('chat', 'user')]

5. Расширить модель Message

class Message(models.Model):
    # Существующие поля...
    chat = models.ForeignKey(Chat, related_name="messages", on_delete=models.CASCADE)
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
    
    # НОВЫЕ ПОЛЯ:
    
    # Ответ на сообщение (упрощенная связь, детали в MessageReply)
    reply_to = models.ForeignKey(
        'self', 
        null=True, 
        blank=True, 
        on_delete=models.SET_NULL,
        related_name='direct_replies',
        help_text="Простая связь для быстрого доступа"
    )
    
    # Редактирование
    is_edited = models.BooleanField(default=False)
    edited_at = models.DateTimeField(null=True, blank=True)
    edit_history = models.JSONField(
        default=list,
        blank=True,
        help_text="История редактирования [{timestamp, old_content}, ...]"
    )
    
    # Удаление
    is_deleted = models.BooleanField(default=False)
    deleted_at = models.DateTimeField(null=True, blank=True)
    deleted_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        help_text="Кто удалил (может отличаться от автора)"
    )
    
    # Вложения
    has_attachments = models.BooleanField(default=False)
    
    # Системные сообщения (вступление, выход и т.д.)
    is_system = models.BooleanField(default=False)
    system_type = models.CharField(max_length=50, blank=True)
    # Типы: 'user_joined', 'user_left', 'chat_created', 'chat_renamed', 
    #       'user_promoted', 'user_demoted', 'settings_changed'
    system_metadata = models.JSONField(
        default=dict,
        blank=True,
        help_text="Дополнительные данные для системных сообщений"
    )
    
    # Важные/закрепленные
    is_pinned = models.BooleanField(default=False)
    pinned_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )
    pinned_at = models.DateTimeField(null=True, blank=True)
    
    # Реакции (можно хранить как JSON)
    reactions = models.JSONField(default=dict, blank=True)
    # Формат: {'👍': [user_id1, user_id2], '❤️': [user_id3]}
    
    # Флаги для специальных типов сообщений
    is_forwarded = models.BooleanField(default=False)
    is_cross_chat = models.BooleanField(
        default=False,
        help_text="Сообщение отправлено не участником чата"
    )
    
    # Треды (для организации обсуждений)
    thread_root = models.ForeignKey(
        'self',
        null=True,
        blank=True,
        on_delete=models.CASCADE,
        related_name='thread_messages',
        help_text="Корневое сообщение треда"
    )
    thread_reply_count = models.IntegerField(default=0)
    
    class Meta:
        ordering = ['created_at']
        indexes = [
            models.Index(fields=['chat', 'created_at']),
            models.Index(fields=['chat', 'is_deleted', 'created_at']),
            models.Index(fields=['author', 'created_at']),
            models.Index(fields=['thread_root', 'created_at']),
            models.Index(fields=['is_pinned', 'chat']),
        ]

6. Добавить модель вложений

class MessageAttachment(models.Model):
    ATTACHMENT_TYPE_CHOICES = [
        ('image', 'Изображение'),
        ('file', 'Файл'),
        ('video', 'Видео'),
        ('audio', 'Аудио'),
    ]
    
    message = models.ForeignKey(Message, on_delete=models.CASCADE, related_name='attachments')
    file = models.FileField(upload_to='chat_attachments/%Y/%m/%d/')
    file_type = models.CharField(max_length=20, choices=ATTACHMENT_TYPE_CHOICES)
    file_name = models.CharField(max_length=255)
    file_size = models.BigIntegerField()
    mime_type = models.CharField(max_length=100)
    uploaded_at = models.DateTimeField(auto_now_add=True)
    
    # Для изображений
    thumbnail = models.ImageField(upload_to='chat_thumbnails/%Y/%m/%d/', null=True, blank=True)

6a. Улучшить модель пересылки сообщений

class ForwardedMessage(models.Model):
    """Информация о пересланных сообщениях (поддержка цепочки пересылок)"""
    message = models.OneToOneField(
        Message, 
        on_delete=models.CASCADE, 
        related_name='forward_info',
        help_text="Текущее пересланное сообщение"
    )
    
    # Оригинальное сообщение (первое в цепочке)
    original_message = models.ForeignKey(
        Message, 
        on_delete=models.SET_NULL, 
        null=True, 
        related_name='forwarded_copies',
        help_text="Самое первое сообщение в цепочке пересылок"
    )
    
    # Непосредственный источник (откуда переслали)
    immediate_source = models.ForeignKey(
        Message,
        on_delete=models.SET_NULL,
        null=True,
        related_name='+',
        help_text="Сообщение, которое было непосредственно переслано"
    )
    
    original_chat = models.ForeignKey(
        Chat, 
        on_delete=models.SET_NULL, 
        null=True, 
        related_name='+'
    )
    original_author = models.ForeignKey(
        settings.AUTH_USER_MODEL, 
        on_delete=models.SET_NULL, 
        null=True, 
        related_name='+'
    )
    
    forwarded_at = models.DateTimeField(auto_now_add=True)
    forwarded_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='forwarded_messages',
        help_text="Кто переслал"
    )
    
    # Счетчик пересылок (для оригинала)
    forward_count = models.IntegerField(default=0, help_text="Сколько раз переслали")
    
    # Сохранить контент на случай удаления оригинала
    preserved_content = models.TextField(blank=True)
    preserved_author_name = models.CharField(max_length=255, blank=True)
    
    class Meta:
        indexes = [
            models.Index(fields=['original_message']),
            models.Index(fields=['forwarded_by', 'forwarded_at']),
        ]

6b. Добавить модель для отправки сообщений в чужие чаты

class CrossChatMessage(models.Model):
    """Сообщения, отправленные в чаты, где пользователь не является участником"""
    
    message = models.OneToOneField(
        Message,
        on_delete=models.CASCADE,
        related_name='cross_chat_info'
    )
    
    sender = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='cross_chat_messages',
        help_text="Отправитель (не член этого чата)"
    )
    
    target_chat = models.ForeignKey(
        Chat,
        on_delete=models.CASCADE,
        related_name='cross_messages',
        help_text="Чат-получатель"
    )
    
    # Разрешение на отправку
    approved = models.BooleanField(default=False, help_text="Одобрено модератором")
    approved_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        help_text="Кто одобрил"
    )
    approved_at = models.DateTimeField(null=True, blank=True)
    
    # Причина/контекст отправки
    reason = models.CharField(
        max_length=500,
        blank=True,
        help_text="Причина отправки сообщения в чужой чат"
    )
    
    # Режим отображения
    DISPLAY_MODE_CHOICES = [
        ('guest', 'Гостевое (с пометкой "Внешнее сообщение")'),
        ('forwarded', 'Как пересланное'),
        ('announcement', 'Как объявление'),
    ]
    display_mode = models.CharField(
        max_length=20,
        choices=DISPLAY_MODE_CHOICES,
        default='guest'
    )
    
    sent_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        indexes = [
            models.Index(fields=['target_chat', 'approved']),
            models.Index(fields=['sender', 'sent_at']),
        ]

7. Улучшить ChatReadState

class ChatReadState(models.Model):
    chat = models.ForeignKey("Chat", on_delete=models.CASCADE, related_name="read_states")
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    
    last_read_at = models.DateTimeField(null=True, blank=True)
    last_read_message = models.ForeignKey(
        Message, 
        null=True, 
        blank=True, 
        on_delete=models.SET_NULL,
        help_text="Последнее прочитанное сообщение"
    )
    
    # Статус набора текста
    is_typing = models.BooleanField(default=False)
    typing_updated_at = models.DateTimeField(null=True, blank=True)
    
    updated_at = models.DateTimeField(auto_now=True)

8. Добавить модель для ответов на сообщения (расширенная)

class MessageReply(models.Model):
    """Дополнительная информация об ответе на сообщение"""
    
    message = models.OneToOneField(
        Message,
        on_delete=models.CASCADE,
        related_name='reply_info',
        help_text="Сообщение-ответ"
    )
    
    replied_to = models.ForeignKey(
        Message,
        on_delete=models.SET_NULL,
        null=True,
        related_name='replies',
        help_text="Исходное сообщение, на которое отвечают"
    )
    
    # Для ответов через чаты (если отвечаешь в другом чате)
    is_cross_chat_reply = models.BooleanField(default=False)
    original_chat = models.ForeignKey(
        Chat,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        help_text="Чат оригинального сообщения (если отличается)"
    )
    
    # Сохраненный контент на случай удаления
    preserved_text = models.TextField(blank=True)
    preserved_author_name = models.CharField(max_length=255, blank=True)
    
    # Тип ответа
    REPLY_TYPE_CHOICES = [
        ('inline', 'Обычный ответ'),
        ('quote', 'Цитирование'),
        ('thread', 'В треде'),
    ]
    reply_type = models.CharField(
        max_length=20,
        choices=REPLY_TYPE_CHOICES,
        default='inline'
    )
    
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        indexes = [
            models.Index(fields=['replied_to']),
        ]

9. Добавить права доступа к чатам

class ChatAccessPermission(models.Model):
    """Права пользователей на отправку сообщений в чужие чаты"""
    
    chat = models.ForeignKey(
        Chat,
        on_delete=models.CASCADE,
        related_name='access_permissions'
    )
    
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='chat_permissions'
    )
    
    # Типы доступа
    PERMISSION_TYPE_CHOICES = [
        ('send_messages', 'Отправка сообщений'),
        ('read_only', 'Только чтение'),
        ('send_with_approval', 'Отправка с модерацией'),
    ]
    permission_type = models.CharField(
        max_length=30,
        choices=PERMISSION_TYPE_CHOICES,
        default='send_with_approval'
    )
    
    # Кто выдал право
    granted_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        related_name='+'
    )
    granted_at = models.DateTimeField(auto_now_add=True)
    
    # Срок действия
    expires_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text="Когда истекает право доступа"
    )
    
    is_active = models.BooleanField(default=True)
    
    class Meta:
        unique_together = [('chat', 'user')]
        indexes = [
            models.Index(fields=['chat', 'is_active']),
            models.Index(fields=['user', 'is_active']),
        ]

9. Убрать ограничения для гибкости

Удалить constraint'ы:

  • unique_main_global_chat
  • unique_main_department_chat

Заменить на поле is_default для автоматического выбора чата по умолчанию.

10. Добавить настройки чата

class ChatSettings(models.Model):
    """Глобальные настройки чата"""
    chat = models.OneToOneField(Chat, on_delete=models.CASCADE, related_name='settings')
    
    # Кто может писать
    only_admins_can_post = models.BooleanField(default=False)
    
    # Кто может приглашать
    members_can_invite = models.BooleanField(default=True)
    
    # Автоудаление сообщений
    auto_delete_messages_after = models.IntegerField(
        null=True, 
        blank=True,
        help_text="Автоматически удалять сообщения старше N дней"
    )
    
    # Максимум участников
    max_members = models.IntegerField(null=True, blank=True)
    
    # Запретить личные сообщения между участниками
    disable_dm_between_members = models.BooleanField(default=False)

Пример миграции (поэтапно)

Этап 1: Базовые поля

# Добавить name, description, avatar в Chat
python manage.py makemigrations
python manage.py migrate

Этап 2: Персональные настройки

# Создать ChatUserSettings
python manage.py makemigrations
python manage.py migrate

Этап 3: Роли участников

# Создать ChatMembership
python manage.py makemigrations
python manage.py migrate

Этап 4: Расширенные сообщения

# Добавить поля в Message, создать MessageAttachment
python manage.py makemigrations
python manage.py migrate

Итоговая архитектура

Chat (базовая информация + настройки)
├── ChatUserSettings (персональные настройки каждого пользователя)
├── ChatMembership (роли и права участников)
├── ChatSettings (глобальные настройки чата)
├── ChatAccessPermission (права внешних пользователей на отправку сообщений)
├── Message
│   ├── MessageAttachment (файлы, изображения)
│   ├── ForwardedMessage (информация о пересылке с цепочкой)
│   ├── MessageReply (расширенная информация об ответах)
│   └── CrossChatMessage (сообщения из чужих чатов)
└── ChatReadState (статус прочтения)

Новые возможности

1. Пересылка сообщений

  • Простая пересылка: отправить сообщение в другой чат с указанием источника
  • Цепочка пересылок: отслеживание всей истории пересылки (оригинал → промежуточные → текущее)
  • Счетчик пересылок: сколько раз сообщение было переслано
  • Сохранение контента: если оригинал удален, контент сохраняется

2. Ответы на сообщения

  • Внутри чата: стандартный reply с цитированием
  • Кросс-чат ответы: ответить на сообщение из другого чата
  • Типы ответов: inline (обычный), quote (цитата), thread (тред)
  • Сохранение контекста: текст и автор сохраняются при удалении оригинала

3. Отправка в чужие чаты

  • Режимы доступа:
    • send_messages - свободная отправка
    • read_only - только чтение
    • send_with_approval - модерация перед публикацией
  • Типы отображения:
    • guest - с пометкой "Внешнее сообщение"
    • forwarded - как пересланное
    • announcement - как объявление
  • Права доступа: временные/постоянные, с указанием срока действия
  • Модерация: одобрение администраторами чата

4. Треды (обсуждения)

  • Организация дискуссий в отдельные ветки
  • Счетчик ответов в треде
  • Корневое сообщение треда

Примеры использования

Пересылка сообщения

# В views или consumers
def forward_message(original_message_id, target_chat_id, user):
    original = Message.objects.get(id=original_message_id)
    
    # Создаем новое сообщение
    forwarded_msg = Message.objects.create(
        chat_id=target_chat_id,
        author=user,
        content=original.content,
        is_forwarded=True,
        has_attachments=original.has_attachments
    )
    
    # Копируем вложения
    for att in original.attachments.all():
        MessageAttachment.objects.create(
            message=forwarded_msg,
            file=att.file,
            file_type=att.file_type,
            file_name=att.file_name,
            file_size=att.file_size,
            mime_type=att.mime_type
        )
    
    # Находим оригинал в цепочке
    if hasattr(original, 'forward_info'):
        # Это уже пересланное - берем самый первый оригинал
        root_original = original.forward_info.original_message
    else:
        root_original = original
    
    # Создаем информацию о пересылке
    ForwardedMessage.objects.create(
        message=forwarded_msg,
        original_message=root_original,
        immediate_source=original,
        original_chat=root_original.chat,
        original_author=root_original.author,
        forwarded_by=user,
        preserved_content=root_original.content,
        preserved_author_name=root_original.author.get_full_name()
    )
    
    # Увеличиваем счетчик
    if hasattr(root_original, 'forward_info'):
        root_original.forward_info.forward_count += 1
        root_original.forward_info.save()
    
    return forwarded_msg

Отправка в чужой чат

def send_to_external_chat(sender, chat_id, content, reason=''):
    chat = Chat.objects.get(id=chat_id)
    
    # Проверяем права доступа
    permission = ChatAccessPermission.objects.filter(
        chat=chat,
        user=sender,
        is_active=True
    ).first()
    
    if not permission:
        # Создаем запрос на отправку с модерацией
        permission = ChatAccessPermission.objects.create(
            chat=chat,
            user=sender,
            permission_type='send_with_approval',
            granted_by=None
        )
    
    # Создаем сообщение
    message = Message.objects.create(
        chat=chat,
        author=sender,
        content=content,
        is_cross_chat=True
    )
    
    # Создаем метаданные
    CrossChatMessage.objects.create(
        message=message,
        sender=sender,
        target_chat=chat,
        approved=(permission.permission_type == 'send_messages'),
        reason=reason,
        display_mode='guest'
    )
    
    return message

Ответ на сообщение из другого чата

def reply_across_chats(user, original_msg_id, reply_content, target_chat_id):
    original_msg = Message.objects.get(id=original_msg_id)
    
    # Создаем ответ в целевом чате
    reply_msg = Message.objects.create(
        chat_id=target_chat_id,
        author=user,
        content=reply_content,
        reply_to=original_msg
    )
    
    # Создаем расширенную информацию
    MessageReply.objects.create(
        message=reply_msg,
        replied_to=original_msg,
        is_cross_chat_reply=True,
        original_chat=original_msg.chat,
        preserved_text=original_msg.content,
        preserved_author_name=original_msg.author.get_full_name(),
        reply_type='quote'
    )
    
    return reply_msg

Приоритет внедрения (для MVP)

  1. Высокий приоритет:

    • name, avatar в Chat
    • ChatUserSettings (закрепление, уведомления)
    • Расширение типов чатов (group, channel)
    • reply_to, is_edited, is_forwarded, is_cross_chat в Message
    • MessageAttachment
    • ForwardedMessage (базовая пересылка)
    • MessageReply (ответы в том же чате)
  2. Средний приоритет:

    • ChatMembership (роли)
    • ChatAccessPermission (права на отправку в чужие чаты)
    • CrossChatMessage (отправка в чужие чаты с модерацией)
    • Кросс-чат ответы
    • Реакции на сообщения
    • Системные сообщения
    • История редактирования
  3. Низкий приоритет:

    • Треды
    • Автоудаление
    • Расширенные разрешения
    • Статус "печатает"
    • Счетчики пересылок

Изменения в UI/UX

После внедрения этих улучшений понадобится:

Основные функции

  • Модалка редактирования чата (название, аватар, описание)
  • Контекстное меню для закрепления чатов
  • Управление участниками (добавить/удалить/изменить роль)
  • Загрузка файлов в сообщениях
  • Ответы на сообщения (quote UI)
  • Реакции на сообщения
  • Редактирование/удаление своих сообщений

Пересылка сообщений

  • Кнопка "Переслать" в контекстном меню сообщения
  • Модалка выбора чатов для пересылки (с поиском)
  • Множественная пересылка (выбрать несколько чатов)
  • Отображение "Переслано от [Имя]" в сообщении
  • Ссылка на оригинальное сообщение (если доступно)
  • Индикатор цепочки пересылок ("Переслано 5 раз")

Ответы между чатами

  • Возможность ответить в другом чате через контекстное меню
  • Выбор целевого чата для ответа
  • Цитата с указанием чата-источника
  • Ссылка "Перейти к оригиналу" (если есть доступ)

Отправка в чужие чаты

  • Модалка "Отправить сообщение в чат"
  • Поиск доступных чатов (даже без участия)
  • Выбор режима отправки (гостевое/пересланное/объявление)
  • Поле "Причина отправки"
  • Статус модерации:
    • "Ожидает одобрения" (желтый значок)
    • "Одобрено" (зеленая галочка)
    • "Отклонено" (красный крестик)
  • Для модераторов: панель одобрения внешних сообщений

Визуальные индикаторы

  • Пересланное сообщение:

    ┌─────────────────────────────────┐
    │ 📤 Переслано от Иван Петров     │
    │ Оригинал: Чат отдела IT         │
    ├─────────────────────────────────┤
    │ Текст сообщения...              │
    └─────────────────────────────────┘
    
  • Внешнее сообщение:

    ┌─────────────────────────────────┐
    │ 🔔 Внешнее сообщение            │
    │ От: Мария Сидорова              │
    │ Причина: Важное объявление      │
    ├─────────────────────────────────┤
    │ Текст сообщения...              │
    └─────────────────────────────────┘
    
  • Ответ из другого чата:

    ┌─────────────────────────────────┐
    │ Иван Петров → Чат отдела IT     │
    │ ↳ В ответ на сообщение из       │
    │   "Общий чат компании"          │
    ├─────────────────────────────────┤
    │ > Исходное сообщение...         │
    │ Ответ...                        │
    └─────────────────────────────────┘
    

Контекстное меню сообщения (расширенное)

  • Ответить
  • Ответить в другом чате →
  • Переслать →
    • В избранное
    • В другой чат...
    • В несколько чатов...
  • Отправить в чужой чат →
  • Скопировать
  • Редактировать (свои)
  • Удалить (свои/админ)
  • Закрепить (админ)
  • Пожаловаться

Права и разрешения (админ панель чата)

  • Настройки чата:
    • Кто может писать (все/только участники/только админы)
    • Разрешить внешние сообщения (да/нет/с модерацией)
    • Кто может приглашать участников
  • Управление внешними отправителями:
    • Список пользователей с правами
    • Добавить/удалить права
    • Установить срок действия
    • Модерация ожидающих сообщений