Updates to frontend and fix a bug where first socket assignment failed

This commit is contained in:
Logan Hunt 2022-04-21 17:23:17 -06:00
parent d9943b201d
commit f6b262ea66
Signed by untrusted user who does not match committer: simponic
GPG Key ID: 52B3774857EB24B1
10 changed files with 161 additions and 196 deletions

3
.gitignore vendored
View File

@ -4,5 +4,6 @@
erl_crash.dump erl_crash.dump
*.ez *.ez
/assets/node_modules/ /assets/node_modules/
/priv/static/ /priv/static/uploads
/priv/static/assets
/deps /deps

View File

@ -126,115 +126,16 @@
white-space: pre-line; white-space: pre-line;
} }
.circle {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex; /* or inline-flex */
align-items: center;
justify-content: center;
}
/* Chat css from: https://www.bootdey.com/snippets/view/card-chat-list#html */ .chat {
.chat-container { max-height: 50vh;
height: 300px;
overflow-y: scroll; overflow-y: scroll;
} }
.chat-list {
padding: 0;
font-size: .8rem;
padding-bottom: 12px;
}
.chat-list li {
margin-bottom: 10px;
overflow: auto;
color: #ffffff;
}
.chat-list .chat-img {
float: left;
width: 48px;
}
.chat-list .chat-img img {
-webkit-border-radius: 50px;
-moz-border-radius: 50px;
border-radius: 50px;
width: 100%;
}
.chat-list .chat-message {
-webkit-border-radius: 50px;
-moz-border-radius: 50px;
border-radius: 50px;
background: #5a99ee;
display: inline-block;
padding: 10px 20px;
position: relative;
}
.chat-list .chat-message:before {
content: "";
position: absolute;
top: 15px;
width: 0;
height: 0;
}
.chat-list .chat-message h5 {
margin: 0 0 5px 0;
font-weight: 600;
line-height: 100%;
font-size: .9rem;
}
.chat-list .chat-message p {
line-height: 18px;
margin: 0;
padding: 0;
}
.chat-list .chat-body {
margin-left: 20px;
float: left;
width: 70%;
}
.chat-list .in .chat-message:before {
left: -12px;
border-bottom: 20px solid transparent;
border-right: 20px solid #5a99ee;
}
.chat-list .out .chat-img {
float: right;
}
.chat-list .out .chat-body {
float: right;
margin-right: 20px;
text-align: right;
}
.chat-list .out .chat-message {
background: #fc6d4c;
}
.chat-list .out .chat-message:before {
right: -12px;
border-bottom: 20px solid transparent;
border-left: 20px solid #fc6d4c;
}
.card .card-header:first-child {
-webkit-border-radius: 0.3rem 0.3rem 0 0;
-moz-border-radius: 0.3rem 0.3rem 0 0;
border-radius: 0.3rem 0.3rem 0 0;
}
.card .card-header {
background: #17202b;
border: 0;
font-size: 1rem;
padding: .65rem 1rem;
position: relative;
font-weight: 600;
color: #ffffff;
}
.content{
margin-top:40px;
}

View File

@ -1,6 +1,6 @@
// We import the CSS which is extracted to its own file by esbuild. // We import the CSS which is extracted to its own file by esbuild.
// Remove this line if you add a your own CSS build pipeline (e.g postcss). // Remove this line if you add a your own CSS build pipeline (e.g postcss).
import "../css/app.css" import "../css/app.css";
// If you want to use Phoenix channels, run `mix help phx.gen.channel` // If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below. // to get started and then uncomment the line below.
@ -20,32 +20,32 @@ import "../css/app.css"
// //
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons. // Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html" import "phoenix_html";
// Establish Phoenix Socket and LiveView configuration. // Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix" import {Socket} from "phoenix";
import {LiveSocket} from "phoenix_live_view" import {LiveSocket} from "phoenix_live_view";
import topbar from "../vendor/topbar" import topbar from "../vendor/topbar";
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}});
// Show progress bar on live navigation and form submits // Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"});
window.addEventListener("phx:page-loading-start", info => topbar.show()) window.addEventListener("phx:page-loading-start", info => topbar.show());
window.addEventListener("phx:page-loading-stop", info => topbar.hide()) window.addEventListener("phx:page-loading-stop", info => topbar.hide());
// connect if there are any LiveViews on the page // connect if there are any LiveViews on the page
liveSocket.connect() liveSocket.connect();
// expose liveSocket on window for web console debug logs and latency simulation: // expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug() // >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim() // >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket window.liveSocket = liveSocket;
// Hack to remove alerts on click // Hack to remove alerts on click
Array.from(window.document.getElementsByClassName('alert')).forEach((x) => x.addEventListener('click', () => x.style.display = "none")) Array.from(window.document.getElementsByClassName('alert')).forEach((x) => x.addEventListener('click', () => x.style.display = "none"));
import RoomChat from "./chat" window.userSocket = new Socket("/socket", {params: {_csrf_token: csrfToken}});
import RoomChat from "./chat";
window.RoomChat = RoomChat; window.RoomChat = RoomChat;
window.userSocket = new Socket("/socket", {params: {_csrf_token: csrfToken}})

