Factor out homepage into its own file

This commit is contained in:
pjht 2024-10-16 13:01:21 -05:00
parent 3078591e2c
commit 014525b683
Signed by: pjht
GPG Key ID: CA239FC6934E6F3A
3 changed files with 267 additions and 277 deletions

View File

@ -1,14 +1,9 @@
pub mod components;
use components::{Alert, Message, Nav};
mod pages;
use components::Nav;
use pages::Home;
use std::time::Duration;
use chrono::Local;
use common::ChatMessage;
use gloo::{events::EventListener, net::http::Request, console::log, timers::future::sleep};
use wasm_bindgen::JsCast;
use web_sys::{js_sys::Uint8Array, CloseEvent, Event, MessageEvent, WebSocket};
use yew::{platform::spawn_local, prelude::*};
use yew::prelude::*;
use yew_router::prelude::*;
#[derive(Clone, Routable, PartialEq)]
@ -32,274 +27,6 @@ fn switch(routes: Route) -> Html {
}
}
#[derive(Clone, Debug)]
enum WsEvent {
#[allow(dead_code)]
Close(CloseEvent),
#[allow(dead_code)]
Open(Event),
}
impl WsEvent {
fn as_close(&self) -> Option<&CloseEvent> {
if let Self::Close(v) = self {
Some(v)
} else {
None
}
}
}
enum HomeMessage {
SubmittedMessage(String),
RecievedMessage(ChatMessage),
WsEvent(WsEvent),
WsReconnect,
Authenticated,
}
#[derive(PartialEq, Eq)]
enum WsState {
Closed,
Open,
}
struct Home {
authenticated: bool,
messages: Vec<ChatMessage>,
chat_ws: Option<WebSocketConn>,
ws_state: WsState,
ws_reconnecting: bool,
message_container_ref: NodeRef,
}
struct WebSocketConn {
ws: WebSocket,
#[allow(dead_code)]
listeners: [EventListener; 3],
}
impl Drop for WebSocketConn {
fn drop(&mut self) {
let _ = self.ws.close();
}
}
impl Home {
fn connect_ws(&mut self, ctx: &Context<Self>) {
let location = web_sys::window().unwrap().location();
let ws_proto = if location.protocol().unwrap() == "https:" {
"wss"
} else {
"ws"
};
let ws_url = format!("{}://{}/api/chat_ws", ws_proto, location.host().unwrap());
log!("Connecting to ", &ws_url);
let ws = WebSocket::new(&ws_url).unwrap();
ws.set_binary_type(web_sys::BinaryType::Arraybuffer);
let msg_callback = ctx.link().callback(|msg: Vec<u8>| {
let msg = ciborium::from_reader(msg.as_slice()).unwrap();
HomeMessage::RecievedMessage(msg)
});
let state_callback = ctx
.link()
.callback(|state: WsEvent| HomeMessage::WsEvent(state));
let cb = state_callback.clone();
let open_ev = EventListener::new(&ws, "open", move |event: &Event| {
cb.emit(WsEvent::Open(event.clone()))
});
let cb = state_callback.clone();
let close_ev = EventListener::new(&ws, "close", move |event: &Event| {
let event = event.dyn_ref::<CloseEvent>().unwrap();
cb.emit(WsEvent::Close(event.clone()))
});
let msg_ev = EventListener::new(&ws, "message", move |event: &Event| {
let event = event.dyn_ref::<MessageEvent>().unwrap();
let bytes = event.data();
let bytes = Uint8Array::new(&bytes).to_vec();
msg_callback.emit(bytes);
});
self.chat_ws = Some(WebSocketConn {
ws,
listeners: [open_ev, close_ev, msg_ev],
});
}
fn authenticate(ctx: &Context<Self>) {
log!("Authenticating to backend");
let auth_cb = ctx.link().callback(|_: ()| HomeMessage::Authenticated);
spawn_local(async move {
let location = web_sys::window().unwrap().location();
let login_url = format!(
"{}//{}/api/login",
location.protocol().unwrap(),
location.host().unwrap()
);
Request::post(&login_url)
.body("")
.unwrap()
.send()
.await
.unwrap();
auth_cb.emit(());
});
}
}
impl Component for Home {
type Message = HomeMessage;
type Properties = ();
fn create(ctx: &Context<Self>) -> Self {
Self::authenticate(ctx);
let mut slf = Self {
authenticated: false,
messages: Vec::new(),
chat_ws: None,
ws_state: WsState::Closed,
ws_reconnecting: false,
message_container_ref: NodeRef::default(),
};
slf.connect_ws(ctx);
slf
}
fn view(&self, ctx: &Context<Self>) -> Html {
let msg_keypress = {
ctx.link().batch_callback(|press: KeyboardEvent| {
if press.key() == "Enter" {
let input = press
.target_dyn_into::<web_sys::HtmlInputElement>()
.unwrap();
let msg = input.value();
input.set_value("");
//if on_mobile() {
// input.blur().unwrap();
//}
Some(HomeMessage::SubmittedMessage(msg))
} else {
None
}
})
};
let disable_input = self.ws_state != WsState::Open || !self.authenticated;
html! {
<div class="myvh-100 d-flex flex-column">
<Nav />
<div class="container-fluid d-flex flex-column flex-grow-1 mt-3">
if self.ws_state != WsState::Open {
<Alert message="Connection to backend lost, trying to reconnect"/>
}
if !self.authenticated {
<Alert message="Authenticating to backend"/>
}
<div
ref={self.message_container_ref.clone()}
class="d-flex border rounded flex-grow-1 flex-column-reverse overflow-auto mb-3"
style="flex-basis: 0"
>
{ self.messages.iter().rev().map(|msg| html!{
<Message message={msg.clone()}/>
}
).collect::<Vec<_>>() }
</div>
<div class="bottom-0 mb-3 mt-3">
<input
onkeydown={msg_keypress}
disabled={disable_input}
type="text"
id="message"
class="w-100 form-control"
/>
</div>
</div>
</div>
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
HomeMessage::SubmittedMessage(msg) => {
let msg = ChatMessage {
message: msg.clone(),
time: Local::now(),
};
self.messages.push(msg.clone());
let mut buf = Vec::new();
ciborium::into_writer(&msg, &mut buf).unwrap();
self.chat_ws
.as_ref()
.unwrap()
.ws
.send_with_u8_array(&buf)
.unwrap();
true
}
HomeMessage::RecievedMessage(msg) => {
self.messages.push(msg);
true
}
HomeMessage::WsEvent(event) => {
if let Some(close_event) = event.as_close() {
let code = close_event.code();
log!("WS connection closed with code", code);
if code == 4000 {
log!("Unauthorized, reauthenticating");
self.ws_state = WsState::Closed;
self.authenticated = false;
Self::authenticate(ctx);
return true;
}
if self.ws_state == WsState::Closed {
log!("Already closed");
if !self.ws_reconnecting {
log!("Reconnecting in 5s");
self.ws_reconnecting = true;
ctx.link().send_future(async move {
sleep(Duration::from_secs(5)).await;
HomeMessage::WsReconnect
});
}
false
} else {
log!("Reconnecting");
self.connect_ws(ctx);
self.ws_state = WsState::Closed;
true
}
} else if self.ws_state != WsState::Open {
log!("WS connection opened");
self.messages.clear();
self.ws_state = WsState::Open;
true
} else {
false
}
}
HomeMessage::WsReconnect => {
if self.ws_state != WsState::Open {
log!("Reconnecting");
self.ws_reconnecting = false;
self.connect_ws(ctx);
}
false
}
HomeMessage::Authenticated => {
self.connect_ws(ctx);
self.authenticated = true;
true
}
}
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
if let Some(message_container) = self.message_container_ref.cast::<web_sys::Element>() {
message_container.set_scroll_top(message_container.scroll_height());
}
}
}
#[function_component]
fn App() -> Html {
html! {

2
frontend/src/pages.rs Normal file
View File

@ -0,0 +1,2 @@
mod home;
pub use home::Home;

261
frontend/src/pages/home.rs Normal file
View File

@ -0,0 +1,261 @@
use std::time::Duration;
use chrono::Local;
use common::ChatMessage;
use gloo::{console::log, events::EventListener, net::http::Request};
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{js_sys::Uint8Array, CloseEvent, Event, MessageEvent, WebSocket};
use yew::{
platform::{spawn_local, time::sleep},
prelude::*,
};
use crate::components::{Alert, Message, Nav};
pub enum HomeMessage {
SubmittedMessage(String),
RecievedMessage(ChatMessage),
WsClose(CloseEvent),
WsOpen,
WsReconnect,
Authenticated,
}
#[derive(PartialEq, Eq)]
enum WsState {
Closed,
Open,
}
pub struct Home {
authenticated: bool,
messages: Vec<ChatMessage>,
chat_ws: Option<WebSocketConn>,
ws_state: WsState,
ws_reconnecting: bool,
message_container_ref: NodeRef,
}
struct WebSocketConn {
ws: WebSocket,
#[allow(dead_code)]
listeners: [EventListener; 3],
}
impl WebSocketConn {
fn send_binary(&self, msg: &[u8]) -> Result<(), JsValue> {
self.ws.send_with_u8_array(msg)
}
}
impl Drop for WebSocketConn {
fn drop(&mut self) {
let _ = self.ws.close();
}
}
impl Home {
fn connect_ws(&mut self, ctx: &Context<Self>) {
let location = web_sys::window().unwrap().location();
let ws_proto = if location.protocol().unwrap() == "https:" {
"wss"
} else {
"ws"
};
let ws_url = format!("{}://{}/api/chat_ws", ws_proto, location.host().unwrap());
log!("Connecting to ", &ws_url);
let ws = WebSocket::new(&ws_url).unwrap();
ws.set_binary_type(web_sys::BinaryType::Arraybuffer);
let link = ctx.link().clone();
let open_ev = EventListener::new(&ws, "open", move |_event| {
link.send_message(HomeMessage::WsOpen);
});
let link = ctx.link().clone();
let close_ev = EventListener::new(&ws, "close", move |event: &Event| {
let event = event.dyn_ref::<CloseEvent>().unwrap();
link.send_message(HomeMessage::WsClose(event.clone()));
});
let link = ctx.link().clone();
let msg_ev = EventListener::new(&ws, "message", move |event: &Event| {
let event = event.dyn_ref::<MessageEvent>().unwrap();
let bytes = event.data();
let bytes = Uint8Array::new(&bytes).to_vec();
let msg = ciborium::from_reader(bytes.as_slice()).unwrap();
link.send_message(HomeMessage::RecievedMessage(msg));
});
self.chat_ws = Some(WebSocketConn {
ws,
listeners: [open_ev, close_ev, msg_ev],
});
}
fn authenticate(ctx: &Context<Self>) {
log!("Authenticating to backend");
let auth_cb = ctx.link().callback(|_: ()| HomeMessage::Authenticated);
spawn_local(async move {
let location = web_sys::window().unwrap().location();
let login_url = format!(
"{}//{}/api/login",
location.protocol().unwrap(),
location.host().unwrap()
);
Request::post(&login_url)
.body("")
.unwrap()
.send()
.await
.unwrap();
auth_cb.emit(());
});
}
}
impl Component for Home {
type Message = HomeMessage;
type Properties = ();
fn create(ctx: &Context<Self>) -> Self {
Self::authenticate(ctx);
let mut slf = Self {
authenticated: false,
messages: Vec::new(),
chat_ws: None,
ws_state: WsState::Closed,
ws_reconnecting: false,
message_container_ref: NodeRef::default(),
};
slf.connect_ws(ctx);
slf
}
fn view(&self, ctx: &Context<Self>) -> Html {
let msg_keypress = {
ctx.link().batch_callback(|press: KeyboardEvent| {
if press.key() == "Enter" {
let input = press
.target_dyn_into::<web_sys::HtmlInputElement>()
.unwrap();
let msg = input.value();
input.set_value("");
//if on_mobile() {
// input.blur().unwrap();
//}
Some(HomeMessage::SubmittedMessage(msg))
} else {
None
}
})
};
let disable_input = self.ws_state != WsState::Open || !self.authenticated;
html! {
<div class="myvh-100 d-flex flex-column">
<Nav />
<div class="container-fluid d-flex flex-column flex-grow-1 mt-3">
if self.ws_state != WsState::Open {
<Alert message="Connection to backend lost, trying to reconnect" />
}
if !self.authenticated {
<Alert message="Authenticating to backend" />
}
<div
ref={self.message_container_ref.clone()}
class="d-flex border rounded flex-grow-1 flex-column-reverse overflow-auto mb-3"
style="flex-basis: 0"
>
{ self.messages.iter().rev().map(|msg| html!{
<Message message={msg.clone()}/>
}
).collect::<Vec<_>>() }
</div>
<div class="bottom-0 mb-3 mt-3">
<input
onkeydown={msg_keypress}
disabled={disable_input}
type="text"
id="message"
class="w-100 form-control"
/>
</div>
</div>
</div>
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
HomeMessage::SubmittedMessage(msg) => {
let msg = ChatMessage {
message: msg.clone(),
time: Local::now(),
};
self.messages.push(msg.clone());
let mut buf = Vec::new();
ciborium::into_writer(&msg, &mut buf).unwrap();
self.chat_ws.as_ref().unwrap().send_binary(&buf).unwrap();
true
}
HomeMessage::RecievedMessage(msg) => {
self.messages.push(msg);
true
}
HomeMessage::WsClose(close_event) => {
let code = close_event.code();
log!("WS connection closed with code", code);
if code == 4000 {
log!("Unauthorized, reauthenticating");
self.ws_state = WsState::Closed;
self.authenticated = false;
Self::authenticate(ctx);
return true;
}
if self.ws_state == WsState::Closed {
log!("Already closed");
if !self.ws_reconnecting {
log!("Reconnecting in 5s");
self.ws_reconnecting = true;
ctx.link().send_future(async move {
sleep(Duration::from_secs(5)).await;
HomeMessage::WsReconnect
});
}
false
} else {
log!("Reconnecting");
self.connect_ws(ctx);
self.ws_state = WsState::Closed;
true
}
}
HomeMessage::WsOpen => {
if self.ws_state != WsState::Open {
log!("WS connection opened");
self.messages.clear();
self.ws_state = WsState::Open;
true
} else {
false
}
}
HomeMessage::WsReconnect => {
if self.ws_state != WsState::Open {
log!("Reconnecting");
self.ws_reconnecting = false;
self.connect_ws(ctx);
}
false
}
HomeMessage::Authenticated => {
self.connect_ws(ctx);
self.authenticated = true;
true
}
}
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
if let Some(message_container) = self.message_container_ref.cast::<web_sys::Element>() {
message_container.set_scroll_top(message_container.scroll_height());
}
}
}