diff --git a/app/public/style.css b/app/public/style.css
index 2c6aa73..07d3d5f 100644
--- a/app/public/style.css
+++ b/app/public/style.css
@@ -17,6 +17,10 @@
--color-green-lowered: hsl(100, 84%, 15%);
--color-red-lowered: hsl(0, 84%, 35%);
+ --color-primary: hsl(25, 95%, 53%);
+ --color-primary-lowered: hsl(25, 95%, 49%);
+ --color-text-primary: hsl(0, 0%, 5%);
+
--shadow-x-offset: 0;
--shadow-y-offset: 0.125rem;
--shadow-size: var(--pad-1);
@@ -51,6 +55,10 @@
--color-green: hsl(100, 94%, 82%);
--color-yellow: oklch(90.1% 0.076 70.697);
--color-purple: hsl(284, 94%, 82%);
+
+ --color-primary: hsl(25, 95%, 63%);
+ --color-primary-lowered: hsl(25, 95%, 59%);
+ --color-text-primary: hsl(0, 0%, 5%);
}
html,
@@ -94,7 +102,7 @@ article {
overflow: auto;
}
-.tabs .tab {
+.tabs:not(.short) .tab {
height: 100%;
}
@@ -195,6 +203,10 @@ video {
padding: var(--pad-2) var(--pad-4);
}
+.card.surface {
+ background: var(--color-surface);
+}
+
/* button */
.button {
--h: 36px;
@@ -215,6 +227,7 @@ video {
text-decoration: none !important;
user-select: none;
appearance: none;
+ overflow: hidden;
}
.button:disabled {
@@ -276,6 +289,15 @@ video {
}
}
+.button.primary {
+ color: var(--color-text-primary) !important;
+ background: var(--color-primary);
+}
+
+.button.primary:hover {
+ background: var(--color-primary-lowered) !important;
+}
+
/* dropdown */
.dropdown {
position: relative;
@@ -468,6 +490,12 @@ svg.icon {
fill: currentColor;
}
+.big_icon svg.icon {
+ width: 18px;
+ height: 18px;
+ position: absolute;
+}
+
button svg {
pointer-events: none;
}
@@ -621,6 +649,10 @@ span {
align-items: flex-end;
}
+.gap_ch {
+ gap: 1ch;
+}
+
/* table */
table {
width: 100%;
@@ -675,7 +707,7 @@ dialog {
border: 0;
}
-dialog.inner {
+dialog .inner {
display: flex;
flex-direction: column;
gap: var(--pad-2);
diff --git a/app/templates_src/chat.lisp b/app/templates_src/chat.lisp
new file mode 100644
index 0000000..7b950d8
--- /dev/null
+++ b/app/templates_src/chat.lisp
@@ -0,0 +1,26 @@
+(text "{% extends \"root.lisp\" %} {% block head %}")
+(title
+ (text "{{ components::chat_name(chat=chat, members=members) }} - {{ name }}"))
+(text "{% endblock %} {% block body %}")
+(div
+ ("class" "flex w_full gap_2 justify_between items_center")
+ (div
+ ("class" "tabs short bar flex")
+ (a
+ ("class" "button tab camo")
+ ("href" "/chats")
+ (text "chats"))
+ (a
+ ("class" "button tab")
+ ("href" "/chats/{{ chat.id }}")
+ (text "{{ components::chat_name(chat=chat, members=members, advanced=true, avatar_size=\"18px\") }}"))))
+(div
+ ("class" "card flex flex_col gap_2")
+ ("style" "flex: 1 0 auto")
+ (text "{% if messages|length == 0 -%}")
+ (i ("class" "flex gap_ch items_center fade") (text "{{ icon \"star\" }} This is the start of the chat!"))
+ (text "{%- endif %}"))
+
+(script
+ (text ""))
+(text "{% endblock %}")
diff --git a/app/templates_src/chats.lisp b/app/templates_src/chats.lisp
new file mode 100644
index 0000000..41b9bda
--- /dev/null
+++ b/app/templates_src/chats.lisp
@@ -0,0 +1,217 @@
+(text "{% extends \"root.lisp\" %} {% block head %}")
+(title
+ (text "My chats - {{ name }}"))
+(text "{% endblock %} {% block body %}")
+(div
+ ("class" "flex w_full gap_2 justify_between items_center")
+ (div
+ ("class" "tabs short bar")
+ (a
+ ("class" "button tab")
+ ("href" "/chats")
+ (text "chats")))
+ (button
+ ("class" "button")
+ ("title" "Create chat")
+ ("onclick" "document.getElementById('create_dialog').showModal()")
+ (text "{{ icon \"plus\" }}")))
+(div
+ ("class" "card flex flex_col gap_2")
+ (text "{% for chat in chats -%}")
+ (div
+ ("class" "card surface w_full flex justify_between items_center gap_2")
+ (a
+ ("class" "flex gap_ch items_center")
+ ("href" "/chats/{{ chat[0].id }}")
+ (text "{{ components::chat_name(chat=chat[0], members=chat[1], advanced=true) }}"))
+ (div
+ ("class" "dropdown")
+ (button
+ ("onclick" "open_dropdown(event)")
+ ("exclude" "dropdown")
+ ("class" "button")
+ (text "{{ icon \"ellipsis\" }}"))
+ (div
+ ("class" "inner")
+ (a
+ ("class" "button")
+ ("href" "/chats/{{ chat[0].id }}")
+ ("target" "_blank")
+ (text "pop open"))
+
+ (text "{% if chat[0].style != \"Direct\" -%}")
+ ; group chat only
+ (button
+ ("class" "button")
+ ("onclick" "rename_gc('{{ chat[0].id }}')")
+ (text "rename"))
+ (text "{%- endif %}")
+
+ (button
+ ("class" "button red")
+ ("onclick" "leave_chat('{{ chat[0].id }}')")
+ (text "leave")))))
+ (text "{%- endfor %}")
+
+ (text "{% if chats|length == 0 -%}")
+ (i ("class" "flex gap_ch items_center fade") (text "{{ icon \"smile\" }} No results, yet!"))
+ (text "{%- endif %}")
+
+ ; pagination
+ (div
+ ("class" "flex w_full items_center gap_2 justify_between")
+ (text "{% if page > 0 -%}")
+ (a
+ ("href" "?page={{ page - 1 }}")
+ ("class" "button surface")
+ (text "{{ icon \"arrow-left\" }} Back"))
+ (text "{%- else -%}")
+ (div null?)
+ (text "{%- endif %}")
+
+ (text "{% if chats|length > 0 -%}")
+ (a
+ ("href" "?page={{ page + 1 }}")
+ ("class" "button surface")
+ (text "Next {{ icon \"arrow-right\" }}"))
+ (text "{%- endif %}")))
+
+(dialog
+ ("id" "create_dialog")
+ (div
+ ("class" "inner")
+ (h2
+ ("class" "text_center w_full")
+ (text "Create chat"))
+
+ (ul ("id" "members_list"))
+ (form
+ ("class" "flex flex_row gap_2 w_full")
+ ("onsubmit" "add_member(event)")
+ (input
+ ("type" "text")
+ ("list" "users_search")
+ ("name" "username")
+ ("id" "username")
+ ("placeholder" "username")
+ ("oninput" "search_users(event)"))
+ (button
+ ("class" "button")
+ (text "Add")))
+
+ (hr ("class" "margin"))
+
+ (div
+ ("class" "flex gap_2 justify_between")
+ (button
+ ("onclick" "document.getElementById('create_dialog').close()")
+ ("class" "button red")
+ (text "Cancel"))
+
+ (button
+ ("class" "button green")
+ ("onclick" "create_chat_from_form()")
+ (text "Create")))))
+
+(datalist ("id" "users_search"))
+
+(script
+ (text "globalThis.CHAT_MEMBERS = [];
+
+ function render_members() {
+ document.getElementById('members_list').innerHTML = \"\";
+ for (const member of CHAT_MEMBERS) {
+ document.getElementById('members_list').innerHTML += `
${member} (remove)`;
+ }
+ }
+
+ function add_member(e) {
+ e.preventDefault();
+ const member = e.target.username.value;
+
+ if (CHAT_MEMBERS.includes(member)) {
+ return;
+ }
+
+ CHAT_MEMBERS.push(member);
+ render_members();
+ e.target.reset();
+ }
+
+ function remove_member(member) {
+ CHAT_MEMBERS.splice(CHAT_MEMBERS.indexOf(member), 1);
+ render_members();
+ }
+
+ function create_chat_from_form(e) {
+ document.getElementById('create_dialog').close();
+
+ fetch(\"/api/v1/chats\", {
+ method: \"POST\",
+ headers: {
+ \"Content-Type\": \"application/json\",
+ },
+ body: JSON.stringify({
+ style: CHAT_MEMBERS.length > 1 ? { Group: { name: \"Untitled\" } } : \"Direct\",
+ members: CHAT_MEMBERS,
+ }),
+ })
+ .then((res) => res.json())
+ .then((res) => {
+ show_message(res.message, res.ok);
+
+ if (res.ok) {
+ window.location.href = `/chats/${res.payload}`;
+ e.target.reset();
+ }
+ });
+ }
+
+ let search_users_timeout;
+ function search_users(e) {
+ if (search_users_timeout) {
+ clearTimeout(search_users_timeout);
+ }
+
+ if (e.target.value.trim().length === 0) {
+ return;
+ }
+
+ search_users_timeout = setTimeout(() => {
+ fetch(\"/api/v1/auth/users/search\", {
+ method: \"POST\",
+ headers: {
+ \"Content-Type\": \"application/json\",
+ },
+ body: JSON.stringify({
+ prefix: e.target.value.trim(),
+ }),
+ })
+ .then((res) => res.json())
+ .then((res) => {
+ if (res.ok) {
+ document.getElementById(\"users_search\").innerHTML = \"\";
+ for (const username of res.payload) {
+ document.getElementById(\"users_search\").innerHTML += ``;
+ }
+ } else {
+ show_message(res.message, res.ok);
+ }
+ });
+ }, 1000);
+ }
+
+ function leave_chat(id) {
+ if (!confirm(\"Are you sure you would like to do this?\")) {
+ return;
+ }
+
+ fetch(`/api/v1/chats/${id}/leave`, {
+ method: \"POST\",
+ })
+ .then((res) => res.json())
+ .then((res) => {
+ show_message(res.message, res.ok);
+ });
+ }"))
+(text "{% endblock %}")
diff --git a/app/templates_src/components.lisp b/app/templates_src/components.lisp
index d8a0f1d..3fa1ee1 100644
--- a/app/templates_src/components.lisp
+++ b/app/templates_src/components.lisp
@@ -7,3 +7,36 @@
("loading" "lazy")
("style" "--size: {{ size }}"))
(text "{%- endmacro %}")
+
+(text "{% macro username(user) -%}")
+(b
+ (text "{% if user.settings.display_name|length > 0 -%}")
+ (text "{{ user.settings.display_name }}")
+ (text "{%- else -%}")
+ (text "{{ user.username }}")
+ (text "{%- endif %}"))
+(text "{%- endmacro %}")
+
+(text "{% macro chat_name(chat, members, advanced=false, avatar_size=\"24px\") -%}")
+(text "{% if advanced -%}")
+; advanced
+(text "{% if chat.style == \"Direct\" -%} {% for member in members -%} {% if member.id != user.id -%}")
+; direct message; user that ISN'T the current user
+(text "{{ components::avatar(id=member.id, size=avatar_size) }}")
+(text "{{ components::username(user=member) }}")
+(text "{%- endif %} {%- endfor %} {%- else -%}")
+; group chat
+(text "{% for member in members -%} {{ components::avatar(id=member.id, size=avatar_size) }} {%- endfor %}")
+(b (text "{{ chat.style.Group.name }}"))
+(text "{%- endif %}")
+(text "{%- else -%}")
+; NOT advanced
+(text "{% if chat.style == \"Direct\" -%} {% for member in members -%} {% if member.id != user.id -%}")
+; direct message; user that ISN'T the current user
+(text "{{ user.username }}")
+(text "{%- endif %} {%- endfor %} {%- else -%}")
+; group chat
+(text "{{ chat.style.Group.name }}")
+(text "{%- endif %}")
+(text "{%- endif %}")
+(text "{%- endmacro %}")
diff --git a/app/templates_src/root.lisp b/app/templates_src/root.lisp
index 87e742b..cb97232 100644
--- a/app/templates_src/root.lisp
+++ b/app/templates_src/root.lisp
@@ -10,8 +10,6 @@
(link ("rel" "stylesheet") ("href" "https://repodelivery.tetratto.com/tetratto/crates/app/src/public/css/utility.css"))
(link ("rel" "stylesheet") ("href" "/public/style.css?v={{ build_code }}"))
- (style (text ":root { --color-primary: {{ theme_color }}; }"))
-
(meta ("name" "theme-color") ("content" "{{ theme_color }}"))
(meta ("property" "og:type") ("content" "website"))
(meta ("property" "og:site_name") ("content" "{{ name }}"))
@@ -28,7 +26,7 @@
(body
; nav
(nav
- ("class" "flex w_full justify_between gap_2 sticky")
+ ("class" "flex w_full justify_between gap_2")
(div
("class" "flex side")
(div
@@ -76,8 +74,7 @@
("onclick" "user_logout()")
(text "logout"))
(text "{%- endif %}")
- (text "{% block dropdown %}{% endblock %}")))
- (a ("class" "button camo") ("href" "/") (b (text "{{ name }}"))))
+ (text "{% block dropdown %}{% endblock %}"))))
(div
("class" "side flex")
diff --git a/src/database/chats.rs b/src/database/chats.rs
index 920089d..b104c6d 100644
--- a/src/database/chats.rs
+++ b/src/database/chats.rs
@@ -1,9 +1,9 @@
use super::DataManager;
use crate::model::{Chat, ChatStyle};
-use oiseau::{PostgresRow, cache::Cache, execute, get, params};
+use oiseau::{PostgresRow, cache::Cache, execute, get, params, query_rows};
use tetratto_core::{
auto_method,
- model::{Error, Result},
+ model::{Error, Result, auth::User},
};
impl DataManager {
@@ -21,6 +21,59 @@ impl DataManager {
auto_method!(get_chat_by_id(usize as i64)@get_chat_from_row -> "SELECT * FROM t_chats WHERE id = $1" --name="chat" --returns=Chat --cache-key-tmpl="twny.chat:{}");
+ pub async fn fill_chat(&self, chat: Chat) -> (Chat, Vec) {
+ let mut members = Vec::new();
+
+ for x in &chat.members {
+ members.push(match self.2.get_user_by_id(*x).await {
+ Ok(x) => x,
+ Err(_) => User::deleted(),
+ });
+ }
+
+ (chat, members)
+ }
+
+ pub async fn fill_chats(&self, chats: Vec) -> Vec<(Chat, Vec)> {
+ let mut out = Vec::new();
+
+ for x in chats {
+ out.push(self.fill_chat(x).await);
+ }
+
+ out
+ }
+
+ /// Get all chats the given member is participating in.
+ pub async fn get_chats_by_member(
+ &self,
+ id: usize,
+ batch: usize,
+ page: usize,
+ ) -> Result> {
+ let conn = match self.0.connect().await {
+ Ok(c) => c,
+ Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
+ };
+
+ let res = query_rows!(
+ &conn,
+ "SELECT * FROM t_chats WHERE members LIKE $1 ORDER BY last_message_created LIMIT $2 OFFSET $3",
+ params![
+ &format!("%{id}%"),
+ &(batch as i64),
+ &((page * batch) as i64)
+ ],
+ |x| { Self::get_chat_from_row(x) }
+ );
+
+ if let Err(e) = res {
+ return Err(Error::DatabaseError(e.to_string()));
+ }
+
+ Ok(res.unwrap())
+ }
+
/// Create a new chat in the database.
///
/// # Arguments
diff --git a/src/database/messages.rs b/src/database/messages.rs
index d49e145..6848a1f 100644
--- a/src/database/messages.rs
+++ b/src/database/messages.rs
@@ -3,7 +3,7 @@ use crate::model::{Message, SocketMessage, SocketMethod};
use oiseau::{
PostgresRow,
cache::{Cache, redis::Commands},
- execute, get, params,
+ execute, get, params, query_rows,
};
use tetratto_core::{
auto_method,
@@ -27,6 +27,32 @@ impl DataManager {
auto_method!(get_message_by_id(usize as i64)@get_message_from_row -> "SELECT * FROM t_messages WHERE id = $1" --name="message" --returns=Message --cache-key-tmpl="twny.message:{}");
+ /// Get messages by their chat ID.
+ pub async fn get_messages_by_chat(
+ &self,
+ id: usize,
+ batch: usize,
+ page: usize,
+ ) -> Result> {
+ let conn = match self.0.connect().await {
+ Ok(c) => c,
+ Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
+ };
+
+ let res = query_rows!(
+ &conn,
+ "SELECT * FROM t_messages WHERE chat = $1 LIMIT $2 OFFSET $3",
+ params![&(id as i64), &(batch as i64), &((page * batch) as i64)],
+ |x| { Self::get_message_from_row(x) }
+ );
+
+ if let Err(e) = res {
+ return Err(Error::DatabaseError(e.to_string()));
+ }
+
+ Ok(res.unwrap())
+ }
+
/// Create a new message in the database.
///
/// # Arguments
diff --git a/src/database/mod.rs b/src/database/mod.rs
index 5f1667a..a7e6bd4 100644
--- a/src/database/mod.rs
+++ b/src/database/mod.rs
@@ -1,6 +1,7 @@
mod chats;
mod messages;
mod sql;
+mod users;
use crate::config::Config;
use buckets_core::{Config as BucketsConfig, DataManager as BucketsManager};
diff --git a/src/database/sql/create_messages.sql b/src/database/sql/create_messages.sql
index 32aaa5e..24e1dd9 100644
--- a/src/database/sql/create_messages.sql
+++ b/src/database/sql/create_messages.sql
@@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS t_messages (
created BIGINT NOT NULL,
edited BIGINT NOT NULL,
owner BIGINT NOT NULL,
- chats BIGINT NOT NULL,
+ chat BIGINT NOT NULL,
content TEXT NOT NULL,
uploads TEXT NOT NULL
);
diff --git a/src/database/users.rs b/src/database/users.rs
new file mode 100644
index 0000000..4569f4e
--- /dev/null
+++ b/src/database/users.rs
@@ -0,0 +1,26 @@
+use super::DataManager;
+use oiseau::{get, params, query_rows};
+use tetratto_core::model::{Error, Result};
+
+impl DataManager {
+ /// Search all users.
+ pub async fn search_users(&self, query: &str, batch: usize) -> Result> {
+ let conn = match self.0.connect().await {
+ Ok(c) => c,
+ Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
+ };
+
+ let res = query_rows!(
+ &conn,
+ "SELECT username, id FROM users WHERE username LIKE $1 LIMIT $2",
+ params![&format!("{query}%"), &(batch as i64),],
+ |x| { get!(x->0(String)) }
+ );
+
+ if let Err(e) = res {
+ return Err(Error::DatabaseError(e.to_string()));
+ }
+
+ Ok(res.unwrap())
+ }
+}
diff --git a/src/routes/api/auth.rs b/src/routes/api/auth.rs
index 276ea38..e0c466a 100644
--- a/src/routes/api/auth.rs
+++ b/src/routes/api/auth.rs
@@ -189,3 +189,28 @@ pub async fn check_totp_request(
payload: Some(!user.totp.is_empty()),
})
}
+
+#[derive(Deserialize)]
+pub struct SearchUsers {
+ pub prefix: String,
+}
+
+pub async fn search_users_request(
+ jar: CookieJar,
+ Extension(data): Extension,
+ Json(props): Json,
+) -> impl IntoResponse {
+ let data = &(data.read().await).0;
+ if get_user_from_token!(jar, data.2).is_none() {
+ return Json(Error::NotAllowed.into());
+ }
+
+ match data.search_users(&props.prefix, 4).await {
+ Ok(x) => Json(ApiReturn {
+ ok: true,
+ message: "Success".to_string(),
+ payload: Some(x),
+ }),
+ Err(e) => Json(e.into()),
+ }
+}
diff --git a/src/routes/api/chats.rs b/src/routes/api/chats.rs
index 294643b..4c15cdf 100644
--- a/src/routes/api/chats.rs
+++ b/src/routes/api/chats.rs
@@ -22,7 +22,7 @@ use tetratto_core::model::{ApiReturn, Error, auth::User};
#[derive(Deserialize)]
pub struct CreateChat {
pub style: ChatStyle,
- pub members: Vec,
+ pub members: Vec,
}
pub async fn create_request(
@@ -37,13 +37,23 @@ pub async fn create_request(
};
if req.members.len() > 2 && req.style == ChatStyle::Direct {
- return Json(Error::DataTooLong("members".to_string()).into());
+ return Json(Error::DataTooLong("members list".to_string()).into());
+ } else if req.members.len() < 1 {
+ return Json(Error::DataTooShort("members list".to_string()).into());
}
match data
.create_chat(Chat::new(req.style, {
- let mut x = req.members;
+ let mut x = Vec::new();
+
x.push(user.id);
+ for y in req.members {
+ x.push(match data.2.get_user_by_username(&y).await {
+ Ok(x) => x.id,
+ Err(e) => return Json(e.into()),
+ })
+ }
+
x
}))
.await
diff --git a/src/routes/api/mod.rs b/src/routes/api/mod.rs
index 63b01fd..cc08846 100644
--- a/src/routes/api/mod.rs
+++ b/src/routes/api/mod.rs
@@ -14,6 +14,7 @@ pub fn routes() -> Router {
"/auth/user/{username}/check_totp",
get(auth::check_totp_request),
)
+ .route("/auth/users/search", post(auth::search_users_request))
// chats
.route("/chats", post(chats::create_request))
.route("/chats/{id}/leave", post(chats::leave_request))
diff --git a/src/routes/pages/chats.rs b/src/routes/pages/chats.rs
new file mode 100644
index 0000000..65559fc
--- /dev/null
+++ b/src/routes/pages/chats.rs
@@ -0,0 +1,87 @@
+use crate::{
+ State, get_user_from_token,
+ routes::{
+ default_context,
+ pages::{PaginatedQuery, misc::render_error},
+ },
+};
+use axum::{
+ Extension,
+ extract::{Path, Query},
+ response::{Html, IntoResponse},
+};
+use axum_extra::extract::CookieJar;
+use tetratto_core::model::Error;
+
+pub async fn list_request(
+ jar: CookieJar,
+ Extension(data): Extension,
+ Query(props): Query,
+) -> impl IntoResponse {
+ let (ref data, ref tera, ref build_code) = *data.read().await;
+ let user = match get_user_from_token!(jar, data.2) {
+ Some(x) => x,
+ None => {
+ return Err(render_error(Error::NotAllowed, tera, data.0.0.clone(), None).await);
+ }
+ };
+
+ let chats = match data.get_chats_by_member(user.id, 12, props.page).await {
+ Ok(x) => data.fill_chats(x).await,
+ Err(e) => {
+ return Err(render_error(e, tera, data.0.0.clone(), Some(user)).await);
+ }
+ };
+
+ let mut ctx = default_context(&data.0.0, &build_code, &Some(user));
+
+ ctx.insert("chats", &chats);
+ ctx.insert("page", &props.page);
+
+ Ok(Html(tera.render("chats.lisp", &ctx).unwrap()))
+}
+
+pub async fn chat_request(
+ jar: CookieJar,
+ Extension(data): Extension,
+ Path(id): Path,
+ Query(props): Query,
+) -> impl IntoResponse {
+ let (ref data, ref tera, ref build_code) = *data.read().await;
+ let user = match get_user_from_token!(jar, data.2) {
+ Some(x) => x,
+ None => {
+ return Err(render_error(Error::NotAllowed, tera, data.0.0.clone(), None).await);
+ }
+ };
+
+ let (chat, members) = match data.get_chat_by_id(id).await {
+ Ok(x) => {
+ if !x.members.contains(&user.id) {
+ return Err(
+ render_error(Error::NotAllowed, tera, data.0.0.clone(), Some(user)).await,
+ );
+ }
+
+ data.fill_chat(x).await
+ }
+ Err(e) => {
+ return Err(render_error(e, tera, data.0.0.clone(), Some(user)).await);
+ }
+ };
+
+ let messages = match data.get_messages_by_chat(id, 12, props.page).await {
+ Ok(x) => x,
+ Err(e) => {
+ return Err(render_error(e, tera, data.0.0.clone(), Some(user)).await);
+ }
+ };
+
+ let mut ctx = default_context(&data.0.0, &build_code, &Some(user));
+
+ ctx.insert("chat", &chat);
+ ctx.insert("members", &members);
+ ctx.insert("messages", &messages);
+
+ Ok(Html(tera.render("chat.lisp", &ctx).unwrap()))
+}
diff --git a/src/routes/pages/misc.rs b/src/routes/pages/misc.rs
index afca4a1..3a8f5a0 100644
--- a/src/routes/pages/misc.rs
+++ b/src/routes/pages/misc.rs
@@ -1,24 +1,30 @@
-use crate::{State, get_user_from_token, routes::default_context};
+use crate::{State, config::Config, get_user_from_token, routes::default_context};
use axum::{
Extension,
response::{Html, IntoResponse},
};
use axum_extra::extract::CookieJar;
-use tetratto_core::model::Error;
+use tera::Tera;
+use tetratto_core::model::{Error, auth::User};
+
+pub async fn render_error(
+ e: Error,
+ tera: &Tera,
+ config: Config,
+ user: Option,
+) -> impl IntoResponse + use<> {
+ let mut ctx = default_context(&config, "", &user);
+ ctx.insert("error", &e.to_string());
+ return Html(tera.render("error.lisp", &ctx).unwrap());
+}
pub async fn not_found_request(
jar: CookieJar,
Extension(data): Extension,
) -> impl IntoResponse {
- let (ref data, ref tera, ref build_code) = *data.read().await;
+ let (ref data, ref tera, _) = *data.read().await;
let user = get_user_from_token!(jar, data.2);
-
- let mut ctx = default_context(&data.0.0, &build_code, &user);
- ctx.insert(
- "error",
- &Error::GeneralNotFound("page".to_string()).to_string(),
- );
- return Html(tera.render("error.lisp", &ctx).unwrap());
+ render_error(Error::NotAllowed, tera, data.0.0.clone(), user).await
}
pub async fn index_request(jar: CookieJar, Extension(data): Extension) -> impl IntoResponse {
diff --git a/src/routes/pages/mod.rs b/src/routes/pages/mod.rs
index 6fbfa5b..137f792 100644
--- a/src/routes/pages/mod.rs
+++ b/src/routes/pages/mod.rs
@@ -1,9 +1,21 @@
+pub mod chats;
pub mod misc;
use axum::routing::{Router, get};
+use serde::Deserialize;
pub fn routes() -> Router {
Router::new()
.route("/", get(misc::index_request))
+ // auth
.route("/login", get(misc::login_request))
+ // chats
+ .route("/chats", get(chats::list_request))
+ .route("/chats/{id}", get(chats::chat_request))
+}
+
+#[derive(Deserialize)]
+pub struct PaginatedQuery {
+ #[serde(default)]
+ pub page: usize,
}