From 2b0907c2ad670a080f06a3e25f6355ba59535eef Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Fri, 20 Mar 2026 10:54:57 +0100 Subject: [PATCH] docs: design spec for group messages and message search Co-Authored-By: Claude Opus 4.6 (1M context) --- ...-03-20-group-messages-and-search-design.md | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-20-group-messages-and-search-design.md diff --git a/docs/superpowers/specs/2026-03-20-group-messages-and-search-design.md b/docs/superpowers/specs/2026-03-20-group-messages-and-search-design.md new file mode 100644 index 0000000..53ef9be --- /dev/null +++ b/docs/superpowers/specs/2026-03-20-group-messages-and-search-design.md @@ -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/`). 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 `` 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/` | Widok czatu grupowego | +| POST | `/wiadomosci/grupa//wyslij` | Wysłanie wiadomości do grupy | +| GET/POST | `/wiadomosci/grupa//zarzadzaj` | Panel zarządzania członkami | +| POST | `/wiadomosci/grupa//dodaj-czlonka` | Dodanie osoby | +| POST | `/wiadomosci/grupa//usun-czlonka` | Usunięcie osoby | +| POST | `/wiadomosci/grupa//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` |