Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Production moved from on-prem VM 249 (10.22.68.249) to OVH VPS (57.128.200.27, inpi-vps-waw01). Updated ALL documentation, slash commands, memory files, architecture docs, and deploy procedures. Added |local_time Jinja filter (UTC→Europe/Warsaw) and converted 155 .strftime() calls across 71 templates so timestamps display in Polish timezone regardless of server timezone. Also includes: created_by_id tracking, abort import fix, ICS calendar fix for missing end times, Pros Poland data cleanup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
190 lines
6.6 KiB
Markdown
190 lines
6.6 KiB
Markdown
# 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/<id>/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/<id> | Szczegóły + członkowie |
|
|
| PATCH | /api/conversations/<id> | Edytuj nazwę/opis |
|
|
| DELETE | /api/conversations/<id> | Usuń (owner) |
|
|
| POST | /api/conversations/<id>/members | Dodaj członka |
|
|
| DELETE | /api/conversations/<id>/members/<uid> | Usuń członka |
|
|
| PATCH | /api/conversations/<id>/settings | Mute/archive |
|
|
|
|
### Wiadomości
|
|
| Method | URL | Opis |
|
|
|--------|-----|------|
|
|
| GET | /api/conversations/<id>/messages | Paginacja cursor-based |
|
|
| POST | /api/conversations/<id>/messages | Wyślij |
|
|
| PATCH | /api/messages/<id> | Edytuj (swoje, max 24h) |
|
|
| DELETE | /api/messages/<id> | Soft delete (swoje) |
|
|
| POST | /api/messages/<id>/forward | Przekaż |
|
|
| POST | /api/conversations/<id>/read | Oznacz przeczytane |
|
|
| POST | /api/conversations/<id>/typing | Typing indicator |
|
|
|
|
### Reakcje i przypięcia
|
|
| Method | URL | Opis |
|
|
|--------|-----|------|
|
|
| POST | /api/messages/<id>/reactions | Dodaj |
|
|
| DELETE | /api/messages/<id>/reactions/<emoji> | Usuń |
|
|
| POST | /api/messages/<id>/pin | Przypnij |
|
|
| DELETE | /api/messages/<id>/pin | Odepnij |
|
|
| GET | /api/conversations/<id>/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: <title> + <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
|