docs: design spec for group messages and message search

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-20 10:54:57 +01:00
parent a8f5dfa4ab
commit 2b0907c2ad

View File

@ -0,0 +1,223 @@
# Wiadomości grupowe i wyszukiwanie — Design Spec
**Data:** 2026-03-20
**Status:** Approved
**Moduł:** Messages (`/wiadomosci`)
## Kontekst
Moduł wiadomości obsługuje wyłącznie komunikację 1:1 (tabela `private_messages` z `sender_id`/`recipient_id`, wątki przez `parent_id`). Brak wyszukiwania i brak możliwości pisania do wielu osób jednocześnie.
## Zakres
Dwie niezależne funkcjonalności:
1. **Wyszukiwanie wiadomości** — pasek search w skrzynce
2. **Wiadomości grupowe** — czaty grupowe (ad-hoc i nazwane grupy)
---
## 1. Wyszukiwanie wiadomości
### Opis
Pasek search na górze listy wiadomości w `/wiadomosci` (inbox) i `/wiadomosci/wyslane` (sent). Jedno pole tekstowe z ikoną lupy, placeholder "Szukaj w wiadomościach...".
### Zakres wyszukiwania
Wyszukuje jednocześnie po:
- **Temacie** (`subject`)
- **Treści** (`content` — stripped z HTML tagów)
- **Nadawcy/Odbiorcy** (imię, nazwisko użytkownika, nazwa firmy powiązanej)
### Implementacja
- PostgreSQL `ILIKE` na polach `subject`, `regexp_replace(content, '<[^>]+>', '', 'g')` (strip HTML) z JOIN na `users` (imię, nazwisko) i `companies` (nazwa firmy). Przy ~150 firmach wydajność wystarczająca bez indeksów FTS
- Parametr `?q=` w URL — zachowanie wyszukiwania po odświeżeniu strony
- Wyniki wyświetlane w tej samej liście co inbox, z zachowaniem paginacji (20/strona)
- Podświetlenie frazy w wynikach
### UI
- Pole tekstowe nad listą wiadomości, pod nagłówkiem strony
- Ikona lupy po lewej, przycisk "x" do czyszczenia po prawej (gdy query niepuste)
- Wyszukiwanie uruchamiane po wciśnięciu Enter lub po 500ms debounce
- Pusta lista wyników → komunikat "Nie znaleziono wiadomości"
---
## 2. Wiadomości grupowe
### Model danych
#### Tabela `message_group`
| Kolumna | Typ | Opis |
|---------|-----|------|
| `id` | Integer, PK | |
| `name` | String(255), nullable | Nazwa grupy (pusta = ad-hoc czat) |
| `description` | Text, nullable | Opis grupy |
| `owner_id` | FK → users.id | Twórca grupy |
| `is_named` | Boolean, default=False | True = trwała nazwana grupa |
| `created_at` | DateTime | |
| `updated_at` | DateTime | Aktualizowane przy nowej wiadomości |
#### Tabela `message_group_member`
| Kolumna | Typ | Opis |
|---------|-----|------|
| `group_id` | FK → message_group.id (CASCADE), PK | |
| `user_id` | FK → users.id (CASCADE), PK | |
| `role` | Enum('owner', 'moderator', 'member') | Rola w grupie |
| `last_read_at` | DateTime, nullable | Timestamp ostatniego odczytu — wiadomości z `created_at > last_read_at` są nieprzeczytane |
| `joined_at` | DateTime | |
| `added_by_id` | FK → users.id, nullable | Kto dodał |
#### Tabela `group_message`
| Kolumna | Typ | Opis |
|---------|-----|------|
| `id` | Integer, PK | |
| `group_id` | FK → message_group.id (CASCADE) | |
| `sender_id` | FK → users.id (SET NULL), nullable | Zachowaj wiadomość po usunięciu usera |
| `content` | Text, NOT NULL | Treść (HTML z Quill) |
| `created_at` | DateTime | |
#### Załączniki
Rozszerzenie istniejącej tabeli `message_attachments`:
- Dodanie nullable `group_message_id` (FK → group_message.id, CASCADE)
- **ALTER** istniejącego `message_id` na nullable (obecnie NOT NULL)
- CHECK constraint: `(message_id IS NOT NULL) != (group_message_id IS NOT NULL)` — dokładnie jedno z dwóch musi być ustawione
#### Śledzenie przeczytanych wiadomości
Podejście: kolumna `last_read_at` na `message_group_member`. Aktualizowana przy otwarciu widoku grupy (`GET /wiadomosci/grupa/<id>`). Liczba nieprzeczytanych = COUNT z `group_message WHERE created_at > last_read_at AND sender_id != user_id`. Endpoint `/api/messages/unread-count` rozszerzony o sumę nieprzeczytanych z grup.
### Role w grupie
| Rola | Może pisać | Może dodawać/usuwać osoby | Może nadawać moderatora | Może usunąć grupę |
|------|-----------|--------------------------|------------------------|-------------------|
| `owner` | tak | tak | tak | tak |
| `moderator` | tak | tak | nie | nie |
| `member` | tak | nie | nie | nie |
- Właściciel może nadać rolę `moderator` dowolnemu uczestnikowi
- Tylko właściciel może nadawać/odbierać rolę moderatora
- Moderator NIE może usunąć właściciela z grupy
- Zapraszać można wyłącznie osoby z aktywnym członkostwem Nordy
- Blokady (`UserBlock`): zablokowany użytkownik nie może być dodany do grupy, w której jest blokujący
- Jeśli konto właściciela zostanie dezaktywowane → najstarszy moderator (lub najstarszy członek) zostaje automatycznie właścicielem
### Tworzenie grupy
Nowy przycisk "Nowa grupa" obok istniejącego "Nowa wiadomość" w `/wiadomosci`.
Formularz:
- Opcjonalna nazwa grupy (pusta → ad-hoc czat wyświetlany jako lista imion)
- Autocomplete wyboru osób (reuse istniejącego autocomplete z compose.html, rozszerzony o multi-select)
- Pierwsza wiadomość w formularzu (Quill editor)
- Załączniki (drag & drop, reuse `MessageUploadService`)
### Widok skrzynki (inbox)
Czaty grupowe pojawiają się w tym samym inbox co wiadomości 1:1:
- Badge z liczbą uczestników (np. "👥 4")
- Nazwane grupy wyświetlane pod nazwą (np. "Zarząd Nordy")
- Ad-hoc czaty wyświetlane jako lista imion (np. "Jan, Anna, Piotr")
- Sortowane po dacie ostatniej wiadomości (razem z 1:1)
- Nieprzeczytane wiadomości oznaczone jak dotychczas (bold + badge)
### Widok czatu grupowego
Styl konwersacji (flat chat):
- Lista wiadomości chronologicznie (najstarsze na górze)
- Każda wiadomość: avatar + imię nadawcy + timestamp + treść
- Na dole: pole do pisania (Quill) + załączniki
- Sidebar (lub sekcja na górze na mobile): lista uczestników z rolami
- Przycisk "Zarządzaj grupą" widoczny dla owner/moderator
### Zarządzanie grupą
Panel dostępny dla owner i moderator:
- Dodawanie nowych osób (autocomplete, tylko aktywni członkowie)
- Usuwanie osób z grupy
- Właściciel dodatkowo: nadawanie/odbieranie roli moderator
- Zmiana nazwy i opisu grupy (tylko owner)
### Powiadomienia
- Nowa wiadomość w grupie → `UserNotification` dla wszystkich uczestników (oprócz nadawcy)
- Email notification (reuse `build_message_notification_email`)
- Treść: "[Nazwa grupy / lista imion] — Nowa wiadomość od {nadawca}"
- Przy obecnej skali (~150 firm) volume powiadomień jest akceptowalny. Jeśli grupy będą duże i aktywne — dodamy throttling/digest w przyszłości
### Ad-hoc vs nazwana grupa
| Cecha | Ad-hoc | Nazwana |
|-------|--------|---------|
| Nazwa | Brak — wyświetlana jako lista imion | Wyświetlana pod swoją nazwą |
| Trwałość | Żyje w historii | Żyje w historii |
| Zarządzanie | Identyczne | Identyczne |
| Różnica | Tylko prezentacja | Tylko prezentacja |
### Wyszukiwanie w grupach
Pasek search z sekcji 1 obejmuje również wiadomości grupowe — szuka po nazwie grupy (zamiennik `subject` dla grup), treści wiadomości i uczestnikach. Wyniki z 1:1 i grup łączone w Pythonie (dwa osobne query, merge po dacie) — nie SQL UNION, bo modele są różne.
### Inbox — łączenie 1:1 i grup
Dwa osobne query:
1. `PrivateMessage` jak dotychczas (inbox: WHERE recipient_id = current_user)
2. `MessageGroup` WHERE current_user jest członkiem, z subquery na ostatnią wiadomość i liczbę nieprzeczytanych
Wyniki łączone w Pythonie, sortowane po dacie ostatniej aktywności, paginowane. Podgląd ostatniej wiadomości pobierany z `group_message` (ORDER BY created_at DESC LIMIT 1 per group).
---
### Avatary — zdjęcia profilowe zamiast inicjałów
Obecne szablony wiadomości wyświetlają inicjały (pierwsza litera imienia) jako avatar. Model `User` ma pole `avatar_path` ze zdjęciem profilowym. Przy okazji implementacji wiadomości grupowych:
- Jeśli użytkownik ma `avatar_path` → wyświetl `<img>` ze zdjęciem
- Jeśli nie → fallback na inicjał (jak dotychczas)
- Dotyczy: inbox, sent, view (wątki 1:1), group_view, compose (autocomplete, preview)
---
## Czego NIE robimy
- Reakcje/emoji na wiadomościach
- Przypinanie wiadomości
- Wątki wewnątrz grupy (flat chat only)
- Wyciszanie grup
- Opuszczanie grupy przez uczestnika (tylko owner/moderator usuwa)
- Limity liczby grup lub uczestników (na razie)
---
## Nowe routes
| Method | URL | Opis |
|--------|-----|------|
| GET | `/wiadomosci/nowa-grupa` | Formularz tworzenia grupy |
| POST | `/wiadomosci/grupa/utworz` | Utworzenie grupy + pierwsza wiadomość |
| GET | `/wiadomosci/grupa/<id>` | Widok czatu grupowego |
| POST | `/wiadomosci/grupa/<id>/wyslij` | Wysłanie wiadomości do grupy |
| GET/POST | `/wiadomosci/grupa/<id>/zarzadzaj` | Panel zarządzania członkami |
| POST | `/wiadomosci/grupa/<id>/dodaj-czlonka` | Dodanie osoby |
| POST | `/wiadomosci/grupa/<id>/usun-czlonka` | Usunięcie osoby |
| POST | `/wiadomosci/grupa/<id>/zmien-role` | Zmiana roli (owner only) |
## Nowe szablony
| Plik | Opis |
|------|------|
| `templates/messages/group_compose.html` | Formularz tworzenia grupy |
| `templates/messages/group_view.html` | Widok czatu grupowego |
| `templates/messages/group_manage.html` | Panel zarządzania członkami |
## Migracje SQL
| Plik | Opis |
|------|------|
| `088_message_groups.sql` | Tabele `message_group`, `message_group_member`, `group_message` |
| `089_message_attachments_group.sql` | Dodanie `group_message_id` do `message_attachments` |