generated from t/malachite
add: chats list page
This commit is contained in:
parent
c48cf78314
commit
747a05d649
16 changed files with 576 additions and 24 deletions
|
@ -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);
|
||||
|
|
26
app/templates_src/chat.lisp
Normal file
26
app/templates_src/chat.lisp
Normal file
|
@ -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 %}")
|
217
app/templates_src/chats.lisp
Normal file
217
app/templates_src/chats.lisp
Normal file
|
@ -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 += `<li>${member} (<a href=\"javascript:remove_member('${member}')\" class=\"red\">remove</a>)</li>`;
|
||||
}
|
||||
}
|
||||
|
||||
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 += `<option value=\"${username}\">${username}</option>`;
|
||||
}
|
||||
} 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 %}")
|
|
@ -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 %}")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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<User>) {
|
||||
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<Chat>) -> Vec<(Chat, Vec<User>)> {
|
||||
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<Vec<Chat>> {
|
||||
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
|
||||
|
|
|
@ -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<Vec<Message>> {
|
||||
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
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
26
src/database/users.rs
Normal file
26
src/database/users.rs
Normal file
|
@ -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<Vec<String>> {
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -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<State>,
|
||||
Json(props): Json<SearchUsers>,
|
||||
) -> 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()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ use tetratto_core::model::{ApiReturn, Error, auth::User};
|
|||
#[derive(Deserialize)]
|
||||
pub struct CreateChat {
|
||||
pub style: ChatStyle,
|
||||
pub members: Vec<usize>,
|
||||
pub members: Vec<String>,
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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))
|
||||
|
|
87
src/routes/pages/chats.rs
Normal file
87
src/routes/pages/chats.rs
Normal file
|
@ -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<State>,
|
||||
Query(props): Query<PaginatedQuery>,
|
||||
) -> 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<State>,
|
||||
Path(id): Path<usize>,
|
||||
Query(props): Query<PaginatedQuery>,
|
||||
) -> 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()))
|
||||
}
|
|
@ -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<User>,
|
||||
) -> 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<State>,
|
||||
) -> 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<State>) -> impl IntoResponse {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue