Meeting Companion
In this post we’re going to build a real-time web application with SignalR and React that lets you create chatrooms and send noises to each other, perfect as a meeting companion when you don’t want everybody coming off mute. If you follow along, you’ll be able to build and deploy your very own copy of Meeting Companion and run it completely free on Microsoft Azure. All of the code is available in my GitHub repository. This is a slightly simplified version of another webapp I built, Soundroom, which can also be found in my GitHub repository.
Project setup
Let’s get started by creating our project directory and adding the required boilerplate. Open up your favourite terminal and run the following commands to create a blank React app for the front-end and a .NET Core web application for the back-end.
mkdir meeting-companion
cd meeting-companion
npx create-react-app .
dotnet new web
yarn build
Next we need to enable static file serving on our backend so that it
can serve our front-end. In Program.cs
, make the following change.
@@ -21,6 +21,7 @@ namespace meeting_companion
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup();
+ webBuilder.UseWebRoot("build");
});
}
}
In Startup.cs
, make the following two changes.
@@ -26,14 +26,11 @@ namespace meeting_companion
app.UseDeveloperExceptionPage();
}
+ app.UseFileServer();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
- endpoints.MapGet("/", async context =>
- {
- await context.Response.WriteAsync("Hello World!");
- });
});
}
}
Now we can build and run the front-end and back-end.
yarn build
dotnet run
You should now be able to open your browser to http://localhost:5000 and see the default React app landing page.
If you’re planning to version-control this project, now would be a
good time to add the bin/
and obj/
directories to .gitignore
. Add the following two lines
anywhere to .gitignore
.
bin/
obj/
Adding SignalR to the back-end
Next up, we will add SignalR to the backend. The first thing we need
to do is create a Hub. Hubs are similar to controllers in that they accept
incoming messages and decide how they get processed. Our app is very
simple and will only require a single hub, so create a new file
called MeetingHub.cs
and add the following code.
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
namespace meeting_companion
{
public class MeetingHub : Hub
{
public Task Send(string sound) => Clients.All.SendAsync(sound);
}
}
This hub can receive Send
messages which contain a
sound
parameter. When it receives one of these messages,
it will forward it straight through to all connected clients. We
don’t need to add any code for storing incoming connections or
anything like that—Hub
base
class. Next we need to register the hub and connect it to the ASP.NET
routing middleware. Back in Startup.cs
, make the
following two change.
@@ -16,6 +16,7 @@ namespace meeting_companion
// ...
public void ConfigureServices(IServiceCollection services)
{
+ services.AddSignalR();
}
// ...
@@ -31,6 +32,7 @@ namespace meeting_companion
app.UseEndpoints(endpoints =>
{
+ endpoints.MapHub<MeetingHub>("/meeting");
});
}
}
That’s all we need to add to the back-end! You could build and run again, but we haven’t added any observable changes yet.
Adding SignalR to the front-end
Next up, we’ll add SignalR to the front-end. The first thing we need to do is add the signalr package.
yarn add @microsoft/signalr
Now we’ll import the signalr
package and create a
connection to our hub. We’re also going to create a helper object to
hide some of the details of interacting with the connection from the
rest of our application. Open src/index.js
and make the
following two changes.
@@ -3,10 +3,21 @@ import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
+import { HubConnectionBuilder } from '@microsoft/signalr';
+
+const connection = new HubConnectionBuilder()
+ .withUrl('/meeting')
+ .build();
+connection.start();
+
+const connectionProxy = {
+ send: sound => connection.invoke('send', sound),
+ recv: (sound, callback) => connection.on(sound, callback)
+};
ReactDOM.render(
<React.StrictMode>
- <App />
+ <App connection={connectionProxy} />
</React.StrictMode>,
document.getElementById('root')
);
Our application can now use the send
method of this
object to send a message to the Send
message handler of
our MeetingHub
, and the recv
method to
register a callback that will fire any time a response message is
received from the server.
Our front-end will now connect to our back-end, but because we haven’t written any logic to send messages, we still won’t be able to observe any interesting behaviour from running the app. Hang in there just a little bit longer.
Building out the front-end
Now it’s time to add some interactivity to the front-end of the app
so we can actually have fun with it. We’re going to add a few
buttons to the screen, and when you press the button it should play a
sound for everybody also connected. This is going to involve using
the send
method of our connection proxy to send a
message to the server, and then the recv
method to
hook up incoming messages from the server to audio players.
The first thing we’ll do is create a Button
component
which displays a button on the screen, adds an audio player, and
wires them both up to the connection, so create a new file called
src/Button.js
and add the following code.
import React from 'react';
export default function Button ({ icon, src, alt, connection }) {
const audio = React.createRef();
connection.recv(icon, () => audio.current.play());
return <>
<button aria-title={alt} onClick={() => connection.send(icon)}>{icon}</button>
<audio ref={audio} src={src} />
</>;
};
The button takes as properties an emoji to display on the button, the
filename of the audio, a title for accessibility, and the connection
object. When the client-side connection receives the icon back as a
message from the server, it will cause the audio player to fire, and
pressing the button will cause the connection to send the icon to the
server’s Send
message handler (which we previously
programmed to forward the message through to all connections).
Next up we need to add a bunch of these buttons to the app. Open up
src/App.js
and replace the App
definition
with the following.
import Button from './Button';
function App({ connection }) {
return (
<div className="App">
<Button connection={connection} icon="👏" alt="Clap" src="/audio/clap.mp3" />
<Button connection={connection} icon="👍" alt="Yes" src="/audio/yes.mp3" />
<Button connection={connection} icon="👎" alt="No" src="/audio/no.mp3" />
<Button connection={connection} icon="🔍" alt="Zoom" src="/audio/zoom.mp3" />
<Button connection={connection} icon="🧏♀️" alt="Louder" src="/audio/louder.mp3" />
<Button connection={connection} icon="💬" alt="Check chat" src="/audio/chat.mp3" />
</div>
);
}
You’ll need to create a public/audio
directory and add
audio files there—
All that’s left now is to fix up the CSS. Open up
src/App.css
and replace all of the content with the
following.
.App {
display: flex;
flex-flow: wrap;
justify-content: center;
}
.App button {
font-size: 5em;
}
Now we finally have something worth running. Run the following again.
yarn build
dotnet run
Now you should see the six buttons on the screen, and when you press any of them you should hear the corresponding audio file. If you open a second tab, pressing a button in either tab should cause audio to play in both tabs.
Adding rooms
The app so far is pretty interesting, but I said we were going to be
able to create separate rooms—
To start with, we’re going to add a new message handler to our
back-end to accept requests to join a room. We’ll also tweak our
Send
message handler to take an additional parameter
to identify the room to which the message should be sent. Open
MeetingHub.cs
and make the following change.
@@ -5,6 +5,9 @@ namespace meeting_companion
{
public class MeetingHub : Hub
{
- public Task Send(string sound) => Clients.All.SendAsync(sound);
+ public Task Join(string room)
+ => Groups.AddToGroupAsync(Context.ConnectionId, room);
+ public Task Send(string room, string sound)
+ => Clients.Group(room).SendAsync(sound);
}
}
That’s all we need from the back-end. We don’t need to write any code
to handle group membership and distribution—Hub
base class.
Now we need to update the front-end to send a Join
message when the connection starts, and change the way we send
Send
messages to include the room name. And we also need
to let the user pick the room name. We’ll be extremely lazy and just
take the room name from the URL hash.
Open src/App.js
and make the following three changes.
@@ -5,13 +5,15 @@ import App from './App';
import * as serviceWorker from './serviceWorker';
import { HubConnectionBuilder } from '@microsoft/signalr';
+const room = window.location.hash.substr(1) || '%DEFAULT%';
+
const connection = new HubConnectionBuilder()
.withUrl('/meeting')
.build();
-connection.start();
+connection.start().then(() => connection.invoke('join', room));
const connectionProxy = {
- send: sound => connection.invoke('send', sound),
+ send: sound => connection.invoke('send', room, sound),
recv: (sound, callback) => connection.on(sound, callback)
};
And that’s all we need! Build and run again, but this time make sure to open a few tabs in the same room and a few in different rooms. You can use http://localhost:5000/#room-1 and http://localhost:5000/#room-2 as examples.
Deploying to Azure
We can deploy this to an App Service, optionally with SignalR Service, both of which have free tiers. The simplest way
to do this which will suffice for this blog post is via the
dreaded right-click
publish, by opening meeting-companion.csproj
in
Visual Studio and using Publish.
Good luck, and I hope my instructions were complete and accurate. If not, check the source code in my GitHub repository or get in touch.