Blog

Kako izgraditi online zajednicu u Next.js-u

· Andrij Tkachenko · 9 min čitanja

Prozor preglednika online zajednice: bočna traka s kanalima, glasovnom sobom, sobom s videom i događajima, u sredini pozornica uživo s dvije osobe, ispod nje srce i podignuta ruka, a pokraj stupac poruka, od kojih su dvije fotografije

Forum čuva ono što su njegovi članovi napisali. Zajednica koja djeluje živo nudi više toga odjednom. Članovi podijele fotografiju i dobiju odgovor dok su još tu, uđu u sobu da razgovaraju, glasom ili s kamerom, gledaju događaje kluba uživo i izlaze na pozornicu s vlastitim radom, i vide tko je upravo tu. U Next.js aplikaciji svaka od tih stvari veza je koja ostaje otvorena, s mnogo ljudi odjednom.

Ovaj članak prolazi kroz svaki dio koji prava aplikacija za zajednicu treba, na potpunom primjeru koji radi: Community, fotografskom klubu iz našeg vodiča za Next.js, s njegovim kanalima, sobama u koje se može svratiti i događajima uživo. Možete ga isprobati uživo: prijavite se kao Mia, domaćin kluba, i pregled portfolija počinje upravo sada. Otvorite studio i izađite u eter. Zatim otvorite klub u privatnom prozoru kao Ava, gledajte pregled, dignite ruku i izađite na pozornicu.

Što online zajednica treba

  • Kanali koji prate tempo. Poruke i fotografije odmah stižu svima koji čitaju, s prikazom tko tipka i reakcijama, i ostaju spremljene za one koji dođu kasnije. Fotografije se otvaraju samo članovima.
  • Sobe u koje se svraća. Glasovne sobe i sobe s kamerama i zaslonima, u koje članovi ulaze i iz njih izlaze kad žele, mnogo ljudi odjednom preko medijskih poslužitelja (kako rade medijski poslužitelji). Domaćini koji nekoga mogu utišati ili ukloniti.
  • Događaji uživo. Jedan domaćin za sve članove, s chatom i reakcijama pokraj slike, i član kojeg se poziva na pozornicu da pokaže svoj rad (kako radi prijenos uživo u Next.js-u).
  • Što se upravo događa. Tko je u kojoj sobi i koji je događaj u eteru, ažurno na svakoj stranici, bez ponovnog učitavanja.
  • Tko je tu. Tko je na mreži i način da im se piše ili ih se nazove.

Sve to radi preko veza koje ostaju otvorene dok god je član na web-stranici, a to serverless funkcija ne može održati (zašto i što učiniti). Svaki dio možete izgraditi sami, na websocketima, WebRTC-u i vlastitim medijskim poslužiteljima. Ili pustite da oblak za rad u stvarnom vremenu vodi veze, pozive i prijenose, dok vaš poslužitelj zadržava ono što je stvarno vaše: članove, pravila i podatke. To radi klub, preko WordPlus Clouda.

Primjer: fotografski klub

Članovi dijele fotografije u kanalima da dobiju povratne informacije, kao što su #critique i #trips. Svraćaju u njegove sobe: u Coffee lounge glasom, u Editing hangout s kamerama i zaslonima. Domaćini vode događaje uživo, pregled portfolija ili tečaj noćne fotografije, na kojima članovi izlaze na pozornicu. To je Next.js aplikacija s App Routerom, Better Authom i Postgresom, a njezin dio za rad u stvarnom vremenu stane u nekoliko datoteka. Pet koraka čini je.

1. Kanali s fotografijama, spremljeni na vašem poslužitelju

Svaki kanal je soba nazvana po svojem broju: channel:2. Web-stranica ih deklarira jednom, po tom uzorku, s pravilom tko smije biti u njemu:

lib/realtime.ts
const MESSAGES = { history: 50, emit: ['message', 'typing', 'reaction', 'read'], webhook: ['message', 'reaction'] };
realtime.room('channel:{id}', MESSAGES, async (user, _params, room) => mayWrite(user, room));

Svaki član smije biti u kanalu, a nitko drugi. Poruke, prikaz tko tipka, reakcije i potvrde čitanja idu sa stranice na stranicu preko WordPlus Clouda, zapečaćeni, a webhookovi message i reaction donose svaki od njih na poslužitelj web-stranice, koji ga sprema u vlastitu bazu podataka. Stranica kanala gotov je chat na toj sobi:

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

