150 lines
16 KiB
Markdown
150 lines
16 KiB
Markdown
# Друзья, сообщения и уведомления
|
||
|
||
Социальные функции работают между пользователями одного сервера. В web и desktop
|
||
открывается раздел «Друзья и общение», на Android — вкладка «Друзья».
|
||
|
||
|
||
На ПК справа над плеером доступна мини-панель друзей и чата шириной до 600 px.
|
||
Она позволяет переписываться без ухода с текущей страницы, сворачивается с сохранением
|
||
черновика и открывает выбранную переписку в полном разделе. На экранах уже 1024 px
|
||
используется полный раздел общения.
|
||
|
||
## Поведение
|
||
|
||
- Поиск по логину и отображаемому имени начинается с двух символов, возвращает до 30 человек.
|
||
Пароли, роли, настройки и другие приватные поля в результат не попадают.
|
||
- Заявка требует явного принятия получателем. Встречная заявка не принимает дружбу автоматически.
|
||
Можно отменить свою заявку, отклонить входящую или удалить друга.
|
||
- Личные сообщения и отправка музыки доступны только текущим друзьям. Блокировка прекращает
|
||
дружбу и закрывает общение в обоих направлениях. Разблокировка дружбу не восстанавливает.
|
||
- История сохраняется после удаления друга, но доступна снова только после новой взаимной дружбы.
|
||
В первой странице чата — последние 50 сообщений; ранние подгружаются отдельно.
|
||
- В сообщении до 4000 символов. Музыкальное вложение — ID трека либо действующая ссылка
|
||
общего доступа к плейлисту. Отзыв ссылки делает вложенный плейлист недоступным.
|
||
- «Поделиться» открывает выбор: скопировать ссылку, вызвать системную отправку (когда она
|
||
поддерживается клиентом) или отправить конкретному другу одним нажатием. Отправка другу
|
||
создаёт сообщение с музыкальным вложением. Повтор после сетевого сбоя использует тот же UUID.
|
||
|
||
## Спокойные уведомления
|
||
|
||
Четыре типа: входящая заявка, принятие заявки, личное сообщение, музыка.
|
||
Центр событий и счётчики сохраняются на сервере, отметки прочтения общие для устройств.
|
||
Нет звуков, автоматических всплывающих окон и запроса разрешений ОС.
|
||
|
||
Список уведомлений возвращает до 100 событий, сначала непрочитанные. По мере подтверждения
|
||
новых становятся доступны более старые непрочитанные события. Открытый видимый чат подтверждает
|
||
сообщения только до последнего полученного номера; более поздние сообщения не теряются.
|
||
В скрытой вкладке и в фоне Android автоматического подтверждения нет.
|
||
|
||
Сейчас это уведомления **внутри работающего приложения**, с обновлением центра каждые 15 секунд
|
||
и открытого чата каждые 3 секунды. Web использует стандартную приостановку фонового polling
|
||
React Query; Android останавливает polling при уходе приложения в фон. Уведомления ОС при
|
||
закрытом клиенте (Web Push/FCM/нативный desktop) в эту реализацию не входят.
|
||
|
||
## Реализация и проверки
|
||
|
||
Контракт: `api/openapi.yaml`, операции `getSocial`, `searchPeople`, `socialAction`,
|
||
`getDirectMessages`, `sendDirectMessage`. Модуль API: `internal/social`.
|
||
Миграция `00019_social.sql` создаёт дружбу, блокировки, сообщения и уведомления.
|
||
Миграции применяются штатным запуском API.
|
||
|
||
Мутации пары сериализованы транзакционной advisory-блокировкой PostgreSQL. Отправка сообщения
|
||
и создание уведомления атомарны. UUID сообщения глобально уникален; повтор с другим содержимым
|
||
возвращает конфликт. Чужое уведомление нельзя отметить прочитанным. Поиск скрывает заблокированные
|
||
аккаунты и личные блокировки, включая обратное направление.
|
||
|
||
Ограничения: до 50 исходящих ожидающих заявок, до 10 новых заявок за минуту,
|
||
до 30 сообщений одному человеку за минуту; тело социального POST ограничено 32 KiB.
|
||
Тексты выводятся обычным текстом, вложения формируют только внутренние маршруты каталога.
|
||
|
||
Проверки: `go test ./internal/social`,
|
||
`TEST_DATABASE_URL=… go test -tags integration ./internal/social`,
|
||
`pnpm --filter @pipu/web test src/App.test.tsx -t 'social|playlist sharing'`,
|
||
`pnpm --filter @pipu/web test:e2e e2e/social.spec.ts e2e/playlists.spec.ts`.
|
||
Интеграционный сценарий включён в `ci-db`.
|
||
|
||
## Активность и приватность
|
||
|
||
В разделе друзей настройка «Показывать мой статус» принимает значения «Никому»
|
||
(по умолчанию), «Друзьям», «Всем». Сервер проверяет аудиторию, дружбу и блокировки.
|
||
При «Никому» сведения о текущем прослушивании не сохраняются. Переключение на это
|
||
значение удаляет активность всех устройств. Публикация имеет последовательный номер
|
||
и срок жизни 90 секунд; пауза снимает статус, потерянное соединение не оставляет вечный статус.
|
||
|
||
Карточка показывает трек и контекст: «Моя волна», плейлист или обычное прослушивание.
|
||
Трек открывается в каталоге. Плейлист доступен для чтения, пока активность видима:
|
||
этот переход не создаёт постоянной ссылки общего доступа и не раскрывает существующую.
|
||
|
||
## Совместное прослушивание
|
||
|
||
Статус «В сети» виден только текущим друзьям и независим от видимости музыки.
|
||
Клиент поддерживает его штатным опросом состояния прослушивания, даже на паузе;
|
||
просмотр социального раздела также обновляет собственный heartbeat. Сервер объединяет
|
||
частые обновления одного пользователя до одного за 10 секунд. Если ни одно устройство
|
||
не отвечает 45 секунд, статус истекает; UI обновляется при следующем опросе друзей.
|
||
«Слушать вместе» отключено для друга не в сети. Сервер проверяет присутствие обоих
|
||
участников при создании и принятии запроса, поэтому устаревшая кнопка не обходит правило.
|
||
Блокировки и ограничения аудитории проверяются до выдачи ошибки об офлайн-статусе.
|
||
|
||
«Подключиться» отправляет запрос хозяину и оставляет управление у него.
|
||
«Пригласить на мой стрим» создаёт собственную комнату: приглашённый становится слушателем.
|
||
Один пользователь состоит в одной комнате; несколько слушателей могут подключиться к хозяину.
|
||
Комната резервируется на время ожидания подтверждения. До принятия запроса
|
||
слушатель не становится участником, а баннер совместного прослушивания не показывается.
|
||
Исходное устройство ведущего поддерживает резервирование, не прерывая свою музыку.
|
||
После успешной отправки окно закрывается; ожидающий запрос можно отменить в разделе общения.
|
||
После отмены или истечения последнего запроса исходное устройство освобождает
|
||
пустую комнату при следующем опросе, не прерывая свою музыку.
|
||
Повторный запрос уже подключённого слушателя не создаёт нового ожидания.
|
||
Второе устройство ведущего не включает звук автоматически и при закрытии не завершает стрим.
|
||
Настройки приёма запросов/приглашений и автоматического принятия независимы,
|
||
обе по умолчанию «Никто». Автопринятие применяется только внутри разрешённой аудитории.
|
||
Дополнительная настройка управляет запуском звука при подключении.
|
||
|
||
Только исходное устройство хозяина публикует состояние комнаты. Слушатели получают
|
||
трек, паузу и позицию с поправкой на серверное время каждые 2 секунды. Обычные кнопки
|
||
управления, очередь и перемотка заблокированы; громкость, реакции и выход доступны.
|
||
Это синхронизация собственных потоков из каталога с допуском дрейфа 1,5 секунды,
|
||
а не ретрансляция аудиобайта хозяина. Браузер может потребовать нажатия «Включить звук».
|
||
После 10 секунд потери связи слушатель ставится на паузу, комната истекает через 30 секунд
|
||
без хозяина, отсутствующий слушатель удаляется через 60 секунд, запрос действует 2 минуты.
|
||
|
||
## Чат
|
||
|
||
Web и desktop используют встроенный экран: список диалогов слева, переписка справа.
|
||
На узком экране список и переписка открываются последовательно; кнопка назад возвращает
|
||
к списку, а поле ввода остаётся над плеером. Android использует общую нижнюю панель.
|
||
Поддерживаются текст и музыка, ответы с цитатой, шесть реакций, редактирование своих
|
||
сообщений, удаление для всех, поиск по тексту, история, даты, прочтение и индикатор набора.
|
||
Удаление очищает исходный текст и музыкальное вложение. Черновики разделены по серверу,
|
||
пользователю и собеседнику. Скрытая переписка не отмечает сообщения прочитанными.
|
||
Просмотр истории выше последних сообщений не вызывает принудительную прокрутку вниз.
|
||
Поиск открывается по кнопке. Длинные сообщения можно развернуть, HTTP(S)-ссылки
|
||
отображаются компактно без декодирования произвольного текста.
|
||
|
||
Список диалогов отсортирован по последнему сообщению; сервер отдаёт короткое превью
|
||
только для текущих друзей, с повторной проверкой блокировок. Удалённый текст очищен.
|
||
Вкладки «Друзья» и «Заявки» отделены от сообщений; приватность и настройки совместного
|
||
прослушивания открываются из компактного индикатора статуса. Удаление друга и блокировка
|
||
доступны через меню и требуют подтверждения.
|
||
|
||
Треки и плейлисты отображаются карточками с названием, обложкой и воспроизведением.
|
||
«Музыка из переписки» использует `getDirectMessages?music=true` с обычным `before`:
|
||
доступна вся история вложений, удалённые сообщения исключены, прочтение не подтверждается.
|
||
Недоступные треки и отозванные ссылки не запускаются; метаданные берутся из каталога
|
||
и публичной проекции плейлиста, а не из текста сообщения.
|
||
Анимации учитывают системное уменьшение движения. Фото, файлы и голосовые не входят в версию.
|
||
|
||
Миграции `00020`–`00022` добавляют активность, комнаты и возможности чата.
|
||
Уведомления дополнены запросом, приглашением и принятием совместного прослушивания.
|
||
|
||
## Сигнал друзей в рекомендациях
|
||
|
||
В настройках рекомендаций можно отключить влияние друзей. Используется только их
|
||
текущая видимая активность с учётом аудитории и блокировок, без скрытой истории.
|
||
Go передаёт до 30 допустимых треков; Python добавляет до 10 кандидатов, небольшой бонус
|
||
с учётом выбранного контекста и не более одного кандидата с источником `friends` на выдачу.
|
||
Дизлайки, недоступные треки и недавние повторы исключаются прежними фильтрами.
|
||
При недоступном рекомендателе резервный подбор добавляет не более одного такого трека
|
||
в каждую вторую выдачу; тематические волны с seed сохраняют свой сценарий.
|