Last active
October 23, 2020 18:34
-
-
Save pootsbook/aa8ad6da28e797ac84dd74a7e723f00e to your computer and use it in GitHub Desktop.
LiveView Chat
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// We need to import the CSS so that webpack will load it. | |
// The MiniCssExtractPlugin is used to separate it out into | |
// its own CSS file. | |
import "../css/app.scss" | |
// webpack automatically bundles all modules in your | |
// entry points. Those entry points can be configured | |
// in "webpack.config.js". | |
// | |
// Import deps with the dep name or local files with a relative path, for example: | |
// | |
// import {Socket} from "phoenix" | |
// import socket from "./socket" | |
// | |
import "phoenix_html" | |
import {Socket} from "phoenix" | |
import NProgress from "nprogress" | |
import {LiveSocket} from "phoenix_live_view" | |
let Hooks = {} | |
// https://github.com/phoenixframework/phoenix_live_view/issues/624 | |
Hooks.BodyInput = { | |
updated(){ | |
this.el.value = this.el.dataset["pending-val"] | |
this.el.focus() | |
} | |
} | |
// Hook to scroll the text after a message has been added | |
// element.scrollTop = where the scroll is currently | |
// element.offsetHeight = height of the scrolled container (inc. border + padding) | |
// element.clientHeight = height of the scrolled container (inc. padding) | |
// element.scrollTo(x, y) = x is upper left of scroll position, i.e. where scrollTop will be | |
// element.scrollTo(top: x, left: y, behavior: smooth|auto) | |
// element.scrollHeight = total height of scroll | |
// e.g. with a container of 840, and a height of 40, if it is scrolled to the bottom, the scrollTop will be 440 | |
// logic: monitor scroll behaviour toggling a property "isBottom"; when a new message comes in, if the isBottom then scroll down. Future manual scroll behaviour will trigger `isBottom: false` | |
// https://stackoverflow.com/questions/39729791/chat-box-auto-scroll-to-bottom | |
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") | |
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: Hooks}) | |
// Show progress bar on live navigation and form submits | |
window.addEventListener("phx:page-loading-start", info => NProgress.start()) | |
window.addEventListener("phx:page-loading-stop", info => NProgress.done()) | |
// connect if there are any LiveViews on the page | |
liveSocket.connect() | |
// expose liveSocket on window for web console debug logs and latency simulation: | |
// >> liveSocket.enableDebug() | |
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session | |
// >> liveSocket.disableLatencySim() | |
window.liveSocket = liveSocket |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Ontmoeting.Chat do | |
use GenServer | |
alias Ontmoeting.Message | |
@topic "chat" | |
# Client | |
def start_link(queue) do | |
GenServer.start_link(__MODULE__, queue, name: Ontmoeting.Chat) | |
end | |
def push_message(message) do | |
GenServer.cast(__MODULE__, {:push, message}) | |
end | |
def list_messages() do | |
GenServer.call(__MODULE__, :list) | |
end | |
# Server (callbacks) | |
@impl true | |
def init(queue) do | |
{:ok, queue} | |
end | |
@impl true | |
def handle_cast({:push, message}, queue) do | |
timed_message = Map.merge(%Message{ time: NaiveDateTime.utc_now }, message) | |
state = Qex.push(queue, timed_message) | |
OntmoetingWeb.Endpoint.broadcast_from(self(), @topic, "message", %{messages: state}) | |
{:noreply, state} | |
end | |
@impl true | |
def handle_call(:list, _from, queue) do | |
{:reply, queue, queue} | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule OntmoetingWeb.ChatLive do | |
use OntmoetingWeb, :live_view | |
@topic "chat" | |
alias Ontmoeting.Chat | |
alias Ontmoeting.Message | |
alias OntmoetingWeb.Presence | |
def mount(_params, _session, socket) do | |
if connected?(socket) do | |
Phoenix.PubSub.subscribe(Ontmoeting.PubSub, @topic) | |
end | |
users = Presence.list(@topic) | |
|> Map.keys | |
socket = assign(socket, | |
messages: Chat.list_messages(), | |
user: nil, | |
users: users) | |
{:ok, socket} | |
end | |
def render(assigns) do | |
~L""" | |
<h1>Chat</h1> | |
<h3>Members</h3> | |
<ul> | |
<%= for user <- @users do %> | |
<li> | |
<%= user %> | |
</li> | |
<% end %> | |
</ul> | |
<h3>Conversation</h3> | |
<dl style="max-height: 400px; overflow: auto"> | |
<%= for message <- @messages do %> | |
<dt><%= message.user %></dt> | |
<dd><%= message.body %></dd> | |
<% end %> | |
</dl> | |
<%= if @user do %> | |
<label><%= @user %>:</label> | |
<form phx-submit="addMessage"> | |
<input type="hidden" name="user" value="<%= @user %>"> | |
<input type="text" name="body" id="message-body-input" data-pending-val="" phx-hook="BodyInput" /> | |
</form> | |
<% else %> | |
<label>Name:</label> | |
<form phx-submit="register"> | |
<input type="text" name="name" /> | |
</form> | |
<% end %> | |
""" | |
end | |
def handle_event("addMessage", %{"user" => user, "body" => body}, socket) do | |
message = %Message{ | |
user: user, | |
body: body, | |
} | |
Chat.push_message(message) | |
socket = assign(socket, :random, "") # socket needs to change for JS Hook to trigger? | |
{:noreply, socket} | |
end | |
def handle_event("register", %{"name" => name}, socket) do | |
Presence.track(self(), @topic, name, %{ name: name }) | |
socket = assign(socket, :user, name) | |
{:noreply, socket} | |
end | |
def handle_info(%{event: "message", payload: state}, socket) do | |
socket = assign(socket, state) | |
{:noreply, socket} | |
end | |
def handle_info(%{event: "presence_diff", payload: _payload}, socket) do | |
users = | |
Presence.list(@topic) | |
|> Enum.map(fn {name, _data} -> | |
name | |
end) | |
socket = assign(socket, :users, users) | |
{:noreply, socket} | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
{:continue, :initialize}
and thendef handle_continue(:initialize, state) do
Other things:
def handle_info(%{event: "message"
assigns entire state which probably clears current user. That is probably a bug.3.In line 84 you match on
messages
but never use it.