Making the screens your own
The call screen, the chat and the list of who is online are the Better Messages screens, ready to use. Your app can change them as far as it needs to, in three steps, each going further than the last:
| What you change | What updates by itself | |
|---|---|---|
| 1. The look | colours, fonts, corners, words | everything |
| 2. A piece | one named piece, such as the ring, a tile or the controls, drawn by your own code | everything around it |
| 3. The code | the whole view, copied into your app | what runs it: the call, the ring, the devices, reconnecting, encryption |
Take the first step that does what you need. The further you go, the more of the screen is yours to keep up to date.
1. The look
- Design tokens on the page:
--bm-color-accent: 37, 99, 235(red, green and blue),--bm-messenger-font,--bm-radius-card,--bm-radius-avatar,--bm-color-bg,--bm-color-text-primaryand the rest of the Better Messages tokens. - Light and dark follow the system.
bm-messages-lightorbm-messages-darkon<body>keeps one. - Words:
strings: { 'End Call': 'Hang up' }, by the English text, on<CallScreen strings>,rt.callScreen( { strings } )orconfigureUi(). The screens come in English, German, Spanish, French, Italian, Dutch, Polish, Croatian and Hebrew.
2. A piece of your own
Replace one piece and keep the rest. Your piece gets the same props ours gets, and ours as its children, so it can wrap ours, change around it, or draw something else.
| Piece | What it is | Its props |
|---|---|---|
RingCard | a call ringing: who calls, answer and reject | user_id, name, type, onAnswer, onReject |
CallStatus | what a call says of itself: calling, connecting, its time | layout, action, text, duration |
VideoTile | the other side of a call between two | participant, action |
SelfView | your own camera in its corner | track |
GridTile | one person in a group call grid or roster | participant, layout |
controls | the call buttons, as a list | ( buttons, call ) => buttons |
OnlineRow | a person in the online list | member, mine, actions |
Message | a message in the chat | message, mine, sender, react |
Composer | where the user writes | send( text ), typing(), disabled, placeholder, maxLength |
EmptyConversation | a chat with no messages yet | |
Avatar, Name | a user picture and name on the call screens | user_id, size |
In React
@wordplus/realtime-ui/react takes your pieces in components:
import { CallScreen, CallControl, Chat } from '@wordplus/realtime-ui/react';
<CallScreen rt={ rt } components={ {
RingCard: ( { name, onAnswer, onReject } ) => <MyRing name={ name } answer={ onAnswer } reject={ onReject } />,
VideoTile: ( { children } ) => <div className="my-frame">{ children }</div>,
controls: ( buttons, call ) => [
...buttons,
<CallControl key="raise" icon="raisehand" label="Raise hand" onClick={ () => raise( call ) } />,
],
} } />
<Chat rt={ rt } room="chat:34:42" components={ { EmptyConversation: () => <p>Ask us anything about your order.</p> } } />
controlsgets our buttons in the dock order and the call (its state, its tracks, what it does), and returns the buttons to show. Drop ours, reorder them, or add your own withCallControl(label,icon,onClick,active). Hanging up stays last, and whatever does not fit folds into More, yours too.- A tile you replace keeps its sound: the other side is still heard whatever your tile draws. Draw their picture with the participant tracks:
participant.video?.attach( videoElement ). - Pieces nest.
<PiecesProvider pieces={ … }>gives them to every screen inside it, and thecomponentsgiven to one screen go over them.
On any page: the elements
The elements (<wordplus-chat>, <wordplus-online>, the call screen) draw in a shadow root of their own. Your stylesheet reaches each piece by its name:
wordplus-calls::part(call-controls) { bottom: 24px; }
wordplus-chat::part(message) { font-size: 15px; }
wordplus-online::part(online-row) { padding-block: 10px; }
The parts: ring-card, call-window, call-status, video-tile, self-view, call-controls, grid-tile, online-row, message, composer and empty-conversation.
To draw a piece with markup of your own, put a <template slot="…"> inside the element. The call screen templates go in a <wordplus-calls> you write on the page:
<wordplus-calls>
<template slot="ring-card">
<div class="ring"><b data-field="name"></b> is calling (<span data-field="type"></span>)
<button data-action="answer">Pick up</button> <button data-action="reject">Not now</button></div>
</template>
<template slot="control" data-control="raise" data-label="Raise hand" data-icon="raisehand"></template>
</wordplus-calls>
<wordplus-chat room="chat:34:42">
<template slot="empty-conversation"><p>Ask us anything about your order.</p></template>
<template slot="message"><div class="line"><b data-field="name"></b>: <span data-field="text"></span></div></template>
</wordplus-chat>
<script>
document.addEventListener( 'wordplus-control', ( e ) => { if ( e.detail.control === 'raise' ) raiseHand(); } );
</script>
data-field="…"takes that value as its text, never as markup:name,type,text,time,avatar,role,status.data-src="avatar"takes it as the elementsrc.data-actionanswers or rejects.data-oursmarks where ours goes inside yours.- A
<style>in the template styles what it draws, inside the element. - The templates fill
ring-card,call-status,empty-conversation,online-rowandmessage. Acontroltemplate adds a button to the dock, and the page hearswordplus-controlwhen it is pressed.
3. The code itself
Copy a view into your app, and change anything in it: its layout, its buttons, how it moves, what it does.
npx @wordplus/realtime-ui add call-screen # or online-list, chat
- Only the view is copied. What runs it stays in the package and keeps getting its fixes: the call (
usePrivateCall,useGroupCall, the ring in every tab, the devices, reconnecting and encryption) under the call screen,useRoomanduseProfilesunder the online list, anduseChat(history, sending, typing, reactions, reads) under the chat. - Where it goes:
components/wordplus/<component>/(--dirfor another folder).wordplus-ui.jsonrecords the version of each file, and.wordplus-ui/keeps ours as you copied it. Commit both. - What each copy holds:
| Component | Its files | Draw yours |
|---|---|---|
call-screen | call-screen.tsx (which layout), private-call.tsx, group-video-call.tsx, group-audio-call.tsx, the grids and their tiles, call-dock.tsx (the control row), device-switch.tsx, add-person.tsx, incoming-call-card.tsx (the ring) | <CallScreen rt={ rt } view={ CallView } components={ { RingCard } } /> |
online-list | online-list.tsx (the list, typing, the count), online-rows.tsx (a row) | <OnlineList rt={ rt } room="shop" /> from your copy |
chat | chat.tsx (the list, the bubbles, the composer) | <Chat rt={ rt } room="chat:34:42" /> from your copy |
- On a page without React,
addalso writeswordplus-ui.elements.tsxandwordplus-ui.build.mjs:node wordplus-ui.build.mjsbuilds your own elements script, with your copies in it, intopublic/wordplus-ui.js. Load it in place of ours:connect( { ui: '/wordplus-ui.js' } ).
Checking a change
- The tests come with the copy:
npx vitest run components/wordplus(withvitest,@testing-library/reactandhappy-dom). They draw every state from calls and rooms made by hand (@wordplus/realtime-ui/testing). - The preview:
npx @wordplus/realtime-ui previewserves every component in every state, yours in place of ours: calling out and in, connecting, in a call, a microphone off, a camera refused, group calls as a member and a moderator, the online list, typing, a conversation and an empty one.?theme=dark,?dir=rtland?width=phoneshow the rest. It reloads as you save, and--out folderbuilds it once. It needsesbuild(npm i -D esbuild). - With your pieces and your styles (as in a React app, step 2):
--pieces app/support/screens.tsx#piecesnames the module and export that hold your pieces,--css app/globals.css(once for each stylesheet) adds your styles after ours, and--class shopputs the classes your CSS is scoped under on the page.wordplus-ui.jsonkeeps them, so the nextpreviewneeds none. The module holds plain React: what only your framework has, such as the Next.js routing, has no place in it.
When ours changes
Nothing changes your copy by itself.
npx @wordplus/realtime-ui diffshows what changed in ours since you copied it, file by file, and whether you changed that file too.npx @wordplus/realtime-ui updatetakes ours into every file you never changed.- Merge the rest by hand, then run the tests and the preview.
npx @wordplus/realtime-ui accept call-screenrecords that yours has ours.
What must keep working
Whatever you change, check these before you ship:
- Every call state: calling out, ringing in, connecting, in a call, reconnecting, ended, and a group call as a member and as its moderator.
- A camera or a microphone refused: the call goes on, and says why.
- Right to left (
?dir=rtl), and the width of a phone (?width=phone). - Screen readers: every button keeps its label (
aria-label, orlabelonCallControl). - Picture in picture and full screen, where the browser has them.
- Sound: the other side is heard, and your own microphone is never played back to you.