From c3ebe3992c1158b62a3720ea648f5ce7d93217b2 Mon Sep 17 00:00:00 2001 From: Johnny Mikhael Date: Mon, 7 Feb 2022 01:26:15 +0100 Subject: [PATCH] Multi User Cursors Page ref: https://koenvangilst.nl/blog/phoenix-live-cursors --- assets/css/app.css | 6 ++ assets/js/app.js | 2 +- assets/js/user_socket.js | 84 +++++++++++++++++++ lib/my_button_app/application.ex | 3 +- lib/my_button_app/names.ex | 25 ++++++ .../channels/cursor_channel.ex | 65 ++++++++++++++ lib/my_button_app_web/channels/presence.ex | 10 +++ lib/my_button_app_web/channels/user_socket.ex | 48 +++++++++++ .../controllers/page_controller.ex | 6 ++ lib/my_button_app_web/endpoint.ex | 1 + lib/my_button_app_web/router.ex | 1 + .../templates/layout/root.html.heex | 3 +- .../templates/page/cursors.html.heex | 20 +++++ .../templates/page/index.html.heex | 3 + .../channels/cursor_channel_test.exs | 27 ++++++ 15 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 assets/js/user_socket.js create mode 100644 lib/my_button_app/names.ex create mode 100644 lib/my_button_app_web/channels/cursor_channel.ex create mode 100644 lib/my_button_app_web/channels/presence.ex create mode 100644 lib/my_button_app_web/channels/user_socket.ex create mode 100644 lib/my_button_app_web/templates/page/cursors.html.heex create mode 100644 test/my_button_app_web/channels/cursor_channel_test.exs diff --git a/assets/css/app.css b/assets/css/app.css index b5ec7ec..61078da 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -7,6 +7,12 @@ * Since we can not split into multiple file and include them using the @import syntax */ + body { + cursor: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260' viewBox='0 0 260 260'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%23a7a7a7' fill-opacity='0.33'%3E%3Cpath d='M24.37 16c.2.65.39 1.32.54 2H21.17l1.17 2.34.45.9-.24.11V28a5 5 0 0 1-2.23 8.94l-.02.06a8 8 0 0 1-7.75 6h-20a8 8 0 0 1-7.74-6l-.02-.06A5 5 0 0 1-17.45 28v-6.76l-.79-1.58-.44-.9.9-.44.63-.32H-20a23.01 23.01 0 0 1 44.37-2zm-36.82 2a1 1 0 0 0-.44.1l-3.1 1.56.89 1.79 1.31-.66a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .86.02l2.88-1.27a3 3 0 0 1 2.43 0l2.88 1.27a1 1 0 0 0 .85-.02l3.1-1.55-.89-1.79-1.42.71a3 3 0 0 1-2.56.06l-2.77-1.23a1 1 0 0 0-.4-.09h-.01a1 1 0 0 0-.4.09l-2.78 1.23a3 3 0 0 1-2.56-.06l-2.3-1.15a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1L.9 19.22a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01zm0-2h-4.9a21.01 21.01 0 0 1 39.61 0h-2.09l-.06-.13-.26.13h-32.31zm30.35 7.68l1.36-.68h1.3v2h-36v-1.15l.34-.17 1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0L2.26 23h2.59l1.36.68a3 3 0 0 0 2.56.06l1.67-.74h3.23l1.67.74a3 3 0 0 0 2.56-.06zM-13.82 27l16.37 4.91L18.93 27h-32.75zm-.63 2h.34l16.66 5 16.67-5h.33a3 3 0 1 1 0 6h-34a3 3 0 1 1 0-6zm1.35 8a6 6 0 0 0 5.65 4h20a6 6 0 0 0 5.66-4H-13.1z'/%3E%3Cpath id='path6_fill-copy' d='M284.37 16c.2.65.39 1.32.54 2H281.17l1.17 2.34.45.9-.24.11V28a5 5 0 0 1-2.23 8.94l-.02.06a8 8 0 0 1-7.75 6h-20a8 8 0 0 1-7.74-6l-.02-.06a5 5 0 0 1-2.24-8.94v-6.76l-.79-1.58-.44-.9.9-.44.63-.32H240a23.01 23.01 0 0 1 44.37-2zm-36.82 2a1 1 0 0 0-.44.1l-3.1 1.56.89 1.79 1.31-.66a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .86.02l2.88-1.27a3 3 0 0 1 2.43 0l2.88 1.27a1 1 0 0 0 .85-.02l3.1-1.55-.89-1.79-1.42.71a3 3 0 0 1-2.56.06l-2.77-1.23a1 1 0 0 0-.4-.09h-.01a1 1 0 0 0-.4.09l-2.78 1.23a3 3 0 0 1-2.56-.06l-2.3-1.15a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01zm0-2h-4.9a21.01 21.01 0 0 1 39.61 0h-2.09l-.06-.13-.26.13h-32.31zm30.35 7.68l1.36-.68h1.3v2h-36v-1.15l.34-.17 1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.56.06l1.67-.74h3.23l1.67.74a3 3 0 0 0 2.56-.06zM246.18 27l16.37 4.91L278.93 27h-32.75zm-.63 2h.34l16.66 5 16.67-5h.33a3 3 0 1 1 0 6h-34a3 3 0 1 1 0-6zm1.35 8a6 6 0 0 0 5.65 4h20a6 6 0 0 0 5.66-4H246.9z'/%3E%3Cpath d='M159.5 21.02A9 9 0 0 0 151 15h-42a9 9 0 0 0-8.5 6.02 6 6 0 0 0 .02 11.96A8.99 8.99 0 0 0 109 45h42a9 9 0 0 0 8.48-12.02 6 6 0 0 0 .02-11.96zM151 17h-42a7 7 0 0 0-6.33 4h54.66a7 7 0 0 0-6.33-4zm-9.34 26a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-4.34a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-4.34a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-7a7 7 0 1 1 0-14h42a7 7 0 1 1 0 14h-9.34zM109 27a9 9 0 0 0-7.48 4H101a4 4 0 1 1 0-8h58a4 4 0 0 1 0 8h-.52a9 9 0 0 0-7.48-4h-42z'/%3E%3Cpath d='M39 115a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm6-8a6 6 0 1 1-12 0 6 6 0 0 1 12 0zm-3-29v-2h8v-6H40a4 4 0 0 0-4 4v10H22l-1.33 4-.67 2h2.19L26 130h26l3.81-40H58l-.67-2L56 84H42v-6zm-4-4v10h2V74h8v-2h-8a2 2 0 0 0-2 2zm2 12h14.56l.67 2H22.77l.67-2H40zm13.8 4H24.2l3.62 38h22.36l3.62-38z'/%3E%3Cpath d='M129 92h-6v4h-6v4h-6v14h-3l.24 2 3.76 32h36l3.76-32 .24-2h-3v-14h-6v-4h-6v-4h-8zm18 22v-12h-4v4h3v8h1zm-3 0v-6h-4v6h4zm-6 6v-16h-4v19.17c1.6-.7 2.97-1.8 4-3.17zm-6 3.8V100h-4v23.8a10.04 10.04 0 0 0 4 0zm-6-.63V104h-4v16a10.04 10.04 0 0 0 4 3.17zm-6-9.17v-6h-4v6h4zm-6 0v-8h3v-4h-4v12h1zm27-12v-4h-4v4h3v4h1v-4zm-6 0v-8h-4v4h3v4h1zm-6-4v-4h-4v8h1v-4h3zm-6 4v-4h-4v8h1v-4h3zm7 24a12 12 0 0 0 11.83-10h7.92l-3.53 30h-32.44l-3.53-30h7.92A12 12 0 0 0 130 126z'/%3E%3Cpath d='M212 86v2h-4v-2h4zm4 0h-2v2h2v-2zm-20 0v.1a5 5 0 0 0-.56 9.65l.06.25 1.12 4.48a2 2 0 0 0 1.94 1.52h.01l7.02 24.55a2 2 0 0 0 1.92 1.45h4.98a2 2 0 0 0 1.92-1.45l7.02-24.55a2 2 0 0 0 1.95-1.52L224.5 96l.06-.25a5 5 0 0 0-.56-9.65V86a14 14 0 0 0-28 0zm4 0h6v2h-9a3 3 0 1 0 0 6H223a3 3 0 1 0 0-6H220v-2h2a12 12 0 1 0-24 0h2zm-1.44 14l-1-4h24.88l-1 4h-22.88zm8.95 26l-6.86-24h18.7l-6.86 24h-4.98zM150 242a22 22 0 1 0 0-44 22 22 0 0 0 0 44zm24-22a24 24 0 1 1-48 0 24 24 0 0 1 48 0zm-28.38 17.73l2.04-.87a6 6 0 0 1 4.68 0l2.04.87a2 2 0 0 0 2.5-.82l1.14-1.9a6 6 0 0 1 3.79-2.75l2.15-.5a2 2 0 0 0 1.54-2.12l-.19-2.2a6 6 0 0 1 1.45-4.46l1.45-1.67a2 2 0 0 0 0-2.62l-1.45-1.67a6 6 0 0 1-1.45-4.46l.2-2.2a2 2 0 0 0-1.55-2.13l-2.15-.5a6 6 0 0 1-3.8-2.75l-1.13-1.9a2 2 0 0 0-2.5-.8l-2.04.86a6 6 0 0 1-4.68 0l-2.04-.87a2 2 0 0 0-2.5.82l-1.14 1.9a6 6 0 0 1-3.79 2.75l-2.15.5a2 2 0 0 0-1.54 2.12l.19 2.2a6 6 0 0 1-1.45 4.46l-1.45 1.67a2 2 0 0 0 0 2.62l1.45 1.67a6 6 0 0 1 1.45 4.46l-.2 2.2a2 2 0 0 0 1.55 2.13l2.15.5a6 6 0 0 1 3.8 2.75l1.13 1.9a2 2 0 0 0 2.5.8zm2.82.97a4 4 0 0 1 3.12 0l2.04.87a4 4 0 0 0 4.99-1.62l1.14-1.9a4 4 0 0 1 2.53-1.84l2.15-.5a4 4 0 0 0 3.09-4.24l-.2-2.2a4 4 0 0 1 .97-2.98l1.45-1.67a4 4 0 0 0 0-5.24l-1.45-1.67a4 4 0 0 1-.97-2.97l.2-2.2a4 4 0 0 0-3.09-4.25l-2.15-.5a4 4 0 0 1-2.53-1.84l-1.14-1.9a4 4 0 0 0-5-1.62l-2.03.87a4 4 0 0 1-3.12 0l-2.04-.87a4 4 0 0 0-4.99 1.62l-1.14 1.9a4 4 0 0 1-2.53 1.84l-2.15.5a4 4 0 0 0-3.09 4.24l.2 2.2a4 4 0 0 1-.97 2.98l-1.45 1.67a4 4 0 0 0 0 5.24l1.45 1.67a4 4 0 0 1 .97 2.97l-.2 2.2a4 4 0 0 0 3.09 4.25l2.15.5a4 4 0 0 1 2.53 1.84l1.14 1.9a4 4 0 0 0 5 1.62l2.03-.87zM152 207a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm6 2a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-11 1a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-6 0a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm3-5a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-8 8a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm3 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4 7a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm5-2a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm5 4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4-6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm6-4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-4-3a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4-3a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-5-4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-24 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm16 5a5 5 0 1 0 0-10 5 5 0 0 0 0 10zm7-5a7 7 0 1 1-14 0 7 7 0 0 1 14 0zm86-29a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm19 9a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-14 5a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-25 1a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm5 4a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm9 0a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm15 1a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm12-2a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-11-14a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-19 0a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm6 5a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-25 15c0-.47.01-.94.03-1.4a5 5 0 0 1-1.7-8 3.99 3.99 0 0 1 1.88-5.18 5 5 0 0 1 3.4-6.22 3 3 0 0 1 1.46-1.05 5 5 0 0 1 7.76-3.27A30.86 30.86 0 0 1 246 184c6.79 0 13.06 2.18 18.17 5.88a5 5 0 0 1 7.76 3.27 3 3 0 0 1 1.47 1.05 5 5 0 0 1 3.4 6.22 4 4 0 0 1 1.87 5.18 4.98 4.98 0 0 1-1.7 8c.02.46.03.93.03 1.4v1h-62v-1zm.83-7.17a30.9 30.9 0 0 0-.62 3.57 3 3 0 0 1-.61-4.2c.37.28.78.49 1.23.63zm1.49-4.61c-.36.87-.68 1.76-.96 2.68a2 2 0 0 1-.21-3.71c.33.4.73.75 1.17 1.03zm2.32-4.54c-.54.86-1.03 1.76-1.49 2.68a3 3 0 0 1-.07-4.67 3 3 0 0 0 1.56 1.99zm1.14-1.7c.35-.5.72-.98 1.1-1.46a1 1 0 1 0-1.1 1.45zm5.34-5.77c-1.03.86-2 1.79-2.9 2.77a3 3 0 0 0-1.11-.77 3 3 0 0 1 4-2zm42.66 2.77c-.9-.98-1.87-1.9-2.9-2.77a3 3 0 0 1 4.01 2 3 3 0 0 0-1.1.77zm1.34 1.54c.38.48.75.96 1.1 1.45a1 1 0 1 0-1.1-1.45zm3.73 5.84c-.46-.92-.95-1.82-1.5-2.68a3 3 0 0 0 1.57-1.99 3 3 0 0 1-.07 4.67zm1.8 4.53c-.29-.9-.6-1.8-.97-2.67.44-.28.84-.63 1.17-1.03a2 2 0 0 1-.2 3.7zm1.14 5.51c-.14-1.21-.35-2.4-.62-3.57.45-.14.86-.35 1.23-.63a2.99 2.99 0 0 1-.6 4.2zM275 214a29 29 0 0 0-57.97 0h57.96zM72.33 198.12c-.21-.32-.34-.7-.34-1.12v-12h-2v12a4.01 4.01 0 0 0 7.09 2.54c.57-.69.91-1.57.91-2.54v-12h-2v12a1.99 1.99 0 0 1-2 2 2 2 0 0 1-1.66-.88zM75 176c.38 0 .74-.04 1.1-.12a4 4 0 0 0 6.19 2.4A13.94 13.94 0 0 1 84 185v24a6 6 0 0 1-6 6h-3v9a5 5 0 1 1-10 0v-9h-3a6 6 0 0 1-6-6v-24a14 14 0 0 1 14-14 5 5 0 0 0 5 5zm-17 15v12a1.99 1.99 0 0 0 1.22 1.84 2 2 0 0 0 2.44-.72c.21-.32.34-.7.34-1.12v-12h2v12a3.98 3.98 0 0 1-5.35 3.77 3.98 3.98 0 0 1-.65-.3V209a4 4 0 0 0 4 4h16a4 4 0 0 0 4-4v-24c.01-1.53-.23-2.88-.72-4.17-.43.1-.87.16-1.28.17a6 6 0 0 1-5.2-3 7 7 0 0 1-6.47-4.88A12 12 0 0 0 58 185v6zm9 24v9a3 3 0 1 0 6 0v-9h-6z'/%3E%3Cpath d='M-17 191a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm19 9a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2H3a1 1 0 0 1-1-1zm-14 5a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-25 1a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm5 4a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm9 0a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm15 1a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm12-2a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2H4zm-11-14a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-19 0a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm6 5a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-25 15c0-.47.01-.94.03-1.4a5 5 0 0 1-1.7-8 3.99 3.99 0 0 1 1.88-5.18 5 5 0 0 1 3.4-6.22 3 3 0 0 1 1.46-1.05 5 5 0 0 1 7.76-3.27A30.86 30.86 0 0 1-14 184c6.79 0 13.06 2.18 18.17 5.88a5 5 0 0 1 7.76 3.27 3 3 0 0 1 1.47 1.05 5 5 0 0 1 3.4 6.22 4 4 0 0 1 1.87 5.18 4.98 4.98 0 0 1-1.7 8c.02.46.03.93.03 1.4v1h-62v-1zm.83-7.17a30.9 30.9 0 0 0-.62 3.57 3 3 0 0 1-.61-4.2c.37.28.78.49 1.23.63zm1.49-4.61c-.36.87-.68 1.76-.96 2.68a2 2 0 0 1-.21-3.71c.33.4.73.75 1.17 1.03zm2.32-4.54c-.54.86-1.03 1.76-1.49 2.68a3 3 0 0 1-.07-4.67 3 3 0 0 0 1.56 1.99zm1.14-1.7c.35-.5.72-.98 1.1-1.46a1 1 0 1 0-1.1 1.45zm5.34-5.77c-1.03.86-2 1.79-2.9 2.77a3 3 0 0 0-1.11-.77 3 3 0 0 1 4-2zm42.66 2.77c-.9-.98-1.87-1.9-2.9-2.77a3 3 0 0 1 4.01 2 3 3 0 0 0-1.1.77zm1.34 1.54c.38.48.75.96 1.1 1.45a1 1 0 1 0-1.1-1.45zm3.73 5.84c-.46-.92-.95-1.82-1.5-2.68a3 3 0 0 0 1.57-1.99 3 3 0 0 1-.07 4.67zm1.8 4.53c-.29-.9-.6-1.8-.97-2.67.44-.28.84-.63 1.17-1.03a2 2 0 0 1-.2 3.7zm1.14 5.51c-.14-1.21-.35-2.4-.62-3.57.45-.14.86-.35 1.23-.63a2.99 2.99 0 0 1-.6 4.2zM15 214a29 29 0 0 0-57.97 0h57.96z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); +} + + /* Alerts and form errors used by phx.new */ .alert { padding: 15px; diff --git a/assets/js/app.js b/assets/js/app.js index a513eaf..9102240 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -3,7 +3,7 @@ // If you want to use Phoenix channels, run `mix help phx.gen.channel` // to get started and then uncomment the line below. -// import "./user_socket.js" +import "./user_socket.js" // You can include dependencies in two ways. // diff --git a/assets/js/user_socket.js b/assets/js/user_socket.js new file mode 100644 index 0000000..a874528 --- /dev/null +++ b/assets/js/user_socket.js @@ -0,0 +1,84 @@ +// NOTE: The contents of this file will only be executed if +// you uncomment its entry in "assets/js/app.js". + +// Bring in Phoenix channels client library: +import { Socket, Presence } from 'phoenix'; + +function cursorTemplate({ x, y, name, color }) { + const li = document.createElement('li'); + li.classList = + 'flex flex-col absolute pointer-events-none whitespace-nowrap overflow-hidden'; + li.style.left = x + 'px'; + li.style.top = y + 'px'; + li.style.color = color; + + li.innerHTML = ` + + + + + + `; + + li.lastChild.style.backgroundColor = color; + li.lastChild.textContent = name; + + return li; +} + +// And connect to the path in "lib/my_button_app_web/endpoint.ex". We pass the +// token for authentication. Read below how it should be used. +// {params: {token: window.userToken}} +let socket = new Socket('/socket', { + params: { token: sessionStorage.userToken } +}); +socket.connect() + +// Now that you are connected, you can join channels with a topic. +// Let's assume you have a channel with a topic named `room` and the +// subtopic is its id - in this case 42: +let channel = socket.channel("cursor:lobby", {}) +channel.join() + .receive('ok', (resp) => { + console.log('Joined successfully', resp); + document.addEventListener('mousemove', (e) => { + const x = e.pageX / window.innerWidth; + const y = e.pageY / window.innerHeight; + channel.push('move', { x, y }); + }); + + }) + .receive('error', (resp) => { + console.log('Unable to join', resp); + }); + +const presence = new Presence(channel); +presence.onSync(() => { + const ul = document.createElement('ul'); + + presence.list((name, { metas: [firstDevice] }) => { + const { x, y, color } = firstDevice; + const cursorLi = cursorTemplate({ + name, + x: x * window.innerWidth, + y: y * window.innerHeight, + color + }); + ul.appendChild(cursorLi); + }); + + document.getElementById('cursor-list').innerHTML = ul.innerHTML; +}); + +export default socket diff --git a/lib/my_button_app/application.ex b/lib/my_button_app/application.ex index c0b5796..60e9153 100644 --- a/lib/my_button_app/application.ex +++ b/lib/my_button_app/application.ex @@ -15,9 +15,10 @@ defmodule MyButtonApp.Application do # Start the PubSub system {Phoenix.PubSub, name: MyButtonApp.PubSub}, # Start the Endpoint (http/https) - MyButtonAppWeb.Endpoint + MyButtonAppWeb.Endpoint, # Start a worker by calling: MyButtonApp.Worker.start_link(arg) # {MyButtonApp.Worker, arg} + MyButtonAppWeb.Presence ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/my_button_app/names.ex b/lib/my_button_app/names.ex new file mode 100644 index 0000000..c2ed376 --- /dev/null +++ b/lib/my_button_app/names.ex @@ -0,0 +1,25 @@ +defmodule MyButtonApp.Names do + + @moduledoc false + + def generate do + title = ~w(Sir Sr Prof Saint Ibn Lady Madam Mistress Herr Dr) |> Enum.random() + name = + [ + ~w(B C D F G H J K L M N P Q R S T V W X Z), + ~w(o a i ij e ee u uu oo aj aa oe ou eu), + ~w(b c d f g h k l m n p q r s t v w x z), + ~w(o a i ij e ee u uu oo aj aa oe ou eu) + ] + |> Enum.map(fn l -> Enum.random(l) end) + |> Enum.join() + + "#{title} #{name}" + end + + def getHSL(s) do + hue = to_charlist(s) |> Enum.sum() |> rem(360) + "hsl(#{hue}, 70%, 40%)" + end + +end diff --git a/lib/my_button_app_web/channels/cursor_channel.ex b/lib/my_button_app_web/channels/cursor_channel.ex new file mode 100644 index 0000000..b99ddb2 --- /dev/null +++ b/lib/my_button_app_web/channels/cursor_channel.ex @@ -0,0 +1,65 @@ +defmodule MyButtonAppWeb.CursorChannel do + + alias MyButtonAppWeb.Presence + use MyButtonAppWeb, :channel + + @impl true + def handle_in("move", %{"x" => x, "y" => y}, socket) do + {:ok, _} = + Presence.update(socket, socket.assigns.current_user, fn previousState -> + Map.merge(previousState, + %{ + online_at: inspect(System.system_time(:second)), + color: MyButtonApp.Names.getHSL(socket.assigns.current_user), + x: x, + y: y + } + ) + end) + + {:noreply, socket} + end + + @impl true + def join("cursor:lobby", payload, socket) do + send(self(), :after_join) + + if authorized?(payload) do + {:ok, socket} + else + {:error, %{reason: "unauthorized"}} + end + end + + @impl true + def handle_info(:after_join, socket) do + {:ok, _} = + Presence.track(socket, socket.assigns.current_user, %{ + online_at: inspect(System.system_time(:second)), + color: MyButtonApp.Names.getHSL(socket.assigns.current_user) + }) + + push(socket, "presence_state", Presence.list(socket)) + {:noreply, socket} + end + + # Channels can be used in a request/response fashion + # by sending replies to requests from the client + @impl true + def handle_in("ping", payload, socket) do + {:reply, {:ok, payload}, socket} + end + + # It is also common to receive messages from the client and + # broadcast to everyone in the current topic (cursor:lobby). + @impl true + def handle_in("shout", payload, socket) do + broadcast(socket, "shout", payload) + {:noreply, socket} + end + + # Add authorization logic here as required. + defp authorized?(_payload) do + true + end +end diff --git a/lib/my_button_app_web/channels/presence.ex b/lib/my_button_app_web/channels/presence.ex new file mode 100644 index 0000000..c9ea65e --- /dev/null +++ b/lib/my_button_app_web/channels/presence.ex @@ -0,0 +1,10 @@ +defmodule MyButtonAppWeb.Presence do + @moduledoc """ + Provides presence tracking to channels and processes. + + See the [`Phoenix.Presence`](https://hexdocs.pm/phoenix/Phoenix.Presence.html) + docs for more details. + """ + use Phoenix.Presence, otp_app: :my_button_app, + pubsub_server: MyButtonApp.PubSub +end diff --git a/lib/my_button_app_web/channels/user_socket.ex b/lib/my_button_app_web/channels/user_socket.ex new file mode 100644 index 0000000..ee1b651 --- /dev/null +++ b/lib/my_button_app_web/channels/user_socket.ex @@ -0,0 +1,48 @@ +defmodule MyButtonAppWeb.UserSocket do + use Phoenix.Socket + + # A Socket handler + # + # It's possible to control the websocket connection and + # assign values that can be accessed by your channel topics. + + ## Channels + + channel "cursor:*", MyButtonAppWeb.CursorChannel + + # Socket params are passed from the client and can + # be used to verify and authenticate a user. After + # verification, you can put default assigns into + # the socket that will be set for all channels, ie + # + # {:ok, assign(socket, :user_id, verified_user_id)} + # + # To deny connection, return `:error`. + # + # See `Phoenix.Token` documentation for examples in + # performing token verification on connect. + @impl true + def connect(%{"token" => token}, socket, _connect_info) do + # max_age: 1209600 is equivalent to two weeks in seconds + case Phoenix.Token.verify(socket, "user socket", token, max_age: 1_209_600) do + {:ok, user_id} -> + {:ok, assign(socket, :current_user, user_id)} + + {:error, _reason} -> + :error + end + end + + # Socket id's are topics that allow you to identify all sockets for a given user: + # + # def id(socket), do: "user_socket:#{socket.assigns.user_id}" + # + # Would allow you to broadcast a "disconnect" event and terminate + # all active sockets and channels for a given user: + # + # Elixir.MyButtonAppWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) + # + # Returning `nil` makes this socket anonymous. + @impl true + def id(_socket), do: nil +end diff --git a/lib/my_button_app_web/controllers/page_controller.ex b/lib/my_button_app_web/controllers/page_controller.ex index d9ef500..d82a8bb 100644 --- a/lib/my_button_app_web/controllers/page_controller.ex +++ b/lib/my_button_app_web/controllers/page_controller.ex @@ -4,4 +4,10 @@ defmodule MyButtonAppWeb.PageController do def index(conn, _params) do render(conn, "index.html") end + + def cursors(conn, _params) do + render(conn, "cursors.html", + user_token: Phoenix.Token.sign(MyButtonAppWeb.Endpoint, "user socket", MyButtonApp.Names.generate()) + ) + end end diff --git a/lib/my_button_app_web/endpoint.ex b/lib/my_button_app_web/endpoint.ex index 6bda3c8..222b4cd 100644 --- a/lib/my_button_app_web/endpoint.ex +++ b/lib/my_button_app_web/endpoint.ex @@ -10,6 +10,7 @@ defmodule MyButtonAppWeb.Endpoint do signing_salt: "mpN8neHa" ] + socket "/socket", MyButtonAppWeb.UserSocket, websocket: true, longpoll: false socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] # Serve at "/" the static files from "priv/static" directory. diff --git a/lib/my_button_app_web/router.ex b/lib/my_button_app_web/router.ex index b6e3acc..7d585c8 100644 --- a/lib/my_button_app_web/router.ex +++ b/lib/my_button_app_web/router.ex @@ -22,6 +22,7 @@ defmodule MyButtonAppWeb.Router do live "/tickets", TicketsLive live "/visitors", VisitorsLive live "/countdown", CountdownLive + get "/cursors", PageController, :cursors end # Other scopes may use custom stacks. diff --git a/lib/my_button_app_web/templates/layout/root.html.heex b/lib/my_button_app_web/templates/layout/root.html.heex index 456ce31..fa33078 100644 --- a/lib/my_button_app_web/templates/layout/root.html.heex +++ b/lib/my_button_app_web/templates/layout/root.html.heex @@ -7,9 +7,10 @@ <%= csrf_meta_tag() %> <%= live_title_tag assigns[:page_title] || "MyButtonApp", suffix: " ยท Phoenix Framework" %> + - + <%= @inner_content %> diff --git a/lib/my_button_app_web/templates/page/cursors.html.heex b/lib/my_button_app_web/templates/page/cursors.html.heex new file mode 100644 index 0000000..218136f --- /dev/null +++ b/lib/my_button_app_web/templates/page/cursors.html.heex @@ -0,0 +1,20 @@ +
+
+ + +
+
    +
