generated from t/malachite
add: user profiles and junk
This commit is contained in:
parent
6b8c33d27f
commit
2bd23f8214
28 changed files with 1139 additions and 108 deletions
125
Cargo.lock
generated
125
Cargo.lock
generated
|
@ -365,7 +365,7 @@ dependencies = [
|
||||||
"pathbufd",
|
"pathbufd",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tetratto-core",
|
"tetratto-core 15.0.2",
|
||||||
"tetratto-shared",
|
"tetratto-shared",
|
||||||
"toml 0.9.5",
|
"toml 0.9.5",
|
||||||
]
|
]
|
||||||
|
@ -1370,6 +1370,7 @@ checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1419,6 +1420,15 @@ dependencies = [
|
||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
|
@ -2125,6 +2135,27 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error-attr2"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error-attr2",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.95"
|
version = "1.0.95"
|
||||||
|
@ -2292,7 +2323,7 @@ dependencies = [
|
||||||
"built",
|
"built",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"interpolate_name",
|
"interpolate_name",
|
||||||
"itertools",
|
"itertools 0.12.1",
|
||||||
"libc",
|
"libc",
|
||||||
"libfuzzer-sys",
|
"libfuzzer-sys",
|
||||||
"log",
|
"log",
|
||||||
|
@ -2674,6 +2705,51 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_valid"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b615bed66931a7a9809b273937adc8a402d038b1e509d027fcaf62f084d33d1"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"itertools 0.13.0",
|
||||||
|
"num-traits",
|
||||||
|
"once_cell",
|
||||||
|
"paste",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_valid_derive",
|
||||||
|
"serde_valid_literal",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_valid_derive"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5fa1a5a21ea5aab06d2e6a6b59837d450fb2be9695be97735a711edfbe79ea07"
|
||||||
|
dependencies = [
|
||||||
|
"itertools 0.13.0",
|
||||||
|
"paste",
|
||||||
|
"proc-macro-error2",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_valid_literal"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd07331596ea967dccf9a35bde71ecd757490e09827b938a5c6226c648e3a25e"
|
||||||
|
dependencies = [
|
||||||
|
"paste",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
|
@ -2837,6 +2913,12 @@ dependencies = [
|
||||||
"unicode-properties",
|
"unicode-properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
@ -2916,8 +2998,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tawny"
|
name = "tawny"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ammonia",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"axum-image",
|
"axum-image",
|
||||||
|
@ -2932,7 +3015,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tera",
|
"tera",
|
||||||
"tetratto-core",
|
"tetratto-core 16.0.2",
|
||||||
"tetratto-shared",
|
"tetratto-shared",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml 0.9.5",
|
"toml 0.9.5",
|
||||||
|
@ -3013,6 +3096,34 @@ dependencies = [
|
||||||
"totp-rs",
|
"totp-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tetratto-core"
|
||||||
|
version = "16.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "380eed8dec18b0dcda3440d47375a1bacf94e42fdcd93d464e27682d005bf356"
|
||||||
|
dependencies = [
|
||||||
|
"async-recursion",
|
||||||
|
"base16ct",
|
||||||
|
"base64",
|
||||||
|
"bitflags 2.9.2",
|
||||||
|
"buckets-core",
|
||||||
|
"emojis",
|
||||||
|
"md-5",
|
||||||
|
"oiseau",
|
||||||
|
"paste",
|
||||||
|
"pathbufd",
|
||||||
|
"regex",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_valid",
|
||||||
|
"tetratto-l10n",
|
||||||
|
"tetratto-shared",
|
||||||
|
"tokio",
|
||||||
|
"toml 0.9.5",
|
||||||
|
"totp-rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-l10n"
|
name = "tetratto-l10n"
|
||||||
version = "12.0.0"
|
version = "12.0.0"
|
||||||
|
@ -3585,6 +3696,12 @@ version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tawny"
|
name = "tawny"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["trisuaso"]
|
authors = ["trisuaso"]
|
||||||
repository = "https://trisua.com/t/tawny"
|
repository = "https://trisua.com/t/tawny"
|
||||||
|
@ -8,7 +8,7 @@ license = "AGPL-3.0-or-later"
|
||||||
homepage = "https://tawny.cc"
|
homepage = "https://tawny.cc"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tetratto-core = "15.0.1"
|
tetratto-core = "16.0.2"
|
||||||
tetratto-shared = "12.0.6"
|
tetratto-shared = "12.0.6"
|
||||||
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
||||||
pathbufd = "0.1.4"
|
pathbufd = "0.1.4"
|
||||||
|
@ -34,3 +34,4 @@ oiseau = { version = "0.1.2", default-features = false, features = ["postgres",
|
||||||
buckets-core = "1.0.4"
|
buckets-core = "1.0.4"
|
||||||
axum-image = "0.1.1"
|
axum-image = "0.1.1"
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
|
ammonia = "4.1.1"
|
||||||
|
|
|
@ -36,6 +36,10 @@ function media_theme_pref() {
|
||||||
}
|
}
|
||||||
|
|
||||||
globalThis.temporary_set_theme = (theme) => {
|
globalThis.temporary_set_theme = (theme) => {
|
||||||
|
if (theme === "Auto") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
document.documentElement.className = theme.toLowerCase();
|
document.documentElement.className = theme.toLowerCase();
|
||||||
|
|
||||||
if (theme === "Light") {
|
if (theme === "Light") {
|
||||||
|
@ -206,3 +210,39 @@ globalThis.submitter_load = (submitter) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// users search
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -102,6 +102,10 @@ function sock_con() {
|
||||||
if (document.getElementById(`message_${msg.body}`)) {
|
if (document.getElementById(`message_${msg.body}`)) {
|
||||||
document.getElementById(`message_${msg.body}`).remove();
|
document.getElementById(`message_${msg.body}`).remove();
|
||||||
}
|
}
|
||||||
|
} else if (msg.method === "MessageUpdate") {
|
||||||
|
const [id, content] = JSON.parse(msg.body);
|
||||||
|
document.getElementById(`${id}_body`).innerHTML =
|
||||||
|
await render_markdown(content);
|
||||||
} else if (msg.method === "ReadReceipt") {
|
} else if (msg.method === "ReadReceipt") {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
read_receipt();
|
read_receipt();
|
||||||
|
@ -179,3 +183,133 @@ function delete_message(id) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function render_markdown(content) {
|
||||||
|
return await (
|
||||||
|
await fetch("/api/v1/markdown", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ content }),
|
||||||
|
})
|
||||||
|
).text();
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit_message_ui(id) {
|
||||||
|
document.getElementById(`${id}_body`).classList.add("hidden");
|
||||||
|
document.getElementById(`${id}_edit_area`).classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit_message(id, e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
fetch(`/api/v1/messages/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: e.target.content.value,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
show_message(res.message, res.ok);
|
||||||
|
} else {
|
||||||
|
document
|
||||||
|
.getElementById(`${id}_body`)
|
||||||
|
.classList.remove("hidden");
|
||||||
|
document
|
||||||
|
.getElementById(`${id}_edit_area`)
|
||||||
|
.classList.add("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_member_to_chat(e, chat_id) {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById("add_user_dialog").close();
|
||||||
|
fetch(`/api/v1/chats/${chat_id}/members/add/${e.target.username.value}`, {
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
show_message(res.message, res.ok);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_chat_info(id, info) {
|
||||||
|
fetch(`/api/v1/chats/${id}/info`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
info,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
show_message(res.message, res.ok);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function rename_chat(id, info) {
|
||||||
|
const new_name = prompt("New name:");
|
||||||
|
|
||||||
|
if (!new_name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
info.name = new_name;
|
||||||
|
update_chat_info(id, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove_member_from_chat(chat_id, uid) {
|
||||||
|
if (!confirm("Are you sure you would like to do this?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/api/v1/chats/${chat_id}/members/remove/${uid}`, {
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
show_message(res.message, res.ok);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_direct_chat_with_user(id) {
|
||||||
|
fetch(`/api/v1/chats`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
style: "Direct",
|
||||||
|
members: [id],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
show_message(res.message, res.ok);
|
||||||
|
} else {
|
||||||
|
window.location.href = `/chats/${res.payload}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
|
@import url("https://repodelivery.tetratto.com/tetratto-aux/lexend.css");
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
|
--hue: 16;
|
||||||
--color-super-lowered: oklch(87.1% 0.006 286.286);
|
--sat: 6%;
|
||||||
--color-lowered: oklch(96.7% 0.001 286.375);
|
--lit: 0%;
|
||||||
--color-surface: oklch(92.9% 0.013 255.508);
|
--color-surface: hsl(var(--hue), var(--sat), calc(90% - var(--lit)));
|
||||||
--color-raised: oklch(98.4% 0.003 247.858);
|
--color-lowered: hsl(var(--hue), var(--sat), calc(86% - var(--lit)));
|
||||||
--color-super-raised: oklch(96.8% 0.007 247.896);
|
--color-raised: hsl(var(--hue), var(--sat), calc(96% - var(--lit)));
|
||||||
--color-text: hsl(0, 0%, 5%);
|
--color-super-lowered: hsl(var(--hue), var(--sat), calc(82% - var(--lit)));
|
||||||
|
--color-super-raised: hsl(var(--hue), var(--sat), calc(99% - var(--lit)));
|
||||||
|
--color-text: hsl(0, 0%, 9%);
|
||||||
|
--color-text-raised: var(--color-text);
|
||||||
|
--color-text-lowered: var(--color-text);
|
||||||
|
|
||||||
--color-link: #2949b2;
|
--color-link: #2949b2;
|
||||||
--color-shadow: rgba(0, 0, 0, 0.08);
|
--color-shadow: rgba(0, 0, 0, 0.08);
|
||||||
|
@ -30,7 +36,7 @@
|
||||||
--pad-3: 0.5rem;
|
--pad-3: 0.5rem;
|
||||||
--pad-4: 1rem;
|
--pad-4: 1rem;
|
||||||
|
|
||||||
--radius: 0.2rem;
|
--radius: 6px;
|
||||||
--nav-height: 36px;
|
--nav-height: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,12 +49,15 @@
|
||||||
|
|
||||||
.dark,
|
.dark,
|
||||||
.dark * {
|
.dark * {
|
||||||
--color-super-lowered: var(--color-super-raised);
|
--hue: 266;
|
||||||
--color-lowered: var(--color-raised);
|
--sat: 14%;
|
||||||
--color-surface: oklch(21% 0.006 285.885);
|
--lit: 12%;
|
||||||
--color-raised: oklch(27.4% 0.006 286.033);
|
--color-surface: hsl(var(--hue), var(--sat), calc(0% + var(--lit)));
|
||||||
--color-super-raised: oklch(37% 0.013 285.805);
|
--color-lowered: hsl(var(--hue), var(--sat), calc(6% + var(--lit)));
|
||||||
--color-text: hsl(0, 0%, 95%);
|
--color-raised: hsl(var(--hue), var(--sat), calc(2% + var(--lit)));
|
||||||
|
--color-super-lowered: hsl(var(--hue), var(--sat), calc(12% + var(--lit)));
|
||||||
|
--color-super-raised: hsl(var(--hue), var(--sat), calc(4% + var(--lit)));
|
||||||
|
--color-text: hsl(0, 0%, 91%);
|
||||||
|
|
||||||
--color-link: #93c5fd;
|
--color-link: #93c5fd;
|
||||||
--color-red: hsl(0, 94%, 82%);
|
--color-red: hsl(0, 94%, 82%);
|
||||||
|
@ -66,6 +75,7 @@ body {
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
letter-spacing: 0.15px;
|
letter-spacing: 0.15px;
|
||||||
font-family:
|
font-family:
|
||||||
|
"Lexend",
|
||||||
"Inter",
|
"Inter",
|
||||||
"Poppins",
|
"Poppins",
|
||||||
"Roboto",
|
"Roboto",
|
||||||
|
@ -212,6 +222,10 @@ video {
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card .card {
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
/* button */
|
/* button */
|
||||||
.button {
|
.button {
|
||||||
--h: 36px;
|
--h: 36px;
|
||||||
|
@ -235,6 +249,10 @@ video {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button:not(nav *, .tab, .dropdown .inner *, .square) {
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
.button:disabled {
|
.button:disabled {
|
||||||
opacity: 50%;
|
opacity: 50%;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
@ -303,6 +321,15 @@ video {
|
||||||
background: var(--color-primary-lowered) !important;
|
background: var(--color-primary-lowered) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button.big {
|
||||||
|
--h: 48px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.icon_only {
|
||||||
|
width: var(--h);
|
||||||
|
}
|
||||||
|
|
||||||
/* dropdown */
|
/* dropdown */
|
||||||
.dropdown {
|
.dropdown {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -380,6 +407,8 @@ select {
|
||||||
line-height: var(--h);
|
line-height: var(--h);
|
||||||
border-left: solid 0px transparent;
|
border-left: solid 0px transparent;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
resize: vertical;
|
||||||
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
input:not([type="checkbox"]):focus {
|
input:not([type="checkbox"]):focus {
|
||||||
|
@ -570,6 +599,10 @@ h6 {
|
||||||
width: -moz-max-content;
|
width: -moz-max-content;
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
|
& * {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
@ -714,6 +747,7 @@ dialog {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: var(--pad-4);
|
padding: var(--pad-4);
|
||||||
border: 0;
|
border: 0;
|
||||||
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog .inner {
|
dialog .inner {
|
||||||
|
@ -765,9 +799,15 @@ menu.col {
|
||||||
padding: var(--pad-2) var(--pad-3);
|
padding: var(--pad-2) var(--pad-3);
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message.mine .body {
|
.message.mine .body {
|
||||||
background: var(--color-primary);
|
background: var(--color-primary);
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message:not(.mine) .body {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
(text "{% extends \"root.lisp\" %} {% block head %}")
|
(text "{% extends \"root.lisp\" %} {% block head %}")
|
||||||
(title
|
(title
|
||||||
(text "{{ components::chat_name(chat=chat, members=members) }} - {{ name }}"))
|
(text "{{ components::chat_name(chat=chat, members=members) }} — {{ config.name }}"))
|
||||||
(text "{% endblock %} {% block body %}")
|
(text "{% endblock %} {% block body %}")
|
||||||
(div
|
(div
|
||||||
("class" "flex w_full gap_2 justify_between items_center")
|
("class" "flex w_full gap_2 justify_between items_center")
|
||||||
|
@ -9,11 +9,16 @@
|
||||||
(a
|
(a
|
||||||
("class" "button tab camo")
|
("class" "button tab camo")
|
||||||
("href" "/chats")
|
("href" "/chats")
|
||||||
(text "chats"))
|
(text "chats")
|
||||||
|
(text "{% if user.missed_messages_count > 0 -%}") (b (text "({{ user.missed_messages_count }})")) (text "{%- endif %}"))
|
||||||
(a
|
(a
|
||||||
("class" "button tab")
|
("class" "button tab")
|
||||||
("href" "/chats/{{ chat.id }}")
|
("href" "/chats/{{ chat.id }}")
|
||||||
(text "{{ components::chat_name(chat=chat, members=members, advanced=true, avatar_size=\"18px\") }}"))))
|
(text "{{ components::chat_name(chat=chat, members=members, advanced=true, avatar_size=\"18px\") }}"))
|
||||||
|
(a
|
||||||
|
("class" "button tab camo")
|
||||||
|
("href" "/chats/{{ chat.id }}/manage")
|
||||||
|
(text "{{ icon \"settings-2\" }} Manage"))))
|
||||||
(div
|
(div
|
||||||
("class" "flex flex_col card_nest reverse")
|
("class" "flex flex_col card_nest reverse")
|
||||||
("style" "flex: 1 0 auto")
|
("style" "flex: 1 0 auto")
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
(text "{% extends \"root.lisp\" %} {% block head %}")
|
(text "{% extends \"root.lisp\" %} {% block head %}")
|
||||||
(title
|
(title
|
||||||
(text "My chats - {{ name }}"))
|
(text "My chats — {{ config.name }}"))
|
||||||
(text "{% endblock %} {% block body %}")
|
(text "{% endblock %} {% block body %}")
|
||||||
(div
|
(div
|
||||||
("class" "flex w_full gap_2 justify_between items_center")
|
("class" "flex w_full gap_2 justify_between items_center")
|
||||||
|
@ -9,9 +9,10 @@
|
||||||
(a
|
(a
|
||||||
("class" "button tab")
|
("class" "button tab")
|
||||||
("href" "/chats")
|
("href" "/chats")
|
||||||
(text "chats")))
|
(text "chats")
|
||||||
|
(text "{% if user.missed_messages_count > 0 -%}") (b (text "({{ user.missed_messages_count }})")) (text "{%- endif %}")))
|
||||||
(button
|
(button
|
||||||
("class" "button")
|
("class" "button square")
|
||||||
("title" "Create chat")
|
("title" "Create chat")
|
||||||
("onclick" "document.getElementById('create_dialog').showModal()")
|
("onclick" "document.getElementById('create_dialog').showModal()")
|
||||||
(text "{{ icon \"plus\" }}")))
|
(text "{{ icon \"plus\" }}")))
|
||||||
|
@ -21,7 +22,7 @@
|
||||||
(div
|
(div
|
||||||
("class" "card surface w_full flex justify_between items_center gap_2")
|
("class" "card surface w_full flex justify_between items_center gap_2")
|
||||||
(a
|
(a
|
||||||
("class" "flex gap_ch items_center")
|
("class" "flush flex gap_ch items_center {% if not user.id in chat[0].last_message_read_by -%} yellow {%- endif %}")
|
||||||
("href" "/chats/{{ chat[0].id }}")
|
("href" "/chats/{{ chat[0].id }}")
|
||||||
(text "{{ components::chat_name(chat=chat[0], members=chat[1], advanced=true) }}"))
|
(text "{{ components::chat_name(chat=chat[0], members=chat[1], advanced=true) }}"))
|
||||||
(div
|
(div
|
||||||
|
@ -29,7 +30,7 @@
|
||||||
(button
|
(button
|
||||||
("onclick" "open_dropdown(event)")
|
("onclick" "open_dropdown(event)")
|
||||||
("exclude" "dropdown")
|
("exclude" "dropdown")
|
||||||
("class" "button")
|
("class" "button icon_only big_icon")
|
||||||
(text "{{ icon \"ellipsis\" }}"))
|
(text "{{ icon \"ellipsis\" }}"))
|
||||||
(div
|
(div
|
||||||
("class" "inner")
|
("class" "inner")
|
||||||
|
@ -39,13 +40,10 @@
|
||||||
("target" "_blank")
|
("target" "_blank")
|
||||||
(text "pop open"))
|
(text "pop open"))
|
||||||
|
|
||||||
(text "{% if chat[0].style != \"Direct\" -%}")
|
(a
|
||||||
; group chat only
|
|
||||||
(button
|
|
||||||
("class" "button")
|
("class" "button")
|
||||||
("onclick" "rename_gc('{{ chat[0].id }}')")
|
("href" "/chats/{{ chat[0].id }}/manage")
|
||||||
(text "rename"))
|
(text "manage"))
|
||||||
(text "{%- endif %}")
|
|
||||||
|
|
||||||
(button
|
(button
|
||||||
("class" "button red")
|
("class" "button red")
|
||||||
|
@ -165,53 +163,7 @@
|
||||||
e.target.reset();
|
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);
|
|
||||||
});
|
|
||||||
}"))
|
}"))
|
||||||
|
|
||||||
|
(script ("src" "/public/messages.js"))
|
||||||
(text "{% endblock %}")
|
(text "{% endblock %}")
|
||||||
|
|
|
@ -55,6 +55,10 @@
|
||||||
(text "{{ icon \"ellipsis\" }}"))
|
(text "{{ icon \"ellipsis\" }}"))
|
||||||
(div
|
(div
|
||||||
("class" "inner surface")
|
("class" "inner surface")
|
||||||
|
(button
|
||||||
|
("class" "button surface")
|
||||||
|
("onclick" "edit_message_ui('{{ message.id }}')")
|
||||||
|
(text "edit"))
|
||||||
(button
|
(button
|
||||||
("class" "button surface red")
|
("class" "button surface red")
|
||||||
("onclick" "delete_message('{{ message.id }}')")
|
("onclick" "delete_message('{{ message.id }}')")
|
||||||
|
@ -63,6 +67,55 @@
|
||||||
|
|
||||||
(div
|
(div
|
||||||
("class" "body no_p_margin")
|
("class" "body no_p_margin")
|
||||||
|
("id" "{{ message.id }}_body")
|
||||||
(text "{{ message.content|markdown|safe }}"))
|
(text "{{ message.content|markdown|safe }}"))
|
||||||
(text "{{ self::avatar(id=message.owner) }}"))
|
(form
|
||||||
|
("class" "body hidden flex flex_row gap_ch")
|
||||||
|
("id" "{{ message.id }}_edit_area")
|
||||||
|
("onsubmit" "edit_message('{{ message.id }}', event)")
|
||||||
|
(textarea
|
||||||
|
("name" "content")
|
||||||
|
("required")
|
||||||
|
(text "{{ message.content|safe }}"))
|
||||||
|
(button
|
||||||
|
("title" "Save")
|
||||||
|
("class" "button")
|
||||||
|
(text "{{ icon \"check\" }}")))
|
||||||
|
|
||||||
|
(a
|
||||||
|
("href" "/@{{ message.owner }}?redirect=true")
|
||||||
|
("target" "_blank")
|
||||||
|
(text "{{ self::avatar(id=message.owner) }}")))
|
||||||
(text "{%- endmacro %}")
|
(text "{%- endmacro %}")
|
||||||
|
|
||||||
|
(text "{% macro theme(user, theme_preference) -%} {% if user %} {% if user.settings.theme_hue -%}")
|
||||||
|
(style
|
||||||
|
(text ":root, * {
|
||||||
|
--hue: {{ user.settings.theme_hue }} !important;
|
||||||
|
}"))
|
||||||
|
|
||||||
|
(text "{%- endif %} {% if user.settings.theme_sat -%}")
|
||||||
|
(style
|
||||||
|
(text ":root, * {
|
||||||
|
--sat: {{ user.settings.theme_sat }} !important;
|
||||||
|
}"))
|
||||||
|
|
||||||
|
(text "{%- endif %} {% if user.settings.theme_lit -%}")
|
||||||
|
(style
|
||||||
|
(text ":root, * {
|
||||||
|
--lit: {{ user.settings.theme_lit }} !important;
|
||||||
|
}"))
|
||||||
|
(text "{%- endif %}")
|
||||||
|
(div
|
||||||
|
("style" "display: none;")
|
||||||
|
(text "{{ self::theme_color(color=user.settings.theme_color_surface, css=\"color-surface\") }} {{ self::theme_color(color=user.settings.theme_color_text, css=\"color-text\") }} {{ self::theme_color(color=user.settings.theme_color_text_link, css=\"color-link\") }} {{ self::theme_color(color=user.settings.theme_color_lowered, css=\"color-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_text_lowered, css=\"color-text-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_super_lowered, css=\"color-super-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_raised, css=\"color-raised\") }} {{ self::theme_color(color=user.settings.theme_color_text_raised, css=\"color-text-raised\") }} {{ self::theme_color(color=user.settings.theme_color_super_raised, css=\"color-super-raised\") }} {{ self::theme_color(color=user.settings.theme_color_primary, css=\"color-primary\") }} {{ self::theme_color(color=user.settings.theme_color_text_primary, css=\"color-text-primary\") }} {{ self::theme_color(color=user.settings.theme_color_primary_lowered, css=\"color-primary-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_secondary, css=\"color-secondary\") }} {{ self::theme_color(color=user.settings.theme_color_text_secondary, css=\"color-text-secondary\") }} {{ self::theme_color(color=user.settings.theme_color_secondary_lowered, css=\"color-secondary-lowered\") }} {% if user.permissions|has_supporter -%}")
|
||||||
|
(style
|
||||||
|
(text "{{ user.settings.theme_custom_css|remove_script_tags|safe }}"))
|
||||||
|
(text "{%- endif %}"))
|
||||||
|
(text "{%- endif %} {%- endmacro %} {% macro theme_color(color, css) -%} {% if color -%}")
|
||||||
|
(style
|
||||||
|
(text ":root,
|
||||||
|
* {
|
||||||
|
--{{ css }}: {{ color|color }} !important;
|
||||||
|
}"))
|
||||||
|
(text "{%- endif %} {%- endmacro %}")
|
||||||
|
|
28
app/templates_src/confirm_dm.lisp
Normal file
28
app/templates_src/confirm_dm.lisp
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
(text "{% extends \"root.lisp\" %} {% block head %}")
|
||||||
|
(title
|
||||||
|
(text "Message {{ profile.username }}? — {{ config.name }}"))
|
||||||
|
(text "{% endblock %} {% block body %}")
|
||||||
|
(div
|
||||||
|
("class" "card flex flex_col gap_ch")
|
||||||
|
(p (text "Are you sure you would like to direct message ") (a ("href" "/@{{ profile.username }}") (text "{{ profile.username }}")) (text "?"))
|
||||||
|
(div
|
||||||
|
("class" "flex gap_2")
|
||||||
|
(text "{% if user -%}")
|
||||||
|
(button
|
||||||
|
("class" "button surface green")
|
||||||
|
("onclick" "create_direct_chat_with_user('{{ profile.username }}')")
|
||||||
|
(text "{{ icon \"arrow-right\" }} Continue"))
|
||||||
|
(text "{% else %}")
|
||||||
|
(a
|
||||||
|
("class" "button surface green")
|
||||||
|
("href" "/login?redirect=/@{{ profile.username }}/confirm_dm")
|
||||||
|
(text "{{ icon \"arrow-right\" }} Sign in to message"))
|
||||||
|
(text "{%- endif %}")
|
||||||
|
|
||||||
|
(a
|
||||||
|
("class" "button surface red")
|
||||||
|
("href" "/")
|
||||||
|
(text "{{ icon \"x\" }} Cancel"))))
|
||||||
|
|
||||||
|
(script ("src" "/public/messages.js"))
|
||||||
|
(text "{% endblock %}")
|
|
@ -1,6 +1,6 @@
|
||||||
(text "{% extends \"root.lisp\" %} {% block head %}")
|
(text "{% extends \"root.lisp\" %} {% block head %}")
|
||||||
(title
|
(title
|
||||||
(text "Error - {{ name }}"))
|
(text "Error — {{ config.name }}"))
|
||||||
(link ("rel" "icon") ("href" "/public/favicon.svg"))
|
(link ("rel" "icon") ("href" "/public/favicon.svg"))
|
||||||
(text "{% endblock %} {% block body %}")
|
(text "{% endblock %} {% block body %}")
|
||||||
(div
|
(div
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
(text "{% extends \"root.lisp\" %} {% block head %}")
|
(text "{% extends \"root.lisp\" %} {% block head %}")
|
||||||
(title
|
(text "{% if user -%}")
|
||||||
(text "{{ name }}"))
|
(meta ("http-equiv" "refresh") ("content" "0; /chats"))
|
||||||
|
(text "{% else %}")
|
||||||
|
(meta ("http-equiv" "refresh") ("content" "0; {{ config.service_hosts.tetratto|safe }}"))
|
||||||
|
(text "{%- endif %}")
|
||||||
(text "{% endblock %} {% block body %}")
|
(text "{% endblock %} {% block body %}")
|
||||||
(div
|
(div
|
||||||
("class" "card")
|
("class" "card")
|
||||||
(h1 (text "{{ name }}")))
|
(a ("href" "{{ config.service_hosts.tetratto }}") (text "Sending you elsewhere...")))
|
||||||
(text "{% endblock %}")
|
(text "{% endblock %}")
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
(text "{% extends \"root.lisp\" %} {% block head %}")
|
(text "{% extends \"root.lisp\" %} {% block head %}")
|
||||||
(title
|
(title
|
||||||
(text "Login — {{ name }}"))
|
(text "Login — {{ config.name }}"))
|
||||||
(text "{% endblock %} {% block body %}")
|
(text "{% endblock %} {% block body %}")
|
||||||
(div
|
(div
|
||||||
("class" "card container small")
|
("class" "card container small")
|
||||||
(h4 (text "Login with Tetratto"))
|
(h4 ("class" "text_center w_full") (text "Log in with Tetratto"))
|
||||||
|
|
||||||
(form
|
(form
|
||||||
("class" "flex flex_col gap_4")
|
("class" "flex flex_col gap_4")
|
||||||
|
@ -63,6 +63,7 @@
|
||||||
document.getElementById(`flow_${flow_page}`).style.display = \"contents\";
|
document.getElementById(`flow_${flow_page}`).style.display = \"contents\";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const search = new URLSearchParams(window.location.search);
|
||||||
async function login(e) {
|
async function login(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -101,7 +102,11 @@
|
||||||
|
|
||||||
// redirect
|
// redirect
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if (search.get(\"redirect\") && (search.get(\"redirect\").startsWith(window.location.origin) || search.get(\"redirect\").startsWith(\"/\"))) {
|
||||||
|
window.location.href = search.get(\"redirect\");
|
||||||
|
} else {
|
||||||
window.location.href = \"/chats\";
|
window.location.href = \"/chats\";
|
||||||
|
}
|
||||||
}, 150);
|
}, 150);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
117
app/templates_src/manage.lisp
Normal file
117
app/templates_src/manage.lisp
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
(text "{% extends \"root.lisp\" %} {% block head %}")
|
||||||
|
(title
|
||||||
|
(text "Manage {{ components::chat_name(chat=chat, members=members) }} — {{ config.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 camo")
|
||||||
|
("href" "/chats/{{ chat.id }}")
|
||||||
|
(text "{{ components::chat_name(chat=chat, members=members, advanced=true, avatar_size=\"18px\") }}"))
|
||||||
|
(a
|
||||||
|
("class" "button tab")
|
||||||
|
("href" "/chats/{{ chat.id }}/manage")
|
||||||
|
(text "{{ icon \"settings-2\" }} Manage"))))
|
||||||
|
(div
|
||||||
|
("class" "flex flex_col gap_4 card")
|
||||||
|
("style" "flex: 1 0 auto")
|
||||||
|
(text "{% if chat.style != \"Direct\" -%}")
|
||||||
|
; gc only
|
||||||
|
(button
|
||||||
|
("class" "button surface")
|
||||||
|
("onclick" "rename_chat('{{ chat.id }}', GC_INFO)")
|
||||||
|
(text "{{ icon \"pencil\" }} rename chat"))
|
||||||
|
|
||||||
|
(script
|
||||||
|
("type" "application/json")
|
||||||
|
("id" "gc_info")
|
||||||
|
(text "{{ chat.style.Group|json_encode() }}"))
|
||||||
|
(script
|
||||||
|
(text "globalThis.GC_INFO = JSON.parse(document.getElementById(\"gc_info\").innerHTML)"))
|
||||||
|
(text "{%- endif %}")
|
||||||
|
|
||||||
|
(ul
|
||||||
|
(li (b (text "Chat name: ")) (span (text "{{ components::chat_name(chat=chat, members=members) }}")))
|
||||||
|
(li (b (text "Chat created: ")) (span (text "{{ chat.created / 1000|int|date(format=\"%Y-%m-%d %H:%M\", timezone=\"Etc/UTC\") }} UTC")))
|
||||||
|
(li (b (text "Last message: ")) (span (text "{{ chat.last_message_created / 1000|int|date(format=\"%Y-%m-%d %H:%M\", timezone=\"Etc/UTC\") }} UTC")))
|
||||||
|
(li
|
||||||
|
(div
|
||||||
|
("class" "flex items_center gap_ch")
|
||||||
|
(b (text "Owner:"))
|
||||||
|
(a
|
||||||
|
("class" "flex items_center gap_1 yellow")
|
||||||
|
("href" "{{ config.service_hosts.tetratto }}/@{{ members[0].username }}")
|
||||||
|
(text "{{ icon \"crown\" }} {{ components::username(user=members[0]) }}")))))
|
||||||
|
|
||||||
|
(hr)
|
||||||
|
(div
|
||||||
|
("class" "flex w_full justify_between items_center gap_2")
|
||||||
|
(h4 (text "Members") ("style" "margin: 0"))
|
||||||
|
(div
|
||||||
|
("class" "flex gap_2")
|
||||||
|
(text "{% if chat.style != \"Direct\" -%}")
|
||||||
|
; gc only
|
||||||
|
(button ("class" "green button surface") ("onclick" "document.getElementById('add_user_dialog').showModal()") (text "add"))
|
||||||
|
(text "{%- endif %}")
|
||||||
|
|
||||||
|
(button ("class" "red button surface") ("onclick" "leave_chat('{{ chat.id }}')") (text "leave"))))
|
||||||
|
|
||||||
|
(div
|
||||||
|
("class" "flex flex_col gap_2")
|
||||||
|
(text "{% for member in members -%}")
|
||||||
|
(div
|
||||||
|
("class" "card surface w_full flex flex_col gap_ch")
|
||||||
|
(a
|
||||||
|
("class" "flush flex items_center gap_ch")
|
||||||
|
("href" "/@{{ member.username }}")
|
||||||
|
(text "{{ components::avatar(id=member.id) }}")
|
||||||
|
(text "{{ components::username(user=member) }}"))
|
||||||
|
(span (text "{% if member.settings.status|length > 0 -%} {{ member.settings.status|markdown|safe }} {%- else -%} No status {%- endif %}"))
|
||||||
|
(text "{% if is_owner -%}")
|
||||||
|
(button
|
||||||
|
("class" "red button")
|
||||||
|
("onclick" "remove_member_from_chat('{{ chat.id }}', '{{ member.id }}')")
|
||||||
|
(text "remove"))
|
||||||
|
(text "{%- endif %}"))
|
||||||
|
(text "{%- endfor %}")))
|
||||||
|
|
||||||
|
(dialog
|
||||||
|
("id" "add_user_dialog")
|
||||||
|
(form
|
||||||
|
("class" "inner")
|
||||||
|
("onsubmit" "add_member_to_chat(event, '{{ chat.id }}')")
|
||||||
|
(h2
|
||||||
|
("class" "text_center w_full")
|
||||||
|
(text "Add user"))
|
||||||
|
|
||||||
|
(input
|
||||||
|
("type" "text")
|
||||||
|
("list" "users_search")
|
||||||
|
("name" "username")
|
||||||
|
("id" "username")
|
||||||
|
("placeholder" "username")
|
||||||
|
("oninput" "search_users(event)"))
|
||||||
|
|
||||||
|
(hr ("class" "margin"))
|
||||||
|
|
||||||
|
(div
|
||||||
|
("class" "flex gap_2 justify_between")
|
||||||
|
(button
|
||||||
|
("onclick" "document.getElementById('add_user_dialog').close()")
|
||||||
|
("class" "button red")
|
||||||
|
("type" "button")
|
||||||
|
(text "Cancel"))
|
||||||
|
|
||||||
|
(button
|
||||||
|
("class" "button green")
|
||||||
|
(text "Add")))))
|
||||||
|
(datalist ("id" "users_search"))
|
||||||
|
|
||||||
|
(script ("src" "/public/messages.js"))
|
||||||
|
(text "{% endblock %}")
|
157
app/templates_src/profile.lisp
Normal file
157
app/templates_src/profile.lisp
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
(text "{% extends \"root.lisp\" %} {% block head %}")
|
||||||
|
(title
|
||||||
|
(text "{{ profile.username }} — {{ config.name }}"))
|
||||||
|
|
||||||
|
(meta
|
||||||
|
("name" "og:title")
|
||||||
|
("content" "{{ profile.username }}"))
|
||||||
|
|
||||||
|
(meta
|
||||||
|
("name" "description")
|
||||||
|
("content" "Message @{{ profile.username }} on {{ config.name }}!"))
|
||||||
|
|
||||||
|
(meta
|
||||||
|
("name" "og:description")
|
||||||
|
("content" "Message @{{ profile.username }} on {{ config.name }}!"))
|
||||||
|
|
||||||
|
(meta
|
||||||
|
("property" "og:type")
|
||||||
|
("content" "profile"))
|
||||||
|
|
||||||
|
(meta
|
||||||
|
("property" "profile:username")
|
||||||
|
("content" "{{ profile.username }}"))
|
||||||
|
|
||||||
|
(meta
|
||||||
|
("name" "og:image")
|
||||||
|
("content" "{{ config.service_hosts.buckets|safe }}/avatars/{{ profile.id }}"))
|
||||||
|
|
||||||
|
(meta
|
||||||
|
("name" "twitter:image")
|
||||||
|
("content" "{{ config.service_hosts.buckets|safe }}/avatars/{{ profile.id }}"))
|
||||||
|
|
||||||
|
(meta
|
||||||
|
("name" "twitter:card")
|
||||||
|
("content" "summary"))
|
||||||
|
|
||||||
|
(meta
|
||||||
|
("name" "twitter:title")
|
||||||
|
("content" "{{ profile.username }}"))
|
||||||
|
|
||||||
|
(meta
|
||||||
|
("name" "twitter:description")
|
||||||
|
("content" "Message @{{ profile.username }} on {{ config.name }}!"))
|
||||||
|
(text "{% endblock %} {% block body %}")
|
||||||
|
(text "{% if not use_user_theme -%} {{ components::theme(user=profile, theme_preference=profile.settings.profile_theme) }} {%- endif %}")
|
||||||
|
|
||||||
|
(text "{% if profile.settings.profile_theme -%}")
|
||||||
|
(script
|
||||||
|
(text "setTimeout(() => {
|
||||||
|
temporary_set_theme(\"{{ profile.settings.profile_theme }}\");
|
||||||
|
}, 150);"))
|
||||||
|
(text "{%- endif %}")
|
||||||
|
|
||||||
|
(div
|
||||||
|
("class" "flex flex_col gap_ch justify_center items_center profile")
|
||||||
|
("style" "height: calc(100dvh - var(--nav-height))")
|
||||||
|
(div
|
||||||
|
("class" "card_nest w_full")
|
||||||
|
("style" "max-width: 25rem")
|
||||||
|
(div
|
||||||
|
("class" "card banner"))
|
||||||
|
(div
|
||||||
|
("class" "card flex flex_col gap_ch")
|
||||||
|
(text "{{ components::avatar(id=profile.id, size=\"160px\") }}")
|
||||||
|
(div
|
||||||
|
("class" "w_full flex items_center justify_between gap_2")
|
||||||
|
(h2 (text "{{ components::username(user=profile) }}"))
|
||||||
|
(button
|
||||||
|
("onclick" "document.getElementById('user_info').showModal()")
|
||||||
|
("class" "button icon_only big_icon")
|
||||||
|
("title" "User info")
|
||||||
|
(text "{{ icon \"info\" }}")))
|
||||||
|
|
||||||
|
(div (text "{{ profile.settings.biography|markdown|safe }}"))
|
||||||
|
|
||||||
|
; user links
|
||||||
|
(div
|
||||||
|
("class" "flex flex_col gap_2")
|
||||||
|
("style" "margin-top: 20px")
|
||||||
|
(text "{% for link in profile.settings.links -%}")
|
||||||
|
(a
|
||||||
|
("class" "button big surface justify_start")
|
||||||
|
("href" "{{ link[1] }}")
|
||||||
|
(text "{{ icon \"link\" }} {{ link[0] }}"))
|
||||||
|
(text "{%- endfor %}"))
|
||||||
|
|
||||||
|
; big action links
|
||||||
|
(div
|
||||||
|
("class" "flex flex_row gap_2")
|
||||||
|
(a
|
||||||
|
("class" "button big surface")
|
||||||
|
("href" "{{ config.service_hosts.tetratto }}/@{{ profile.username }}")
|
||||||
|
(text "{{ icon \"external-link\" }} Full profile"))
|
||||||
|
|
||||||
|
(text "{% if user -%} {% if profile.id != user.id -%}")
|
||||||
|
(button
|
||||||
|
("class" "button big surface")
|
||||||
|
("onclick" "create_direct_chat_with_user('{{ profile.username }}')")
|
||||||
|
(text "{{ icon \"message-circle\" }} Message"))
|
||||||
|
(text "{%- endif %} {% else %}")
|
||||||
|
(a
|
||||||
|
("class" "button big surface")
|
||||||
|
("href" "/login?redirect=/@{{ profile.username }}/confirm_dm")
|
||||||
|
(text "{{ icon \"message-circle\" }} Message"))
|
||||||
|
(text "{%- endif %}")))))
|
||||||
|
|
||||||
|
(dialog
|
||||||
|
("id" "user_info")
|
||||||
|
(div
|
||||||
|
("class" "inner")
|
||||||
|
(h2
|
||||||
|
("class" "text_center w_full")
|
||||||
|
(text "User info"))
|
||||||
|
|
||||||
|
(ul
|
||||||
|
(li (b (text "Username: ")) (text "{{ profile.username }}"))
|
||||||
|
(li (b (text "Joined: ")) (text "{{ profile.created / 1000|int|date(format=\"%Y-%m-%d %H:%M\", timezone=\"Etc/UTC\") }} UTC"))
|
||||||
|
(li (b (text "Followers: ")) (a ("href" "{{ config.service_hosts.tetratto }}/@{{ profile.username }}/followers") (text "{{ profile.follower_count }}")))
|
||||||
|
(li (b (text "Following: ")) (a ("href" "{{ config.service_hosts.tetratto }}/@{{ profile.username }}/following") (text "{{ profile.following_count }}")))
|
||||||
|
(li (b (text "Posts: ")) (a ("href" "{{ config.service_hosts.tetratto }}/@{{ profile.username }}") (text "{{ profile.post_count }}"))))
|
||||||
|
|
||||||
|
(hr ("class" "margin"))
|
||||||
|
(div
|
||||||
|
("class" "flex gap_2 justify_right")
|
||||||
|
(button
|
||||||
|
("onclick" "document.getElementById('user_info').close()")
|
||||||
|
("class" "button red")
|
||||||
|
("type" "button")
|
||||||
|
(text "Close")))))
|
||||||
|
|
||||||
|
(style
|
||||||
|
(text ".profile .avatar {
|
||||||
|
margin: -120px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .banner {
|
||||||
|
background: url(\"{{ config.service_hosts.buckets }}/banners/{{ profile.id }}\") no-repeat center !important;
|
||||||
|
background-size: cover !important;
|
||||||
|
border-radius: var(--radius) var(--radius) 0 0;
|
||||||
|
height: 225px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card_nest .card:nth-child(2) {
|
||||||
|
border-radius: 0 0 var(--radius) var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
.card_nest .card {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}"))
|
||||||
|
(script ("src" "/public/messages.js"))
|
||||||
|
(text "{% endblock %}")
|
|
@ -12,10 +12,10 @@
|
||||||
|
|
||||||
(meta ("name" "theme-color") ("content" "{{ theme_color }}"))
|
(meta ("name" "theme-color") ("content" "{{ theme_color }}"))
|
||||||
(meta ("property" "og:type") ("content" "website"))
|
(meta ("property" "og:type") ("content" "website"))
|
||||||
(meta ("property" "og:site_name") ("content" "{{ name }}"))
|
(meta ("property" "og:site_name") ("content" "{{ config.name }}"))
|
||||||
|
|
||||||
(meta ("property" "og:title") ("content" "{{ name }}"))
|
(meta ("property" "og:title") ("content" "{{ config.name }}"))
|
||||||
(meta ("property" "twitter:title") ("content" "{{ name }}"))
|
(meta ("property" "twitter:title") ("content" "{{ config.name }}"))
|
||||||
(link ("rel" "icon") ("href" "/public/favicon.svg"))
|
(link ("rel" "icon") ("href" "/public/favicon.svg"))
|
||||||
|
|
||||||
(script ("src" "/public/app.js?v={{ build_code }}") ("defer"))
|
(script ("src" "/public/app.js?v={{ build_code }}") ("defer"))
|
||||||
|
@ -24,6 +24,9 @@
|
||||||
(text "{% block head %}{% endblock %}"))
|
(text "{% block head %}{% endblock %}"))
|
||||||
|
|
||||||
(body
|
(body
|
||||||
|
("class" "{% if user and user.settings.use_system_font -%} use_system_font {%- endif %}")
|
||||||
|
(text "{% if user and use_user_theme -%} {{ components::theme(user=user, theme_preference=user.settings.theme_preference) }} {%- endif %}")
|
||||||
|
|
||||||
; nav
|
; nav
|
||||||
(nav
|
(nav
|
||||||
("class" "flex w_full justify_between gap_2")
|
("class" "flex w_full justify_between gap_2")
|
||||||
|
@ -44,7 +47,7 @@
|
||||||
("class" "inner left")
|
("class" "inner left")
|
||||||
(a
|
(a
|
||||||
("class" "button")
|
("class" "button")
|
||||||
("href" "/")
|
("href" "{% if user -%} /chats {%- else -%} / {%- endif %}")
|
||||||
(text "home"))
|
(text "home"))
|
||||||
(a
|
(a
|
||||||
("class" "button")
|
("class" "button")
|
||||||
|
@ -61,10 +64,6 @@
|
||||||
("href" "{{ config.service_hosts.tetratto }}/auth/register")
|
("href" "{{ config.service_hosts.tetratto }}/auth/register")
|
||||||
(text "sign up"))
|
(text "sign up"))
|
||||||
(text "{%- else -%}")
|
(text "{%- else -%}")
|
||||||
(a
|
|
||||||
("class" "button")
|
|
||||||
("href" "/chats")
|
|
||||||
(text "my chats"))
|
|
||||||
(a
|
(a
|
||||||
("class" "button")
|
("class" "button")
|
||||||
("href" "{{ config.service_hosts.tetratto }}/settings")
|
("href" "{{ config.service_hosts.tetratto }}/settings")
|
||||||
|
@ -74,7 +73,17 @@
|
||||||
("onclick" "user_logout()")
|
("onclick" "user_logout()")
|
||||||
(text "logout"))
|
(text "logout"))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
(text "{% block dropdown %}{% endblock %}"))))
|
(text "{% block dropdown %}{% endblock %}")))
|
||||||
|
(text "{% if user -%}")
|
||||||
|
(a
|
||||||
|
("href" "{{ config.service_hosts.tetratto }}/notifs")
|
||||||
|
("class" "button camo fade")
|
||||||
|
(text "{% if user.notification_count > 0 -%}")
|
||||||
|
(span ("class" "red") (text "{{ icon \"bell-dot\" }}"))
|
||||||
|
(text "{% else %}")
|
||||||
|
(text "{{ icon \"bell\" }}")
|
||||||
|
(text "{%- endif %}"))
|
||||||
|
(text "{%- endif %}"))
|
||||||
|
|
||||||
(div
|
(div
|
||||||
("class" "side flex")
|
("class" "side flex")
|
||||||
|
|
|
@ -26,6 +26,8 @@ pub struct Config {
|
||||||
/// buckets config.
|
/// buckets config.
|
||||||
#[serde(default = "default_uploads_dir")]
|
#[serde(default = "default_uploads_dir")]
|
||||||
pub uploads_dir: String,
|
pub uploads_dir: String,
|
||||||
|
/// The host of this service. Required for notifications.
|
||||||
|
pub host: String,
|
||||||
/// Database configuration.
|
/// Database configuration.
|
||||||
#[serde(default = "default_database")]
|
#[serde(default = "default_database")]
|
||||||
pub database: DatabaseConfig,
|
pub database: DatabaseConfig,
|
||||||
|
@ -72,6 +74,7 @@ impl Default for Config {
|
||||||
real_ip_header: default_real_ip_header(),
|
real_ip_header: default_real_ip_header(),
|
||||||
service_hosts: default_service_hosts(),
|
service_hosts: default_service_hosts(),
|
||||||
uploads_dir: default_uploads_dir(),
|
uploads_dir: default_uploads_dir(),
|
||||||
|
host: String::new(),
|
||||||
database: default_database(),
|
database: default_database(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::DataManager;
|
use super::DataManager;
|
||||||
use crate::model::{Chat, ChatStyle};
|
use crate::model::{Chat, ChatStyle};
|
||||||
use oiseau::{PostgresRow, cache::Cache, execute, get, params, query_rows};
|
use oiseau::{PostgresRow, cache::Cache, execute, get, params, query_row, query_rows};
|
||||||
use tetratto_core::{
|
use tetratto_core::{
|
||||||
auto_method,
|
auto_method,
|
||||||
model::{Error, Result, auth::User},
|
model::{Error, Result, auth::User},
|
||||||
|
@ -44,6 +44,30 @@ impl DataManager {
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the direct message chat between the two given members.
|
||||||
|
pub async fn get_direct_chat_by_members(&self, u1: usize, u2: usize) -> Result<Chat> {
|
||||||
|
let conn = match self.0.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = query_row!(
|
||||||
|
&conn,
|
||||||
|
"SELECT * FROM t_chats WHERE members = $1 OR members = $2",
|
||||||
|
params![
|
||||||
|
&serde_json::to_string(&[u1, u2]).unwrap(),
|
||||||
|
&serde_json::to_string(&[u2, u1]).unwrap(),
|
||||||
|
],
|
||||||
|
|x| { Ok(Self::get_chat_from_row(x)) }
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
/// Get all chats the given member is participating in.
|
/// Get all chats the given member is participating in.
|
||||||
pub async fn get_chats_by_member(
|
pub async fn get_chats_by_member(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -232,7 +232,7 @@ impl DataManager {
|
||||||
message.chat,
|
message.chat,
|
||||||
SocketMessage {
|
SocketMessage {
|
||||||
method: SocketMethod::MessageUpdate,
|
method: SocketMethod::MessageUpdate,
|
||||||
body: serde_json::to_string(&(message.id, content)).unwrap(),
|
body: serde_json::to_string(&(message.id.to_string(), content)).unwrap(),
|
||||||
}
|
}
|
||||||
.to_string(),
|
.to_string(),
|
||||||
) {
|
) {
|
||||||
|
|
16
src/main.rs
16
src/main.rs
|
@ -5,6 +5,7 @@ mod macros;
|
||||||
mod markdown;
|
mod markdown;
|
||||||
mod model;
|
mod model;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
mod sanitize;
|
||||||
|
|
||||||
use crate::database::DataManager;
|
use crate::database::DataManager;
|
||||||
use axum::{Extension, Router};
|
use axum::{Extension, Router};
|
||||||
|
@ -12,7 +13,7 @@ use config::Config;
|
||||||
use nanoneo::core::element::Render;
|
use nanoneo::core::element::Render;
|
||||||
use std::{collections::HashMap, env::var, net::SocketAddr, process::exit, sync::Arc};
|
use std::{collections::HashMap, env::var, net::SocketAddr, process::exit, sync::Arc};
|
||||||
use tera::{Tera, Value};
|
use tera::{Tera, Value};
|
||||||
use tetratto_core::html;
|
use tetratto_core::{html, model::permissions::FinePermission};
|
||||||
use tetratto_shared::hash::salt;
|
use tetratto_shared::hash::salt;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tower_http::{
|
use tower_http::{
|
||||||
|
@ -31,6 +32,17 @@ fn render_markdown(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Va
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn color_escape(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
||||||
|
Ok(sanitize::color_escape(value.as_str().unwrap()).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_supporter(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
||||||
|
Ok(FinePermission::from_bits(value.as_u64().unwrap() as u32)
|
||||||
|
.unwrap()
|
||||||
|
.check(FinePermission::SUPPORTER)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
|
||||||
fn remove_script_tags(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
fn remove_script_tags(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
||||||
Ok(value
|
Ok(value
|
||||||
.as_str()
|
.as_str()
|
||||||
|
@ -103,6 +115,8 @@ async fn main() {
|
||||||
|
|
||||||
tera.register_filter("markdown", render_markdown);
|
tera.register_filter("markdown", render_markdown);
|
||||||
tera.register_filter("remove_script_tags", remove_script_tags);
|
tera.register_filter("remove_script_tags", remove_script_tags);
|
||||||
|
tera.register_filter("color", color_escape);
|
||||||
|
tera.register_filter("has_supporter", check_supporter);
|
||||||
|
|
||||||
// create app
|
// create app
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
|
|
|
@ -46,6 +46,15 @@ impl Chat {
|
||||||
last_message_read_by: Vec::new(),
|
last_message_read_by: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the chat's owner.
|
||||||
|
pub fn owner(&self) -> usize {
|
||||||
|
if self.style == ChatStyle::Direct {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
*self.members.first().unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
|
|
@ -17,7 +17,10 @@ use futures_util::{sink::SinkExt, stream::StreamExt};
|
||||||
use oiseau::cache::{Cache, redis::Commands};
|
use oiseau::cache::{Cache, redis::Commands};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tetratto_core::model::{ApiReturn, Error, auth::User};
|
use tetratto_core::model::{
|
||||||
|
ApiReturn, Error,
|
||||||
|
auth::{Notification, User},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct CreateChat {
|
pub struct CreateChat {
|
||||||
|
@ -28,20 +31,42 @@ pub struct CreateChat {
|
||||||
pub async fn create_request(
|
pub async fn create_request(
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
Extension(data): Extension<State>,
|
Extension(data): Extension<State>,
|
||||||
Json(req): Json<CreateChat>,
|
Json(mut req): Json<CreateChat>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let data = &(data.read().await).0;
|
let data = &(data.read().await).0;
|
||||||
let user = match get_user_from_token!(jar, data.2) {
|
let user = match get_user_from_token!(jar, data.2) {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => return Json(Error::NotAllowed.into()),
|
None => return Json(Error::NotAllowed.into()),
|
||||||
};
|
};
|
||||||
|
req.members.dedup();
|
||||||
|
|
||||||
if req.members.len() > 2 && req.style == ChatStyle::Direct {
|
if (req.members.len() > 2 && req.style == ChatStyle::Direct)
|
||||||
|
| (req.members.len() > 10 && req.style != ChatStyle::Direct)
|
||||||
|
{
|
||||||
return Json(Error::DataTooLong("members list".to_string()).into());
|
return Json(Error::DataTooLong("members list".to_string()).into());
|
||||||
} else if req.members.len() < 1 {
|
} else if req.members.len() < 1 {
|
||||||
return Json(Error::DataTooShort("members list".to_string()).into());
|
return Json(Error::DataTooShort("members list".to_string()).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.style == ChatStyle::Direct
|
||||||
|
&& let Ok(chat) = data
|
||||||
|
.get_direct_chat_by_members(
|
||||||
|
user.id,
|
||||||
|
match data.2.get_user_by_username(&req.members[0]).await {
|
||||||
|
Ok(x) => x.id,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
// if we already have a direct chat with this person, don't create a new one
|
||||||
|
return Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Success".to_string(),
|
||||||
|
payload: chat.id.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
match data
|
match data
|
||||||
.create_chat(Chat::new(req.style, {
|
.create_chat(Chat::new(req.style, {
|
||||||
let mut x = Vec::new();
|
let mut x = Vec::new();
|
||||||
|
@ -49,7 +74,19 @@ pub async fn create_request(
|
||||||
x.push(user.id);
|
x.push(user.id);
|
||||||
for y in req.members {
|
for y in req.members {
|
||||||
x.push(match data.2.get_user_by_username(&y).await {
|
x.push(match data.2.get_user_by_username(&y).await {
|
||||||
Ok(x) => x.id,
|
Ok(x) => {
|
||||||
|
if x.settings.private_chats
|
||||||
|
&& data
|
||||||
|
.2
|
||||||
|
.get_userfollow_by_initiator_receiver(x.id, user.id)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
x.id
|
||||||
|
}
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => return Json(e.into()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -114,6 +151,119 @@ pub async fn leave_request(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn add_member_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path((id, username)): Path<(usize, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data.2) {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// get other user and check if we're blocked
|
||||||
|
let other_user = match data.2.get_user_by_username(&username).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if data
|
||||||
|
.2
|
||||||
|
.get_userblock_by_initiator_receiver(other_user.id, user.id)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return Json(Error::NotAllowed.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if other_user.settings.private_chats
|
||||||
|
&& data
|
||||||
|
.2
|
||||||
|
.get_userfollow_by_initiator_receiver(other_user.id, user.id)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return Json(Error::NotAllowed.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
let mut chat = match data.get_chat_by_id(id).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if chat.style == ChatStyle::Direct || chat.members.contains(&other_user.id) {
|
||||||
|
return Json(Error::NotAllowed.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if chat.style != ChatStyle::Direct && chat.members.len() > 10 {
|
||||||
|
// can only have a maximum of 10 members in one chat
|
||||||
|
return Json(Error::DataTooLong("members list".to_string()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
chat.members.push(other_user.id);
|
||||||
|
match data.update_chat_members(chat.id, chat.members).await {
|
||||||
|
Ok(_) => {
|
||||||
|
if let Err(e) = data
|
||||||
|
.2
|
||||||
|
.create_notification(Notification::new(
|
||||||
|
"You've been added to a chat!".to_string(),
|
||||||
|
format!(
|
||||||
|
"You were added to a [chat]({}/chats/{}) by [@{}](/api/v1/auth/user/find/{})",
|
||||||
|
data.0.0.host, chat.id, user.username, user.id
|
||||||
|
),
|
||||||
|
other_user.id,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Json(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Success".to_string(),
|
||||||
|
payload: (),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_member_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path((id, uid)): Path<(usize, usize)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data.2) {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut chat = match data.get_chat_by_id(id).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if chat.style == ChatStyle::Direct || !chat.members.contains(&uid) || !(chat.owner() == user.id)
|
||||||
|
{
|
||||||
|
return Json(Error::NotAllowed.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
chat.members
|
||||||
|
.remove(chat.members.iter().position(|x| *x == uid).unwrap());
|
||||||
|
|
||||||
|
match data.update_chat_members(chat.id, chat.members).await {
|
||||||
|
Ok(_) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Success".to_string(),
|
||||||
|
payload: (),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct UpdateChatInfo {
|
pub struct UpdateChatInfo {
|
||||||
pub info: GroupChatInfo,
|
pub info: GroupChatInfo,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{State, get_user_from_token, model::Message};
|
use crate::{State, get_user_from_token, markdown::render_markdown, model::Message};
|
||||||
use axum::{Extension, Json, body::Bytes, extract::Path, response::IntoResponse};
|
use axum::{Extension, Json, body::Bytes, extract::Path, response::IntoResponse};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
use axum_image::{encode::save_webp_buffer, extract::JsonMultipart};
|
use axum_image::{encode::save_webp_buffer, extract::JsonMultipart};
|
||||||
|
@ -87,6 +87,17 @@ pub async fn create_request(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update users
|
||||||
|
for member in chat.members {
|
||||||
|
if member == user.id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = data.2.incr_user_missed_messages(member).await {
|
||||||
|
return Json(e.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
Json(ApiReturn {
|
Json(ApiReturn {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
@ -145,3 +156,7 @@ pub async fn update_content_request(
|
||||||
Err(e) => Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn render_markdown_request(Json(req): Json<UpdateMessageContent>) -> impl IntoResponse {
|
||||||
|
render_markdown(&req.content)
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ use axum::routing::{Router, delete, get, post, put};
|
||||||
|
|
||||||
pub fn routes() -> Router {
|
pub fn routes() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
.route("/markdown", post(messages::render_markdown_request))
|
||||||
// auth
|
// auth
|
||||||
.route("/auth/login", post(auth::login_request))
|
.route("/auth/login", post(auth::login_request))
|
||||||
.route("/auth/logout", post(auth::logout_request))
|
.route("/auth/logout", post(auth::logout_request))
|
||||||
|
@ -18,6 +19,14 @@ pub fn routes() -> Router {
|
||||||
// chats
|
// chats
|
||||||
.route("/chats", post(chats::create_request))
|
.route("/chats", post(chats::create_request))
|
||||||
.route("/chats/{id}/leave", post(chats::leave_request))
|
.route("/chats/{id}/leave", post(chats::leave_request))
|
||||||
|
.route(
|
||||||
|
"/chats/{id}/members/add/{username}",
|
||||||
|
post(chats::add_member_request),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/chats/{id}/members/remove/{uid}",
|
||||||
|
post(chats::remove_member_request),
|
||||||
|
)
|
||||||
.route("/chats/{id}/info", post(chats::update_info_request))
|
.route("/chats/{id}/info", post(chats::update_info_request))
|
||||||
.route("/chats/{id}/_connect", get(chats::subscription_handler))
|
.route("/chats/{id}/_connect", get(chats::subscription_handler))
|
||||||
.route(
|
.route(
|
||||||
|
|
|
@ -19,10 +19,10 @@ pub fn routes() -> Router {
|
||||||
|
|
||||||
pub fn default_context(config: &Config, build_code: &str, user: &Option<User>) -> Context {
|
pub fn default_context(config: &Config, build_code: &str, user: &Option<User>) -> Context {
|
||||||
let mut ctx = Context::new();
|
let mut ctx = Context::new();
|
||||||
ctx.insert("name", &config.name);
|
|
||||||
ctx.insert("theme_color", &config.theme_color);
|
ctx.insert("theme_color", &config.theme_color);
|
||||||
ctx.insert("build_code", &build_code);
|
ctx.insert("build_code", &build_code);
|
||||||
ctx.insert("user", &user);
|
ctx.insert("user", &user);
|
||||||
ctx.insert("config", &config);
|
ctx.insert("config", &config);
|
||||||
|
ctx.insert("use_user_theme", &true);
|
||||||
ctx
|
ctx
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,10 @@ pub async fn list_request(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Err(e) = data.2.update_user_missed_messages_count(user.id, 0).await {
|
||||||
|
return Err(render_error(e, tera, data.0.0.clone(), Some(user)).await);
|
||||||
|
}
|
||||||
|
|
||||||
let chats = match data.get_chats_by_member(user.id, 12, props.page).await {
|
let chats = match data.get_chats_by_member(user.id, 12, props.page).await {
|
||||||
Ok(x) => data.fill_chats(x).await,
|
Ok(x) => data.fill_chats(x).await,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
@ -182,3 +186,42 @@ pub async fn read_receipt_request(
|
||||||
ctx.insert("chat", &chat);
|
ctx.insert("chat", &chat);
|
||||||
Ok(Html(tera.render("read_receipt.lisp", &ctx).unwrap()))
|
Ok(Html(tera.render("read_receipt.lisp", &ctx).unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn manage_chat_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
) -> 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 is_owner = chat.owner() == user.id;
|
||||||
|
|
||||||
|
let mut ctx = default_context(&data.0.0, &build_code, &Some(user));
|
||||||
|
|
||||||
|
ctx.insert("chat", &chat);
|
||||||
|
ctx.insert("members", &members);
|
||||||
|
ctx.insert("is_owner", &is_owner);
|
||||||
|
|
||||||
|
Ok(Html(tera.render("manage.lisp", &ctx).unwrap()))
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
use crate::{State, config::Config, get_user_from_token, routes::default_context};
|
use crate::{State, config::Config, get_user_from_token, routes::default_context};
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension,
|
Extension,
|
||||||
|
extract::{Path, Query},
|
||||||
response::{Html, IntoResponse},
|
response::{Html, IntoResponse},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
|
use serde::Deserialize;
|
||||||
use tera::Tera;
|
use tera::Tera;
|
||||||
use tetratto_core::model::{Error, auth::User};
|
use tetratto_core::model::{Error, auth::User};
|
||||||
|
|
||||||
|
@ -52,3 +54,72 @@ pub async fn login_request(jar: CookieJar, Extension(data): Extension<State>) ->
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ProfileQuery {
|
||||||
|
#[serde(default)]
|
||||||
|
pub redirect: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn profile_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(username): Path<String>,
|
||||||
|
Query(req): Query<ProfileQuery>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let (ref data, ref tera, ref build_code) = *data.read().await;
|
||||||
|
let user = get_user_from_token!(jar, data.2);
|
||||||
|
|
||||||
|
let profile = match data.2.get_user_by_username(&username).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(render_error(e, tera, data.0.0.clone(), user).await);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if req.redirect {
|
||||||
|
return Ok(Html(format!(
|
||||||
|
"<!doctype html><html><head><meta http-equiv=\"refresh\" content=\"0; url=/@{}\" /></head><body></body></html>",
|
||||||
|
profile.username
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_self = if let Some(ref ua) = user {
|
||||||
|
ua.id == profile.id
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ctx = default_context(&data.0.0, &build_code, &user);
|
||||||
|
|
||||||
|
ctx.insert("profile", &profile);
|
||||||
|
if let Some(ua) = user {
|
||||||
|
if !ua.settings.disable_other_themes | is_self {
|
||||||
|
ctx.insert("use_user_theme", &false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.insert("use_user_theme", &false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Html(tera.render("profile.lisp", &ctx).unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn confirm_dm_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(username): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let (ref data, ref tera, ref build_code) = *data.read().await;
|
||||||
|
let user = get_user_from_token!(jar, data.2);
|
||||||
|
|
||||||
|
let profile = match data.2.get_user_by_username(&username).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(render_error(e, tera, data.0.0.clone(), user).await);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ctx = default_context(&data.0.0, &build_code, &user);
|
||||||
|
ctx.insert("profile", &profile);
|
||||||
|
Ok(Html(tera.render("confirm_dm.lisp", &ctx).unwrap()))
|
||||||
|
}
|
||||||
|
|
|
@ -7,11 +7,15 @@ use serde::Deserialize;
|
||||||
pub fn routes() -> Router {
|
pub fn routes() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(misc::index_request))
|
.route("/", get(misc::index_request))
|
||||||
|
// profile
|
||||||
|
.route("/@{username}", get(misc::profile_request))
|
||||||
|
.route("/@{username}/confirm_dm", get(misc::confirm_dm_request))
|
||||||
// auth
|
// auth
|
||||||
.route("/login", get(misc::login_request))
|
.route("/login", get(misc::login_request))
|
||||||
// chats
|
// chats
|
||||||
.route("/chats", get(chats::list_request))
|
.route("/chats", get(chats::list_request))
|
||||||
.route("/chats/{id}", get(chats::chat_request))
|
.route("/chats/{id}", get(chats::chat_request))
|
||||||
|
.route("/chats/{id}/manage", get(chats::manage_chat_request))
|
||||||
.route(
|
.route(
|
||||||
"/chats/_templates/message/{id}",
|
"/chats/_templates/message/{id}",
|
||||||
get(chats::single_message_request),
|
get(chats::single_message_request),
|
||||||
|
|
28
src/sanitize.rs
Normal file
28
src/sanitize.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use ammonia::Builder;
|
||||||
|
|
||||||
|
/// Escape profile colors
|
||||||
|
pub fn color_escape(color: &str) -> String {
|
||||||
|
remove_tags(
|
||||||
|
&color
|
||||||
|
.replace(";", "")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", "%gt;")
|
||||||
|
.replace("}", "")
|
||||||
|
.replace("{", "")
|
||||||
|
.replace("url(\"", "url(\"/api/v1/util/proxy?url=")
|
||||||
|
.replace("url('", "url('/api/v1/util/proxy?url=")
|
||||||
|
.replace("url(https://", "url(/api/v1/util/proxy?url=https://"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clean profile metadata
|
||||||
|
pub fn remove_tags(input: &str) -> String {
|
||||||
|
Builder::default()
|
||||||
|
.rm_tags(&["img", "a", "span", "p", "h1", "h2", "h3", "h4", "h5", "h6"])
|
||||||
|
.clean(input)
|
||||||
|
.to_string()
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace("&", "&")
|
||||||
|
.replace("</script>", "</not-script")
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue