Skip to content

Commit b1e182a

Browse files
authored
[Broadcaster] Add chat (#13)
1 parent 40ad093 commit b1e182a

File tree

10 files changed

+262
-66
lines changed

10 files changed

+262
-66
lines changed

broadcaster/assets/css/app.css

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,39 @@
33
@import "tailwindcss/utilities";
44

55
/* This file is for your main application CSS */
6+
.invalid-input {
7+
border-color: #d52b4d;
8+
}
9+
10+
.chat-message {
11+
display: flex;
12+
flex-direction: column;
13+
padding-top: 0.5rem;
14+
padding-bottom: 0.5rem;
15+
}
16+
17+
.chat-nickname {
18+
font-weight: 600;
19+
}
20+
21+
/* from https://stackoverflow.com/a/38994837/9620900 */
22+
/* Hiding scrollbar for Chrome, Safari and Opera */
23+
#chat-messages::-webkit-scrollbar {
24+
display: none;
25+
}
26+
27+
/* Hiding scrollbar for IE, Edge and Firefox */
28+
#chat-messages {
29+
scrollbar-width: none; /* Firefox */
30+
-ms-overflow-style: none; /* IE and Edge */
31+
}
32+
33+
#chat-input::-webkit-scrollbar {
34+
display: none;
35+
}
36+
37+
/* Hiding scrollbar for IE, Edge and Firefox */
38+
#chat-input {
39+
scrollbar-width: none; /* Firefox */
40+
-ms-overflow-style: none; /* IE and Edge */
41+
}

broadcaster/assets/js/app.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
22
// to get started and then uncomment the line below.
3-
import "./user_socket.js"
3+
// import "./user_socket.js"
44

55
// You can include dependencies in two ways.
66
//
@@ -18,12 +18,12 @@ import "./user_socket.js"
1818
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
1919
import "phoenix_html"
2020
// Establish Phoenix Socket and LiveView configuration.
21-
import {Socket} from "phoenix"
22-
import {LiveSocket} from "phoenix_live_view"
21+
import { Socket } from "phoenix"
22+
import { LiveSocket } from "phoenix_live_view"
2323
import topbar from "../vendor/topbar"
2424

25-
import {Home} from "./home.js"
26-
import {Player} from "./player.js"
25+
import { Home } from "./home.js"
26+
import { Player } from "./player.js"
2727

2828
let Hooks = {}
2929
Hooks.Home = Home
@@ -32,12 +32,12 @@ Hooks.Player = Player
3232
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
3333
let liveSocket = new LiveSocket("/live", Socket, {
3434
longPollFallbackMs: 2500,
35-
params: {_csrf_token: csrfToken},
35+
params: { _csrf_token: csrfToken },
3636
hooks: Hooks,
3737
})
3838

3939
// Show progress bar on live navigation and form submits
40-
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
40+
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" })
4141
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
4242
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
4343

broadcaster/assets/js/chat.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Socket, Presence } from "phoenix"
2+
3+
export async function connectChat() {
4+
const viewercount = document.getElementById("viewercount");
5+
const chatToggler = document.getElementById("chat-toggler");
6+
const chat = document.getElementById("chat");
7+
const chatMessages = document.getElementById("chat-messages");
8+
const chatInput = document.getElementById("chat-input");
9+
const chatNickname = document.getElementById("chat-nickname");
10+
const chatButton = document.getElementById("chat-button");
11+
12+
let socket = new Socket("/socket", { params: { token: window.userToken } });
13+
14+
socket.connect();
15+
16+
const channel = socket.channel("stream:chat");
17+
const presence = new Presence(channel);
18+
19+
send = function() {
20+
body = chatInput.value.trim();
21+
if (body != "") {
22+
channel.push("chat_msg", { body: body });
23+
chatInput.value = "";
24+
}
25+
}
26+
27+
presence.onSync(() => {
28+
viewercount.innerText = presence.list().length;
29+
});
30+
31+
channel.join()
32+
.receive("ok", resp => { console.log("Joined chat channel successfully", resp) })
33+
.receive("error", resp => { console.log("Unable to join chat channel", resp) });
34+
35+
channel.on("join_chat_resp", resp => {
36+
if (resp.result === 'success') {
37+
chatButton.innerText = "Send";
38+
chatButton.onclick = send;
39+
chatNickname.disabled = true;
40+
chatInput.disabled = false;
41+
chatInput.onkeydown = (ev) => {
42+
if (ev.key === 'Enter') {
43+
// prevent from adding a new line in our text area
44+
ev.preventDefault();
45+
send();
46+
}
47+
}
48+
} else {
49+
chatNickname.classList.add('invalid-input');
50+
}
51+
});
52+
53+
channel.on("chat_msg", msg => {
54+
if (msg.nickname == undefined || msg.body == undefined) return;
55+
56+
const chatMessage = document.createElement('div');
57+
chatMessage.classList.add('chat-message');
58+
59+
const nickname = document.createElement('div');
60+
nickname.classList.add('chat-nickname');
61+
nickname.innerText = msg.nickname;
62+
63+
const body = document.createElement('div');
64+
body.innerText = msg.body;
65+
66+
chatMessage.appendChild(nickname);
67+
chatMessage.appendChild(body);
68+
69+
chatMessages.appendChild(chatMessage);
70+
71+
// scroll to the bottom after adding a message
72+
chatMessages.scrollTop = chatMessages.scrollHeight;
73+
74+
// allow for 1 scroll history
75+
if (chatMessages.scrollHeight > 2 * chatMessages.clientHeight) {
76+
chatMessages.removeChild(chatMessages.children[0]);
77+
}
78+
})
79+
80+
chatButton.onclick = () => {
81+
channel.push("join_chat", { nickname: chatNickname.value });
82+
};
83+
84+
chatNickname.onclick = () => {
85+
chatNickname.classList.remove("invalid-input");
86+
}
87+
88+
chatToggler.onclick = () => {
89+
if (window.getComputedStyle(chat).display === "none") {
90+
chat.style.display = "flex";
91+
92+
// For screen's width lower than 1024,
93+
// eiter show video player or chat at the same time.
94+
if (window.innerWidth < 1024) {
95+
document.getElementById("videoplayer-wrapper").style.display = "none";
96+
}
97+
} else {
98+
chat.style.display = "none";
99+
100+
if (window.innerWidth < 1024) {
101+
document.getElementById("videoplayer-wrapper").style.display = "block";
102+
}
103+
}
104+
}
105+
}

broadcaster/assets/js/home.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { connectChat } from "./chat.js"
2+
13
const pcConfig = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] };
24
const whepEndpoint = `${window.location.origin}/api/whep`
3-
const videoPlayer = document.getElementById("videoPlayer");
5+
const videoPlayer = document.getElementById("videoplayer");
46
const candidates = [];
57
let patchEndpoint;
68

@@ -21,7 +23,7 @@ async function sendCandidate(candidate) {
2123
}
2224
}
2325

24-
async function connect() {
26+
async function connectMedia() {
2527
const pc = new RTCPeerConnection(pcConfig);
2628

2729
pc.ontrack = event => videoPlayer.srcObject = event.streams[0];
@@ -75,6 +77,7 @@ async function connect() {
7577

7678
export const Home = {
7779
mounted() {
78-
connect()
80+
connectMedia()
81+
connectChat()
7982
}
8083
}

broadcaster/assets/js/user_socket.js

Lines changed: 0 additions & 27 deletions
This file was deleted.

broadcaster/lib/broadcaster/application.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ defmodule Broadcaster.Application do
1818
BroadcasterWeb.Presence,
1919
Broadcaster.PeerSupervisor,
2020
Broadcaster.Forwarder,
21-
{Registry, name: Broadcaster.PeerRegistry, keys: :unique}
21+
{Registry, name: Broadcaster.PeerRegistry, keys: :unique},
22+
{Registry, name: Broadcaster.ChatNicknamesRegistry, keys: :unique}
2223
]
2324

2425
# See https://hexdocs.pm/elixir/Supervisor.html

broadcaster/lib/broadcaster_web/channels/stream_channel.ex

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,51 @@ defmodule BroadcasterWeb.StreamChannel do
44
alias BroadcasterWeb.Presence
55

66
@impl true
7-
def join("stream:chat", %{"name" => name}, socket) do
7+
def join("stream:chat", _, socket) do
88
send(self(), :after_join)
9-
{:ok, assign(socket, :name, name)}
9+
{:ok, assign(socket, :nickname, nil)}
10+
end
11+
12+
@impl true
13+
def handle_in("chat_msg", _, %{assigns: %{nickname: nil}} = socket) do
14+
{:noreply, socket}
1015
end
1116

1217
@impl true
1318
def handle_in("chat_msg", %{"body" => body}, socket) do
14-
broadcast!(socket, "chat_msg", %{body: body, name: socket.assigns.name})
19+
broadcast!(socket, "chat_msg", %{body: body, nickname: socket.assigns.nickname})
1520
{:noreply, socket}
1621
end
1722

23+
@impl true
24+
def handle_in("join_chat", %{"nickname" => nickname}, socket) do
25+
case register(nickname) do
26+
:ok ->
27+
socket = assign(socket, :nickname, nickname)
28+
:ok = push(socket, "join_chat_resp", %{"result" => "success"})
29+
{:noreply, socket}
30+
31+
:error ->
32+
:ok = push(socket, "join_chat_resp", %{"result" => "error"})
33+
{:noreply, socket}
34+
end
35+
end
36+
1837
@impl true
1938
def handle_info(:after_join, socket) do
2039
{:ok, _} = Presence.track(socket, socket.assigns.user_id, %{})
2140
push(socket, "presence_state", Presence.list(socket))
2241
{:noreply, socket}
2342
end
43+
44+
defp register(nickname), do: do_register(String.trim(nickname))
45+
46+
defp do_register(""), do: :error
47+
48+
defp do_register(nickname) do
49+
case Registry.register(Broadcaster.ChatNicknamesRegistry, nickname, nil) do
50+
{:ok, _} -> :ok
51+
{:error, _} -> :error
52+
end
53+
end
2454
end

broadcaster/lib/broadcaster_web/components/layouts/app.html.heex

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,46 @@
11
<header class="px-4 sm:px-6 lg:px-8">
2-
<div class="flex items-center justify-between border-b border-brand/15 py-3 text-sm">
2+
<div class="flex flex-col lg:flex-row items-center justify-between border-b border-brand/15 py-3 text-sm">
33
<div class="flex items-center gap-4">
44
<a href="/">
55
<img src={~p"/images/logo.svg"} width="225" />
66
</a>
77
</div>
8-
<div class="flex items-center gap-4 font-semibold leading-6 text-brand/80">
9-
<div class="flex gap-1 px-4">
10-
<p id="viewercount">0</p>
11-
<.icon name="hero-user-solid" />
8+
<div class="flex">
9+
<div class="flex items-center gap-4 font-semibold leading-6 text-brand/80">
10+
<div class="flex gap-1">
11+
<p id="viewercount">0</p>
12+
<.icon name="hero-user-solid" />
13+
</div>
14+
<div id="chat-toggler">
15+
<.icon name="hero-chat-bubble-bottom-center" />
16+
</div>
17+
<a href="https://github.com/elixir-webrtc/ex_webrtc" class="hover:text-brand">
18+
GitHub
19+
</a>
20+
<a
21+
href="https://hexdocs.pm/ex_webrtc/readme.html"
22+
class="rounded-lg bg-brand/10 px-2 py-1 hover:bg-brand/20"
23+
>
24+
Docs <span aria-hidden="true">&rarr;</span>
25+
</a>
1226
</div>
13-
<a href="https://github.com/elixir-webrtc/ex_webrtc" class="hover:text-brand">
14-
GitHub
15-
</a>
16-
<a
17-
href="https://hexdocs.pm/ex_webrtc/readme.html"
18-
class="rounded-lg bg-brand/10 px-2 py-1 hover:bg-brand/20"
19-
>
20-
Docs <span aria-hidden="true">&rarr;</span>
21-
</a>
2227
</div>
2328
</div>
2429
</header>
25-
<main class="flex-1">
26-
<div class="m-auto py-7">
30+
<!--
31+
flex-1 will make our main element grow as much as its parent allows it to do so.
32+
However, if one of main's children grows more than main, main will also grow.
33+
That's because parent cannot be smaller than its content.
34+
This is enforced by min-h and min-w set to auto by default.
35+
As a consequence, we will never experience overflow in children elements.
36+
In particular, overflow-scroll on chat div won't have any effect.
37+
To change this behaviour, we set min-h-0 to allow our main element to shrink to the size
38+
smaller than its content. Setting overflow-hidden or overflow-auto has the same effect.
39+
I put this comment as it took me at least 5 hours to understand what's going on.
40+
See https://stackoverflow.com/a/36247448/9620900
41+
-->
42+
<main class="flex-1 min-h-0">
43+
<div class="h-full py-7 px-7">
2744
<.flash_group flash={@flash} />
2845
<%= @inner_content %>
2946
</div>

broadcaster/lib/broadcaster_web/components/layouts/root.html.heex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
1212
</script>
1313
</head>
14-
<body class="bg-white antialiased w-screen h-screen flex flex-col">
14+
<body class="bg-white antialiased h-svh flex flex-col">
1515
<%= @inner_content %>
1616
</body>
1717
</html>

0 commit comments

Comments
 (0)