messages je vlastita ruta web-stranice za starije poruke, koje se učitavaju dok član pomiče razgovor prema gore. upload polju za pisanje daje spajalicu, a prima i fotografije zalijepljene ili ispuštene na chat: svaka odlazi na vlastitu rutu web-stranice čim se odabere, a poruka nosi samo mjesto na kojem se otvara. WordPlus Cloud fotografiju nikad ne vidi. Ruta odlučuje prema pravilu same sobe:

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

realtime.mayJoin() pokreće isto pravilo kao ulazak u sobu, pa tko smije čitati kanal, smije u njemu podijeliti fotografiju, a nitko drugi. Svaka fotografija otvara se na vlastitoj adresi, koja ponovno provjerava tko je otvara: netko izvan kluba ne dobiva ništa, kakvu god adresu imao.

2. Glasovne sobe o kojima brinu domaćini

Soba je grupni poziv nazvan po svojem broju, lounge:3, u koji svaki član može svratiti. Pravilo domaćine kluba čini njezinim domaćinima:

lib/realtime.ts
realtime.groupCalls('lounge:{id}', async (user) => {
    if (!(await isMember(user))) return false;
    return (await isHost(user)) ? { admin: true } : true;
});

Soba se prikazuje na vlastitoj stranici, a u njoj je gumb za ulazak:

app/(app)/lounges/[id]/lounge.tsx
<CallSpot rt={rt} call={room} className="h-[calc(100dvh-14rem)] rounded-2xl">
    <GroupCallButton rt={rt} room={room} type={kind} title={name}>
        {people.length > 0 ? 'Drop in' : `Open ${name}`}
    </GroupCallButton>
</CallSpot>

type je audio za glasovnu sobu i video za sobu s kamerama. Prije nego što član uđe, CallSpot prikazuje ono što je u njemu: tko je upravo u sobi i gumb. Nakon ulaska poziv zauzima to mjesto: slika svakoga, ili njihove kamere i zasloni, i kontrole. Član ulazi s isključenim mikrofonom i kamerom i uključuje ih iz poziva, pa svraćanje nikad ne otvara mikrofon iznenada. Na drugoj stranici web-stranice soba se nastavlja u prozoru u kutu i vraća se kad se njezina stranica ponovno otvori.

admin: true sve je što domaćinima treba. Na zaslonu domaćina svaka pločica ostalih ima utišavanje i uklanjanje, koje zaslon poziva traži istom rutom, a ona ih prihvaća samo od domaćina.

3. Događaji uživo, s članovima na pozornici

Događaj je prijenos uživo nazvan po svojem broju: event:5. Njegovo pravilo kaže tko je tko: domaćin događaja izlazi u eter, a svaki drugi član gleda:

lib/realtime.ts
realtime.streams(
    'event:{id}',
    async (user, { id }) => {
        const event = await eventOf(id);
        if (!event || !(await isMember(user))) return false;
        return String(user) === event.host ? { role: 'host', quality: '1080p' } : 'viewer';
    },
    { stage: true, reactions: true },
);

Domaćin izlazi u eter iz vlastitog studija, stranice web-stranice s velikom slikom, ispod nje Go live, a pokraj nje chat i podignute ruke:

app/(app)/studio/[id]/page.tsx
<LiveStream
    name={`event:${event.id}`}
    view="studio-page"
    title={event.title}
    url={`/events/${event.id}`}
    backUrl={`/events/${event.id}`}
/>

Stranica događaja svakom članu daje player s njegovim chatom, isti <LiveStream> pod istim imenom. Domaćin može izaći u eter i iz OBS-a, s adresom i ključem koje daje studio. stage: true je pozornica: član digne ruku iz playera, domaćin je vidi pod Guests i poziva ga, a čim član pritisne Join stage, njegova kamera i mikrofon izlaze u eter pokraj domaćina, za sve koji gledaju.

4. Što se upravo događa, na svakoj stranici

Pokraj svake stranice bočna traka pokazuje tko je u kojoj sobi i koji je događaj u eteru, s brojem gledatelja. Web-stranica ne vodi nikakvu tablicu o tome. WordPlus Cloud to zna:

lib/realtime.ts
export async function liveNow(): Promise<Map<string, number>> {
    const live = new Map<string, number>();
    for (const stream of await realtime.listStreams()) {
        const event = idIn(stream.name, 'event');
        if (event) live.set(event, stream.viewers);
    }
    return live;
}

realtime.listCalls() i realtime.call(name).get() govore isto o sobama i o tome tko je u kojoj. Da sve ostane ažurno, WordPlus Cloud web-stranici šalje webhook svaki put kad netko uđe u sobu ili iz nje izađe, ili kad događaj izađe u eter ili završi. Web-stranica to kaže u sobi kojoj se pridružuje svaka stranica člana, a kad događaj izađe u eter, javlja to svakom članu:

lib/realtime.ts
realtime.on('stream.live', async ({ stream }) => {
    const id = idIn(stream, 'event');
    const event = id ? await eventOf(id) : null;
    if (!event) return;
    await realtime.to('club').emit('changed');
    const everyone = (await members()).map((person) => person.id).filter((person) => person !== event.host);
    await realtime.toUser(everyone).emit('live', { event: event.id, title: event.title, host: event.hostName });
});

U layoutu svaka stranica čuje oboje:

app/(app)/live.tsx
useRoom('club', { changed: () => router.refresh() });

useUserEvent('live', (data: { event: string; title: string; host: string }) => {
    if (path !== `/events/${data.event}`) show({ text: `${data.host} is live: ${data.title}`, href: `/events/${data.event}`, live: true });
});

Prvo čini da svaka stranica sama sebe ponovno zatraži od poslužitelja, bez ponovnog učitavanja, pa bočna traka i otvorena stranica ostaju ažurne. Drugo prikazuje obavijest na kojoj god stranici član bio: „Mia is live: Live portfolio review”, koja otvara događaj. group.joined, group.left i stream.ended kažu changed u istoj sobi.

5. Tko je na mreži

WordPlus Cloud zna tko je na mreži: svaka stranica čijeg je člana web-stranica potvrdila čini tog člana prisutnim na mreži, a stranica saznaje tko dolazi i odlazi, bez ikakvog zahtjeva poslužitelju web-stranice:

app/(app)/members/directory.tsx
const { online } = useOnline(people.map((person) => person.id));
const here = people.filter((person) => online[person.id]);
const away = people.filter((person) => !online[person.id]);

Popis članova prvo prikazuje one na mreži, svakoga s Message i Call, a pokraj svake stranice layout na isti način navodi klub, s označenim domaćinima. Stranica člana kaže je li na mreži ili kad je posljednji put viđen.

Što dolazi uz to

  • Razgovori u dvoje. Stranica člana sadrži vaš razgovor s njim, zajedno s fotografijama, a vaš poziv prikazuje se pokraj poruka.
  • Reakcije i chat na svakom događaju, pokraj slike, i izlazak u eter iz OBS-a ili bilo kojeg enkodera koji šalje preko WHIP-a.
  • Dijeljenje zaslona u sobi s kamerama, iz kontrola poziva.
  • Imena i lica iz vaše aplikacije. Svaka pločica, poruka i pozornica prikazuje ime i fotografiju člana onako kako ih daje profile web-stranice, a u WordPlus Cloudu se ne vodi nikakav popis korisnika.
  • Oduzimanje pristupa. Kad netko napusti klub, pravila od tada kažu ne, a realtime.call(name).remove(user) odmah ga uklanja iz sobe.

Isprobajte, pa izgradite svoju

U demu uživo klub na jedan dan pripada samo vama, pa što god ondje podijelite ili koga god sretnete, nitko drugi to ne vidi, a događaj ili soba sami završavaju nakon 10 minuta. Ispod svake stranice ploča How this page works prikazuje kod koji je pokreće, korak po korak, s vrijednostima stranice koju imate otvorenu.

Vodič kroz web-stranicu zajednice izgrađuje cijelu web-stranicu i u cijelosti donosi svaku datoteku njezina dijela za rad u stvarnom vremenu. Prijenosi uživo u Next.js-u i Pozivi u Next.js-u pokazuju svaki način da prijenos ili poziv stavite u vlastitu aplikaciju. Za tim umjesto kluba, videokonferencije u Next.js-u izgrađuju sastanke timskog intraneta.

WordPlus Cloud for Apps ima cijenu prema istodobnim vezama, od 1.000 naviše, uz mjesečnu ili godišnju naplatu. S 1.000 veza prijenosi uživo nisu obračunati do 50 gledatelja istodobno, a grupni pozivi do 32 osobe na videu istodobno, pri čemu se osoba samo sa zvukom računa kao pola. Svakih daljnjih 1.000 veza dodaje 20 gledatelja i 12 osoba na videu. Iznad toga svaki dodatni gledatelj ili osoba na videu troši kredit po minuti, prijenos samo dok je u eteru, a pozivi između dviju osoba ne troše ništa. Pogledajte prijenose uživo za aplikacije, videopozive za aplikacije ili cijene za aplikacije. Pitanja o vašoj integraciji? Pišite nam.