basic chat stuff

This commit is contained in:
Alain Zscheile 2022-11-26 17:27:01 +01:00
parent d17c40c033
commit be957afe44
10 changed files with 239 additions and 51 deletions

View File

@ -25,6 +25,7 @@ import "phoenix_html"
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"
import socket from "./user_socket.js"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})

85
assets/js/user_socket.js Normal file
View File

@ -0,0 +1,85 @@
// 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} from "phoenix"
// And connect to the path in "lib/chat_web/endpoint.ex". We pass the
// token for authentication. Read below how it should be used.
let socket = new Socket("/socket", {params: {token: window.userToken}})
// When you connect, you'll often need to authenticate the client.
// For example, imagine you have an authentication plug, `MyAuth`,
// which authenticates the session and assigns a `:current_user`.
// If the current user exists you can assign the user's token in
// the connection for use in the layout.
//
// In your "lib/chat_web/router.ex":
//
// pipeline :browser do
// ...
// plug MyAuth
// plug :put_user_token
// end
//
// defp put_user_token(conn, _) do
// if current_user = conn.assigns[:current_user] do
// token = Phoenix.Token.sign(conn, "user socket", current_user.id)
// assign(conn, :user_token, token)
// else
// conn
// end
// end
//
// Now you need to pass this token to JavaScript. You can do so
// inside a script tag in "lib/chat_web/templates/layout/app.html.heex":
//
// <script>window.userToken = "<%= assigns[:user_token] %>";</script>
//
// You will need to verify the user token in the "connect/3" function
// in "lib/chat_web/channels/user_socket.ex":
//
// 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, :user, user_id)}
//
// {:error, reason} ->
// :error
// end
// end
//
// Finally, connect to the socket:
socket.connect()
let channel = socket.channel('room:lobby', {}); // connect to chat "room"
channel.on('shout', function (payload) { // listen to the 'shout' event
let li = document.createElement("li"); // create new list item DOM element
let name = payload.name || 'guest'; // get name from payload or set default
li.innerHTML = '<b>' + name + '</b>: ' + payload.message; // set li contents
ul.appendChild(li); // append to list
});
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) });
let ul = document.getElementById('msg-list'); // list of messages.
let name = document.getElementById('name'); // name of message sender
let msg = document.getElementById('msg'); // message input field
// "listen" for the [Enter] keypress event to send a message:
msg.addEventListener('keypress', function (event) {
if (event.keyCode == 13 && msg.value.length > 0) { // don't sent empty msg.
channel.push('shout', { // send the message to the server on "shout" channel
name: name.value || "guest", // get value of "name" of person sending the message. Set guest as default
message: msg.value // get message text (value) from msg input field.
});
msg.value = ''; // reset the message input field for next message.
}
});
export default socket

View File

@ -0,0 +1,32 @@
defmodule ChatWeb.RoomChannel do
use ChatWeb, :channel
@impl true
def join("room:lobby", payload, socket) do
if authorized?(payload) do
{:ok, socket}
else
{:error, %{reason: "unauthorized"}}
end
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 (room: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

View File

@ -0,0 +1,41 @@
defmodule ChatWeb.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 "room:lobby", ChatWeb.RoomChannel
# 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(_params, socket, _connect_info) do
{:ok, socket}
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.ChatWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
@impl true
def id(_socket), do: nil
end

View File

@ -12,6 +12,10 @@ defmodule ChatWeb.Endpoint do
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
socket "/socket", ChatWeb.UserSocket,
websocket: true,
longpoll: false
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phx.digest

View File

@ -12,17 +12,9 @@
<body>
<header>
<section class="container">
<nav>
<ul>
<li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
<%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
<li><%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home) %></li>
<% end %>
</ul>
<nav role="navigation">
<h1 style="padding-top: 15px">Chat Example</h1>
</nav>
<a href="https://phoenixframework.org/" class="phx-logo">
<img src={Routes.static_path(@conn, "/images/phoenix.png")} alt="Phoenix Framework Logo"/>
</a>
</section>
</header>
<%= @inner_content %>

View File

@ -1,41 +1,13 @@
<section class="phx-hero">
<h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
<p>Peace of mind from prototype to production</p>
</section>
<!-- The list of messages will appear here: -->
<ul id="msg-list" style="list-style: none; min-height:200px;">
</ul>
<div class="row">
<div class="col-xs-3" style="width: 20%; margin-left: 0;">
<input type="text" id="name" class="form-control" placeholder="Your Name" style="border: 1px black solid; font-size: 1.3em;" autofocus>
</div>
<div class="col-xs-9" style="width: 100%; margin-left: 1%; ">
<input type="text" id="msg" class="form-control" placeholder="Your Message" style="border: 1px black solid; font-size: 1.3em;">
</div>
</div>
<section class="row">
<article class="column">
<h2>Resources</h2>
<ul>
<li>
<a href="https://hexdocs.pm/phoenix/overview.html">Guides &amp; Docs</a>
</li>
<li>
<a href="https://github.com/phoenixframework/phoenix">Source</a>
</li>
<li>
<a href="https://github.com/phoenixframework/phoenix/blob/v1.6/CHANGELOG.md">v1.6 Changelog</a>
</li>
</ul>
</article>
<article class="column">
<h2>Help</h2>
<ul>
<li>
<a href="https://elixirforum.com/c/phoenix-forum">Forum</a>
</li>
<li>
<a href="https://web.libera.chat/#elixir">#elixir on Libera Chat (IRC)</a>
</li>
<li>
<a href="https://twitter.com/elixirphoenix">Twitter @elixirphoenix</a>
</li>
<li>
<a href="https://elixir-slackin.herokuapp.com/">Elixir on Slack</a>
</li>
<li>
<a href="https://discord.gg/elixir">Elixir on Discord</a>
</li>
</ul>
</article>
</section>

View File

@ -0,0 +1,27 @@
defmodule ChatWeb.RoomChannelTest do
use ChatWeb.ChannelCase
setup do
{:ok, _, socket} =
ChatWeb.UserSocket
|> socket("user_id", %{some: :assign})
|> subscribe_and_join(ChatWeb.RoomChannel, "room: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 room: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

View File

@ -3,6 +3,6 @@ defmodule ChatWeb.PageControllerTest do
test "GET /", %{conn: conn} do
conn = get(conn, "/")
assert html_response(conn, 200) =~ "Welcome to Phoenix!"
assert html_response(conn, 200) =~ "Chat Example"
end
end

View File

@ -0,0 +1,34 @@
defmodule ChatWeb.ChannelCase do
@moduledoc """
This module defines the test case to be used by
channel tests.
Such tests rely on `Phoenix.ChannelTest` and also
import other functionality to make it easier
to build common data structures and query the data layer.
Finally, if the test case interacts with the database,
we enable the SQL sandbox, so changes done to the database
are reverted at the end of every test. If you are using
PostgreSQL, you can even run database tests asynchronously
by setting `use ChatWeb.ChannelCase, async: true`, although
this option is not recommended for other databases.
"""
use ExUnit.CaseTemplate
using do
quote do
# Import conveniences for testing with channels
import Phoenix.ChannelTest
import ChatWeb.ChannelCase
# The default endpoint for testing
@endpoint ChatWeb.Endpoint
end
end
setup _tags do
:ok
end
end