# Messaging Redesign — Conversation-Based System **Data:** 2026-03-27 **Status:** Zaakceptowany **Zakres:** Przebudowa systemu wiadomości z email-like (Odebrane/Wysłane) na konwersacyjny (Messenger/WhatsApp) ## Decyzje architektoniczne 1. **Ujednolicony model** — 1:1 i grupy w jednym modelu `Conversation` + `Message`. Rozmowa 1:1 = konwersacja z 2 uczestnikami. 2. **Real-time: SSE** — Server-Sent Events + Redis pub/sub. Jedno połączenie SSE per użytkownik. 3. **Migracja danych** — istniejące wiadomości migrowane do nowego modelu. Stare tabele zostają jako backup. ## Model danych ### conversations | Kolumna | Typ | Opis | |---------|-----|------| | id | Serial PK | | | name | String(255), nullable | Null dla 1:1, nadana nazwa dla grup | | is_group | Boolean | False = 1:1, True = grupa | | owner_id | FK → users | Twórca | | created_at | DateTime | | | updated_at | DateTime | Aktualizowane przy każdej wiadomości | | last_message_id | FK → messages, nullable | Denormalizacja dla listy | ### conversation_members | Kolumna | Typ | Opis | |---------|-----|------| | conversation_id | FK → conversations, PK | | | user_id | FK → users, PK | | | role | String(20) | 'owner', 'member' | | last_read_at | DateTime | Read receipts | | is_muted | Boolean | Wyciszenie email + push | | is_archived | Boolean | Ukrycie z listy | | joined_at | DateTime | | | added_by_id | FK → users, nullable | | ### messages | Kolumna | Typ | Opis | |---------|-----|------| | id | Serial PK | | | conversation_id | FK → conversations | | | sender_id | FK → users | | | content | Text | HTML (Quill) | | reply_to_id | FK → messages, nullable | Cytowanie | | edited_at | DateTime, nullable | | | is_deleted | Boolean | Soft delete | | link_preview | JSONB, nullable | {url, title, description, image} | | created_at | DateTime | | ### message_reactions | Kolumna | Typ | Opis | |---------|-----|------| | id | Serial PK | | | message_id | FK → messages | | | user_id | FK → users | | | emoji | String(10) | | | created_at | DateTime | | | UNIQUE | (message_id, user_id, emoji) | | ### message_pins | Kolumna | Typ | Opis | |---------|-----|------| | id | Serial PK | | | conversation_id | FK → conversations | | | message_id | FK → messages | | | pinned_by_id | FK → users | | | created_at | DateTime | | ### message_attachments Istniejąca tabela. Nowa kolumna `new_message_id` FK → messages, nullable. ## SSE Real-time ### Endpoint `GET /api/messages/stream` — jedno połączenie per użytkownik. ### Zdarzenia | Event | Dane | Kiedy | |-------|------|-------| | new_message | conversation_id, message JSON | Nowa wiadomość | | message_read | conversation_id, user_id, read_at | Przeczytano | | typing | conversation_id, user_id, user_name | Ktoś pisze (TTL 3s) | | reaction | message_id, user_id, emoji, action | Reakcja | | message_edited | message_id, new_content, edited_at | Edycja | | message_deleted | message_id, conversation_id | Usunięcie | | message_pinned | message_id, conversation_id, pinned_by | Przypięcie | | presence | user_id, status, last_seen | Online/offline | ### Infrastruktura - Redis pub/sub do rozgłaszania między workerami Gunicorn - Online status: Redis SETEX z TTL 60s, heartbeat co 30s - Typing: POST /api/conversations//typing → Redis publish, TTL 3s ## API Endpoints ### Konwersacje | Method | URL | Opis | |--------|-----|------| | GET | /wiadomosci | Widok konwersacyjny (HTML) | | GET | /api/conversations | Lista konwersacji JSON | | POST | /api/conversations | Nowa (deduplikacja 1:1) | | GET | /api/conversations/ | Szczegóły + członkowie | | PATCH | /api/conversations/ | Edytuj nazwę/opis | | DELETE | /api/conversations/ | Usuń (owner) | | POST | /api/conversations//members | Dodaj członka | | DELETE | /api/conversations//members/ | Usuń członka | | PATCH | /api/conversations//settings | Mute/archive | ### Wiadomości | Method | URL | Opis | |--------|-----|------| | GET | /api/conversations//messages | Paginacja cursor-based | | POST | /api/conversations//messages | Wyślij | | PATCH | /api/messages/ | Edytuj (swoje, max 24h) | | DELETE | /api/messages/ | Soft delete (swoje) | | POST | /api/messages//forward | Przekaż | | POST | /api/conversations//read | Oznacz przeczytane | | POST | /api/conversations//typing | Typing indicator | ### Reakcje i przypięcia | Method | URL | Opis | |--------|-----|------| | POST | /api/messages//reactions | Dodaj | | DELETE | /api/messages//reactions/ | Usuń | | POST | /api/messages//pin | Przypnij | | DELETE | /api/messages//pin | Odepnij | | GET | /api/conversations//pins | Lista przypiętych | ### Inne | Method | URL | Opis | |--------|-----|------| | GET | /api/messages/stream | SSE | | GET | /api/users/presence | Online status (batch) | | POST | /api/messages/upload | Upload pliku | ## Frontend ### Desktop - Lewy panel (380px): lista konwersacji posortowana po updated_at - Prawy panel: nagłówek (avatar, imię, status, typing) + wiadomości (bąbelki) + input (Quill) ### Wiadomości - Bąbelki: moje (niebieskie, prawo) / cudze (szare, lewo) - Separatory dat - Reply-to: cytat nad odpowiedzią - Edytowane: etykieta "(edytowano)" - Usunięte: "Wiadomość usunięta" - Załączniki inline (obrazy jako podgląd, pliki jako pill) - Reakcje: pill badges pod bąbelkiem - Link preview: karta z tytułem + opisem - Read receipts 1:1: ptaszki (wysłano/doręczono/przeczytano), hover → timestampy - Read receipts grupa: awatary (max 4 + "+N") ### Menu kontekstowe (hover/long-press) Odpowiedz, Reaguj (6 emoji), Przekaż, Przypnij, Edytuj, Usuń ### Mobile (< 768px) Lista LUB chat (nie oba). Przycisk "Wróć". Menu kontekstowe jako bottom sheet. ## Email notifications ``` if member.is_muted → nie wysyłaj elif not user.notify_email_messages → nie wysyłaj else → wysyłaj ``` Wyciszona konwersacja: ikona 🔇 na liście. ## Link preview - Backend wykrywa URL, pobiera stronę (timeout 3s), parsuje og:title/og:description/og:image - Fallback: + <meta description> - Tylko pierwszy URL, brak preview dla wewnętrznych linków - Zapisane w messages.link_preview (JSONB) ## Migracja danych 1. Prywatne wiadomości: grupowanie po parach sender/recipient → conversation (is_group=False) 2. Grupy: message_group → conversation (is_group=True) 3. Załączniki: nowy FK new_message_id 4. Walidacja: count before = count after, read receipts zachowane 5. Stare tabele zostają jako backup ## Zależności infrastrukturalne - Redis na VM produkcyjnej (pub/sub + presence cache) - Nginx: SSE wymaga wyłączenia buforowania (`proxy_buffering off`) dla /api/messages/stream