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:
parent
a8f5dfa4ab
commit
2b0907c2ad
@ -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` |
|
||||
Loading…
Reference in New Issue
Block a user