\ No newline at end of file diff --git a/lib/my_button_app_web/templates/page/index.html.heex b/lib/my_button_app_web/templates/page/index.html.heex index 19da49c..f192aa2 100644 --- a/lib/my_button_app_web/templates/page/index.html.heex +++ b/lib/my_button_app_web/templates/page/index.html.heex @@ -18,6 +18,9 @@
  • Countdown For Sales (Server Push)
  • +
  • + Multi User Cursors Page (Phoenix Channels) +
  • diff --git a/test/my_button_app_web/channels/cursor_channel_test.exs b/test/my_button_app_web/channels/cursor_channel_test.exs new file mode 100644 index 0000000..fdcfc33 --- /dev/null +++ b/test/my_button_app_web/channels/cursor_channel_test.exs @@ -0,0 +1,27 @@ +defmodule MyButtonAppWeb.CursorChannelTest do + use MyButtonAppWeb.ChannelCase + + setup do + {:ok, _, socket} = + MyButtonAppWeb.UserSocket + |> socket("user_id", %{some: :assign}) + |> subscribe_and_join(MyButtonAppWeb.CursorChannel, "cursor:lobby") + + %{socket: socket} + end + + test "ping replies with status ok", %{socket: socket} do + ref = push(socket, "ping", %{"hello" => "there"}) + assert_reply ref, :ok, %{"hello" => "there"} + end + + test "shout broadcasts to cursor:lobby", %{socket: socket} do + push(socket, "shout", %{"hello" => "all"}) + assert_broadcast "shout", %{"hello" => "all"} + end + + test "broadcasts are pushed to the client", %{socket: socket} do + broadcast_from!(socket, "broadcast", %{"some" => "data"}) + assert_push "broadcast", %{"some" => "data"} + end +end