9.2 KiB
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:
- Wyszukiwanie wiadomości — pasek search w skrzynce
- 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
ILIKEna polachsubject,regexp_replace(content, '<[^>]+>', '', 'g')(strip HTML) z JOIN nausers(imię, nazwisko) icompanies(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_idna 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ę
moderatordowolnemu 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 →
UserNotificationdla 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:
PrivateMessagejak dotychczas (inbox: WHERE recipient_id = current_user)MessageGroupWHERE 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 |