Dodaj prawdziwe wiadomosci miedzy uzytkownikami o ogloszeniach
Deploy Polska Lokalnie / deploy (push) Successful in 12s

Dotad rozmowy z kupujacymi istnialy tylko w przegladarce nadawcy: nic nie
trafialo na serwer, "odczytano" i odpowiedz sprzedajacego byly symulowane
timerem. Wzorem Gieldy Komunalnej:

Backend (/api/conversations):
- Conversation: jeden watek na pare (ogloszenie, kupujacy), sprzedajacy to
  wlasciciel ogloszenia; tytul ogloszenia zapisany na wypadek jego usuniecia.
  Wiadomosci w istniejacej tabeli messages z nowa kolumna conversation_id,
  wiec rozmowa z administracja (conversation_id = null) dziala bez zmian.
- start/list/get/reply, blokada (UserBlock, w obu kierunkach), ukrycie
  rozmowy u siebie (nowa wiadomosc przywraca ja obu stronom). Dostep tylko
  dla stron rozmowy, filtr niedozwolonych slow jak w reszcie serwisu.
- Po zatwierdzeniu transakcji odbiorca dostaje powiadomienie w serwisie
  i - jesli ma wlaczone powiadomienia o wiadomosciach - e-mail (w tle).
- /api/messages/unread-count wlicza rozmowy o ogloszeniach.
- Zgloszenie uzytkownika z rozmowy: zgloszonego, ogloszenie i tresc rozmowy
  serwer bierze z bazy, nie z przegladarki.

Frontend:
- Adresy /wiadomosci/:conversationId, /wiadomosci/administracja
  i /wiadomosci/nowa/:listingId zamiast rozmowy w location.state.
- Lista rozmow odswiezana co 30 s, otwarta rozmowa co 4 s.
- Zdjecia w wiadomosciach usuniete - serwer ich nie przyjmuje (rozmowa
  z administracja i tak je gubila).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-21 23:15:06 +02:00
parent d655bcc9b1
commit 5eee33b986
20 changed files with 1267 additions and 488 deletions
+394 -404
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -45,6 +45,10 @@ export const ROUTES = {
notifications: '/powiadomienia',
favorites: '/ulubione',
messages: '/wiadomosci',
// Rozmowa o ogloszeniu (numer z API) albo "administracja" - rozmowa z obsluga serwisu.
messagesConversation: '/wiadomosci/:conversationId',
// Nowa rozmowa z ogloszenia - watek powstaje na serwerze dopiero z pierwsza wiadomoscia.
messagesNew: '/wiadomosci/nowa/:listingId',
admin: '/admin',
listingDetail: '/oferta/:id',
publicProfile: '/profil/:id',
@@ -96,6 +100,17 @@ export function publicProfilePath(id: number): string {
return `/profil/${id}`;
}
// Identyfikator rozmowy z obsluga serwisu w adresie /wiadomosci/:conversationId.
export const ADMIN_CONVERSATION_SLUG = 'administracja';
export function conversationPath(id: number | typeof ADMIN_CONVERSATION_SLUG): string {
return `/wiadomosci/${id}`;
}
export function newConversationPath(listingId: number): string {
return `/wiadomosci/nowa/${listingId}`;
}
// Dedykowana strona edycji wlasnego ogloszenia.
export function listingEditPath(id: number): string {
return `/edytuj-ogloszenie/${id}`;
@@ -217,6 +232,8 @@ export const PROTECTED_PATHS: string[] = [
ROUTES.notifications,
ROUTES.favorites,
ROUTES.messages,
ROUTES.messagesConversation,
ROUTES.messagesNew,
ROUTES.comparison,
ROUTES.guideAdd,
];
+1 -71
View File
@@ -16974,76 +16974,6 @@ a.city-card:hover {
margin: 0;
}
.message-photo {
background: transparent;
border: 0;
border-radius: 10px;
cursor: zoom-in;
display: block;
margin-bottom: 8px;
padding: 0;
}
.message-photo img {
border-radius: 10px;
display: block;
max-height: 260px;
max-width: 100%;
object-fit: cover;
transition: opacity 0.12s ease;
}
.message-photo:hover img {
opacity: 0.92;
}
.message-photo:focus-visible {
outline: 2px solid #17a764;
outline-offset: 2px;
}
.messages-composer-attachment {
align-items: center;
background: #f4f8fb;
border: 1px solid #dbe6f0;
border-radius: 10px;
display: grid;
gap: 10px;
grid-template-columns: 40px minmax(0, 1fr) 28px;
margin: 8px 0 0;
padding: 6px 8px;
}
.messages-composer-attachment img {
border-radius: 8px;
height: 40px;
object-fit: cover;
width: 40px;
}
.messages-composer-attachment span {
color: #33506e;
font-size: 12px;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.messages-composer-attachment button {
background: transparent;
border: 0;
border-radius: 6px;
color: #5f7289;
font-size: 20px;
line-height: 1;
min-height: 28px;
}
.messages-composer-attachment button:hover {
background: #e6eef7;
}
.message-business-card {
background:
radial-gradient(120% 140% at 0% 0%, rgba(121, 186, 255, 0.18) 0%, rgba(121, 186, 255, 0) 52%),
@@ -17222,7 +17152,7 @@ a.city-card:hover {
border-top: 1px solid #e8eef5;
display: grid;
gap: 8px;
grid-template-columns: 34px 78px minmax(0, 1fr) 34px;
grid-template-columns: 78px minmax(0, 1fr) 34px;
padding: 0 10px;
}
+25 -1
View File
@@ -1,5 +1,5 @@
import { test, expect, type Page } from '@playwright/test';
import { ROUTES } from '../src/routes';
import { ROUTES, ADMIN_CONVERSATION_SLUG, conversationPath, newConversationPath } from '../src/routes';
// Standard projektu (AGENTS.md): kazdy widok ma wlasny URL obslugiwany przez React Router,
// a nawigacja odbywa sie przez <Link>/<NavLink>. Te testy pilnuja tej zasady.
@@ -28,6 +28,8 @@ const PROTECTED_ROUTES = [
ROUTES.account,
ROUTES.favorites,
ROUTES.messages,
conversationPath(ADMIN_CONVERSATION_SLUG),
newConversationPath(1),
ROUTES.add,
ROUTES.admin,
`${ROUTES.admin}/leady`,
@@ -373,3 +375,25 @@ test.describe('Naglowek', () => {
await expect(brand).toContainText('ogłoszenia za darmo');
});
});
test.describe('Wiadomosci', () => {
test('"Napisz wiadomosc" w ogloszeniu otwiera rozmowe pod wlasnym adresem', async ({ page }) => {
await login(page);
await goto(page, ROUTES.buy);
await page.locator('.real-result-card').first().waitFor();
const href = await page.locator('.real-result-card .card-stretched-link').first().getAttribute('href');
await goto(page, href!);
await page.getByRole('button', { name: /Napisz wiadomość/ }).first().click();
await page.waitForFunction(() => /^\/wiadomosci\/nowa\/\d+$/.test(window.location.pathname));
// Adres szkicu dziala po odswiezeniu - rozmowa nie zalezy od stanu nawigacji.
await page.reload();
await expect(page.locator('.messages-composer input[type=text]')).toBeVisible();
});
test('nieistniejaca rozmowa wraca na liste wiadomosci', async ({ page }) => {
await login(page);
await goto(page, conversationPath(999999));
await page.waitForFunction(() => window.location.pathname === '/wiadomosci');
});
});