Blog

Come creare una chat tra acquirenti e venditori per un marketplace in Next.js

· Andrij Tkachenko · 8 min di lettura

Una finestra del browser con la scheda di un articolo a sinistra, un giradischi con un cartellino del prezzo e due pulsanti per un’offerta, e una conversazione a destra, fumetti di messaggi tra due persone e un riquadro video nella chat

Una chat tra acquirenti e venditori sembra una sola funzione, e in un marketplace fatto con Next.js sono parecchie. Chi compra chiede di un articolo e riceve la risposta mentre lo sta ancora guardando. La conversazione appartiene solo a quelle due persone, con foto che nessun altro può aprire. A volte una foto non basta, e una breve videochiamata mostra l’articolo prima che qualcuno si metta in viaggio. Un’offerta deve raggiungere chi vende su qualsiasi pagina dell’app, e la risposta deve tornare allo stesso modo. E tutte le persone che guardano un articolo dovrebbero vederne il prezzo, e se è ancora in vendita, appena cambia qualcosa.

Questo articolo passa in rassegna ogni parte che serve a un’app vera, con un esempio completo e funzionante: il marketplace della nostra guida Next.js, dove i vicini comprano e vendono le loro cose. Puoi provarlo dal vivo: entra come Omar, con il giradischi in vendita, poi apri il sito in una finestra privata come Ava, con un’offerta di 130 $ che aspetta una risposta. Cambia l’offerta come Ava, e Omar lo vede subito, su qualsiasi pagina.

Cosa serve alla chat di un marketplace

  • Una conversazione per articolo e acquirente. Ogni persona che chiede di un articolo ha la sua conversazione con chi lo vende, e nessun altro la legge, né gli altri acquirenti né gli altri venditori.
  • Messaggi che arrivano mentre guardi. Un messaggio passa da pagina a pagina appena viene inviato, con l’indicatore di scrittura e le conferme di lettura, e viene salvato nel tuo database. Le connessioni che servono restano aperte finché è aperta la pagina, e una funzione serverless non può tenerle (perché, e cosa fare).
  • Foto conservate da te. Chi compra vuole vedere un dettaglio da vicino, e chi vende manda una foto. Le foto restano sul tuo server, e solo le due persone della conversazione possono aprirle.
  • Una chiamata per vedere l’articolo. In video, dalla conversazione, prima che qualcuno si sposti.
  • Offerte che nessuna pagina può falsificare. Un’offerta, e il sì o il no di chi vende, arrivano dal tuo server, mai da una pagina, ed entrambe le parti le vedono subito.
  • Il marketplace com’è adesso. Quante altre persone guardano un articolo, il suo nuovo prezzo, e se è stato riservato o venduto, su ogni pagina che lo mostra.

Puoi costruire tutto questo da te, con websocket e WebRTC tuoi. Oppure lasci che un cloud in tempo reale trasporti i messaggi, le chiamate e i conteggi, mentre il tuo server tiene ciò che è davvero tuo: chi può parlare con chi, le conversazioni e le offerte. È quello che fa il marketplace, con WordPlus Cloud.

L’esempio: un marketplace

I vicini mettono in vendita le loro cose con foto e prezzo. Chi vuole comprare apre un articolo, preme Message Omar, e si apre la conversazione su quell’articolo, con l’articolo e l’offerta sopra la chat. È un’app Next.js con l’App Router, Better Auth e Postgres, e la sua parte in tempo reale sta in pochi file. Cinque passi ne costruiscono le conversazioni.

1. Una conversazione per articolo e acquirente

Ogni conversazione è una stanza che prende il nome dal suo numero nel database, deal:7, e riunisce un articolo, chi lo vende e una persona che lo compra. Il sito dichiara queste stanze una sola volta, con quel modello, con quello che una pagina può inviarvi e con la regola:

lib/realtime.ts
realtime.room(
    'deal:{id}',
    { history: 50, emit: ['message', 'typing', 'reaction', 'read'], webhook: ['message', 'reaction'] },
    async (user, { id }) => mayTalk(user, id),
);
lib/market.ts
export async function mayTalk(user: string | number | null, deal: string): Promise<boolean> {
    return user !== null && (await partiesOf(deal)).includes(String(user));
}

mayTalk chiede al database chi sono le due persone della conversazione, e non fa entrare nessun altro. Una pagina chiede di entrare sulla route del sito stesso, /api/realtime/auth, che applica la regola con la sessione del sito e risponde con un pass firmato con il segreto dell’app, o con il motivo del rifiuto. Poi messaggi, indicatori di scrittura, reazioni e conferme di lettura passano da pagina a pagina attraverso WordPlus Cloud, e webhook manda ogni messaggio e reazione anche al sito, che li salva in Postgres:

lib/realtime.ts
realtime.on('message', async ({ room, from, data, event_id, at }) => {
    const deal = idIn(room, 'deal');
    if (!from || !deal) return;
    await keepAndTell({ id: event_id, deal, from: String(from), text: String(data.text ?? ''), files: data.files as unknown[], at });
});

keepAndTell salva il messaggio una sola volta, in base all’id del suo evento, e avvisa l’altra persona della conversazione con realtime.toUser(), che raggiunge ogni pagina aperta da quella persona: una notifica che apre la conversazione.

2. La chat nella pagina, con le foto da te

La conversazione è una chat già pronta, quella di Better Messages, nel tuo albero:

app/(app)/messages/[id]/chat.tsx
<Chat
    rt={useRealtime()}
    room={room}
    to={to}
    name={name}
    avatar={avatar}
    call
    messages={`/api/messages?room=${room}`}
    upload="/api/files"
    accept="image/jpeg,image/png,image/webp,image/gif"
    maxFileSize={15 * 1024 * 1024}
/>

Mostra i messaggi che il tuo server ha salvato, e carica i più vecchi quando si scorre verso l’alto (messages), con l’indicatore di scrittura, le reazioni, le conferme di lettura e il campo per scrivere. to, name e avatar mettono l’altra persona nella sua barra in alto. upload accetta foto: ognuna sale sulla route del sito appena viene scelta, e il messaggio porta solo il posto in cui si trova, così WordPlus Cloud non vede mai la foto. La route chiede la stessa regola della stanza:

app/api/files/route.ts
if (!me || !deal || !(file instanceof File) || !(await realtime.mayJoin(room, me))) {
    return Response.json({ error: 'This photo is not for this conversation.' }, { status: 403 });
}

realtime.mayJoin applica la regola di deal:{id} per la persona che ha fatto l’accesso, così la conversazione e le sue foto non sono mai in disaccordo su chi può usarle. La route che serve una foto chiede di nuovo a mayTalk, e chiunque altro riceve un 404, anche con l’indirizzo della foto.

3. Una chiamata per vedere l’articolo

lib/realtime.ts
realtime.calls('deal:{id}', async (user, other, { id }) => (await mayTalk(user, id)) && (await mayTalk(other, id)));

La chiamata porta il nome della conversazione, e le due persone della conversazione possono chiamarsi lì. call sulla chat mette i pulsanti per audio e video nella sua barra in alto, e mostra la chiamata nella chat stessa, accanto ai messaggi, così chi compra può chiedere di un graffio mentre chi vende gira il giradischi verso la telecamera. Su un’altra pagina del sito la chiamata continua in una finestra nell’angolo. Una chiamata tra due persone va direttamente tra i loro browser quando può, attraverso un relay quando una rete lo impedisce, ed è cifrata end-to-end su ogni percorso (come implementare le videochiamate in Next.js).

4. Offerte dette dal tuo server

Un’offerta è una promessa di denaro, quindi nessuna pagina può inviarla. L’emit della stanza lascia fuori le offerte, e solo le server action del sito ne creano una, la salvano e avvisano le due persone:

app/(app)/actions.ts
export async function offer(deal: string, amount: number): Promise<{ error: string } | null> {
    const me = await currentUserId();
    if (!me) return { error: 'Sign in first.' };
    const made = await makeOffer(deal, me, amount);
    if (typeof made === 'string') return { error: made };
    const found = (await dealOf(deal))!;
    await realtime.to(`deal:${deal}`).emit('changed');
    await realtime.toUser(found.listing.seller).emit('offer', { deal, name: found.buyerName, amount, title: found.listing.title });
    return null;
}

realtime.to() dice changed nella stanza della conversazione, ed entrambe le sue pagine chiedono di nuovo il loro contenuto al server, senza ricaricare. realtime.toUser() raggiunge chi vende su qualsiasi pagina, come notifica: «Ava offers $135 for Wooden turntable, works perfectly». Accept o Decline torna allo stesso modo a chi compra. Quando un’offerta viene accettata, l’articolo resta riservato a quel prezzo per chi l’ha fatta, le altre offerte aperte su di esso vengono rifiutate, e anche ogni persona che le aveva fatte lo viene a sapere:

app/(app)/actions.ts
for (const other of answered.declined) {
    await realtime.toUser(other.buyer).emit('answer', {
        deal: other.deal, name: seller, accepted: false, amount: other.amount, title: answered.listing.title,
    });
}

5. Chi sta guardando, e il marketplace com’è

La pagina di un articolo entra in una stanza tutta sua, listing:7, pubblica e contata da WordPlus Cloud, senza nessuna tua tabella:

lib/realtime.ts
realtime.room('listing:{id}', { public: true, count: true });
app/(app)/live.tsx
const { count } = useRoom(`listing:${id}`, { changed: () => router.refresh() });
const others = count === null ? 0 : count - 1;

La pagina mostra «2 other people are looking at this», e il numero cambia man mano che le persone arrivano e se ne vanno. La stessa stanza sente changed quando il prezzo scende, o l’articolo viene riservato o venduto, e la pagina lo mostra subito. Ogni pagina di chi ha fatto l’accesso entra in un’altra stanza, market, in cui parla solo il server, così il marketplace stesso mostra un articolo nuovo, un nuovo prezzo o un articolo venduto, senza ricaricare.

Cosa viene incluso

  • Indicatore di scrittura e conferme di lettura, nei due sensi, come in un’app di messaggi.
  • Una casella di posta: ogni conversazione, in acquisto e in vendita, la più recente per prima, e una notifica di ogni nuovo messaggio su qualsiasi pagina.
  • Se chi vende è online, o quando lo è stato l’ultima volta, sulla pagina dell’articolo, da useOnline().
  • Nomi e volti dalla tua app. La chat e la chiamata mostrano ogni vicino con il nome e la foto che dà il tuo sito, senza nessun elenco di utenti conservato in WordPlus Cloud.
  • Sigillato lungo la strada. Ciò che passa per una stanza privata attraversa WordPlus Cloud sigillato con una chiave che deriva la tua app, e i server di WordPlus Cloud non hanno mai quella chiave.

Provalo, poi costruisci il tuo

Nella demo dal vivo, i vicini e le loro cose sono solo tuoi per un giorno, quindi quello che scrivi o offri lì non lo vede nessun altro. Sotto ogni pagina, How this page works mostra il codice che la fa funzionare, un passo alla volta, con i valori della pagina che hai aperto.

La guida al marketplace costruisce il sito intero, con ogni file della sua parte in tempo reale, e per riunioni di molte persone nello stesso tipo di app leggi come implementare le videoconferenze in Next.js.

WordPlus Cloud for Apps ha un prezzo in base alle connessioni simultanee, a partire da 1.000, con fatturazione mensile o annuale. Messaggi, offerte, il conteggio di chi sta guardando e le chiamate tra due persone non consumano crediti. Scopri WordPlus Cloud per le app, o i prezzi per le app. Domande sulla tua integrazione? Scrivici.