View File

@ -1,11 +1,110 @@
let RoomChat = { const gruvboxColors = [
connect(socket, postId) { "#b8bb26",
let channel = socket.channel(`post:${postId}`) "#fabd2f",
"#83a598",
"#d3869b",
"#8ec07c",
"#458588",
"#cc241d",
"#d65d0e",
"#bdae93",
];
const generateGruvboxFromString = (string) =>
gruvboxColors[Array.from(string).map((x) => x.charCodeAt(0)).reduce((a, x) => a+x, 0) % gruvboxColors.length];
const RoomChat = (() => {
let channel;
const connect = (socket, postId) => {
channel = socket.channel(`post:${postId}`);
channel.join() channel.join()
.receive("ok", resp => { console.log("Joined successfully: ", resp) }) .receive("ok", resp => { console.log("Joined successfully: ", resp); })
.receive("error", resp => { console.log("Unable to join: ", resp) }) .receive("error", resp => { console.log("Unable to join: ", resp); });
return channel; return channel;
}, };
}
const scrollToBottom = (element) => {
element.scrollTop = element.scrollHeight;
};
const appendComment = ({user, body, id, user_id, inserted_at}, element) => {
const messageElement = document.createElement("div");
messageElement.innerHTML = `
<div class="d-flex flex-row card border rounded m-2 align-items-center">
<div class="m-2">
<div class="circle" style="background:${generateGruvboxFromString(user)}">${user.charAt(0)}</div>
</div>
<div class="m-2">
<div class="comment">
<div class="comment-header">
<span class="comment-username">${user}</span>
<span class="text-muted">${new Date(inserted_at).toLocaleString()}</span>
</div>
<div class="comment-body">
${body}
</div>
</div>
</div>
</div>
`;
element.appendChild(messageElement);
scrollToBottom(element);
};
const leaveChannel = () => {
if (channel) {
channel.leave();
console.log(channel);
}
};
const main = (post_id) => {
leaveChannel();
const chatWindow = document.getElementById("chat");
window.userSocket.connect();
channel = connect(window.userSocket, post_id);
channel.on('shout', (comment) => {
appendComment(comment, chatWindow);
});
channel.on('initial-comments', ({comments}) => {
comments.forEach((comment) => {
appendComment(comment, chatWindow);
});
scrollToBottom(chatWindow);
});
channel.on('join', ({ user }) => {
const joinElement = document.createElement("div");
joinElement.innerHTML = `
<div class="m-2 card border rounded p-2 text-muted">
join: ${user}
</div>
`;
chatWindow.appendChild(joinElement);
scrollToBottom(chatWindow);
});
channel.on('left', ({ user }) => {
console.log(user, "left");
});
};
const submitForm = (e) => {
e.preventDefault();
let message = e.target.elements.message.value;
if (message) {
channel.push("send", {body: message});
e.target.elements.message.value = "";
}
return false;
};
return { main, submitForm };
})();
window.addEventListener('load', () => {
window.addEventListener('phx:initial-post', (e) => RoomChat.main(e.detail.id));
});
export default RoomChat; export default RoomChat;

View File

@ -24,11 +24,12 @@ defmodule AggieditWeb.PostChannel do
|> Enum.map(fn c -> Aggiedit.Post.Comment.serialize(c) end) |> Enum.map(fn c -> Aggiedit.Post.Comment.serialize(c) end)
push(socket, "initial-comments", %{:comments => comments}) push(socket, "initial-comments", %{:comments => comments})
broadcast!(socket, "join", %{user: socket.assigns.current_user.username})
{:noreply, socket} {:noreply, socket}
end end
@impl true @impl true
def handle_in("send", %{"body" => comment}=body, socket) do def handle_in("send", %{"body" => comment}, socket) do
{:ok, comment} = Rooms.comment_post(socket.assigns.post, socket.assigns.current_user, comment) {:ok, comment} = Rooms.comment_post(socket.assigns.post, socket.assigns.current_user, comment)
broadcast!(socket, "shout", Aggiedit.Post.Comment.serialize(comment)) broadcast!(socket, "shout", Aggiedit.Post.Comment.serialize(comment))
{:reply, :ok, socket} {:reply, :ok, socket}

View File

@ -39,7 +39,9 @@ defmodule AggieditWeb.PostLive.FormComponent do
filename = "#{upload.uuid}.#{extension}" filename = "#{upload.uuid}.#{extension}"
dest = Path.join("priv/static/uploads", filename) dest = Path.join("priv/static/uploads", filename)
with :ok <- File.mkdir_p(Path.dirname(dest)) do
File.cp!(data.path, dest) File.cp!(data.path, dest)
end
{:ok, upload} = Uploads.create_upload(%{ {:ok, upload} = Uploads.create_upload(%{
file: filename, file: filename,

View File

@ -1,4 +1,3 @@
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<div class="container"> <div class="container">
<div> <div>
@ -6,7 +5,7 @@
</div> </div>
<div> <div>
<%= if Ecto.assoc_loaded?(@post.upload) && !is_nil(@post.upload) do %> <%= if Ecto.assoc_loaded?(@post.upload) && !is_nil(@post.upload) do %>
<img src={Routes.static_path(@socket, "/uploads/#{@post.upload.file}")} class="img-fluid"/> <img src={Routes.static_path(@socket, "/uploads/#{@post.upload.file}")} class="img-fluid" style="max-height: 40vh"/>
<% end %> <% end %>
</div> </div>
<div class="post-body"> <div class="post-body">
@ -16,21 +15,18 @@
<span><%= live_patch "Edit", to: Routes.post_show_path(@socket, :edit, @room, @post), class: "button" %></span> | <span><%= live_patch "Edit", to: Routes.post_show_path(@socket, :edit, @room, @post), class: "button" %></span> |
<% end %> <% end %>
<span><%= live_redirect "Back", to: Routes.post_index_path(@socket, :index, @room) %></span> <span><%= live_redirect "Back", to: Routes.post_index_path(@socket, :index, @room) %></span>
</div>
</div>
<!-- chat container from https://www.bootdey.com/snippets/view/card-chat-list#html --> <div class="border rounded p-2 m-2">
<div class="container content mt-2"> <div class="chat" id="chat">
<div class="row">
<div class="col-xl-6 col-lg-6 col-md-6 col-sm-12 col-12">
<div class="card">
<div class="card-header">Chat</div>
<div class="card-body chat-container">
<ul class="chat-list" id="chat">
</ul>
</div> </div>
</div> </div>
<form class="border rounded p-2 m-2" onsubmit="return RoomChat.submitForm(event)">
<div class="form-group m-2">
<label for="message">Message</label>
<input type="text" class="form-control" id="message" name="message" placeholder="Message">
</div> </div>
<button type="submit" class="m-2 btn btn-primary">Submit</button>
</form>
</div> </div>
</div> </div>
@ -47,45 +43,3 @@
/> />
</.modal> </.modal>
<% end %> <% end %>
<script>
const scrollToBottom = (element) => {
element.scrollIntoView({ behavior: 'smooth', block: 'end' });
};
const appendComment = ({user, body, id, user_id, inserted_at}, element) => {
const messageElement = document.createElement("div");
messageElement.innerHTML = `
<li class="${user_id == <%= @current_user.id %> ? 'out' : 'in'}" id=${id}>
<div class="chat-body">
<div class="chat-message">
<h5>${user}</h5>
<p>${body}</p>
</div>
</div>
</li>
`;
element.appendChild(messageElement);
scrollToBottom(element);
};
let channel;
window.addEventListener('phx:initial-post', (e) => {
const chatWindow = document.getElementById("chat");
window.userSocket.connect();
channel = window.RoomChat.connect(window.userSocket, e.detail.id);
channel.on('shout', (comment) => {
appendComment(comment, chatWindow);
});
channel.on('initial-comments', ({comments}) => {
comments.forEach((comment) => {
appendComment(comment, chatWindow);
});
scrollToBottom(chatWindow);
});
channel.push("send", {body: "Hello!"});
});
</script>

View File

@ -8,6 +8,7 @@
<%= live_title_tag assigns[:page_title] || "Aggiedit" %> <%= live_title_tag assigns[:page_title] || "Aggiedit" %>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/> <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
<script phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script> <script phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script>
<link rel="icon" type="image/x-icon" href={Routes.static_path(@conn, "/favicon.ico")}/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>

File diff suppressed because one or more lines are too long

BIN
priv/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB