diff --git a/Cargo.lock b/Cargo.lock
index e7589f3..c93a829 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1603,6 +1603,7 @@ checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [
"equivalent",
"hashbrown",
+ "serde",
]
[[package]]
@@ -1667,6 +1668,15 @@ dependencies = [
"either",
]
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
[[package]]
name = "itoa"
version = "1.0.15"
@@ -2379,6 +2389,27 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "proc-macro2"
version = "1.0.95"
@@ -2587,7 +2618,7 @@ dependencies = [
"built",
"cfg-if",
"interpolate_name",
- "itertools",
+ "itertools 0.12.1",
"libc",
"libfuzzer-sys",
"log",
@@ -3028,6 +3059,51 @@ dependencies = [
"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 2.0.104",
+]
+
+[[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]]
name = "sha1"
version = "0.10.6"
@@ -3214,6 +3290,12 @@ dependencies = [
"unicode-properties",
]
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
[[package]]
name = "subtle"
version = "2.6.1"
@@ -3370,7 +3452,7 @@ dependencies = [
"serde",
"serde_json",
"tera",
- "tetratto-core 16.0.0",
+ "tetratto-core 16.0.3",
"tetratto-l10n 12.0.0",
"tetratto-shared 12.0.6",
"tokio",
@@ -3407,7 +3489,7 @@ dependencies = [
[[package]]
name = "tetratto-core"
-version = "16.0.0"
+version = "16.0.3"
dependencies = [
"async-recursion",
"base16ct",
@@ -3423,6 +3505,7 @@ dependencies = [
"reqwest",
"serde",
"serde_json",
+ "serde_valid",
"tetratto-l10n 12.0.0",
"tetratto-shared 12.0.6",
"tokio",
@@ -4027,6 +4110,12 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
[[package]]
name = "unicode-width"
version = "0.2.1"
diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs
index 393fe54..449dedd 100644
--- a/crates/app/src/assets.rs
+++ b/crates/app/src/assets.rs
@@ -117,11 +117,6 @@ pub const MOD_PROFILE: &str = include_str!("./public/html/mod/profile.lisp");
pub const MOD_WARNINGS: &str = include_str!("./public/html/mod/warnings.lisp");
pub const MOD_STATS: &str = include_str!("./public/html/mod/stats.lisp");
-pub const CHATS_APP: &str = include_str!("./public/html/chats/app.lisp");
-pub const CHATS_STREAM: &str = include_str!("./public/html/chats/stream.lisp");
-pub const CHATS_MESSAGE: &str = include_str!("./public/html/chats/message.lisp");
-pub const CHATS_CHANNELS: &str = include_str!("./public/html/chats/channels.lisp");
-
pub const STACKS_LIST: &str = include_str!("./public/html/stacks/list.lisp");
pub const STACKS_FEED: &str = include_str!("./public/html/stacks/feed.lisp");
pub const STACKS_MANAGE: &str = include_str!("./public/html/stacks/manage.lisp");
@@ -357,11 +352,6 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
write_template!(html_path->"mod/warnings.html"(crate::assets::MOD_WARNINGS) --config=config --lisp plugins);
write_template!(html_path->"mod/stats.html"(crate::assets::MOD_STATS) --config=config --lisp plugins);
- write_template!(html_path->"chats/app.html"(crate::assets::CHATS_APP) -d "chats" --config=config --lisp plugins);
- write_template!(html_path->"chats/stream.html"(crate::assets::CHATS_STREAM) --config=config --lisp plugins);
- write_template!(html_path->"chats/message.html"(crate::assets::CHATS_MESSAGE) --config=config --lisp plugins);
- write_template!(html_path->"chats/channels.html"(crate::assets::CHATS_CHANNELS) --config=config --lisp plugins);
-
write_template!(html_path->"stacks/list.html"(crate::assets::STACKS_LIST) -d "stacks" --config=config --lisp plugins);
write_template!(html_path->"stacks/feed.html"(crate::assets::STACKS_FEED) --config=config --lisp plugins);
write_template!(html_path->"stacks/manage.html"(crate::assets::STACKS_MANAGE) --config=config --lisp plugins);
diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml
index 77e351e..01943c0 100644
--- a/crates/app/src/langs/en-US.toml
+++ b/crates/app/src/langs/en-US.toml
@@ -175,6 +175,7 @@ version = "1.0.0"
"settings:tab.general" = "General"
"settings:tab.account" = "Account"
"settings:tab.profile" = "Profile"
+"settings:tab.experience" = "Experience"
"settings:tab.theme" = "Theme"
"settings:tab.sessions" = "Sessions"
"settings:tab.grants" = "Grants"
diff --git a/crates/app/src/public/css/root.css b/crates/app/src/public/css/root.css
index c38372d..144dd9a 100644
--- a/crates/app/src/public/css/root.css
+++ b/crates/app/src/public/css/root.css
@@ -1,4 +1,5 @@
@import url("utility.css");
+@import url("https://repodelivery.tetratto.com/tetratto-aux/lexend.css");
:root {
color-scheme: light dark;
@@ -85,12 +86,6 @@
box-sizing: border-box;
}
-@font-face {
- font-family: "Lexend";
- src: url("https://repodelivery.tetratto.com/fonts/lexend_variable.woff2")
- format("woff2");
-}
-
html,
body {
line-height: 1.5;
@@ -193,15 +188,6 @@ p {
margin-bottom: var(--pad-4);
}
-body:not(.use_system_font) {
- & p:not(b *),
- & span:not(.notification, .name, b *, button *, a *, .dropdown *, nav *),
- & input,
- & textarea {
- font-variation-settings: "wght" 325;
- }
-}
-
.no_p_margin p:last-child {
margin-bottom: 0;
}
diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css
index bec7d76..12f83b1 100644
--- a/crates/app/src/public/css/style.css
+++ b/crates/app/src/public/css/style.css
@@ -1256,6 +1256,11 @@ details summary::-webkit-details-marker {
display: none;
}
+details summary.button {
+ height: max-content;
+ justify-content: start;
+}
+
details[open] > summary {
position: relative;
color: var(--color-text-lowered) !important;
@@ -1288,7 +1293,7 @@ details.accordion {
details.accordion summary {
background: var(--color-lowered);
border-radius: var(--radius);
- padding: var(--pad-3) var(--pad-4);
+ padding: var(--pad-3) var(--pad-4) !important;
margin: 0;
width: 100%;
user-select: none;
diff --git a/crates/app/src/public/html/auth/register.lisp b/crates/app/src/public/html/auth/register.lisp
index 0fd9ae9..1fa5da1 100644
--- a/crates/app/src/public/html/auth/register.lisp
+++ b/crates/app/src/public/html/auth/register.lisp
@@ -174,5 +174,5 @@
(text "Or, ")
(a
("href" "/auth/login")
- (text "login")))
+ (text "log in")))
(text "{% endblock %}")
diff --git a/crates/app/src/public/html/body.lisp b/crates/app/src/public/html/body.lisp
index 599106e..890b60e 100644
--- a/crates/app/src/public/html/body.lisp
+++ b/crates/app/src/public/html/body.lisp
@@ -101,6 +101,9 @@
atto[\"hooks::spotify_time_text\"](); // spotify durations
atto[\"hooks::verify_emoji\"]();
+ globalThis.CURRENT_USER = \"{% if user -%} {{ user.username }} {%- endif %}\";
+ trigger(\"me::token_links\");
+
fix_atto_links();
if (document.getElementById(\"tokens\")) {
@@ -126,14 +129,6 @@
trigger(\"me::seen\");
trigger(\"streams::user\", [\"{{ user.id }}\"]);
- if (!window.location.pathname.startsWith(\"/chats/\")) {
- if (window.socket) {
- window.socket.send(\"Close\");
- window.socket = undefined;
- console.log(\"socket disconnect\");
- }
- }
-
if (window.location.pathname.startsWith(\"/reference\")) {
window.location.reload();
}
diff --git a/crates/app/src/public/html/chats/app.lisp b/crates/app/src/public/html/chats/app.lisp
deleted file mode 100644
index 11ab239..0000000
--- a/crates/app/src/public/html/chats/app.lisp
+++ /dev/null
@@ -1,509 +0,0 @@
-(text "{% extends \"root.html\" %} {% block head %}")
-(title
- (text "Chats — {{ config.name }}"))
-(link ("rel" "stylesheet") ("data-turbo-temporary" "true") ("href" "/css/chats.css?v=tetratto-{{ random_cache_breaker }}"))
-(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"chats\", hide_user_menu=true) }}")
-(nav
- ("class" "chats_nav")
- (button
- ("class" "flex gap_2 items_center active")
- ("onclick" "toggle_sidebars(event)")
- (text "{{ icon \"panel-left\" }} {% if community -%}")
- (b
- ("class" "name shorter")
- (text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}"))
- (text "{% else %}")
- (b
- (text "{{ text \"chats:label.my_chats\" }}"))
- (text "{%- endif %}")))
-(div
- ("class" "flex")
- (div
- ("class" "sidebar flex flex_col items_center gap_2")
- ("id" "community_list")
- ("style" "width: var(--list-bar-width)")
- (a
- ("href" "/chats/0/0")
- ("class" "button lowered channel_icon {% if selected_community == 0 -%}selected{%- endif %}")
- ("data-turbo" "false")
- (text "{{ icon \"message-circle\" }}"))
- (text "{% for community in communities %} {% if community.id != 0 -%}")
- (a
- ("href" "/chats/{{ community.id }}/0")
- ("class" "button lowered channel_icon {% if selected_community == community.id -%}selected{%- endif %}")
- ("data-turbo" "false")
- (text "{{ components::community_avatar(id=community.id, community=community, size=\"48px\") }}"))
- (text "{%- endif %} {% endfor %}"))
- (div
- ("class" "sidebar flex flex_col gap_2 justify_between")
- ("id" "channels_list")
- (div
- ("class" "flex flex_col gap_2 w_full")
- (div
- ("class" "title flex items_center justify_between channel_header")
- (text "{% if community -%}")
- (b
- ("class" "name shorter")
- (text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}"))
- (text "{% else %}")
- (b
- (text "{{ text \"chats:label.my_chats\" }}"))
- (text "{%- endif %} {% if selected_community != 0 -%}")
- (div
- ("class" "dropdown")
- (button
- ("class" "camo small")
- ("onclick" "trigger('atto::hooks::dropdown', [event])")
- ("exclude" "dropdown")
- (text "{{ icon \"ellipsis\" }}"))
- (div
- ("class" "inner")
- (a
- ("href" "/community/{{ selected_community }}")
- (text "{{ icon \"book-heart\" }}")
- (span
- (text "{{ text \"communities:label.show_community\" }}")))
- (text "{% if can_manage_channels -%}")
- (a
- ("href" "/community/{{ selected_community }}/manage")
- (text "{{ icon \"settings\" }}")
- (span
- (text "{{ text \"general:action.manage\" }}")))
- (text "{%- endif %}")))
- (text "{%- endif %}"))
- (text "{% if can_manage_channels -%}")
- (a
- ("class" "button w_full justify_start lowered")
- ("href" "/community/{{ selected_community }}/manage#/channels")
- (text "{{ icon \"plus\" }}")
- (span
- (text "{{ text \"communities:action.create_channel\" }}")))
- (text "{%- endif %}")
- (turbo-frame
- ("id" "channels_list_frame")
- ("src" "/chats/{{ selected_community }}/{{ selected_channel }}/_channels")
- ("target" "_top")))
- (text "{{ components::user_plate(user=user, show_menu=true) }}"))
- (text "{% if channel -%}")
- (div
- ("class" "w_full flex flex_col gap_2 padded_section")
- ("id" "stream")
- ("style" "padding: var(--pad-4)")
- (turbo-frame
- ("id" "stream_body_frame")
- ("src" "/chats/{{ selected_community }}/{{ selected_channel }}/_stream?page={{ page }}&message={{ message }}"))
- (form
- ("class" "card flex flex_row gap_2")
- ("onsubmit" "create_message_from_form(event)")
- (textarea
- ("type" "text")
- ("name" "content")
- ("id" "content")
- ("placeholder" "message {{ channel.title }}")
- ("required" "")
- ("minlength" "2")
- ("maxlength" "2048")
- ("style" "min-height: 48px !important; height: 48px"))
- (button
- ("class" "camo send_button")
- ("title" "Send")
- (text "{{ icon \"send-horizontal\" }}"))))
- (text "{%- endif %}")
-
- ; emoji picker
- (text "{{ components::emoji_picker(element_id=\"\", render_dialog=true, render_button=false) }}")
- (input ("id" "react_emoji_picker_field") ("class" "hidden") ("type" "hidden"))
-
- (script
- (text "window.EMOJI_PICKER_MODE = \"replace\";
- document.getElementById(\"react_emoji_picker_field\").addEventListener(\"change\", (e) => {
- if (!EMOJI_PICKER_REACTION_MESSAGE_ID) {
- return;
- }
-
- const emoji = e.target.value === \"::\" ? \":heart:\" : e.target.value;
- trigger(\"me::message_react\", [document.getElementById(`message-${EMOJI_PICKER_REACTION_MESSAGE_ID}`), EMOJI_PICKER_REACTION_MESSAGE_ID, emoji]);
- });"))
-
- ; ...
- (script
- (text "window.CURRENT_PAGE = Number.parseInt(\"{{ page }}\");
- window.VIEWING_SINGLE = \"{{ message }}\".length > 0;
- window.CHAT_PROPS = {
- selected_community: \"{{ selected_community }}\",
- selected_channel: \"{{ selected_channel }}\",
- membership_role: Number.parseInt(\"{{ membership_role }}\"),
- };
-
- window.SIDEBARS_OPEN = false;
- if (new URLSearchParams(window.location.search).get(\"nav\") === \"true\") {
- window.SIDEBARS_OPEN = true;
- }
-
- if (
- window.SIDEBARS_OPEN &&
- !document.body.classList.contains(\"sidebars_shown\")
- ) {
- toggle_sidebars();
- window.SIDEBARS_OPEN = true;
- }
-
- for (const anchor of document.querySelectorAll(\"[data-turbo=false]\")) {
- anchor.href += `?nav=${window.SIDEBARS_OPEN}`;
- }
-
- function mention_user(username) {
- document.getElementById(\"content\").value += ` @${username} `;
- }
-
- function toggle_sidebars() {
- window.SIDEBARS_OPEN = !window.SIDEBARS_OPEN;
-
- for (const anchor of document.querySelectorAll(
- \"[data-turbo=false]\",
- )) {
- anchor.href = anchor.href.replace(
- `?nav=${!window.SIDEBARS_OPEN}`,
- `?nav=${window.SIDEBARS_OPEN}`,
- );
- }
-
- const community_list = document.getElementById(\"community_list\");
- const channels_list = document.getElementById(\"channels_list\");
-
- if (document.body.classList.contains(\"sidebars_shown\")) {
- // hide
- document.body.classList.remove(\"sidebars_shown\");
- community_list.style.left = \"-200%\";
- channels_list.style.left = \"-200%\";
- } else {
- // show
- document.body.classList.add(\"sidebars_shown\");
- community_list.style.left = \"0\";
- channels_list.style.left = \"var(--list-bar-width)\";
- }
- }
-
- globalThis.add_member = async (id) => {
- await trigger(\"atto::debounce\", [\"channels::add_member\"]);
- const member = await trigger(\"atto::prompt\", [\"Member username:\"]);
-
- if (!member) {
- return;
- }
-
- fetch(`/api/v1/channels/${id}/add`, {
- method: \"POST\",
- headers: {
- \"Content-Type\": \"application/json\",
- },
- body: JSON.stringify({
- member,
- }),
- })
- .then((res) => res.json())
- .then((res) => {
- trigger(\"atto::toast\", [
- res.ok ? \"success\" : \"error\",
- res.message,
- ]);
- });
- };
-
- globalThis.mute_channel = async (id, mute = true) => {
- await trigger(\"atto::debounce\", [\"channels::mute\"]);
- fetch(`/api/v1/channels/${id}/mute`, {
- method: mute ? \"POST\" : \"DELETE\",
- })
- .then((res) => res.json())
- .then((res) => {
- trigger(\"atto::toast\", [
- res.ok ? \"success\" : \"error\",
- res.message,
- ]);
-
- if (res.ok) {
- if (mute) {
- document.querySelector(`[ui_ident=channel\\\\.mute\\\\:${id}]`).classList.add(\"hidden\");
- document.querySelector(`[ui_ident=channel\\\\.unmute\\\\:${id}]`).classList.remove(\"hidden\");
- } else {
- document.querySelector(`[ui_ident=channel\\\\.mute\\\\:${id}]`).classList.remove(\"hidden\");
- document.querySelector(`[ui_ident=channel\\\\.unmute\\\\:${id}]`).classList.add(\"hidden\");
- }
- }
- });
- };
-
- globalThis.update_channel_title = async (id) => {
- await trigger(\"atto::debounce\", [\"channels::update_title\"]);
- const title = await trigger(\"atto::prompt\", [\"New channel title:\"]);
-
- if (!title) {
- return;
- }
-
- fetch(`/api/v1/channels/${id}/title`, {
- method: \"POST\",
- headers: {
- \"Content-Type\": \"application/json\",
- },
- body: JSON.stringify({
- title,
- }),
- })
- .then((res) => res.json())
- .then((res) => {
- trigger(\"atto::toast\", [
- res.ok ? \"success\" : \"error\",
- res.message,
- ]);
- });
- };
-
- globalThis.kick_member = async (cid, uid) => {
- if (
- !(await trigger(\"atto::confirm\", [
- \"Are you sure you would like to do this?\",
- ]))
- ) {
- return;
- }
-
- fetch(`/api/v1/channels/${cid}/kick`, {
- method: \"POST\",
- headers: {
- \"Content-Type\": \"application/json\",
- },
- body: JSON.stringify({
- member: uid,
- }),
- })
- .then((res) => res.json())
- .then((res) => {
- trigger(\"atto::toast\", [
- res.ok ? \"success\" : \"error\",
- res.message,
- ]);
- });
- };
-
- globalThis.delete_channel = async (id) => {
- if (
- !(await trigger(\"atto::confirm\", [
- \"Are you sure you would like to do this?\",
- ]))
- ) {
- return;
- }
-
- fetch(`/api/v1/channels/${id}`, {
- method: \"DELETE\",
- })
- .then((res) => res.json())
- .then((res) => {
- trigger(\"atto::toast\", [
- res.ok ? \"success\" : \"error\",
- res.message,
- ]);
- });
- };"))
- (script
- ("id" "socket_init")
- ("data-turbo-permanent" "true")
- (text "globalThis.socket_init = () => {
- if (window.socket) {
- window.socket.send(\"Close\");
- window.socket.close();
- window.socket = undefined;
- console.log(\"closed lingering\");
- }
-
- if (window.CHAT_PROPS.selected_community !== \"0\") {
- const endpoint = `${window.location.origin.replace(\"http\", \"ws\")}/api/v1/_connect/${window.CHAT_PROPS.selected_community}`;
- const socket = new WebSocket(endpoint);
- window.socket = socket;
- window.socket_id = window.CHAT_PROPS.selected_community;
- } else {
- const endpoint = `${window.location.origin.replace(\"http\", \"ws\")}/api/v1/_connect/${window.CHAT_PROPS.selected_channel}`;
- const socket = new WebSocket(endpoint);
- window.socket = socket;
- window.socket_id = window.CHAT_PROPS.selected_channel;
- }
-
- if (window.CHANNEL_NOTIFS_INTERVAL) {
- window.clearInterval(window.CHANNEL_NOTIFS_INTERVAL);
- }
-
- window.CHANNEL_NOTIFS_INTERVAL = setInterval(() => {
- if (!window.CHAT_PROPS.selected_channel) {
- return;
- }
-
- if (!window.location.href.includes(\"{{ selected_channel }}\")) {
- window.clearInterval(window.CHANNEL_NOTIFS_INTERVAL);
- return;
- }
-
- fetch(
- `/api/v1/notifications/tag/chats/${window.CHAT_PROPS.selected_channel}`,
- { method: \"DELETE\" },
- );
- }, 10000);
-
- window.socket.addEventListener(\"open\", () => {
- // auth
- window.socket.send(
- JSON.stringify({
- method: \"Headers\",
- data: JSON.stringify({
- // SocketHeaders
- is_channel: window.SUBSCRIBE_CHANNEL,
- }),
- }),
- );
- });
-
- setTimeout(() => {
- window.LAST_MESSAGE_AUTHOR_ID = null;
- window.socket.addEventListener(\"message\", async (event) => {
- if (event.data === \"Ping\") {
- return socket.send(\"Pong\");
- }
-
- const msg = JSON.parse(event.data);
-
- if (
- msg.method === \"Message\" &&
- window.CURRENT_PAGE === 0 &&
- window.VIEWING_SINGLE
- ) {
- const [channel_id, data] = JSON.parse(msg.data);
- if (channel_id !== window.CHAT_PROPS.selected_channel) {
- // message not for us... maybe send notification later
- // something like /api/v1/messages/{id}/mark_unread
- return;
- }
-
- if (document.getElementById(\"stream_body\")) {
- const element = document.createElement(\"div\");
- element.style.display = \"contents\";
-
- const message_owner = JSON.parse(msg.data)[1].owner;
- element.innerHTML = await (
- await fetch(
- `/chats/${window.CHAT_PROPS.selected_community}/${window.CHAT_PROPS.selected_channel}/_render`,
- {
- method: \"POST\",
- headers: {
- \"Content-Type\": \"application/json\",
- },
- body: JSON.stringify({
- data: msg.data,
- grouped:
- message_owner ===
- window.LAST_MESSAGE_AUTHOR_ID,
- }),
- },
- )
- ).text();
-
- document
- .getElementById(\"stream_body\")
- .prepend(element);
- clean_text();
-
- window.LAST_MESSAGE_AUTHOR_ID = message_owner;
- } else {
- console.log(\"abandoned remote\");
- socket.close();
- }
- } else if (msg.method === \"Delete\") {
- const data = JSON.parse(msg.data);
- if (document.getElementById(`message-${data.id}`)) {
- document
- .getElementById(`message-${data.id}`)
- .remove();
- }
- }
- });
-
- globalThis.create_message_from_form = async (e) => {
- e.preventDefault();
- await trigger(\"atto::debounce\", [\"messages::create\"]);
-
- fetch(\"/api/v1/messages\", {
- method: \"POST\",
- headers: {
- \"Content-Type\": \"application/json\",
- },
- body: JSON.stringify({
- content: e.target.content.value.trim(),
- channel: window.CHAT_PROPS.selected_channel,
- }),
- })
- .then((res) => res.json())
- .then((res) => {
- if (!res.ok) {
- trigger(\"atto::toast\", [\"error\", res.message]);
- }
-
- e.target.reset();
- });
- };
-
- globalThis.delete_message = async (id) => {
- if (
- !(await trigger(\"atto::confirm\", [
- \"Are you sure you would like to do this?\",
- ]))
- ) {
- return;
- }
-
- fetch(`/api/v1/messages/${id}`, {
- method: \"DELETE\",
- })
- .then((res) => res.json())
- .then((res) => {
- trigger(\"atto::toast\", [
- res.ok ? \"success\" : \"error\",
- res.message,
- ]);
- });
- };
-
- const clean_text = () => {
- trigger(\"atto::clean_date_codes\");
- trigger(\"atto::hooks::online_indicator\");
- trigger(\"atto::hooks::check_message_reactions\");
- };
-
- document.addEventListener(
- \"turbo:before-frame-render\",
- (event) => {
- setTimeout(clean_text, 50);
- },
- );
-
- setTimeout(clean_text, 150);
- }, 250);
- };"))
- (text "{% if selected_channel -%}")
- (script
- (text "window.SUBSCRIBE_CHANNEL = \"{{ selected_community }}\" === \"0\";
-
- setTimeout(() => {
- if (!window.SUBSCRIBE_CHANNEL) {
- // sub community
- if (window.socket_id !== \"{{ selected_community }}\") {
- socket_init();
- }
- } else {
- // sub channel
- if (window.socket_id !== \"{{ selected_channel }}\") {
- socket_init();
- }
- }
- }, 100);"))
- (text "{%- endif %}"))
-(text "{% endblock %}")
diff --git a/crates/app/src/public/html/chats/channels.lisp b/crates/app/src/public/html/chats/channels.lisp
deleted file mode 100644
index ced2f93..0000000
--- a/crates/app/src/public/html/chats/channels.lisp
+++ /dev/null
@@ -1,80 +0,0 @@
-(text "{%- import \"components.html\" as components -%}")
-(turbo-frame
- ("id" "channels_list_frame")
- (div
- ("class" "channels_list_half flex flex_col gap_2 {% if selected_community != 0 or selected_channel == 0%}no_members{%- endif -%}")
- (text "{% for channel in channels %}")
- (div
- ("class" "flex flex_row gap_1")
- (a
- ("class" "w_full justify_start button {% if selected_channel == channel.id -%}lowered{% else %}camo{%- endif %}")
- ("href" "/chats/{{ selected_community }}/{{ channel.id }}")
- ("data-turbo" "{{ selected_community == '0' }}")
- (text "{{ icon \"rss\" }}")
- (b
- ("class" "name shortest")
- (text "{{ channel.title }}")))
- (div
- ("class" "dropdown")
- (button
- ("class" "big_icon {% if selected_channel == channel.id -%}lowered{% else %}camo{%- endif %}")
- ("onclick" "trigger('atto::hooks::dropdown', [event])")
- ("exclude" "dropdown")
- ("style" "width: 32px")
- (text "{{ icon \"ellipsis\" }}"))
- (div
- ("class" "inner")
- (button
- ("onclick" "trigger('atto::copy_text', ['{{ channel.id }}'])")
- (icon (text "copy"))
- (str (text "general:action.copy_id")))
- (text "{% if user.id == channel.owner or can_manage_channels -%}")
- ; owner/manager controls
- (button
- ("onclick" "add_member('{{ channel.id }}')")
- (text "{{ icon \"user-plus\" }}")
- (span
- (text "{{ text \"chats:action.add_someone\" }}")))
- (button
- ("onclick" "update_channel_title('{{ channel.id }}')")
- (text "{{ icon \"pencil\" }}")
- (span
- (text "{{ text \"chats:action.rename\" }}")))
- (button
- ("onclick" "delete_channel('{{ channel.id }}')")
- ("class" "red")
- (text "{{ icon \"trash\" }}")
- (span
- (text "{{ text \"general:action.delete\" }}")))
- (text "{%- endif %} {% if selected_community == 0 %}")
- ; mute/unmute
- (button
- ("class" "{% if channel.id in user.channel_mutes -%} hidden {%- endif %}")
- ("ui_ident" "channel.mute:{{ channel.id }}")
- ("onclick" "mute_channel('{{ channel.id }}')")
- (icon (text "bell-off"))
- (span
- (str (text "chats:action.mute"))))
- (button
- ("class" "{% if not channel.id in user.channel_mutes -%} hidden {%- endif %}")
- ("ui_ident" "channel.unmute:{{ channel.id }}")
- ("onclick" "mute_channel('{{ channel.id }}', false)")
- (icon (text "bell-ring"))
- (span
- (str (text "chats:action.unmute"))))
- ; ...
- (text "{% if user.id != channel.owner -%}")
- ; group chat member controls
- (button
- ("onclick" "kick_member('{{ channel.id }}', '{{ user.id }}')")
- ("class" "red")
- (text "{{ icon \"door-open\" }}")
- (span
- (text "{{ text \"chats:action.leave\" }}")))
- (text "{%- endif %} {%- endif %}"))))
- (text "{% endfor %}"))
- (text "{% if selected_community == 0 and selected_channel -%}")
- (div
- ("class" "members_list_half flex flex_col gap_2")
- (text "{% for member in members %} {{ components::user_plate(user=member, show_kick=user.id == channel.owner) }} {% endfor %}"))
- (text "{%- endif %}"))
diff --git a/crates/app/src/public/html/chats/message.lisp b/crates/app/src/public/html/chats/message.lisp
deleted file mode 100644
index 4d5946c..0000000
--- a/crates/app/src/public/html/chats/message.lisp
+++ /dev/null
@@ -1 +0,0 @@
-(text "{%- import \"components.html\" as components -%} {{ components::message(user=user, message=message, grouped=grouped) }}")
diff --git a/crates/app/src/public/html/chats/stream.lisp b/crates/app/src/public/html/chats/stream.lisp
deleted file mode 100644
index af891e5..0000000
--- a/crates/app/src/public/html/chats/stream.lisp
+++ /dev/null
@@ -1,55 +0,0 @@
-(text "{%- import \"components.html\" as components -%}")
-(turbo-frame
- ("id" "stream_body_frame")
- (div
- ("class" "gap_2")
- ("id" "stream_body")
- (text "{% if page != 0 -%}")
- (div
- ("class" "card flex gap_2 small lowered flex_wrap")
- (b
- (text "{{ text \"chats:label.viewing_old_messages\" }}"))
- (a
- ("href" "/chats/{{ community }}/{{ channel.id }}/_stream?page={{ page - 1}}")
- ("class" "button small")
- ("onclick" "window.CURRENT_PAGE -= 1")
- (text "{{ text \"chats:label.go_back\" }}")))
- (text "{%- endif %} {% if message -%}")
- (div
- ("class" "card flex gap_2 small lowered flex_wrap")
- (b
- (text "{{ text \"chats:label.viewing_single_message\" }}"))
- (a
- ("href" "/chats/{{ community }}/{{ channel.id }}?page={{ page }}")
- ("class" "button small")
- ("onclick" "window.VIEWING_SINGLE = false")
- ("target" "_top")
- (text "{{ text \"chats:label.go_back\" }}")))
- (text "{{ components::message(user=message_owner, message=message, grouped=false) }} {% else %} {% for message in messages %} {{ components::message(user=message[1], message=message[0], grouped=message[2]) }} {% endfor %} {%- endif %} {% if messages|length > 0 -%}")
- (div
- ("class" "flex gap_2 w_full justify_center")
- (a
- ("class" "button")
- ("href" "/chats/{{ community }}/{{ channel.id }}/_stream?page={{ page + 1 }}")
- ("onclick" "window.CURRENT_PAGE += 1")
- (text "{{ icon \"clock\" }}")
- (span
- (text "{{ text \"chats:label.view_older\" }}")))
- (text "{% if page != 0 -%}")
- (a
- ("class" "button lowered")
- ("href" "/chats/{{ community }}/{{ channel }}/_stream?page={{ page - 1 }}")
- ("onclick" "window.CURRENT_PAGE -= 1")
- (text "{{ icon \"rewind\" }}")
- (span
- (text "{{ text \"chats:label.view_more_recent\" }}")))
- (text "{%- endif %}"))
- (text "{%- endif %}"))
- (style
- (text "#stream_body {
- height: 100%;
- display: flex;
- justify-content: flex-start;
- flex-direction: column-reverse;
- overflow: auto;
- }")))
diff --git a/crates/app/src/public/html/communities/create_post.lisp b/crates/app/src/public/html/communities/create_post.lisp
index 7e3be37..938f019 100644
--- a/crates/app/src/public/html/communities/create_post.lisp
+++ b/crates/app/src/public/html/communities/create_post.lisp
@@ -361,6 +361,7 @@
false,
document.getElementById(\"community_to_post_to\")
.selectedOptions[0].getAttribute(\"is_stack\") === \"true\",
+ e.target.file_picker ? e.target.file_picker.files : [],
]);
// update settings
diff --git a/crates/app/src/public/html/communities/settings.lisp b/crates/app/src/public/html/communities/settings.lisp
index f95a900..a32a573 100644
--- a/crates/app/src/public/html/communities/settings.lisp
+++ b/crates/app/src/public/html/communities/settings.lisp
@@ -29,14 +29,6 @@
(text "{{ text \"communities:tab.members\" }}"))))
(div
("class" "row")
- (text "{% if can_manage_channels -%}")
- (a
- ("href" "#/channels")
- ("data-tab-button" "channels")
- (text "{{ icon \"rss\" }}")
- (span
- (text "{{ text \"communities:tab.channels\" }}")))
- (text "{%- endif %}")
(a
("href" "#/topics")
("data-tab-button" "topics")
@@ -147,7 +139,7 @@
(button
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}"))))))
+ (str (text "general:action.save")))))))
(div
("class" "card_nest")
("ui_ident" "danger_zone")
@@ -170,7 +162,7 @@
("onclick" "save_context()")
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}")))
+ (str (text "general:action.save"))))
(a
("href" "/community/{{ community.title }}")
("class" "button secondary")
@@ -273,167 +265,11 @@
("onclick" "update_user_role(document.getElementById('uid').value, document.getElementById('role').value)")
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}"))))
+ (str (text "general:action.save")))))
(div
("class" "card flex flex_col gap_2")
("id" "permission_builder"))))
- (text "{% if can_manage_channels -%}")
- (div
- ("class" "card lowered w_full hidden flex flex_col gap_2")
- ("data-tab" "channels")
- (div
- ("class" "card_nest")
- (div
- ("class" "card small")
- (b
- (text "{{ text \"communities:action.create_channel\" }}")))
- (form
- ("class" "card flex flex_col gap_2")
- ("onsubmit" "create_channel_from_form(event)")
- (div
- ("class" "flex flex_col gap_1")
- (label
- ("for" "title")
- (str (text "communities:label.name")))
- (input
- ("type" "text")
- ("name" "title")
- ("id" "title")
- ("placeholder" "name")
- ("required" "")
- ("minlength" "2")
- ("maxlength" "32")))
- (button
- (str (text "communities:action.create")))))
- (text "{% for channel in channels %}")
- (div
- ("class" "card_nest")
- (div
- ("class" "card small")
- (b
- (text "{{ channel.position }} "))
- (text "{{ channel.title }}"))
- (div
- ("class" "card flex gap_2")
- (button
- ("class" "red lowered small")
- ("onclick" "delete_channel('{{ channel.id }}')")
- (text "{{ text \"general:action.delete\" }}"))
- (button
- ("class" "lowered small")
- ("onclick" "update_channel_position('{{ channel.id }}')")
- (text "{{ text \"chats:action.move\" }}"))
- (button
- ("class" "lowered small")
- ("onclick" "update_channel_title('{{ channel.id }}')")
- (text "{{ text \"chats:action.rename\" }}"))))
- (text "{% endfor %}"))
- (script
- (text "globalThis.delete_channel = async (id) => {
- if (
- !(await trigger(\"atto::confirm\", [
- \"Are you sure you would like to do this?\",
- ]))
- ) {
- return;
- }
-
- fetch(`/api/v1/channels/${id}`, {
- method: \"DELETE\",
- })
- .then((res) => res.json())
- .then((res) => {
- trigger(\"atto::toast\", [
- res.ok ? \"success\" : \"error\",
- res.message,
- ]);
- });
- };
-
- globalThis.update_channel_position = async (id) => {
- await trigger(\"atto::debounce\", [\"channels::move\"]);
-
- const position = Number.parseInt(
- await trigger(\"atto::prompt\", [
- \"New channel position (number):\",
- ]),
- );
-
- if (!position && position !== 0) {
- return alert(\"Must be a number!\");
- }
-
- fetch(`/api/v1/channels/${id}/move`, {
- method: \"POST\",
- headers: {
- \"Content-Type\": \"application/json\",
- },
- body: JSON.stringify({
- position,
- }),
- })
- .then((res) => res.json())
- .then((res) => {
- trigger(\"atto::toast\", [
- res.ok ? \"success\" : \"error\",
- res.message,
- ]);
- });
- };
-
- globalThis.update_channel_title = async (id) => {
- await trigger(\"atto::debounce\", [\"channels::update_title\"]);
- const title = await trigger(\"atto::prompt\", [\"New channel title:\"]);
-
- if (!title) {
- return;
- }
-
- fetch(`/api/v1/channels/${id}/title`, {
- method: \"POST\",
- headers: {
- \"Content-Type\": \"application/json\",
- },
- body: JSON.stringify({
- title,
- }),
- })
- .then((res) => res.json())
- .then((res) => {
- trigger(\"atto::toast\", [
- res.ok ? \"success\" : \"error\",
- res.message,
- ]);
- });
- };
-
- async function create_channel_from_form(e) {
- e.preventDefault();
- await trigger(\"atto::debounce\", [\"channels::create\"]);
-
- fetch(\"/api/v1/channels\", {
- method: \"POST\",
- headers: {
- \"Content-Type\": \"application/json\",
- },
- body: JSON.stringify({
- title: e.target.title.value,
- community: \"{{ community.id }}\",
- }),
- })
- .then((res) => res.json())
- .then((res) => {
- trigger(\"atto::toast\", [
- res.ok ? \"success\" : \"error\",
- res.message,
- ]);
-
- if (res.ok) {
- window.location.reload();
- }
- });
- }"))
- (text "{%- endif %} {% if can_manage_emojis -%}")
+ (text "{% if can_manage_emojis -%}")
(div
("class" "card lowered w_full hidden flex flex_col gap_2")
("data-tab" "emojis")
@@ -987,7 +823,7 @@
MANAGE_PINS: 1 << 7,
MANAGE_COMMUNITY: 1 << 8,
MANAGE_QUESTIONS: 1 << 9,
- MANAGE_CHANNELS: 1 << 10,
+ UNUSED_0: 1 << 10,
MANAGE_MESSAGES: 1 << 11,
MANAGE_EMOJIS: 1 << 12,
},
diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp
index 9dea998..53e316d 100644
--- a/crates/app/src/public/html/components.lisp
+++ b/crates/app/src/public/html/components.lisp
@@ -706,7 +706,9 @@
("cy" "12")
("r" "6"))))
-(text "{%- endif %} {%- endmacro %} {% macro theme(user, theme_preference) -%} {% if user %} {% if user.settings.theme_hue -%}")
+(text "{%- endif %} {%- endmacro %}")
+
+(text "{% macro theme(user, theme_preference) -%} {% if user %} {% if user.settings.theme_hue -%}")
(style
(text ":root, * {
--hue: {{ user.settings.theme_hue }} !important;
@@ -739,7 +741,6 @@
setTimeout(() => {
match_user_theme();
}, 150);"))
-
(text "{%- endif %}")
(div
("style" "display: none;")
@@ -748,7 +749,6 @@
(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,
@@ -756,7 +756,9 @@
--{{ css }}: {{ color|color }} !important;
}"))
-(text "{%- endif %} {%- endmacro %} {% macro question(question, owner, asking_about=false, show_community=true, secondary=false, profile=false) -%}")
+(text "{%- endif %} {%- endmacro %}")
+
+(text "{% macro question(question, owner, asking_about=false, show_community=true, secondary=false, profile=false) -%}")
(div
("class" "card question {% if secondary -%}secondary{%- endif %} flex gap_2")
(text "{% if owner.id == 0 or question.context.mask_owner -%}")
@@ -860,8 +862,8 @@
(div
("class" "card_nest")
(div
- ("class" "card small flex items_center gap_2")
- (text "{{ icon \"message-circle-heart\" }}")
+ ("class" "card small flex items_center gap_2 flex_wrap")
+ (icon (text "message-circle-heart"))
(span
("class" "no_p_margin")
(text "{% if header -%} {{ header|markdown|safe }} {% else %} {{ text \"requests:label.ask_question\" }} {%- endif %}")))
@@ -1143,106 +1145,8 @@
(div
("style" "display: contents;")
(text "{% if key == \"Spotify\" -%} {{ icon \"spotify\" }} {% elif key == \"LastFm\" %} {{ icon \"last_fm\" }} {%- endif %}"))
-(text "{%- endmacro %} {% macro connection_url(key, value) -%} {% if value[0].data.url %} {{ value[0].data.url }} {% elif key == \"LastFm\" %} https://last.fm/user/{{ value[0].data.name }} {%- endif %} {%- endmacro %} {% macro message_actions(can_manage_message, owner, message, owner) -%}")
-(div
- ("class" "dropdown")
- (button
- ("class" "camo small")
- ("onclick" "trigger('atto::hooks::dropdown', [event])")
- ("exclude" "dropdown")
- ("title" "More options")
- (text "{{ icon \"ellipsis\" }}"))
- (div
- ("class" "inner")
- (text "{% if can_manage_message or (user and user.id == message.owner) -%}")
- (button
- ("class" "red")
- ("onclick" "delete_message('{{ message.id }}')")
- (text "{{ icon \"trash\" }}")
- (span
- (text "{{ text \"general:action.delete\" }}")))
- (text "{%- endif %}")
- (button
- ("onclick" "window.location.href = `${window.location.origin}/chats/{{ community }}/{{ channel }}?message={{ message.id }}`")
- (text "{{ icon \"external-link\" }}")
- (span
- (text "{{ text \"general:action.open\" }}")))
- (button
- ("onclick" "trigger('atto::copy_text', [`${window.location.origin}/chats/{{ community }}/{{ channel }}?message={{ message.id }}`])")
- (text "{{ icon \"copy\" }}")
- (span
- (text "{{ text \"general:action.copy_link\" }}")))
- (button
- ("onclick" "mention_user('{{ owner.username }}')")
- (text "{{ icon \"at-sign\" }}")
- (span
- (text "{{ text \"chats:action.mention_user\" }}")))))
-
-(text "{%- endmacro %} {% macro message(user, message, can_manage_message=false, grouped=false) -%}")
-(div
- ("class" "card secondary message flex gap_2 {% if grouped -%}grouped{%- endif %}")
- ("id" "message-{{ message.id }}")
- (text "{% if not grouped -%}")
- (a
- ("href" "/@{{ user.username }}")
- ("target" "_top")
- (text "{{ self::avatar(id=user.id, size=\"42px\") }}"))
- (text "{%- endif %}")
- (div
- ("class" "flex flex_col gap_1 w_full")
- (text "{% if not grouped -%}")
- (div
- ("class" "flex gap_2 w_full justify_between flex_wrap")
- (div
- ("class" "flex gap_2")
- (text "{{ self::full_username(user=user) }} {% if message.edited != message.created %}")
- (span
- ("class" "date")
- (text "{{ message.edited }}")
- (sup
- ("title" "Edited")
- (text "*")))
- (text "{% else %}")
- (span
- ("class" "date")
- (text "{{ message.created }}"))
- (text "{%- endif %}"))
- (div
- ("class" "flex gap_2 hidden")
- ("onclick" "window.EMOJI_PICKER_REACTION_MESSAGE_ID = '{{ message.id }}'")
- (text "{{ self::emoji_picker(element_id=\"react_emoji_picker_field\", render_dialog=false, render_button=true, small=true) }}")
- (text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}")))
- (text "{%- endif %}")
- (div
- ("class" "flex w_full gap_2 justify_between")
- (div
- ("class" "flex flex_col gap_2")
- (span
- ("class" "no_p_margin")
- (text "{{ message.content|markdown|safe }}"))
-
- (div
- ("class" "flex w_full gap_1 flex_wrap")
- ("onclick" "window.EMOJI_PICKER_REACTION_MESSAGE_ID = '{{ message.id }}'")
- ("hook" "check_message_reactions")
- ("hook-arg:id" "{{ message.id }}")
-
- (text "{% for emoji,num in message.reactions -%}")
- (button
- ("class" "small lowered")
- ("ui_ident" "emoji_{{ emoji }}")
- ("onclick" "trigger('me::message_react', [event.target.parentElement.parentElement, '{{ message.id }}', '{{ emoji }}'])")
- (span (text "{{ emoji|emojis|safe }} {{ num }}")))
- (text "{%- endfor %}")))
- (text "{% if grouped -%}")
- (div
- ("class" "hidden flex gap_2 items_center")
- ("onclick" "window.EMOJI_PICKER_REACTION_MESSAGE_ID = '{{ message.id }}'")
- (text "{{ self::emoji_picker(element_id=\"react_emoji_picker_field\", render_dialog=false, render_button=true, small=true) }}")
- (text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}"))
- (text "{%- endif %}"))))
-
-(text "{%- endmacro %} {% macro user_menu() -%}")
+(text "{%- endmacro %} {% macro connection_url(key, value) -%} {% if value[0].data.url %} {{ value[0].data.url }} {% elif key == \"LastFm\" %} https://last.fm/user/{{ value[0].data.name }} {%- endif %} {%- endmacro %}")
+(text "{% macro user_menu() -%}")
(div
("class" "inner")
(b
@@ -1590,7 +1494,7 @@
(text "{% macro create_post_options() -%}")
(div
("class" "flex gap_2 flex_wrap")
- (text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if not quoting -%} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {%- endif %} {%- endif %}")
+ (text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {%- endif %}")
(button
("class" "small square lowered")
@@ -2048,12 +1952,6 @@
(text "{{ icon \"circle-minus\" }}")
(span
(text "{{ text \"communities:action.leave\" }}")))
- (a
- ("href" "/chats/{{ community.id }}/0")
- ("class" "button lowered")
- (text "{{ icon \"message-circle\" }}")
- (span
- (text "{{ text \"communities:label.chats\" }}")))
(text "{% if user and can_post -%}")
(a
("href" "/communities/intents/post?community={{ community.id }}")
@@ -2092,12 +1990,6 @@
});
};"))
(text "{%- endif %} {% else %}")
- (a
- ("href" "/chats/{{ community.id }}/0")
- ("class" "button lowered")
- (text "{{ icon \"message-circle\" }}")
- (span
- (text "{{ text \"communities:label.chats\" }}")))
(text "{% if not community.is_forum -%}")
(a
("href" "/communities/intents/post?community={{ community.id }}")
diff --git a/crates/app/src/public/html/developer/app.lisp b/crates/app/src/public/html/developer/app.lisp
index 4d4d9c8..1b4060e 100644
--- a/crates/app/src/public/html/developer/app.lisp
+++ b/crates/app/src/public/html/developer/app.lisp
@@ -95,7 +95,7 @@
(button
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}")))))
+ (str (text "general:action.save"))))))
(div
("class" "card_nest")
(div
@@ -120,7 +120,7 @@
(button
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}")))))
+ (str (text "general:action.save"))))))
(div
("class" "card_nest")
(div
@@ -145,7 +145,7 @@
(button
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}")))))
+ (str (text "general:action.save"))))))
(div
("class" "card_nest")
(div
@@ -185,7 +185,7 @@
(button
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}")))))
+ (str (text "general:action.save"))))))
(div
("class" "card_nest")
(div
diff --git a/crates/app/src/public/html/journals/app.lisp b/crates/app/src/public/html/journals/app.lisp
index 337d39c..8f52ed0 100644
--- a/crates/app/src/public/html/journals/app.lisp
+++ b/crates/app/src/public/html/journals/app.lisp
@@ -255,7 +255,7 @@
(button
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}")))))))
+ (str (text "general:action.save"))))))))
; users should also be able to manage the journal's sub directories here
(details
diff --git a/crates/app/src/public/html/macros.lisp b/crates/app/src/public/html/macros.lisp
index f71deeb..db07623 100644
--- a/crates/app/src/public/html/macros.lisp
+++ b/crates/app/src/public/html/macros.lisp
@@ -68,10 +68,12 @@
(div
("class" "inner")
+ (text "{% if config.service_hosts.tawny -%}")
(a
- ("href" "/chats/0/0")
+ ("href" "{{ config.service_hosts.tawny }}/api/v1/auth/set_token?token=")
(icon (text "message-circle"))
(str (text "communities:label.chats")))
+ (text "{%- endif %}")
(a
("href" "/mail")
(icon (text "mail"))
@@ -354,7 +356,7 @@
(str (text "forge:tab.tickets"))))
(text "{%- endmacro %}")
-(text "{% macro profile_settings_nav_options() -%}")
+(text "{% macro user_settings_nav_options() -%}")
(a
("data-tab-button" "account")
("class" "active")
@@ -368,6 +370,12 @@
(text "{{ icon \"user-round\" }}")
(span
(text "{{ text \"settings:tab.profile\" }}")))
+(a
+ ("data-tab-button" "experience")
+ ("href" "#/experience")
+ (text "{{ icon \"settings-2\" }}")
+ (span
+ (text "{{ text \"settings:tab.experience\" }}")))
(a
("data-tab-button" "theme")
("href" "#/theme")
@@ -393,4 +401,10 @@
(text "{{ icon \"book-user\" }}")
(span
(text "{{ text \"settings:tab.close_friends\" }}")))
+(a
+ ("data-tab-button" "presets")
+ ("href" "#/presets")
+ (icon (text "cooking-pot"))
+ (span
+ (str (text "settings:tab.presets"))))
(text "{%- endmacro %}")
diff --git a/crates/app/src/public/html/mod/profile.lisp b/crates/app/src/public/html/mod/profile.lisp
index 843f40c..efac7d7 100644
--- a/crates/app/src/public/html/mod/profile.lisp
+++ b/crates/app/src/public/html/mod/profile.lisp
@@ -358,7 +358,7 @@
("onclick" "update_user_role(Number.parseInt(document.getElementById('role').value))")
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}"))))
+ (str (text "general:action.save")))))
(div
("class" "card lowered flex flex_col gap_2")
("id" "permission_builder")))
@@ -376,7 +376,7 @@
("onclick" "update_user_secondary_role(Number.parseInt(document.getElementById('secondary_role').value))")
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}"))))
+ (str (text "general:action.save")))))
(div
("class" "card lowered flex flex_col gap_2")
("id" "secondary_permission_builder")))
@@ -409,7 +409,7 @@
SUPPORTER: 1 << 19,
MANAGE_REQUESTS: 1 << 20,
MANAGE_QUESTIONS: 1 << 21,
- MANAGE_CHANNELS: 1 << 22,
+ UNUSED_0: 1 << 22,
MANAGE_MESSAGES: 1 << 23,
MANAGE_UPLOADS: 1 << 24,
MANAGE_EMOJIS: 1 << 25,
diff --git a/crates/app/src/public/html/mod/stats.lisp b/crates/app/src/public/html/mod/stats.lisp
index 9d02f2e..3a3bbeb 100644
--- a/crates/app/src/public/html/mod/stats.lisp
+++ b/crates/app/src/public/html/mod/stats.lisp
@@ -20,16 +20,11 @@
(text "Active user streams: "))
(span
(text "{{ active_users }}")))
- (li
- (b
- (text "Active chat subscriptions: "))
- (span
- (text "{{ active_users_chats }}")))
(li
(b
(text "Socket tasks: "))
(span
- (text "{{ (active_users_chats + active_users) * 3 }}"))))
+ (text "{{ active_users * 3 }}"))))
(hr)
(ul
diff --git a/crates/app/src/public/html/post/post.lisp b/crates/app/src/public/html/post/post.lisp
index ad6fcb0..61214cf 100644
--- a/crates/app/src/public/html/post/post.lisp
+++ b/crates/app/src/public/html/post/post.lisp
@@ -148,7 +148,7 @@
("onclick" "save_context()")
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}")))
+ (str (text "general:action.save"))))
(script
(text "setTimeout(async () => {
const ui = await ns(\"ui\");
@@ -286,7 +286,7 @@
("class" "flex gap_2")
(text "{{ components::emoji_picker(element_id=\"new_content\", render_dialog=false) }}")
(button
- (text "{{ text \"general:action.save\" }}")))))
+ (str (text "general:action.save"))))))
(script
(text "async function edit_post_from_form(e) {
e.preventDefault();
diff --git a/crates/app/src/public/html/profile/base.lisp b/crates/app/src/public/html/profile/base.lisp
index 72d9ebf..31e6404 100644
--- a/crates/app/src/public/html/profile/base.lisp
+++ b/crates/app/src/public/html/profile/base.lisp
@@ -115,7 +115,7 @@
("class" "fade")
(text "{{ profile.username }}"))))
(div
- ("class" "card flex flex_col items_center gap_2")
+ ("class" "card flex flex_col items_center small gap_2")
("id" "social")
(text "{% if profile.settings.status -%}")
(p
@@ -159,7 +159,20 @@
(div
("id" "bio")
("class" "card small no_p_margin")
- (text "{{ profile.settings.biography|markdown|safe }}"))
+ (text "{{ profile.settings.biography|markdown|safe }}")
+
+ (text "{% if profile.settings.location|length > 0 -%}")
+ (span ("class" "flex items_center gap_2 flex_wrap") (icon (text "map-pin")) (text "{{ profile.settings.location }}"))
+ (text "{%- endif %}")
+
+ (text "{% for link in profile.settings.links -%}")
+ (span
+ ("class" "flex items_center gap_2 flex_wrap")
+ (icon (text "link"))
+ (a
+ ("href" "{{ link[1] }}")
+ (text "{{ link[0] }}")))
+ (text "{%- endfor %}"))
(div
("class" "card flex flex_col gap_2")
(text "{% if user -%}")
@@ -265,12 +278,18 @@
(span
(text "{{ text \"auth:action.unblock\" }}")))
(text "{%- endif %} {% if not profile.settings.private_chats or is_following_you %}")
- (button
- ("onclick" "create_group_chat()")
- ("class" "lowered")
- (text "{{ icon \"message-circle\" }}")
- (span
- (text "{{ text \"auth:action.message\" }}")))
+ (text "{% if config.service_hosts.tawny -%}")
+ (a
+ ("href" "{{ config.service_hosts.tawny }}/@{{ profile.username }}")
+ ("class" "button lowered")
+ (icon (text "egg"))
+ (span (text "Tawny")))
+ (a
+ ("href" "{{ config.service_hosts.tawny }}/@{{ profile.username }}/confirm_dm")
+ ("class" "button lowered")
+ (icon (text "message-circle"))
+ (span (str (text "auth:action.message"))))
+ (text "{%- endif %}")
(text "{%- endif %} {% if not profile.settings.private_mails or is_following_you %}")
(a
("href" "/mail/compose?receivers={{ profile.username }}")
@@ -294,31 +313,7 @@
(text "{{ text \"general:action.manage\" }}")))
(text "{%- endif %}")
(script
- (text "globalThis.create_group_chat = async () => {
- fetch(\"/api/v1/channels/group\", {
- method: \"POST\",
- headers: {
- \"Content-Type\": \"application/json\",
- },
- body: JSON.stringify({
- title: \"{{ user.username }} & {{ profile.username }}\",
- members: [\"{{ profile.id }}\"],
- }),
- })
- .then((res) => res.json())
- .then((res) => {
- trigger(\"atto::toast\", [
- res.ok ? \"success\" : \"error\",
- res.message,
- ]);
-
- if (res.ok) {
- window.location.href = `/chats/0/${res.payload}`;
- }
- });
- };
-
- globalThis.request_transfer = async () => {
+ (text "globalThis.request_transfer = async () => {
await trigger(\"atto::debounce\", [\"economy::transfer\"]);
const amount = Number.parseInt((await trigger(\"atto::prompt\", [\"Request amount:\"])) || \"0\");
diff --git a/crates/app/src/public/html/profile/posts.lisp b/crates/app/src/public/html/profile/posts.lisp
index 7e1ee28..ee24aaf 100644
--- a/crates/app/src/public/html/profile/posts.lisp
+++ b/crates/app/src/public/html/profile/posts.lisp
@@ -2,7 +2,6 @@
(div
("style" "display: contents")
(text "{{ components::create_question_form(receiver=profile.id, header=profile.settings.motivational_header, drawing_enabled=profile.settings.enable_drawings, allow_anonymous=profile.settings.allow_anonymous_questions) }}"))
-
(text "{%- endif %}")
(text "{{ macros::profile_nav(selected=\"posts\") }}")
(div
diff --git a/crates/app/src/public/html/profile/replies.lisp b/crates/app/src/public/html/profile/replies.lisp
index 66b5ec3..0494e2a 100644
--- a/crates/app/src/public/html/profile/replies.lisp
+++ b/crates/app/src/public/html/profile/replies.lisp
@@ -24,5 +24,4 @@
(div
("class" "card flex flex_col gap_4")
(text "{% for post in posts %} {% if post[2].read_access == \"Everybody\" -%} {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[3], post=post[0], owner=post[1], secondary=true, community=post[2], show_community=true, can_manage_post=is_self) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2], can_manage_post=is_self, poll=post[5]) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=posts|length) }}")))
-
(text "{% endblock %}")
diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp
index 0a69f2b..7a91e0d 100644
--- a/crates/app/src/public/html/profile/settings.lisp
+++ b/crates/app/src/public/html/profile/settings.lisp
@@ -7,7 +7,7 @@
; nav desktop
(menu
("class" "desktop col")
- (text "{{ macros::profile_settings_nav_options() }}"))
+ (text "{{ macros::user_settings_nav_options() }}"))
; content
(main
@@ -35,7 +35,7 @@
(span ("class" "current_tab_text") (text "account")))
(div
("class" "inner left")
- (text "{{ macros::profile_settings_nav_options() }}"))))
+ (text "{{ macros::user_settings_nav_options() }}"))))
; ...
(div
@@ -43,81 +43,70 @@
("data-tab" "presets")
(div
("class" "card lowered flex flex_col gap_2")
- (a
- ("href" "#/account")
- ("class" "button secondary")
- (icon (text "arrow-left"))
- (span
- (str (text "general:action.back"))))
- (div
- ("class" "card_nest")
+ (p (text "Not sure where to start? Try some settings presets!"))
+ (details
+ ("class" "w_full accordion")
+ (summary
+ ("class" "button raised")
+ (icon (text "rss"))
+ (text "Microblogging"))
+
(div
- ("class" "card flex items_center gap_2 small")
- (icon (text "cooking-pot"))
- (span
- (str (text "settings:tab.presets"))))
+ ("class" "inner flex flex_col gap_2")
+ (p ("class" "fade") (text "Focus on yourself and your communities."))
+ (ul ("id" "preset_microblogging_ul"))
+ (button
+ ("onclick" "apply_preset(PRESET_MICROBLOGGING)")
+ (icon (text "settings"))
+ (str (text "general:action.apply")))))
+
+ (details
+ ("class" "w_full accordion")
+ (summary
+ ("class" "button raised")
+ (icon (text "message-circle-heart"))
+ (text "Q&A"))
+
(div
- ("class" "card flex flex_col gap_2 secondary")
- (p (text "Not sure where to start? Try some settings presets!"))
- (details
- ("class" "w_full accordion")
- (summary
- (icon (text "rss"))
- (text "Microblogging"))
+ ("class" "inner flex flex_col gap_2")
+ (p ("class" "fade") (text "Just like Neospring!"))
+ (ul ("id" "preset_questions_ul"))
+ (button
+ ("onclick" "apply_preset(PRESET_QUESTIONS)")
+ (icon (text "settings"))
+ (str (text "general:action.apply")))))
- (div
- ("class" "inner flex flex_col gap_2")
- (p ("class" "fade") (text "Focus on yourself and your communities."))
- (ul ("id" "preset_microblogging_ul"))
- (button
- ("onclick" "apply_preset(PRESET_MICROBLOGGING)")
- (icon (text "settings"))
- (str (text "general:action.apply")))))
+ (details
+ ("class" "w_full accordion")
+ (summary
+ ("class" "button raised")
+ (icon (text "key"))
+ (text "Private"))
- (details
- ("class" "w_full accordion")
- (summary
- (icon (text "message-circle-heart"))
- (text "Q&A"))
+ (div
+ ("class" "inner flex flex_col gap_2")
+ (p ("class" "fade") (text "This preset allows you to keep your profile and posts hidden to people you aren't following."))
+ (ul ("id" "preset_private_ul"))
+ (button
+ ("onclick" "apply_preset(PRESET_PRIVATE)")
+ (icon (text "settings"))
+ (str (text "general:action.apply")))))
- (div
- ("class" "inner flex flex_col gap_2")
- (p ("class" "fade") (text "Just like Neospring!"))
- (ul ("id" "preset_questions_ul"))
- (button
- ("onclick" "apply_preset(PRESET_QUESTIONS)")
- (icon (text "settings"))
- (str (text "general:action.apply")))))
+ (details
+ ("class" "w_full accordion")
+ (summary
+ ("class" "button raised")
+ (icon (text "eye-closed"))
+ (text "NSFW"))
- (details
- ("class" "w_full accordion")
- (summary
- (icon (text "key"))
- (text "Private"))
-
- (div
- ("class" "inner flex flex_col gap_2")
- (p ("class" "fade") (text "This preset allows you to keep your profile and posts hidden to people you aren't following."))
- (ul ("id" "preset_private_ul"))
- (button
- ("onclick" "apply_preset(PRESET_PRIVATE)")
- (icon (text "settings"))
- (str (text "general:action.apply")))))
-
- (details
- ("class" "w_full accordion")
- (summary
- (icon (text "eye-closed"))
- (text "NSFW"))
-
- (div
- ("class" "inner flex flex_col gap_2")
- (p ("class" "fade") (text "NSFW content is allowed if it is hidden from main timelines. This preset will help you do that quickly."))
- (ul ("id" "preset_nsfw_ul"))
- (button
- ("onclick" "apply_preset(PRESET_NSFW)")
- (icon (text "settings"))
- (str (text "general:action.apply")))))))))
+ (div
+ ("class" "inner flex flex_col gap_2")
+ (p ("class" "fade") (text "NSFW content is allowed if it is hidden from main timelines. This preset will help you do that quickly."))
+ (ul ("id" "preset_nsfw_ul"))
+ (button
+ ("onclick" "apply_preset(PRESET_NSFW)")
+ (icon (text "settings"))
+ (str (text "general:action.apply")))))))
(div
("class" "w_full flex flex_col gap_2")
@@ -180,61 +169,6 @@
(span
(text "{{ text \"settings:tab.billing\" }}"))))
(text "{%- endif %}")
-
- (div
- ("class" "card_nest")
- ("ui_ident" "home_timeline")
- (div
- ("class" "card small")
- (b
- (text "Home timeline")))
- (div
- ("class" "card")
- (select
- ("onchange" "window.SETTING_SET_FUNCTIONS[0]('default_timeline', event.target.selectedOptions[0].value.startsWith('{') ? JSON.parse(event.target.selectedOptions[0].value) : event.target.selectedOptions[0].value)")
- (option
- ("value" "MyCommunities")
- ("selected" "{% if home == '/' -%}true{% else %}false{%- endif %}")
- (text "My communities"))
- (option
- ("value" "MyCommunitiesQuestions")
- ("selected" "{% if home == '/questions' -%}true{% else %}false{%- endif %}")
- (text "My communities (questions)"))
- (option
- ("value" "PopularPosts")
- ("selected" "{% if home == '/popular' -%}true{% else %}false{%- endif %}")
- (text "Popular"))
- (option
- ("value" "PopularQuestions")
- ("selected" "{% if home == '/popular/questions' -%}true{% else %}false{%- endif %}")
- (text "Popular (questions)"))
- (option
- ("value" "FollowingPosts")
- ("selected" "{% if home == '/following' -%}true{% else %}false{%- endif %}")
- (text "Following"))
- (option
- ("value" "FollowingQuestions")
- ("selected" "{% if home == '/following/questions' -%}true{% else %}false{%- endif %}")
- (text "Following (questions)"))
- (option
- ("value" "AllPosts")
- ("selected" "{% if home == '/all' -%}true{% else %}false{%- endif %}")
- (text "All"))
- (option
- ("value" "AllQuestions")
- ("selected" "{% if home == '/all/questions' -%}true{% else %}false{%- endif %}")
- (text "All (questions)"))
- (text "{% for stack in stacks %}")
- (text "")
- (text "{% endfor %}"))
- (span
- ("class" "fade")
- (text "This represents the timeline the home button takes you to."))))
(div
("class" "card_nest desktop")
("ui_ident" "notifications")
@@ -282,7 +216,7 @@
(button
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}"))))))
+ (str (text "general:action.save")))))))
(div
("class" "card_nest")
("ui_ident" "delete_account")
@@ -337,7 +271,7 @@
("id" "save_button")
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}"))))
+ (str (text "general:action.save")))))
(div
("class" "w_full flex flex_col gap_2 hidden")
("data-tab" "account/security")
@@ -444,7 +378,7 @@
(button
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}")))))))))
+ (str (text "general:action.save"))))))))))
(div
("class" "w_full flex flex_col gap_2 hidden")
("data-tab" "account/following")
@@ -991,13 +925,13 @@
(span
("class" "fade")
(text "Use an image of 1100x350px for the best results."))))
+
(div
("class" "card_nest")
("ui_ident" "default_profile_page")
(div
("class" "card small")
- (b
- (text "Default profile tab")))
+ (b (text "Default profile tab")))
(div
("class" "card")
(select
@@ -1013,10 +947,36 @@
(span
("class" "fade")
(text "This represents the timeline that is shown on your profile by default."))))
+
+ (div
+ ("class" "card_nest")
+ ("ui_ident" "user_links")
+ (div
+ ("class" "card small")
+ (b (text "My links")))
+ (div
+ ("class" "card flex flex_col gap_2")
+ (button
+ ("onclick" "add_link()")
+ (icon (text "plus"))
+ (text "Add link"))
+
+ (ul ("id" "user_links")))))
+ (button
+ ("onclick" "save_settings()")
+ ("id" "save_button")
+ (icon (text "check"))
+ (span
+ (str (text "general:action.save")))))
+ (div
+ ("class" "w_full hidden flex flex_col gap_2")
+ ("data-tab" "experience")
+ (div
+ ("class" "card lowered flex flex_col gap_2")
+ ("id" "experience_settings")
(div
("class" "flex flex_col gap_2")
("ui_ident" "show_presets")
- (hr ("class" "margin"))
(div
("class" "card_nest")
(div
@@ -1028,13 +988,67 @@
(p
(text "Quickly set up your account with ")
(a ("href" "/settings#/presets") (text "settings presets"))
- (text "!"))))))
+ (text "!")))))
+ (div
+ ("class" "card_nest")
+ ("ui_ident" "home_timeline")
+ (div
+ ("class" "card small")
+ (b
+ (text "Home timeline")))
+ (div
+ ("class" "card")
+ (select
+ ("onchange" "window.SETTING_SET_FUNCTIONS[0]('default_timeline', event.target.selectedOptions[0].value.startsWith('{') ? JSON.parse(event.target.selectedOptions[0].value) : event.target.selectedOptions[0].value)")
+ (option
+ ("value" "MyCommunities")
+ ("selected" "{% if home == '/' -%}true{% else %}false{%- endif %}")
+ (text "My communities"))
+ (option
+ ("value" "MyCommunitiesQuestions")
+ ("selected" "{% if home == '/questions' -%}true{% else %}false{%- endif %}")
+ (text "My communities (questions)"))
+ (option
+ ("value" "PopularPosts")
+ ("selected" "{% if home == '/popular' -%}true{% else %}false{%- endif %}")
+ (text "Popular"))
+ (option
+ ("value" "PopularQuestions")
+ ("selected" "{% if home == '/popular/questions' -%}true{% else %}false{%- endif %}")
+ (text "Popular (questions)"))
+ (option
+ ("value" "FollowingPosts")
+ ("selected" "{% if home == '/following' -%}true{% else %}false{%- endif %}")
+ (text "Following"))
+ (option
+ ("value" "FollowingQuestions")
+ ("selected" "{% if home == '/following/questions' -%}true{% else %}false{%- endif %}")
+ (text "Following (questions)"))
+ (option
+ ("value" "AllPosts")
+ ("selected" "{% if home == '/all' -%}true{% else %}false{%- endif %}")
+ (text "All"))
+ (option
+ ("value" "AllQuestions")
+ ("selected" "{% if home == '/all/questions' -%}true{% else %}false{%- endif %}")
+ (text "All (questions)"))
+ (text "{% for stack in stacks %}")
+ (text "")
+ (text "{% endfor %}"))
+ (span
+ ("class" "fade")
+ (text "This represents the timeline the home button takes you to.")))))
(button
("onclick" "save_settings()")
("id" "save_button")
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}"))))
+ (str (text "general:action.save")))))
(div
("class" "card w_full lowered hidden flex flex_col gap_2")
("data-tab" "sessions")
@@ -1193,7 +1207,7 @@
("id" "save_button")
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}"))))
+ (str (text "general:action.save")))))
(div
("class" "card w_full lowered hidden flex flex_col gap_2")
("data-tab" "grants")
@@ -1595,7 +1609,7 @@
`data:image/png;base64,${qr}`;
document.getElementById(
\"totp_recovery_codes\",
- ).innerText = recovery_codes.join(\"\n\");
+ ).innerText = recovery_codes.join(\"\\n\");
document.getElementById(\"totp_stuff\").style.display =
\"contents\";
@@ -1787,12 +1801,13 @@
document.getElementById(\"account_settings\");
const profile_settings =
document.getElementById(\"profile_settings\");
+ const experience_settings =
+ document.getElementById(\"experience_settings\");
const theme_settings = document.getElementById(\"theme_settings\");
ui.refresh_container(account_settings, [
\"supporter_ad\",
\"account_settings_tabs\",
- \"home_timeline\",
\"notifications\",
\"change_username\",
\"delete_account\",
@@ -1802,6 +1817,11 @@
\"change_avatar\",
\"change_banner\",
\"default_profile_page\",
+ \"user_links\",
+ ]);
+ ui.refresh_container(experience_settings, [
+ \"supporter_ad\",
+ \"home_timeline\",
\"show_presets\",
]);
ui.refresh_container(theme_settings, [
@@ -1814,7 +1834,7 @@
]);
ui.generate_settings_ui(
- account_settings,
+ profile_settings,
[
[
[\"display_name\", \"Display name\"],
@@ -1835,50 +1855,23 @@
'This biography is only shown to users you are not following while your account is private.',
},
],
+ [
+ [\"location\", \"Location\"],
+ \"{{ profile.settings.location }}\",
+ \"input\",
+ ],
[[\"status\", \"Status\"], settings.status, \"textarea\"],
[
[\"warning\", \"Profile warning\"],
settings.warning,
\"textarea\",
],
- [[\"muted\", \"Muted phrases\"], settings.muted.join(\"\\n\"), \"textarea\", {
- embed_html:
- 'Muted phrases should all be on new lines.',
- }],
- [[], \"Accessibility\", \"title\"],
- [
- [\"large_text\", \"Increase UI text size\"],
- \"{{ profile.settings.large_text }}\",
- \"checkbox\",
- ],
- [
- [\"use_system_font\", \"Always use system font instead\"],
- \"{{ profile.settings.use_system_font }}\",
- \"checkbox\",
- ],
- [
- [\"paged_timelines\", \"Make timelines paged instead of infinitely scrolled\"],
- \"{{ profile.settings.paged_timelines }}\",
- \"checkbox\",
- ],
- [
- [\"auto_clear_notifs\", \"Automatically clear all notifications when you open the notifications page\"],
- \"{{ profile.settings.auto_clear_notifs }}\",
- \"checkbox\",
- ],
],
settings,
- {
- muted: (new_muted) => {
- settings.muted = new_muted
- .split(\"\\n\")
- .map((t) => t.trim());
- },
- },
);
ui.generate_settings_ui(
- profile_settings,
+ experience_settings,
[
[[], \"Privacy\", \"title\"],
[
@@ -1988,6 +1981,10 @@
\"{{ profile.settings.hide_username_badges }}\",
\"checkbox\",
],
+ [[\"muted\", \"Muted phrases\"], settings.muted.join(\"\\n\"), \"textarea\", {
+ embed_html:
+ 'Muted phrases should all be on new lines.',
+ }],
[[], \"Questions\", \"title\"],
[
[
@@ -2074,8 +2071,36 @@
\"{{ profile.settings.disable_achievements }}\",
\"checkbox\",
],
+ [[], \"Accessibility\", \"title\"],
+ [
+ [\"large_text\", \"Increase UI text size\"],
+ \"{{ profile.settings.large_text }}\",
+ \"checkbox\",
+ ],
+ [
+ [\"use_system_font\", \"Always use system font instead\"],
+ \"{{ profile.settings.use_system_font }}\",
+ \"checkbox\",
+ ],
+ [
+ [\"paged_timelines\", \"Make timelines paged instead of infinitely scrolled\"],
+ \"{{ profile.settings.paged_timelines }}\",
+ \"checkbox\",
+ ],
+ [
+ [\"auto_clear_notifs\", \"Automatically clear all notifications when you open the notifications page\"],
+ \"{{ profile.settings.auto_clear_notifs }}\",
+ \"checkbox\",
+ ],
],
settings,
+ {
+ muted: (new_muted) => {
+ settings.muted = new_muted
+ .split(\"\\n\")
+ .map((t) => t.trim());
+ },
+ },
);
const can_use_custom_css =
@@ -2351,5 +2376,40 @@
anchor.click();
anchor.remove();
};
+
+ // links
+ function render_links() {
+ document.getElementById(\"user_links\").innerHTML = \"\";
+
+ let i = 0;
+ for (const link of settings.links) {
+ document.getElementById(\"user_links\").innerHTML += `
${link[0]} (${link[1]}) (delete)`;
+ i += 1;
+ }
+ }
+
+ globalThis.add_link = async () => {
+ const label = await trigger(\"atto::prompt\", [\"Link label:\"]);
+
+ if (!label) {
+ return;
+ }
+
+ const url = await trigger(\"atto::prompt\", [\"Link URL:\"]);
+
+ if (!url) {
+ return;
+ }
+
+ settings.links.push([label, url]);
+ render_links();
+ }
+
+ globalThis.remove_link = (idx) => {
+ settings.links.splice(idx, 1);
+ document.getElementById(`link_${idx}`).remove();
+ }
+
+ render_links();
});"))))
(text "{% endblock %}")
diff --git a/crates/app/src/public/html/root.lisp b/crates/app/src/public/html/root.lisp
index 5073670..6af46ef 100644
--- a/crates/app/src/public/html/root.lisp
+++ b/crates/app/src/public/html/root.lisp
@@ -33,6 +33,7 @@
classes: {},
service_hosts: {
buckets: \"{{ config.service_hosts.buckets|safe }}\",
+ tawny: \"{{ config.service_hosts.tawny|safe }}\",
}
};
diff --git a/crates/app/src/public/html/stacks/add_user.lisp b/crates/app/src/public/html/stacks/add_user.lisp
index 80ab718..d95db7a 100644
--- a/crates/app/src/public/html/stacks/add_user.lisp
+++ b/crates/app/src/public/html/stacks/add_user.lisp
@@ -9,7 +9,7 @@
("class" "card_nest")
(div
("class" "card small flex items_center gap_2")
- (text "{{ components::avatar(id=add_user.username, size=\"24px\") }}")
+ (text "{{ components::avatar(id=add_user.id, size=\"24px\") }}")
(text "{{ components::full_username(user=add_user) }}"))
(div
("class" "card flex flex_col gap_2")
diff --git a/crates/app/src/public/html/stacks/manage.lisp b/crates/app/src/public/html/stacks/manage.lisp
index 95a4942..e34fa4d 100644
--- a/crates/app/src/public/html/stacks/manage.lisp
+++ b/crates/app/src/public/html/stacks/manage.lisp
@@ -117,7 +117,7 @@
(button
(icon (text "check"))
(span
- (text "{{ text \"general:action.save\" }}"))))))
+ (str (text "general:action.save")))))))
(text "{% if not stack.is_locked -%}")
(div
("class" "card_nest")
diff --git a/crates/app/src/public/js/atto.js b/crates/app/src/public/js/atto.js
index 847baad..08b3f5c 100644
--- a/crates/app/src/public/js/atto.js
+++ b/crates/app/src/public/js/atto.js
@@ -860,7 +860,8 @@ media_theme_pref();
anchor.href.startsWith("https://buy.stripe.com") ||
anchor.href.startsWith("https://billing.stripe.com") ||
anchor.href.startsWith("https://last.fm") ||
- anchor.href.startsWith("atto://")
+ anchor.href.startsWith("atto://") ||
+ anchor.href.startsWith(_app_base.service_hosts.tawny)
) {
continue;
}
diff --git a/crates/app/src/public/js/me.js b/crates/app/src/public/js/me.js
index ef41ad8..beb74d6 100644
--- a/crates/app/src/public/js/me.js
+++ b/crates/app/src/public/js/me.js
@@ -311,19 +311,30 @@
community,
do_not_redirect = false,
is_stack = false,
+ uploads = [],
) => {
await trigger("atto::debounce", ["posts::create"]);
return new Promise((resolve, _) => {
- fetch(`/api/v1/posts/${id}/repost`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
+ // create body
+ const body = new FormData();
+
+ for (const file of uploads) {
+ body.append(file.name, file);
+ }
+
+ body.append(
+ "body",
+ JSON.stringify({
content,
community: !is_stack ? community : "0",
stack: is_stack ? community : "0",
}),
+ );
+
+ // send
+ fetch(`/api/v1/posts/${id}/repost`, {
+ method: "POST",
+ body,
})
.then((res) => res.json())
.then((res) => {
@@ -662,6 +673,15 @@
});
// token switcher
+ self.define("token_links", ({ $ }) => {
+ for (const anchor of Array.from(document.querySelectorAll("a"))) {
+ if (anchor.href.endsWith("/set_token?token=")) {
+ anchor.href += $.LOGIN_ACCOUNT_TOKENS[globalThis.CURRENT_USER];
+ continue;
+ }
+ }
+ });
+
self.define("append_associations", (_, tokens) => {
fetch("/api/v1/auth/user/me/append_associations", {
method: "PUT",
diff --git a/crates/app/src/routes/api/v1/auth/profile.rs b/crates/app/src/routes/api/v1/auth/profile.rs
index e378567..31a5018 100644
--- a/crates/app/src/routes/api/v1/auth/profile.rs
+++ b/crates/app/src/routes/api/v1/auth/profile.rs
@@ -153,28 +153,8 @@ pub async fn update_user_settings_request(
}
// check lengths
- if req.display_name.len() > 32 {
- return Json(Error::DataTooLong("display name".to_string()).into());
- }
-
- if req.warning.len() > 2048 {
- return Json(Error::DataTooLong("warning".to_string()).into());
- }
-
- if req.status.len() > 256 {
- return Json(Error::DataTooLong("status".to_string()).into());
- }
-
- if req.biography.len() > 4096 {
- return Json(Error::DataTooLong("warning".to_string()).into());
- }
-
- if req.mail_signature.len() > 2048 {
- return Json(Error::DataTooLong("mail signature".to_string()).into());
- }
-
- if req.forum_signature.len() > 2048 {
- return Json(Error::DataTooLong("forum signature".to_string()).into());
+ if let Err(e) = req.verify_values() {
+ return Json(e.into());
}
// check percentage themes
diff --git a/crates/app/src/routes/api/v1/channels/channels.rs b/crates/app/src/routes/api/v1/channels/channels.rs
deleted file mode 100644
index 2059a0f..0000000
--- a/crates/app/src/routes/api/v1/channels/channels.rs
+++ /dev/null
@@ -1,354 +0,0 @@
-use axum::{Extension, Json, extract::Path, response::IntoResponse};
-use crate::cookie::CookieJar;
-use tetratto_core::model::{oauth, channels::Channel, ApiReturn, Error};
-use crate::{
- get_user_from_token,
- routes::api::v1::{
- CreateChannel, CreateGroupChannel, KickMember, UpdateChannelPosition, UpdateChannelTitle,
- },
- State,
-};
-
-pub async fn create_request(
- jar: CookieJar,
- Extension(data): Extension,
- Json(req): Json,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityCreateChannels) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- match data
- .create_channel(Channel::new(
- match req.community.parse::() {
- Ok(c) => c,
- Err(e) => return Json(Error::MiscError(e.to_string()).into()),
- },
- user.id,
- 0,
- req.title,
- ))
- .await
- {
- Ok(_) => Json(ApiReturn {
- ok: true,
- message: "Channel created".to_string(),
- payload: (),
- }),
- Err(e) => Json(e.into()),
- }
-}
-
-pub async fn create_group_request(
- jar: CookieJar,
- Extension(data): Extension,
- Json(req): Json,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- let mut members: Vec = Vec::new();
-
- for member in req.members {
- members.push(match member.parse::() {
- Ok(c) => c,
- Err(e) => return Json(Error::MiscError(e.to_string()).into()),
- })
- }
-
- // check for existing
- if members.len() == 1 {
- let other_user = members.first().unwrap().to_owned();
- if let Ok(channel) = data.get_channel_by_owner_member(user.id, other_user).await {
- return Json(ApiReturn {
- ok: true,
- message: "Channel exists".to_string(),
- payload: Some(channel.id.to_string()),
- });
- }
- }
-
- // check member permissions
- for member in &members {
- let other_user = match data.get_user_by_id(member.to_owned()).await {
- Ok(ua) => ua,
- Err(e) => return Json(e.into()),
- };
-
- if other_user.settings.private_chats
- && data
- .get_userfollow_by_initiator_receiver(other_user.id, user.id)
- .await
- .is_err()
- {
- return Json(Error::NotAllowed.into());
- }
- }
-
- // ...
- let mut props = Channel::new(0, user.id, 0, req.title);
- props.members = members;
- let id = props.id;
-
- match data.create_channel(props).await {
- Ok(_) => Json(ApiReturn {
- ok: true,
- message: "Channel created".to_string(),
- payload: Some(id.to_string()),
- }),
- Err(e) => Json(e.into()),
- }
-}
-
-pub async fn delete_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path(id): Path,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- match data.delete_channel(id, &user).await {
- Ok(_) => Json(ApiReturn {
- ok: true,
- message: "Channel deleted".to_string(),
- payload: (),
- }),
- Err(e) => Json(e.into()),
- }
-}
-
-pub async fn update_title_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path(id): Path,
- Json(req): Json,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- match data.update_channel_title(id, &user, &req.title).await {
- Ok(_) => Json(ApiReturn {
- ok: true,
- message: "Channel updated".to_string(),
- payload: (),
- }),
- Err(e) => Json(e.into()),
- }
-}
-
-pub async fn update_position_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path(id): Path,
- Json(req): Json,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- match data.update_channel_position(id, &user, req.position).await {
- Ok(_) => Json(ApiReturn {
- ok: true,
- message: "Channel updated".to_string(),
- payload: (),
- }),
- Err(e) => Json(e.into()),
- }
-}
-
-pub async fn add_member_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path(id): Path,
- Json(req): Json,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- match data.add_channel_member(id, user, req.member).await {
- Ok(_) => Json(ApiReturn {
- ok: true,
- message: "Member added".to_string(),
- payload: (),
- }),
- Err(e) => Json(e.into()),
- }
-}
-
-pub async fn kick_member_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path(id): Path,
- Json(req): Json,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- match data
- .remove_channel_member(
- id,
- user,
- match req.member.parse::() {
- Ok(c) => c,
- Err(e) => return Json(Error::MiscError(e.to_string()).into()),
- },
- )
- .await
- {
- Ok(_) => Json(ApiReturn {
- ok: true,
- message: "Member removed".to_string(),
- payload: (),
- }),
- Err(e) => Json(e.into()),
- }
-}
-
-pub async fn get_dm_channels_request(
- jar: CookieJar,
- Extension(data): Extension,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- match data.get_channels_by_user(user.id).await {
- Ok(c) => Json(ApiReturn {
- ok: true,
- message: "Success".to_string(),
- payload: Some(c),
- }),
- Err(e) => Json(e.into()),
- }
-}
-
-pub async fn get_community_channels_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path(id): Path,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- if data
- .get_membership_by_owner_community_no_void(user.id, id)
- .await
- .is_err()
- {
- // must be a member of the community to request channels
- return Json(Error::NotAllowed.into());
- }
-
- match data.get_channels_by_community(id).await {
- Ok(c) => Json(ApiReturn {
- ok: true,
- message: "Success".to_string(),
- payload: Some(c),
- }),
- Err(e) => Json(e.into()),
- }
-}
-
-pub async fn get_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path(id): Path,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- if get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels).is_none() {
- return Json(Error::NotAllowed.into());
- }
-
- match data.get_channel_by_id(id).await {
- Ok(c) => Json(ApiReturn {
- ok: true,
- message: "Success".to_string(),
- payload: Some(c),
- }),
- Err(e) => Json(e.into()),
- }
-}
-
-pub async fn mute_channel_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path(id): Path,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageChannelMutes) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- if user.channel_mutes.contains(&id) {
- return Json(Error::MiscError("Channel already muted".to_string()).into());
- }
-
- user.channel_mutes.push(id);
- match data
- .update_user_channel_mutes(user.id, user.channel_mutes)
- .await
- {
- Ok(_) => Json(ApiReturn {
- ok: true,
- message: "Channel muted".to_string(),
- payload: (),
- }),
- Err(e) => Json(e.into()),
- }
-}
-
-pub async fn unmute_channel_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path(id): Path,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageChannelMutes) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- let pos = match user.channel_mutes.iter().position(|x| *x == id) {
- Some(x) => x,
- None => return Json(Error::MiscError("Channel not muted".to_string()).into()),
- };
-
- user.channel_mutes.remove(pos);
- match data
- .update_user_channel_mutes(user.id, user.channel_mutes)
- .await
- {
- Ok(_) => Json(ApiReturn {
- ok: true,
- message: "Channel muted".to_string(),
- payload: (),
- }),
- Err(e) => Json(e.into()),
- }
-}
diff --git a/crates/app/src/routes/api/v1/channels/message_reactions.rs b/crates/app/src/routes/api/v1/channels/message_reactions.rs
deleted file mode 100644
index 5f5c79c..0000000
--- a/crates/app/src/routes/api/v1/channels/message_reactions.rs
+++ /dev/null
@@ -1,103 +0,0 @@
-use crate::{get_user_from_token, routes::api::v1::CreateMessageReaction, State};
-use axum::{Extension, Json, extract::Path, response::IntoResponse};
-use crate::cookie::CookieJar;
-use tetratto_core::model::{channels::MessageReaction, oauth, ApiReturn, Error};
-
-pub async fn get_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path(id): Path,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReact) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- match data
- .get_message_reactions_by_owner_message(user.id, id)
- .await
- {
- Ok(r) => Json(ApiReturn {
- ok: true,
- message: "Reactions exists".to_string(),
- payload: Some(r),
- }),
- Err(e) => Json(e.into()),
- }
-}
-
-pub async fn create_request(
- jar: CookieJar,
- Extension(data): Extension,
- Json(req): Json,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReact) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- let message_id = match req.message.parse::() {
- Ok(n) => n,
- Err(e) => return Json(Error::MiscError(e.to_string()).into()),
- };
-
- // check for existing reaction
- if let Ok(r) = data
- .get_message_reaction_by_owner_message_emoji(user.id, message_id, &req.emoji)
- .await
- {
- if let Err(e) = data.delete_message_reaction(r.id, &user).await {
- return Json(e.into());
- } else {
- return Json(ApiReturn {
- ok: true,
- message: "Reaction removed".to_string(),
- payload: (),
- });
- }
- }
-
- // create reaction
- match data
- .create_message_reaction(MessageReaction::new(user.id, message_id, req.emoji), &user)
- .await
- {
- Ok(_) => Json(ApiReturn {
- ok: true,
- message: "Reaction created".to_string(),
- payload: (),
- }),
- Err(e) => Json(e.into()),
- }
-}
-
-pub async fn delete_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path((id, emoji)): Path<(usize, String)>,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReact) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- let reaction = match data
- .get_message_reaction_by_owner_message_emoji(user.id, id, &emoji)
- .await
- {
- Ok(r) => r,
- Err(e) => return Json(e.into()),
- };
-
- match data.delete_message_reaction(reaction.id, &user).await {
- Ok(_) => Json(ApiReturn {
- ok: true,
- message: "Reaction deleted".to_string(),
- payload: (),
- }),
- Err(e) => Json(e.into()),
- }
-}
diff --git a/crates/app/src/routes/api/v1/channels/messages.rs b/crates/app/src/routes/api/v1/channels/messages.rs
deleted file mode 100644
index 92a5c48..0000000
--- a/crates/app/src/routes/api/v1/channels/messages.rs
+++ /dev/null
@@ -1,352 +0,0 @@
-use std::{collections::HashMap, time::Duration};
-use axum::{
- extract::{
- ws::{Message as WsMessage, WebSocket, WebSocketUpgrade},
- Path, Query,
- },
- response::IntoResponse,
- Extension, Json,
-};
-use crate::cookie::CookieJar;
-use tetratto_core::{
- cache::{Cache, redis::Commands},
- model::{
- oauth,
- auth::User,
- channels::Message,
- socket::{PacketType, SocketMessage, SocketMethod},
- ApiReturn, Error,
- },
- DataManager,
-};
-use crate::{
- get_user_from_token,
- routes::{api::v1::CreateMessage, pages::PaginatedQuery},
- State,
-};
-use serde::Deserialize;
-use futures_util::{sink::SinkExt, stream::StreamExt};
-
-#[derive(Clone, Deserialize)]
-pub struct SocketHeaders {
- pub is_channel: bool,
-}
-
-/// Handle a subscription to the websocket.
-pub async fn subscription_handler(
- jar: CookieJar,
- ws: WebSocketUpgrade,
- Extension(data): Extension,
- Path(id): Path,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadSockets) {
- Some(ua) => ua,
- None => return Err(Error::NotAllowed.to_string()),
- };
-
- let data = data.clone();
- Ok(ws.on_upgrade(|socket| async move {
- tokio::spawn(async move {
- handle_socket(socket, data, id, user).await;
- });
- }))
-}
-
-pub async fn handle_socket(socket: WebSocket, db: DataManager, community_id: String, user: User) {
- let (mut sink, mut stream) = socket.split();
- let mut headers: Option = None;
-
- let channel_id = format!("chats/{community_id}");
-
- // handle incoming messages on socket
- let dbc = db.clone();
- if let Some(Ok(WsMessage::Text(text))) = stream.next().await {
- let data: SocketMessage = match serde_json::from_str(&text.to_string()) {
- Ok(t) => t,
- Err(_) => {
- let _ = sink.close().await;
- return;
- }
- };
-
- if data.method != SocketMethod::Headers && headers.is_none() {
- // we've sent something else before authenticating... that's not right
- let _ = sink.close().await;
- return;
- }
-
- match data.method {
- SocketMethod::Headers => {
- let data: SocketHeaders = data.data();
-
- headers = Some(data.clone());
-
- if data.is_channel {
- // verify permissions for single channel
- let channel = match dbc
- .get_channel_by_id(match community_id.parse::() {
- Ok(c) => c,
- Err(_) => {
- let _ = sink.close().await;
- return;
- }
- })
- .await
- {
- Ok(c) => c,
- Err(_) => {
- let _ = sink.close().await;
- return;
- }
- };
-
- let membership = match dbc
- .get_membership_by_owner_community(user.id, channel.id)
- .await
- {
- Ok(ua) => ua,
- Err(_) => {
- let _ = sink.close().await;
- return;
- }
- };
-
- if !channel.check_read(user.id, Some(membership.role)) {
- let _ = sink.close().await;
- return;
- }
- }
- }
- _ => {
- let _ = sink.close().await;
- return;
- }
- }
- } else {
- sink.close().await.unwrap();
- return;
- }
-
- // get channel permissions
- let headers = headers.unwrap();
-
- let mut channel_read_statuses: HashMap = HashMap::new();
- if !headers.is_channel {
- // check permissions for every channel in community
- let community_id = match community_id.parse::() {
- Ok(c) => c,
- Err(_) => return,
- };
-
- let membership = match dbc
- .get_membership_by_owner_community(user.id, community_id)
- .await
- {
- Ok(ua) => ua,
- Err(_) => {
- return;
- }
- };
-
- for channel in dbc.get_channels_by_community(community_id).await.unwrap() {
- channel_read_statuses.insert(
- channel.id,
- channel.check_read(user.id, Some(membership.role)),
- );
- }
- }
-
- // ...
- let mut recv_task = tokio::spawn(async move {
- while let Some(Ok(WsMessage::Text(text))) = stream.next().await {
- if text != "Close" {
- continue;
- }
-
- // yes, this is an "unclean" disconnection from the socket...
- // i don't care, it works
- drop(stream);
- break;
- }
- });
-
- let dbc = db.clone();
- let channel_id_c = channel_id.clone();
- let mut redis_task = tokio::spawn(async move {
- // forward messages from redis to the socket
- let mut pubsub = dbc.0.1.client.get_async_pubsub().await.unwrap();
-
- pubsub.subscribe(user.id).await.unwrap();
- pubsub.subscribe(channel_id_c).await.unwrap();
-
- // listen for pubsub messages
- let mut pubsub = pubsub.into_on_message();
- while let Some(msg) = pubsub.next().await {
- // payload is a stringified SocketMessage
- let smsg = msg.get_payload::().unwrap();
- let packet: SocketMessage = serde_json::from_str(&smsg).unwrap();
-
- if packet.method == SocketMethod::Forward(PacketType::Ping) {
- // forward with custom message
- if sink.send(WsMessage::Text("Ping".into())).await.is_err() {
- drop(sink);
- break;
- }
- } else if packet.method == SocketMethod::Message {
- // check perms and then forward
- let d: (String, Message) = packet.data();
-
- if let Some(cs) = channel_read_statuses.get(&d.1.channel) {
- if !cs {
- continue;
- }
- } else if !headers.is_channel {
- // since we didn't select by just a channel, there HAS to be
- // an entry for the channel for us to check this message
- continue;
- // we don't need to check messages when we're subscribed to
- // a channel, since that is checked on headers submission when
- // we subscribe to a channel
- }
-
- if sink.send(WsMessage::Text(smsg.into())).await.is_err() {
- drop(sink);
- break;
- }
- } else {
- // forward to client
- if sink.send(WsMessage::Text(smsg.into())).await.is_err() {
- drop(sink);
- break;
- }
- }
- }
- });
-
- let db2c = db.0.1.clone();
- let heartbeat_task = tokio::spawn(async move {
- let mut con = db2c.get_con().await;
- let mut heartbeat = tokio::time::interval(Duration::from_secs(10));
-
- loop {
- con.publish::(
- user.id,
- serde_json::to_string(&SocketMessage {
- method: SocketMethod::Forward(PacketType::Ping),
- data: "Ping".to_string(),
- })
- .unwrap(),
- )
- .unwrap();
-
- heartbeat.tick().await;
- }
- });
-
- db.0.1
- .incr("atto.active_connections:chats".to_string())
- .await;
-
- tokio::select! {
- _ = (&mut recv_task) => redis_task.abort(),
- _ = (&mut redis_task) => recv_task.abort()
- }
-
- heartbeat_task.abort(); // kill
- db.0.1
- .decr("atto.active_connections:chats".to_string())
- .await;
- tracing::info!("socket terminate");
-}
-
-pub async fn create_request(
- jar: CookieJar,
- Extension(data): Extension,
- Json(req): Json,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateMessages) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- match data
- .create_message(Message::new(
- match req.channel.parse::() {
- Ok(c) => c,
- Err(e) => return Json(Error::MiscError(e.to_string()).into()),
- },
- user.id,
- req.content,
- ))
- .await
- {
- Ok(_) => Json(ApiReturn {
- ok: true,
- message: "Message created".to_string(),
- payload: (),
- }),
- Err(e) => Json(e.into()),
- }
-}
-
-pub async fn delete_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path(id): Path,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data, oauth::AppScope::UserDeleteMessages) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- match data.delete_message(id, user).await {
- Ok(_) => Json(ApiReturn {
- ok: true,
- message: "Message deleted".to_string(),
- payload: (),
- }),
- Err(e) => Json(e.into()),
- }
-}
-
-pub async fn from_channel_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path(id): Path,
- Query(props): Query,
-) -> impl IntoResponse {
- let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateMessages) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
-
- let channel = match data.get_channel_by_id(id).await {
- Ok(c) => c,
- Err(e) => return Json(e.into()),
- };
-
- let membership = match data
- .get_membership_by_owner_community(user.id, channel.community)
- .await
- {
- Ok(m) => m,
- Err(e) => return Json(e.into()),
- };
-
- if !channel.check_read(user.id, Some(membership.role)) {
- return Json(Error::NotAllowed.into());
- }
-
- match data.get_messages_by_channel(id, 24, props.page).await {
- Ok(m) => Json(ApiReturn {
- ok: true,
- message: "Success".to_string(),
- payload: Some(m),
- }),
- Err(e) => Json(e.into()),
- }
-}
diff --git a/crates/app/src/routes/api/v1/channels/mod.rs b/crates/app/src/routes/api/v1/channels/mod.rs
deleted file mode 100644
index 33792c3..0000000
--- a/crates/app/src/routes/api/v1/channels/mod.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-pub mod channels;
-pub mod message_reactions;
-pub mod messages;
diff --git a/crates/app/src/routes/api/v1/communities/posts.rs b/crates/app/src/routes/api/v1/communities/posts.rs
index e22a7ff..43e2fb3 100644
--- a/crates/app/src/routes/api/v1/communities/posts.rs
+++ b/crates/app/src/routes/api/v1/communities/posts.rs
@@ -241,7 +241,7 @@ pub async fn create_repost_request(
jar: CookieJar,
Extension(data): Extension,
Path(id): Path,
- Json(req): Json,
+ JsonMultipart(images, req): JsonMultipart,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreatePosts) {
@@ -264,13 +264,69 @@ pub async fn create_repost_request(
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
};
+ // check sizes
+ for img in &images {
+ if img.len() > MAXIMUM_FILE_SIZE {
+ return Json(Error::FileTooLarge.into());
+ }
+ }
+
+ // create uploads
+ for _ in 0..images.len() {
+ props.uploads.push(
+ match data
+ .2
+ .create_upload(MediaUpload::new(
+ MediaType::Webp,
+ props.owner,
+ "post_media".to_string(),
+ ))
+ .await
+ {
+ Ok(u) => u.id,
+ Err(e) => return Json(Error::MiscError(e.to_string()).into()),
+ },
+ );
+ }
+
// ...
+ let uploads = props.uploads.clone();
match data.create_post(props).await {
- Ok(id) => Json(ApiReturn {
- ok: true,
- message: "Post reposted".to_string(),
- payload: Some(id.to_string()),
- }),
+ Ok(id) => {
+ // write to uploads
+ for (i, upload_id) in uploads.iter().enumerate() {
+ let image = match images.get(i) {
+ Some(img) => img,
+ None => {
+ if let Err(e) = data.2.delete_upload(*upload_id).await {
+ return Json(Error::MiscError(e.to_string()).into());
+ }
+
+ continue;
+ }
+ };
+
+ let upload = match data.2.get_upload_by_id(*upload_id).await {
+ Ok(u) => u,
+ Err(e) => return Json(Error::MiscError(e.to_string()).into()),
+ };
+
+ if let Err(e) = save_webp_buffer(
+ &upload.path(&data.2.0.0.directory).to_string(),
+ image.to_vec(),
+ None,
+ ) {
+ return Json(Error::MiscError(e.to_string()).into());
+ }
+ }
+
+ // ...
+ Json(ApiReturn {
+ ok: true,
+ message: "Post reposted".to_string(),
+ payload: Some(id.to_string()),
+ })
+ }
Err(e) => Json(e.into()),
}
}
diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs
index 03593c7..b4e0e4c 100644
--- a/crates/app/src/routes/api/v1/mod.rs
+++ b/crates/app/src/routes/api/v1/mod.rs
@@ -2,7 +2,6 @@ pub mod ads;
pub mod app_data;
pub mod apps;
pub mod auth;
-pub mod channels;
pub mod communities;
pub mod domains;
pub mod journals;
@@ -55,19 +54,6 @@ pub fn routes() -> Router {
.route("/reactions", post(reactions::create_request))
.route("/reactions/{id}", get(reactions::get_request))
.route("/reactions/{id}", delete(reactions::delete_request))
- // message reactions
- .route(
- "/message_reactions",
- post(channels::message_reactions::create_request),
- )
- .route(
- "/message_reactions/{id}",
- get(channels::message_reactions::get_request),
- )
- .route(
- "/message_reactions/{id}/{emoji}",
- delete(channels::message_reactions::delete_request),
- )
// communities
.route(
"/communities/find/{id}",
@@ -586,57 +572,6 @@ pub fn routes() -> Router {
"/service_hooks/stripe/checkout/success",
get(auth::connections::stripe::handle_stupid_fucking_checkout_success_session),
)
- // channels
- .route("/channels", post(channels::channels::create_request))
- .route(
- "/channels/group",
- post(channels::channels::create_group_request),
- )
- .route(
- "/channels/{id}/title",
- post(channels::channels::update_title_request),
- )
- .route(
- "/channels/{id}/move",
- post(channels::channels::update_position_request),
- )
- .route("/channels/{id}", delete(channels::channels::delete_request))
- .route(
- "/channels/{id}/add",
- post(channels::channels::add_member_request),
- )
- .route(
- "/channels/{id}/kick",
- post(channels::channels::kick_member_request),
- )
- .route(
- "/channels/{id}/mute",
- post(channels::channels::mute_channel_request),
- )
- .route(
- "/channels/{id}/mute",
- delete(channels::channels::unmute_channel_request),
- )
- .route("/channels/{id}", get(channels::channels::get_request))
- .route(
- "/channels/community/{id}",
- get(channels::channels::get_community_channels_request),
- )
- .route(
- "/channels/dms",
- get(channels::channels::get_dm_channels_request),
- )
- // messages
- .route(
- "/_connect/{id}",
- any(channels::messages::subscription_handler),
- )
- .route("/messages", post(channels::messages::create_request))
- .route("/messages/{id}", delete(channels::messages::delete_request))
- .route(
- "/messages/from_channel/{id}",
- get(channels::messages::from_channel_request),
- )
// emojis
.route(
"/lookup_emoji",
@@ -1012,39 +947,6 @@ pub struct CreateQuestion {
pub asking_about: String,
}
-#[derive(Deserialize)]
-pub struct CreateChannel {
- pub title: String,
- pub community: String,
-}
-
-#[derive(Deserialize)]
-pub struct CreateGroupChannel {
- pub title: String,
- pub members: Vec,
-}
-
-#[derive(Deserialize)]
-pub struct UpdateChannelTitle {
- pub title: String,
-}
-
-#[derive(Deserialize)]
-pub struct UpdateChannelPosition {
- pub position: i32,
-}
-
-#[derive(Deserialize)]
-pub struct CreateMessage {
- pub content: String,
- pub channel: String,
-}
-
-#[derive(Deserialize)]
-pub struct KickMember {
- pub member: String,
-}
-
#[derive(Deserialize)]
pub struct CreateStack {
pub name: String,
@@ -1185,12 +1087,6 @@ pub struct RenderMarkdown {
pub content: String,
}
-#[derive(Deserialize)]
-pub struct CreateMessageReaction {
- pub message: String,
- pub emoji: String,
-}
-
#[derive(Deserialize)]
pub struct UpdateNoteDir {
pub dir: String,
diff --git a/crates/app/src/routes/pages/chats.rs b/crates/app/src/routes/pages/chats.rs
deleted file mode 100644
index 4134888..0000000
--- a/crates/app/src/routes/pages/chats.rs
+++ /dev/null
@@ -1,380 +0,0 @@
-use super::{render_error, ChatsAppQuery, PaginatedQuery};
-use crate::{State, assets::initial_context, get_lang, get_user_from_token};
-use axum::{
- extract::{Path, Query},
- response::{Html, IntoResponse, Redirect},
- Extension, Json,
-};
-use crate::cookie::CookieJar;
-use tetratto_core::model::{
- channels::Message, communities_permissions::CommunityPermission, permissions::FinePermission,
- Error,
-};
-use serde::Deserialize;
-
-#[derive(Deserialize)]
-pub struct RenderMessage {
- pub data: String,
- pub grouped: bool,
-}
-
-pub async fn redirect_request() -> impl IntoResponse {
- Redirect::to("/chats/0/0")
-}
-
-/// `/chats/{community}/{channel}`
-///
-/// `/chats/0` is for channels the user is part of (not in a community)
-pub async fn app_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path((selected_community, selected_channel)): Path<(usize, usize)>,
- Query(props): Query,
-) -> impl IntoResponse {
- let data = data.read().await;
- let user = match get_user_from_token!(jar, data.0) {
- Some(ua) => ua,
- None => {
- return Err(Html(
- render_error(Error::NotAllowed, &jar, &data, &None).await,
- ));
- }
- };
-
- let membership = match data
- .0
- .get_membership_by_owner_community(user.id, selected_community)
- .await
- {
- Ok(m) => m,
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- };
-
- let can_manage_channels = membership.role.check(CommunityPermission::MANAGE_CHANNELS)
- | user.permissions.check(FinePermission::MANAGE_CHANNELS);
-
- let communities = match data.0.get_memberships_by_owner(user.id).await {
- Ok(p) => match data.0.fill_communities(p).await {
- Ok(p) => p,
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- },
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- };
-
- if selected_community != 0 && selected_channel == 0 {
- let channels = match data.0.get_channels_by_community(selected_community).await {
- Ok(p) => p,
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- };
-
- if let Some(channel) = channels.first() {
- return Ok(Html(format!(
- "",
- selected_community, channel.id, props.nav
- )));
- }
- }
-
- let community = if selected_community != 0 {
- match data.0.get_community_by_id(selected_community).await {
- Ok(p) => Some(p),
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- }
- } else {
- None
- };
-
- let channel = if selected_channel != 0 {
- match data.0.get_channel_by_id(selected_channel).await {
- Ok(p) => {
- if !p.check_read(user.id, Some(membership.role)) {
- return Err(Html(
- render_error(Error::NotAllowed, &jar, &data, &Some(user)).await,
- ));
- }
-
- Some(p)
- }
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- }
- } else {
- None
- };
-
- let lang = get_lang!(jar, data.0);
- let mut context = initial_context(&data.0.0.0, lang, &Some(user.clone())).await;
-
- context.insert("selected_community", &selected_community);
- context.insert("selected_channel", &selected_channel);
- context.insert("membership_role", &membership.role.bits());
- context.insert("page", &props.page);
- context.insert("message", &props.message);
-
- context.insert(
- "can_manage_channels",
- &if selected_community == 0 {
- false
- } else {
- can_manage_channels
- },
- );
-
- context.insert(
- "can_manage_channel",
- &if selected_community == 0 {
- if let Some(ref channel) = channel {
- channel.members.contains(&user.id) | (channel.owner == user.id)
- } else {
- false
- }
- } else {
- can_manage_channels
- },
- );
-
- context.insert("community", &community);
- context.insert("channel", &channel);
- context.insert("communities", &communities);
-
- // return
- Ok(Html(data.1.render("chats/app.html", &context).unwrap()))
-}
-
-/// `/chats/{community}/{channel}/_stream`
-pub async fn stream_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path((community, channel)): Path<(usize, usize)>,
- Query(props): Query,
-) -> impl IntoResponse {
- let data = data.read().await;
- let user = match get_user_from_token!(jar, data.0) {
- Some(ua) => ua,
- None => {
- return Err(Html(
- render_error(Error::NotAllowed, &jar, &data, &None).await,
- ));
- }
- };
-
- let ignore_users = crate::ignore_users_gen!(user!, data);
-
- let channel = match data.0.get_channel_by_id(channel).await {
- Ok(c) => c,
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- };
-
- let membership = match data
- .0
- .get_membership_by_owner_community(user.id, community)
- .await
- {
- Ok(m) => m,
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- };
-
- if !channel.check_read(user.id, Some(membership.role)) {
- return Err(Html(
- render_error(Error::NotAllowed, &jar, &data, &Some(user)).await,
- ));
- }
-
- let can_manage_messages = membership.role.check(CommunityPermission::MANAGE_MESSAGES)
- | user.permissions.check(FinePermission::MANAGE_MESSAGES);
-
- let messages = if props.message == 0 {
- match data
- .0
- .get_messages_by_channel(channel.id, 24, props.page)
- .await
- {
- Ok(p) => match data.0.fill_messages(p, &ignore_users).await {
- Ok(p) => p,
- Err(e) => {
- return Err(Html(render_error(e, &jar, &data, &Some(user)).await));
- }
- },
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- }
- } else {
- Vec::new()
- };
-
- let message = if props.message == 0 {
- None
- } else {
- Some(match data.0.get_message_by_id(props.message).await {
- Ok(p) => p,
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- })
- };
-
- let message_owner = if let Some(ref message) = message {
- Some(match data.0.get_user_by_id(message.owner).await {
- Ok(p) => p,
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- })
- } else {
- None
- };
-
- let lang = get_lang!(jar, data.0);
- let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
-
- context.insert("messages", &messages);
- context.insert("message", &message);
- context.insert("message_owner", &message_owner);
- context.insert("can_manage_messages", &can_manage_messages);
-
- context.insert("page", &props.page);
- context.insert("community", &community);
- context.insert("channel", &channel);
-
- // return
- Ok(Html(data.1.render("chats/stream.html", &context).unwrap()))
-}
-
-/// `/chats/{community}/{channel}/_render`
-pub async fn message_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path((community, channel)): Path<(usize, usize)>,
- Json(req): Json,
-) -> impl IntoResponse {
- let data = data.read().await;
- let user = match get_user_from_token!(jar, data.0) {
- Some(ua) => ua,
- None => {
- return Err(Html(
- render_error(Error::NotAllowed, &jar, &data, &None).await,
- ));
- }
- };
-
- let message: (String, Message) = match serde_json::from_str(&req.data) {
- Ok(m) => m,
- Err(e) => {
- return Err(Html(
- render_error(Error::MiscError(e.to_string()), &jar, &data, &Some(user)).await,
- ));
- }
- };
-
- let message = message.1;
-
- let membership = match data
- .0
- .get_membership_by_owner_community(user.id, community)
- .await
- {
- Ok(m) => m,
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- };
-
- let can_manage_messages = membership.role.check(CommunityPermission::MANAGE_MESSAGES)
- | user.permissions.check(FinePermission::MANAGE_MESSAGES);
-
- let owner = match data.0.get_user_by_id(message.owner).await {
- Ok(p) => p,
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- };
-
- let lang = get_lang!(jar, data.0);
- let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
-
- context.insert("can_manage_messages", &can_manage_messages);
- context.insert("message", &message);
- context.insert("user", &owner);
-
- context.insert("channel", &channel);
- context.insert("community", &community);
- context.insert("grouped", &req.grouped);
-
- // return
- Ok(Html(data.1.render("chats/message.html", &context).unwrap()))
-}
-
-/// `/chats/{community}/{channel/_channels`
-pub async fn channels_request(
- jar: CookieJar,
- Extension(data): Extension,
- Path((community, channel_id)): Path<(usize, usize)>,
- Query(props): Query,
-) -> impl IntoResponse {
- let data = data.read().await;
- let user = match get_user_from_token!(jar, data.0) {
- Some(ua) => ua,
- None => {
- return Err(Html(
- render_error(Error::NotAllowed, &jar, &data, &None).await,
- ));
- }
- };
-
- let channels = if community == 0 {
- match data.0.get_channels_by_user(user.id).await {
- Ok(p) => p,
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- }
- } else {
- match data.0.get_channels_by_community(community).await {
- Ok(p) => p,
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- }
- };
-
- let channel = if channel_id != 0 {
- Some(match data.0.get_channel_by_id(channel_id).await {
- Ok(p) => p,
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- })
- } else {
- None
- };
-
- let members = if community == 0 && channel.is_some() {
- let ignore_users = crate::ignore_users_gen!(user!, data);
-
- let mut channel = channel.as_ref().unwrap().clone();
- channel.members.insert(0, channel.owner); // include the owner in the members list (at the start)
-
- Some(
- match data.0.fill_members(&channel.members, ignore_users).await {
- Ok(p) => p,
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- },
- )
- } else {
- None
- };
-
- let membership = match data
- .0
- .get_membership_by_owner_community(user.id, community)
- .await
- {
- Ok(m) => m,
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- };
-
- let can_manage_channels = membership.role.check(CommunityPermission::MANAGE_CHANNELS)
- | user.permissions.check(FinePermission::MANAGE_CHANNELS);
-
- let lang = get_lang!(jar, data.0);
- let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
-
- context.insert("channels", &channels);
- context.insert("page", &props.page);
-
- context.insert("can_manage_channels", &can_manage_channels);
- context.insert("members", &members);
- context.insert("channel", &channel);
-
- context.insert("selected_community", &community);
- context.insert("selected_channel", &channel_id);
-
- // return
- Ok(Html(
- data.1.render("chats/channels.html", &context).unwrap(),
- ))
-}
diff --git a/crates/app/src/routes/pages/communities.rs b/crates/app/src/routes/pages/communities.rs
index 1e2ce88..8e309d2 100644
--- a/crates/app/src/routes/pages/communities.rs
+++ b/crates/app/src/routes/pages/communities.rs
@@ -793,14 +793,6 @@ pub async fn settings_request(
));
}
- let channels = match data.0.get_channels_by_community(community.id).await {
- Ok(p) => p,
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
- };
-
- let can_manage_channels = membership.role.check(CommunityPermission::MANAGE_CHANNELS)
- | user.permissions.check(FinePermission::MANAGE_CHANNELS);
-
let emojis = match data.0.get_emojis_by_community(community.id).await {
Ok(p) => p,
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
@@ -814,10 +806,6 @@ pub async fn settings_request(
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
context.insert("community", &community);
-
- context.insert("can_manage_channels", &can_manage_channels);
- context.insert("channels", &channels);
-
context.insert("can_manage_emojis", &can_manage_emojis);
context.insert("emojis", &emojis);
diff --git a/crates/app/src/routes/pages/mod.rs b/crates/app/src/routes/pages/mod.rs
index 8ab6da5..80e4bb7 100644
--- a/crates/app/src/routes/pages/mod.rs
+++ b/crates/app/src/routes/pages/mod.rs
@@ -1,5 +1,4 @@
pub mod auth;
-pub mod chats;
pub mod communities;
pub mod developer;
pub mod economy;
@@ -12,10 +11,7 @@ pub mod mod_panel;
pub mod profile;
pub mod stacks;
-use axum::{
- routing::{get, post},
- Router,
-};
+use axum::{routing::get, Router};
use crate::{cookie::CookieJar, routes::pages::misc::TimelineOrderMode};
use serde::Deserialize;
use tetratto_core::model::{Error, auth::User};
@@ -115,21 +111,6 @@ pub fn routes() -> Router {
.route("/post/{id}/reposts", get(communities::reposts_request))
.route("/post/{id}/likes", get(communities::likes_request))
.route("/question/{id}", get(communities::question_request))
- // chats
- .route("/chats", get(chats::redirect_request))
- .route("/chats/{community}/{channel}", get(chats::app_request))
- .route(
- "/chats/{community}/{channel}/_stream",
- get(chats::stream_request),
- )
- .route(
- "/chats/{community}/{channel}/_render",
- post(chats::message_request),
- )
- .route(
- "/chats/{community}/{channel}/_channels",
- get(chats::channels_request),
- )
// forge
.route("/forges", get(forge::home_request))
.route("/forge/{title}", get(forge::info_request))
@@ -200,16 +181,6 @@ pub struct PaginatedQuery {
pub before: usize,
}
-#[derive(Deserialize)]
-pub struct ChatsAppQuery {
- #[serde(default)]
- pub page: usize,
- #[serde(default)]
- pub nav: bool,
- #[serde(default)]
- pub message: usize,
-}
-
#[derive(Deserialize)]
pub struct ProfileQuery {
#[serde(default)]
diff --git a/crates/app/src/routes/pages/mod_panel.rs b/crates/app/src/routes/pages/mod_panel.rs
index 2b82cf1..d232325 100644
--- a/crates/app/src/routes/pages/mod_panel.rs
+++ b/crates/app/src/routes/pages/mod_panel.rs
@@ -286,18 +286,6 @@ pub async fn stats_request(jar: CookieJar, Extension(data): Extension) ->
let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
- context.insert(
- "active_users_chats",
- &data
- .0
- .0
- .1
- .get("atto.active_connections:chats".to_string())
- .await
- .unwrap_or("0".to_string())
- .parse::()
- .unwrap(),
- );
context.insert(
"active_users",
&data
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
index 5a4dcdf..a001303 100644
--- a/crates/core/Cargo.toml
+++ b/crates/core/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "tetratto-core"
description = "The core behind Tetratto"
-version = "16.0.0"
+version = "16.0.3"
edition = "2024"
readme = "../../README.md"
authors.workspace = true
@@ -50,3 +50,4 @@ oiseau = { version = "0.1.2", default-features = false, features = [
paste = { version = "1.0.15", optional = true }
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
buckets-core = "1.0.4"
+serde_valid = "1.0.5"
diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs
index 5c852a3..0e3af22 100644
--- a/crates/core/src/config.rs
+++ b/crates/core/src/config.rs
@@ -264,6 +264,9 @@ pub struct ServiceHostsConfig {
/// Littleweb browser host.
#[serde(default)]
pub littleweb: String,
+ /// Tawny host .
+ #[serde(default)]
+ pub tawny: String,
}
impl Default for ServiceHostsConfig {
@@ -271,6 +274,7 @@ impl Default for ServiceHostsConfig {
Self {
buckets: String::new(),
littleweb: String::new(),
+ tawny: String::new(),
}
}
}
diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs
index 4b277be..be378ef 100644
--- a/crates/core/src/database/auth.rs
+++ b/crates/core/src/database/auth.rs
@@ -123,14 +123,14 @@ impl DataManager {
was_purchased: get!(x->25(i32)) as i8 == 1,
browser_session: get!(x->26(String)),
ban_reason: get!(x->27(String)),
- channel_mutes: serde_json::from_str(&get!(x->28(String)).to_string()).unwrap(),
- is_deactivated: get!(x->29(i32)) as i8 == 1,
- ban_expire: get!(x->30(i64)) as usize,
- coins: get!(x->31(i32)),
- checkouts: serde_json::from_str(&get!(x->32(String)).to_string()).unwrap(),
- applied_configurations: serde_json::from_str(&get!(x->33(String)).to_string()).unwrap(),
- last_policy_consent: get!(x->34(i64)) as usize,
- close_friends_stack: get!(x->35(i64)) as usize,
+ is_deactivated: get!(x->28(i32)) as i8 == 1,
+ ban_expire: get!(x->29(i64)) as usize,
+ coins: get!(x->30(i32)),
+ checkouts: serde_json::from_str(&get!(x->31(String)).to_string()).unwrap(),
+ applied_configurations: serde_json::from_str(&get!(x->32(String)).to_string()).unwrap(),
+ last_policy_consent: get!(x->33(i64)) as usize,
+ close_friends_stack: get!(x->34(i64)) as usize,
+ missed_messages_count: get!(x->35(i32)) as usize,
}
}
@@ -287,7 +287,7 @@ impl DataManager {
let res = execute!(
&conn,
- "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36)",
+ "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35)",
params![
&(data.id as i64),
&(data.created as i64),
@@ -317,7 +317,6 @@ impl DataManager {
&if data.was_purchased { 1_i32 } else { 0_i32 },
&data.browser_session,
&data.ban_reason,
- &serde_json::to_string(&data.channel_mutes).unwrap(),
&if data.is_deactivated { 1_i32 } else { 0_i32 },
&(data.ban_expire as i64),
&(data.coins as i32),
@@ -709,15 +708,17 @@ impl DataManager {
self.cache_clear_user(&other_user).await;
- // create audit log entry
- self.create_audit_log_entry(AuditLogEntry::new(
- user.id,
- format!(
- "invoked `update_user_is_deactivated` with x value `{}` and y value `{}`",
- other_user.id, x
- ),
- ))
- .await?;
+ // create audit log entry (if we aren't the user that is being updated)
+ if user.id != other_user.id {
+ self.create_audit_log_entry(AuditLogEntry::new(
+ user.id,
+ format!(
+ "invoked `update_user_is_deactivated` with x value `{}` and y value `{}`",
+ other_user.id, x
+ ),
+ ))
+ .await?;
+ }
// ...
Ok(())
@@ -1163,4 +1164,8 @@ impl DataManager {
auto_method!(decr_user_request_count()@get_user_by_id -> "UPDATE users SET request_count = request_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --decr=request_count);
auto_method!(get_user_by_invite_code(i64)@get_user_from_row -> "SELECT * FROM users WHERE invite_code = $1" --name="user" --returns=User);
+
+ auto_method!(update_user_missed_messages_count(i32)@get_user_by_id -> "UPDATE users SET missed_messages_count = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
+ auto_method!(incr_user_missed_messages()@get_user_by_id -> "UPDATE users SET missed_messages_count = missed_messages_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --incr);
+ auto_method!(decr_user_missed_messages()@get_user_by_id -> "UPDATE users SET missed_messages_count = missed_messages_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --decr=notification_count);
}
diff --git a/crates/core/src/database/channels.rs b/crates/core/src/database/channels.rs
deleted file mode 100644
index c1e9938..0000000
--- a/crates/core/src/database/channels.rs
+++ /dev/null
@@ -1,325 +0,0 @@
-use oiseau::cache::Cache;
-use crate::model::moderation::AuditLogEntry;
-use crate::model::{
- Error, Result, auth::User, permissions::FinePermission,
- communities_permissions::CommunityPermission, channels::Channel,
-};
-use crate::{auto_method, DataManager};
-use oiseau::{PostgresRow, execute, get, query_row, query_rows, params};
-
-impl DataManager {
- /// Get a [`Channel`] from an SQL row.
- pub(crate) fn get_channel_from_row(x: &PostgresRow) -> Channel {
- Channel {
- id: get!(x->0(i64)) as usize,
- community: get!(x->1(i64)) as usize,
- owner: get!(x->2(i64)) as usize,
- created: get!(x->3(i64)) as usize,
- minimum_role_read: get!(x->4(i32)) as u32,
- minimum_role_write: get!(x->5(i32)) as u32,
- position: get!(x->6(i32)) as usize,
- members: serde_json::from_str(&get!(x->7(String))).unwrap(),
- title: get!(x->8(String)),
- last_message: get!(x->9(i64)) as usize,
- }
- }
-
- auto_method!(get_channel_by_id(usize as i64)@get_channel_from_row -> "SELECT * FROM channels WHERE id = $1" --name="channel" --returns=Channel --cache-key-tmpl="atto.channel:{}");
-
- /// Get all member profiles from a channel members list.
- pub async fn fill_members(
- &self,
- members: &Vec,
- ignore_users: Vec,
- ) -> Result> {
- let mut out = Vec::new();
-
- for member in members {
- if ignore_users.contains(member) {
- continue;
- }
-
- out.push(self.get_user_by_id(member.to_owned()).await?);
- }
-
- Ok(out)
- }
-
- /// Get all channels by community.
- ///
- /// # Arguments
- /// * `community` - the ID of the community to fetch channels for
- pub async fn get_channels_by_community(&self, community: usize) -> Result> {
- let conn = match self.0.connect().await {
- Ok(c) => c,
- Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
- };
-
- let res = query_rows!(
- &conn,
- "SELECT * FROM channels WHERE community = $1 ORDER BY position ASC",
- &[&(community as i64)],
- |x| { Self::get_channel_from_row(x) }
- );
-
- if res.is_err() {
- return Err(Error::GeneralNotFound("channel".to_string()));
- }
-
- Ok(res.unwrap())
- }
-
- /// Get all channels by user.
- ///
- /// # Arguments
- /// * `user` - the ID of the user to fetch channels for
- pub async fn get_channels_by_user(&self, user: usize) -> Result> {
- let conn = match self.0.connect().await {
- Ok(c) => c,
- Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
- };
-
- let res = query_rows!(
- &conn,
- "SELECT * FROM channels WHERE (owner = $1 OR members LIKE $2) AND community = 0 ORDER BY last_message DESC",
- params![&(user as i64), &format!("%{user}%")],
- |x| { Self::get_channel_from_row(x) }
- );
-
- if res.is_err() {
- return Err(Error::GeneralNotFound("channel".to_string()));
- }
-
- Ok(res.unwrap())
- }
-
- /// Get a channel given its `owner` and a member.
- ///
- /// # Arguments
- /// * `owner` - the ID of the owner
- /// * `member` - the ID of the member
- pub async fn get_channel_by_owner_member(
- &self,
- owner: usize,
- member: usize,
- ) -> Result {
- 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 channels WHERE owner = $1 AND members = $2 AND community = 0 ORDER BY created DESC",
- params![&(owner as i64), &format!("[{member}]")],
- |x| { Ok(Self::get_channel_from_row(x)) }
- );
-
- if res.is_err() {
- return Err(Error::GeneralNotFound("channel".to_string()));
- }
-
- Ok(res.unwrap())
- }
-
- /// Create a new channel in the database.
- ///
- /// # Arguments
- /// * `data` - a mock [`Channel`] object to insert
- pub async fn create_channel(&self, data: Channel) -> Result<()> {
- let user = self.get_user_by_id(data.owner).await?;
-
- // check user permission in community
- if data.community != 0 {
- let membership = self
- .get_membership_by_owner_community(user.id, data.community)
- .await?;
-
- if !membership.role.check(CommunityPermission::MANAGE_CHANNELS)
- && !user.permissions.check(FinePermission::MANAGE_CHANNELS)
- {
- return Err(Error::NotAllowed);
- }
- }
- // check members
- else {
- for member in &data.members {
- if self
- .get_userblock_by_initiator_receiver(member.to_owned(), data.owner)
- .await
- .is_ok()
- {
- return Err(Error::NotAllowed);
- }
- }
- }
-
- // ...
- let conn = match self.0.connect().await {
- Ok(c) => c,
- Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
- };
-
- let res = execute!(
- &conn,
- "INSERT INTO channels VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
- params![
- &(data.id as i64),
- &(data.community as i64),
- &(data.owner as i64),
- &(data.created as i64),
- &(data.minimum_role_read as i32),
- &(data.minimum_role_write as i32),
- &(data.position as i32),
- &serde_json::to_string(&data.members).unwrap(),
- &data.title,
- &(data.last_message as i64)
- ]
- );
-
- if let Err(e) = res {
- return Err(Error::DatabaseError(e.to_string()));
- }
-
- Ok(())
- }
-
- pub async fn delete_channel(&self, id: usize, user: &User) -> Result<()> {
- let channel = self.get_channel_by_id(id).await?;
-
- // check user permission in community
- if user.id != channel.owner {
- let membership = self
- .get_membership_by_owner_community(user.id, channel.community)
- .await?;
-
- if !membership.role.check(CommunityPermission::MANAGE_CHANNELS) {
- return Err(Error::NotAllowed);
- }
- }
-
- // ...
- let conn = match self.0.connect().await {
- Ok(c) => c,
- Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
- };
-
- let res = execute!(&conn, "DELETE FROM channels WHERE id = $1", &[&(id as i64)]);
-
- if let Err(e) = res {
- return Err(Error::DatabaseError(e.to_string()));
- }
-
- // delete messages
- let res = execute!(
- &conn,
- "DELETE FROM messages WHERE channel = $1",
- &[&(id as i64)]
- );
-
- if let Err(e) = res {
- return Err(Error::DatabaseError(e.to_string()));
- }
-
- // ...
- self.0.1.remove(format!("atto.channel:{}", id)).await;
- Ok(())
- }
-
- pub async fn add_channel_member(&self, id: usize, user: User, member: String) -> Result<()> {
- let mut y = self.get_channel_by_id(id).await?;
-
- if user.id != y.owner && member != user.username {
- if !user.permissions.check(FinePermission::MANAGE_CHANNELS) {
- return Err(Error::NotAllowed);
- } else {
- self.create_audit_log_entry(AuditLogEntry::new(
- user.id,
- format!("invoked `add_channel_member` with x value `{member}`"),
- ))
- .await?
- }
- }
-
- // check permissions
- let member = self.get_user_by_username(&member).await?;
-
- if self
- .get_userblock_by_initiator_receiver(member.id, user.id)
- .await
- .is_ok()
- {
- return Err(Error::NotAllowed);
- }
-
- // ...
- y.members.push(member.id);
-
- let conn = match self.0.connect().await {
- Ok(c) => c,
- Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
- };
-
- let res = execute!(
- &conn,
- "UPDATE channels SET members = $1 WHERE id = $2",
- params![&serde_json::to_string(&y.members).unwrap(), &(id as i64)]
- );
-
- if let Err(e) = res {
- return Err(Error::DatabaseError(e.to_string()));
- }
-
- self.0.1.remove(format!("atto.channel:{}", id)).await;
-
- Ok(())
- }
-
- pub async fn remove_channel_member(&self, id: usize, user: User, member: usize) -> Result<()> {
- let mut y = self.get_channel_by_id(id).await?;
-
- if user.id != y.owner && member != user.id {
- if !user.permissions.check(FinePermission::MANAGE_CHANNELS) {
- return Err(Error::NotAllowed);
- } else {
- self.create_audit_log_entry(AuditLogEntry::new(
- user.id,
- format!("invoked `remove_channel_member` with x value `{member}`"),
- ))
- .await?
- }
- }
-
- y.members
- .remove(match y.members.iter().position(|x| *x == member) {
- Some(i) => i,
- None => return Err(Error::GeneralNotFound("member".to_string())),
- });
-
- let conn = match self.0.connect().await {
- Ok(c) => c,
- Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
- };
-
- let res = execute!(
- &conn,
- "UPDATE channels SET members = $1 WHERE id = $2",
- params![&serde_json::to_string(&y.members).unwrap(), &(id as i64)]
- );
-
- if let Err(e) = res {
- return Err(Error::DatabaseError(e.to_string()));
- }
-
- self.0.1.remove(format!("atto.channel:{}", id)).await;
-
- Ok(())
- }
-
- auto_method!(update_channel_title(&str)@get_channel_by_id:FinePermission::MANAGE_CHANNELS; -> "UPDATE channels SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}");
- auto_method!(update_channel_position(i32)@get_channel_by_id:FinePermission::MANAGE_CHANNELS; -> "UPDATE channels SET position = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}");
- auto_method!(update_channel_minimum_role_read(i32)@get_channel_by_id:FinePermission::MANAGE_CHANNELS; -> "UPDATE channels SET minimum_role_read = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}");
- auto_method!(update_channel_minimum_role_write(i32)@get_channel_by_id:FinePermission::MANAGE_CHANNELS; -> "UPDATE channels SET minimum_role_write = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}");
- auto_method!(update_channel_members(Vec)@get_channel_by_id:FinePermission::MANAGE_CHANNELS; -> "UPDATE channels SET members = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.channel:{}");
- auto_method!(update_channel_last_message(i64) -> "UPDATE channels SET last_message = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}");
-}
diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs
index 8df91ae..d778076 100644
--- a/crates/core/src/database/common.rs
+++ b/crates/core/src/database/common.rs
@@ -26,8 +26,6 @@ impl DataManager {
execute!(&conn, common::CREATE_TABLE_REQUESTS).unwrap();
execute!(&conn, common::CREATE_TABLE_QUESTIONS).unwrap();
execute!(&conn, common::CREATE_TABLE_IPBLOCKS).unwrap();
- execute!(&conn, common::CREATE_TABLE_CHANNELS).unwrap();
- execute!(&conn, common::CREATE_TABLE_MESSAGES).unwrap();
execute!(&conn, common::CREATE_TABLE_EMOJIS).unwrap();
execute!(&conn, common::CREATE_TABLE_STACKS).unwrap();
execute!(&conn, common::CREATE_TABLE_DRAFTS).unwrap();
@@ -37,7 +35,6 @@ impl DataManager {
execute!(&conn, common::CREATE_TABLE_STACKBLOCKS).unwrap();
execute!(&conn, common::CREATE_TABLE_JOURNALS).unwrap();
execute!(&conn, common::CREATE_TABLE_NOTES).unwrap();
- execute!(&conn, common::CREATE_TABLE_MESSAGE_REACTIONS).unwrap();
execute!(&conn, common::CREATE_TABLE_INVITE_CODES).unwrap();
execute!(&conn, common::CREATE_TABLE_DOMAINS).unwrap();
execute!(&conn, common::CREATE_TABLE_SERVICES).unwrap();
@@ -55,10 +52,6 @@ impl DataManager {
.1
.set("atto.active_connections:users".to_string(), "0".to_string())
.await;
- self.0
- .1
- .set("atto.active_connections:chats".to_string(), "0".to_string())
- .await;
self.2.init().await.expect("failed to init buckets manager");
Ok(())
diff --git a/crates/core/src/database/communities.rs b/crates/core/src/database/communities.rs
index 1290bdd..cd7a6ab 100644
--- a/crates/core/src/database/communities.rs
+++ b/crates/core/src/database/communities.rs
@@ -375,11 +375,6 @@ impl DataManager {
return Err(Error::DatabaseError(e.to_string()));
}
- // remove channels
- for channel in self.get_channels_by_community(id).await? {
- self.delete_channel(channel.id, &user).await?;
- }
-
// remove images
let avatar = PathBufD::current().extend(&[
self.0.0.dirs.media.as_str(),
diff --git a/crates/core/src/database/drivers/common.rs b/crates/core/src/database/drivers/common.rs
index 6a16c2d..6cff2ef 100644
--- a/crates/core/src/database/drivers/common.rs
+++ b/crates/core/src/database/drivers/common.rs
@@ -14,8 +14,6 @@ pub const CREATE_TABLE_USER_WARNINGS: &str = include_str!("./sql/create_user_war
pub const CREATE_TABLE_REQUESTS: &str = include_str!("./sql/create_requests.sql");
pub const CREATE_TABLE_QUESTIONS: &str = include_str!("./sql/create_questions.sql");
pub const CREATE_TABLE_IPBLOCKS: &str = include_str!("./sql/create_ipblocks.sql");
-pub const CREATE_TABLE_CHANNELS: &str = include_str!("./sql/create_channels.sql");
-pub const CREATE_TABLE_MESSAGES: &str = include_str!("./sql/create_messages.sql");
pub const CREATE_TABLE_EMOJIS: &str = include_str!("./sql/create_emojis.sql");
pub const CREATE_TABLE_STACKS: &str = include_str!("./sql/create_stacks.sql");
pub const CREATE_TABLE_DRAFTS: &str = include_str!("./sql/create_drafts.sql");
@@ -25,7 +23,6 @@ pub const CREATE_TABLE_APPS: &str = include_str!("./sql/create_apps.sql");
pub const CREATE_TABLE_STACKBLOCKS: &str = include_str!("./sql/create_stackblocks.sql");
pub const CREATE_TABLE_JOURNALS: &str = include_str!("./sql/create_journals.sql");
pub const CREATE_TABLE_NOTES: &str = include_str!("./sql/create_notes.sql");
-pub const CREATE_TABLE_MESSAGE_REACTIONS: &str = include_str!("./sql/create_message_reactions.sql");
pub const CREATE_TABLE_INVITE_CODES: &str = include_str!("./sql/create_invite_codes.sql");
pub const CREATE_TABLE_DOMAINS: &str = include_str!("./sql/create_domains.sql");
pub const CREATE_TABLE_SERVICES: &str = include_str!("./sql/create_services.sql");
diff --git a/crates/core/src/database/drivers/sql/create_channels.sql b/crates/core/src/database/drivers/sql/create_channels.sql
deleted file mode 100644
index 83f7ff6..0000000
--- a/crates/core/src/database/drivers/sql/create_channels.sql
+++ /dev/null
@@ -1,12 +0,0 @@
-CREATE TABLE IF NOT EXISTS channels (
- id BIGINT NOT NULL PRIMARY KEY,
- community BIGINT NOT NULL,
- owner BIGINT NOT NULL,
- created BIGINT NOT NULL,
- minimum_role_read INT NOT NULL,
- minimum_role_write INT NOT NULL,
- position INT NOT NULL,
- members TEXT NOT NULL,
- title TEXT NOT NULL,
- last_message BIGINT NOT NULL
-)
diff --git a/crates/core/src/database/drivers/sql/create_message_reactions.sql b/crates/core/src/database/drivers/sql/create_message_reactions.sql
deleted file mode 100644
index f13a033..0000000
--- a/crates/core/src/database/drivers/sql/create_message_reactions.sql
+++ /dev/null
@@ -1,8 +0,0 @@
-CREATE TABLE IF NOT EXISTS message_reactions (
- id BIGINT NOT NULL PRIMARY KEY,
- created BIGINT NOT NULL,
- owner BIGINT NOT NULL,
- message BIGINT NOT NULL,
- emoji TEXT NOT NULL,
- UNIQUE (owner, message, emoji)
-)
diff --git a/crates/core/src/database/drivers/sql/create_messages.sql b/crates/core/src/database/drivers/sql/create_messages.sql
deleted file mode 100644
index 235d8dc..0000000
--- a/crates/core/src/database/drivers/sql/create_messages.sql
+++ /dev/null
@@ -1,10 +0,0 @@
-CREATE TABLE IF NOT EXISTS messages (
- id BIGINT NOT NULL PRIMARY KEY,
- channel BIGINT NOT NULL,
- owner BIGINT NOT NULL,
- created BIGINT NOT NULL,
- edited BIGINT NOT NULL,
- content TEXT NOT NULL,
- context TEXT NOT NULL,
- reactions TEXT NOT NULL
-)
diff --git a/crates/core/src/database/drivers/sql/create_users.sql b/crates/core/src/database/drivers/sql/create_users.sql
index 11b97f8..8d6aad3 100644
--- a/crates/core/src/database/drivers/sql/create_users.sql
+++ b/crates/core/src/database/drivers/sql/create_users.sql
@@ -27,12 +27,12 @@ CREATE TABLE IF NOT EXISTS users (
was_purchased INT NOT NULL,
browser_session TEXT NOT NULL,
ban_reason TEXT NOT NULL,
- channel_mutes TEXT NOT NULL,
is_deactivated INT NOT NULL,
ban_expire BIGINT NOT NULL,
coins INT NOT NULL,
checkouts TEXT NOT NULL,
applied_configurations TEXT NOT NULL,
last_policy_consent BIGINT NOT NULL,
- close_friends_stack BIGINT NOT NULL
+ close_friends_stack BIGINT NOT NULL,
+ missed_messages_count INT NOT NULL
)
diff --git a/crates/core/src/database/drivers/sql/version_migrations.sql b/crates/core/src/database/drivers/sql/version_migrations.sql
index e9e731f..8f5524b 100644
--- a/crates/core/src/database/drivers/sql/version_migrations.sql
+++ b/crates/core/src/database/drivers/sql/version_migrations.sql
@@ -85,3 +85,11 @@ ADD COLUMN IF NOT EXISTS close_friends_stack BIGINT DEFAULT 0;
-- stacks is_locked
ALTER TABLE stacks
ADD COLUMN IF NOT EXISTS is_locked INT DEFAULT 0;
+
+-- users missed_messages_count
+ALTER TABLE users
+ADD COLUMN IF NOT EXISTS missed_messages_count INT DEFAULT 0;
+
+-- users channel_mutes
+ALTER TABLE users
+DROP COLUMN IF EXISTS channel_mutes;
diff --git a/crates/core/src/database/message_reactions.rs b/crates/core/src/database/message_reactions.rs
deleted file mode 100644
index 4134049..0000000
--- a/crates/core/src/database/message_reactions.rs
+++ /dev/null
@@ -1,183 +0,0 @@
-use oiseau::{cache::Cache, query_rows};
-use crate::model::{
- Error, Result,
- auth::{Notification, User},
- permissions::FinePermission,
- channels::MessageReaction,
-};
-use crate::{auto_method, DataManager};
-
-use oiseau::{PostgresRow, execute, get, query_row, params};
-
-impl DataManager {
- /// Get a [`MessageReaction`] from an SQL row.
- pub(crate) fn get_message_reaction_from_row(x: &PostgresRow) -> MessageReaction {
- MessageReaction {
- id: get!(x->0(i64)) as usize,
- created: get!(x->1(i64)) as usize,
- owner: get!(x->2(i64)) as usize,
- message: get!(x->3(i64)) as usize,
- emoji: get!(x->4(String)),
- }
- }
-
- auto_method!(get_message_reaction_by_id()@get_message_reaction_from_row -> "SELECT * FROM message_reactions WHERE id = $1" --name="message_reaction" --returns=MessageReaction --cache-key-tmpl="atto.message_reaction:{}");
-
- /// Get message_reactions by `owner` and `message`.
- pub async fn get_message_reactions_by_owner_message(
- &self,
- owner: usize,
- message: usize,
- ) -> Result> {
- let conn = match self.0.connect().await {
- Ok(c) => c,
- Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
- };
-
- let res = query_rows!(
- &conn,
- "SELECT * FROM message_reactions WHERE owner = $1 AND message = $2",
- &[&(owner as i64), &(message as i64)],
- |x| { Self::get_message_reaction_from_row(x) }
- );
-
- if res.is_err() {
- return Err(Error::GeneralNotFound("message_reaction".to_string()));
- }
-
- Ok(res.unwrap())
- }
-
- /// Get a message_reaction by `owner`, `message`, and `emoji`.
- pub async fn get_message_reaction_by_owner_message_emoji(
- &self,
- owner: usize,
- message: usize,
- emoji: &str,
- ) -> Result {
- 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 message_reactions WHERE owner = $1 AND message = $2 AND emoji = $3",
- params![&(owner as i64), &(message as i64), &emoji],
- |x| { Ok(Self::get_message_reaction_from_row(x)) }
- );
-
- if res.is_err() {
- return Err(Error::GeneralNotFound("message_reaction".to_string()));
- }
-
- Ok(res.unwrap())
- }
-
- /// Create a new message_reaction in the database.
- ///
- /// # Arguments
- /// * `data` - a mock [`MessageReaction`] object to insert
- pub async fn create_message_reaction(&self, data: MessageReaction, user: &User) -> Result<()> {
- let conn = match self.0.connect().await {
- Ok(c) => c,
- Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
- };
-
- let mut message = self.get_message_by_id(data.message).await?;
- let channel = self.get_channel_by_id(message.channel).await?;
-
- // ...
- let res = execute!(
- &conn,
- "INSERT INTO message_reactions VALUES ($1, $2, $3, $4, $5)",
- params![
- &(data.id as i64),
- &(data.created as i64),
- &(data.owner as i64),
- &(data.message as i64),
- &data.emoji
- ]
- );
-
- if let Err(e) = res {
- return Err(Error::DatabaseError(e.to_string()));
- }
-
- // incr corresponding
- if let Some(x) = message.reactions.get(&data.emoji) {
- message.reactions.insert(data.emoji.clone(), x + 1);
- } else {
- message.reactions.insert(data.emoji.clone(), 1);
- }
-
- self.update_message_reactions(message.id, message.reactions)
- .await?;
-
- // send notif
- if message.owner != user.id {
- self
- .create_notification(Notification::new(
- "Your message has received a reaction!".to_string(),
- format!(
- "[@{}](/api/v1/auth/user/find/{}) has reacted \"{}\" to your [message](/chats/{}/{}?message={})!",
- user.username, user.id, data.emoji, channel.community, channel.id, message.id
- ),
- message.owner,
- ))
- .await?;
- }
-
- // return
- Ok(())
- }
-
- pub async fn delete_message_reaction(&self, id: usize, user: &User) -> Result<()> {
- let message_reaction = self.get_message_reaction_by_id(id).await?;
-
- if user.id != message_reaction.owner
- && !user.permissions.check(FinePermission::MANAGE_REACTIONS)
- {
- return Err(Error::NotAllowed);
- }
-
- let mut message = self.get_message_by_id(message_reaction.message).await?;
-
- let conn = match self.0.connect().await {
- Ok(c) => c,
- Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
- };
-
- let res = execute!(
- &conn,
- "DELETE FROM message_reactions WHERE id = $1",
- &[&(id as i64)]
- );
-
- if let Err(e) = res {
- return Err(Error::DatabaseError(e.to_string()));
- }
-
- self.0
- .1
- .remove(format!("atto.message_reaction:{}", id))
- .await;
-
- // decr message reaction count
- if let Some(x) = message.reactions.get(&message_reaction.emoji) {
- if *x == 1 {
- // there are no 0 of this reaction
- message.reactions.remove(&message_reaction.emoji);
- } else {
- // decr 1
- message.reactions.insert(message_reaction.emoji, x - 1);
- }
- }
-
- self.update_message_reactions(message.id, message.reactions)
- .await?;
-
- // return
- Ok(())
- }
-}
diff --git a/crates/core/src/database/messages.rs b/crates/core/src/database/messages.rs
deleted file mode 100644
index 6b7c037..0000000
--- a/crates/core/src/database/messages.rs
+++ /dev/null
@@ -1,380 +0,0 @@
-use std::collections::HashMap;
-use oiseau::cache::Cache;
-use crate::model::auth::Notification;
-use crate::model::moderation::AuditLogEntry;
-use crate::model::socket::{SocketMessage, SocketMethod};
-use crate::model::{
- Error, Result, auth::User, permissions::FinePermission,
- communities_permissions::CommunityPermission, channels::Message,
-};
-use serde::Serialize;
-use tetratto_shared::unix_epoch_timestamp;
-use crate::{auto_method, DataManager};
-
-use oiseau::{PostgresRow, cache::redis::Commands};
-
-use oiseau::{execute, get, query_rows, params};
-
-#[derive(Serialize)]
-struct DeleteMessageEvent {
- pub id: String,
-}
-
-impl DataManager {
- /// Get a [`Message`] from an SQL row.
- pub(crate) fn get_message_from_row(x: &PostgresRow) -> Message {
- Message {
- id: get!(x->0(i64)) as usize,
- channel: get!(x->1(i64)) as usize,
- owner: get!(x->2(i64)) as usize,
- created: get!(x->3(i64)) as usize,
- edited: get!(x->4(i64)) as usize,
- content: get!(x->5(String)),
- context: serde_json::from_str(&get!(x->6(String))).unwrap(),
- reactions: serde_json::from_str(&get!(x->7(String))).unwrap(),
- }
- }
-
- auto_method!(get_message_by_id(usize as i64)@get_message_from_row -> "SELECT * FROM messages WHERE id = $1" --name="message" --returns=Message --cache-key-tmpl="atto.message:{}");
-
- /// Complete a vector of just messages with their owner as well.
- ///
- /// # Returns
- /// `(message, owner, group with previous messages in ui)`
- pub async fn fill_messages(
- &self,
- messages: Vec,
- ignore_users: &[usize],
- ) -> Result> {
- let mut out: Vec<(Message, User, bool)> = Vec::new();
-
- let mut users: HashMap = HashMap::new();
- for (i, message) in messages.iter().enumerate() {
- let next_owner: usize = match messages.get(i + 1) {
- Some(m) => m.owner,
- None => 0,
- };
-
- let owner = message.owner;
-
- if ignore_users.contains(&owner) {
- continue;
- }
-
- if let Some(user) = users.get(&owner) {
- out.push((message.to_owned(), user.clone(), next_owner == owner));
- } else {
- let user = self.get_user_by_id_with_void(owner).await?;
- users.insert(owner, user.clone());
- out.push((message.to_owned(), user, next_owner == owner));
- }
- }
-
- Ok(out)
- }
-
- /// Get all messages by channel (paginated).
- ///
- /// # Arguments
- /// * `channel` - the ID of the community to fetch channels for
- /// * `batch` - the limit of items in each page
- /// * `page` - the page number
- pub async fn get_messages_by_channel(
- &self,
- channel: usize,
- batch: usize,
- page: usize,
- ) -> Result> {
- let conn = match self.0.connect().await {
- Ok(c) => c,
- Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
- };
-
- let res = query_rows!(
- &conn,
- "SELECT * FROM messages WHERE channel = $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
- &[&(channel as i64), &(batch as i64), &((page * batch) as i64)],
- |x| { Self::get_message_from_row(x) }
- );
-
- if res.is_err() {
- return Err(Error::GeneralNotFound("message".to_string()));
- }
-
- Ok(res.unwrap())
- }
-
- /// Create a new message in the database.
- ///
- /// # Arguments
- /// * `data` - a mock [`Message`] object to insert
- pub async fn create_message(&self, mut data: Message) -> Result<()> {
- if data.content.len() < 2 {
- return Err(Error::DataTooLong("content".to_string()));
- }
-
- if data.content.len() > 2048 {
- return Err(Error::DataTooLong("content".to_string()));
- }
-
- let owner = self.get_user_by_id(data.owner).await?;
- let channel = self.get_channel_by_id(data.channel).await?;
-
- // check user permission in community
- let membership = self
- .get_membership_by_owner_community(owner.id, channel.community)
- .await?;
-
- // check user permission to post in channel
- if !channel.check_post(owner.id, Some(membership.role)) {
- return Err(Error::NotAllowed);
- }
-
- // send mention notifications
- let mut already_notified: HashMap = HashMap::new();
- for username in User::parse_mentions(&data.content) {
- let user = {
- if let Some(ua) = already_notified.get(&username) {
- ua.to_owned()
- } else {
- let user = self.get_user_by_username(&username).await?;
-
- // check blocked status
- if self
- .get_userblock_by_initiator_receiver(user.id, data.owner)
- .await
- .is_ok()
- {
- return Err(Error::NotAllowed);
- }
-
- // check private status
- if user.settings.private_profile {
- if self
- .get_userfollow_by_initiator_receiver(user.id, data.owner)
- .await
- .is_err()
- {
- return Err(Error::NotAllowed);
- }
- }
-
- // check if the user can read the channel
- let membership = self
- .get_membership_by_owner_community(user.id, channel.community)
- .await?;
-
- if !channel.check_read(user.id, Some(membership.role)) {
- continue;
- }
-
- // create notif
- self.create_notification(Notification::new(
- "You've been mentioned in a message!".to_string(),
- format!(
- "[@{}](/api/v1/auth/user/find/{}) has mentioned you in their [message](/chats/{}/{}?message={}).",
- owner.username, owner.id, channel.community, data.channel, data.id
- ),
- user.id,
- ))
- .await?;
-
- // ...
- already_notified.insert(username.to_owned(), user.clone());
- user
- }
- };
-
- data.content = data.content.replace(
- &format!("@{username}"),
- &format!(
- "@{username}",
- user.id
- ),
- );
- }
-
- // send notifs to members (if this message isn't associated with a channel)
- if channel.community == 0 {
- for member in [channel.members, vec![channel.owner]].concat() {
- if member == owner.id {
- continue;
- }
-
- let user = self.get_user_by_id(member).await?;
- if user.channel_mutes.contains(&channel.id) {
- continue;
- }
-
- let mut notif = Notification::new(
- "You've received a new message!".to_string(),
- format!(
- "[@{}](/api/v1/auth/user/find/{}) has sent a [message](/chats/{}/{}?message={}) in [{}](/chats/{}/{}).",
- owner.username,
- owner.id,
- channel.community,
- data.channel,
- data.id,
- channel.title,
- channel.community,
- data.channel
- ),
- member,
- );
-
- notif.tag = format!("chats/{}", channel.id);
- self.create_notification(notif).await?;
- }
- }
-
- // ...
- let conn = match self.0.connect().await {
- Ok(c) => c,
- Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
- };
-
- let res = execute!(
- &conn,
- "INSERT INTO messages VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
- params![
- &(data.id as i64),
- &(data.channel as i64),
- &(data.owner as i64),
- &(data.created as i64),
- &(data.edited as i64),
- &data.content,
- &serde_json::to_string(&data.context).unwrap(),
- &serde_json::to_string(&data.reactions).unwrap(),
- ]
- );
-
- if let Err(e) = res {
- return Err(Error::DatabaseError(e.to_string()));
- }
-
- // post event
- let mut con = self.0.1.get_con().await;
-
- if let Err(e) = con.publish::(
- if channel.community != 0 {
- // broadcast to community ws
- format!("chats/{}", channel.community)
- } else {
- // broadcast to channel ws
- format!("chats/{}", channel.id)
- },
- serde_json::to_string(&SocketMessage {
- method: SocketMethod::Message,
- data: serde_json::to_string(&(data.channel.to_string(), data)).unwrap(),
- })
- .unwrap(),
- ) {
- return Err(Error::MiscError(e.to_string()));
- }
-
- // update channel position
- self.update_channel_last_message(channel.id, unix_epoch_timestamp() as i64)
- .await?;
-
- // ...
- Ok(())
- }
-
- pub async fn delete_message(&self, id: usize, user: User) -> Result<()> {
- let message = self.get_message_by_id(id).await?;
- let channel = self.get_channel_by_id(message.channel).await?;
-
- // check user permission in community
- if user.id != message.owner {
- let membership = self
- .get_membership_by_owner_community(user.id, channel.community)
- .await?;
-
- if !membership.role.check(CommunityPermission::MANAGE_MESSAGES)
- && !user.permissions.check(FinePermission::MANAGE_MESSAGES)
- {
- return Err(Error::NotAllowed);
- } else if user.permissions.check(FinePermission::MANAGE_MESSAGES) {
- self.create_audit_log_entry(AuditLogEntry::new(
- user.id,
- format!("invoked `delete_message` with x value `{id}`"),
- ))
- .await?
- }
- }
-
- // ...
- let conn = match self.0.connect().await {
- Ok(c) => c,
- Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
- };
-
- let res = execute!(&conn, "DELETE FROM messages WHERE id = $1", &[&(id as i64)]);
-
- if let Err(e) = res {
- return Err(Error::DatabaseError(e.to_string()));
- }
-
- self.0.1.remove(format!("atto.message:{}", id)).await;
-
- // post event
- let mut con = self.0.1.get_con().await;
-
- if let Err(e) = con.publish::(
- if channel.community != 0 {
- // broadcast to community ws
- format!("chats/{}", channel.community)
- } else {
- // broadcast to channel ws
- format!("chats/{}", channel.id)
- },
- serde_json::to_string(&SocketMessage {
- method: SocketMethod::Delete,
- data: serde_json::to_string(&DeleteMessageEvent { id: id.to_string() }).unwrap(),
- })
- .unwrap(),
- ) {
- return Err(Error::MiscError(e.to_string()));
- }
-
- // ...
- Ok(())
- }
-
- pub async fn update_message_content(&self, id: usize, user: User, x: String) -> Result<()> {
- let y = self.get_message_by_id(id).await?;
-
- if user.id != y.owner {
- if !user.permissions.check(FinePermission::MANAGE_MESSAGES) {
- return Err(Error::NotAllowed);
- } else {
- self.create_audit_log_entry(AuditLogEntry::new(
- user.id,
- format!("invoked `update_message_content` with x value `{id}`"),
- ))
- .await?
- }
- }
-
- // ...
- let conn = match self.0.connect().await {
- Ok(c) => c,
- Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
- };
-
- let res = execute!(
- &conn,
- "UPDATE messages SET content = $1, edited = $2 WHERE id = $2",
- params![&x, &(unix_epoch_timestamp() as i64), &(id as i64)]
- );
-
- if let Err(e) = res {
- return Err(Error::DatabaseError(e.to_string()));
- }
-
- // return
- Ok(())
- }
-
- auto_method!(update_message_reactions(HashMap) -> "UPDATE messages SET reactions = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.message:{}");
-}
diff --git a/crates/core/src/database/mod.rs b/crates/core/src/database/mod.rs
index 1adbf88..b027ea9 100644
--- a/crates/core/src/database/mod.rs
+++ b/crates/core/src/database/mod.rs
@@ -3,7 +3,6 @@ pub mod app_data;
mod apps;
mod audit_log;
mod auth;
-mod channels;
mod common;
mod communities;
pub mod connections;
@@ -17,8 +16,6 @@ mod ipblocks;
mod journals;
mod letters;
mod memberships;
-mod message_reactions;
-mod messages;
mod notes;
mod notifications;
mod polls;
diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs
index 806552b..fbfa51a 100644
--- a/crates/core/src/model/auth.rs
+++ b/crates/core/src/model/auth.rs
@@ -1,4 +1,6 @@
use std::collections::HashMap;
+use crate::model::{Error, Result};
+
use super::{
oauth::AuthGrant,
permissions::{FinePermission, SecondaryPermission},
@@ -10,6 +12,7 @@ use tetratto_shared::{
snow::Snowflake,
unix_epoch_timestamp,
};
+use serde_valid::Validate;
/// `(ip, token, creation timestamp)`
pub type Token = (String, String, usize);
@@ -82,9 +85,6 @@ pub struct User {
/// The reason the user was banned.
#[serde(default)]
pub ban_reason: String,
- /// IDs of channels the user has muted.
- #[serde(default)]
- pub channel_mutes: Vec,
/// If the user is deactivated. Deactivated users act almost like deleted
/// users, but their data is not wiped.
#[serde(default)]
@@ -113,6 +113,9 @@ pub struct User {
/// (the user) to post to it.
#[serde(default)]
pub close_friends_stack: usize,
+ /// The number of messages this user has missed.
+ #[serde(default)]
+ pub missed_messages_count: usize,
}
pub type UserConnections =
@@ -187,13 +190,16 @@ impl Default for DefaultProfileTabChoice {
}
}
-#[derive(Clone, Debug, Serialize, Deserialize, Default)]
+#[derive(Clone, Debug, Serialize, Deserialize, Default, Validate)]
pub struct UserSettings {
#[serde(default)]
+ #[validate(max_length = 32)]
pub display_name: String,
#[serde(default)]
+ #[validate(max_length = 4096)]
pub biography: String,
#[serde(default)]
+ #[validate(max_length = 2048)]
pub warning: String,
#[serde(default)]
pub private_profile: bool,
@@ -303,6 +309,7 @@ pub struct UserSettings {
pub private_mails: bool,
/// The user's status. Shows over connection info.
#[serde(default)]
+ #[validate(max_length = 256)]
pub status: String,
/// The mime type of the user's banner.
#[serde(default = "mime_avif")]
@@ -365,9 +372,11 @@ pub struct UserSettings {
pub hide_social_follows: bool,
/// The signature automatically attached to new mail letters.
#[serde(default)]
+ #[validate(max_length = 2048)]
pub mail_signature: String,
/// The signature automatically attached to new forum posts.
#[serde(default)]
+ #[validate(max_length = 2048)]
pub forum_signature: String,
/// If coin transfer requests are disabled.
#[serde(default)]
@@ -381,6 +390,26 @@ pub struct UserSettings {
/// If the user's system font is always used over Lexend.
#[serde(default)]
pub use_system_font: bool,
+ /// The user's location. This isn't actually verified or anything, so it can really
+ /// be whatever the user wants.
+ #[serde(default)]
+ #[validate(max_length = 128)]
+ pub location: String,
+ /// External links for the user's other profiles on other websites.
+ #[serde(default)]
+ #[validate(max_items = 5)]
+ #[validate(unique_items)]
+ pub links: Vec<(String, String)>,
+}
+
+impl UserSettings {
+ pub fn verify_values(&self) -> Result<()> {
+ if let Err(e) = self.validate() {
+ return Err(Error::MiscError(e.to_string()));
+ }
+
+ Ok(())
+ }
}
fn mime_avif() -> String {
@@ -429,7 +458,6 @@ impl User {
was_purchased: false,
browser_session: String::new(),
ban_reason: String::new(),
- channel_mutes: Vec::new(),
is_deactivated: false,
ban_expire: 0,
coins: 0,
@@ -437,6 +465,7 @@ impl User {
applied_configurations: Vec::new(),
last_policy_consent: created,
close_friends_stack: 0,
+ missed_messages_count: 0,
}
}
diff --git a/crates/core/src/model/channels.rs b/crates/core/src/model/channels.rs
deleted file mode 100644
index 84180c4..0000000
--- a/crates/core/src/model/channels.rs
+++ /dev/null
@@ -1,133 +0,0 @@
-use std::collections::HashMap;
-
-use serde::{Serialize, Deserialize};
-use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
-
-use super::communities_permissions::CommunityPermission;
-
-/// A channel is a more "chat-like" feed in communities.
-#[derive(Clone, Serialize, Deserialize)]
-pub struct Channel {
- pub id: usize,
- pub community: usize,
- pub owner: usize,
- pub created: usize,
- /// The minimum role (as bits) that can read this channel.
- pub minimum_role_read: u32,
- /// The minimum role (as bits) that can write to this channel.
- pub minimum_role_write: u32,
- /// The position of this channel in the UI.
- ///
- /// Top (0) to bottom.
- pub position: usize,
- /// The members of the chat (ids). Should be empty if `community > 0`.
- ///
- /// The owner should not be a member of the channel since any member can update members.
- pub members: Vec,
- /// The title of the channel.
- pub title: String,
- /// The timestamp of the last message in the channel.
- pub last_message: usize,
-}
-
-impl Channel {
- /// Create a new [`Channel`].
- pub fn new(community: usize, owner: usize, position: usize, title: String) -> Self {
- let created = unix_epoch_timestamp();
-
- Self {
- id: Snowflake::new().to_string().parse::().unwrap(),
- community,
- owner,
- created,
- minimum_role_read: (CommunityPermission::DEFAULT | CommunityPermission::MEMBER).bits(),
- minimum_role_write: (CommunityPermission::DEFAULT | CommunityPermission::MEMBER).bits(),
- position,
- members: Vec::new(),
- title,
- last_message: created,
- }
- }
-
- /// Check if the given `uid` can post in the channel.
- pub fn check_post(&self, uid: usize, membership: Option) -> bool {
- let mut is_member = false;
-
- if let Some(membership) = membership {
- is_member = membership.bits() >= self.minimum_role_write
- }
-
- (uid == self.owner) | is_member | self.members.contains(&uid)
- }
-
- /// Check if the given `uid` can post in the channel.
- pub fn check_read(&self, uid: usize, membership: Option) -> bool {
- let mut is_member = false;
-
- if let Some(membership) = membership {
- is_member = membership.bits() >= self.minimum_role_read
- }
-
- (uid == self.owner) | is_member | self.members.contains(&uid)
- }
-}
-
-#[derive(Clone, Serialize, Deserialize)]
-pub struct Message {
- pub id: usize,
- pub channel: usize,
- pub owner: usize,
- pub created: usize,
- pub edited: usize,
- pub content: String,
- pub context: MessageContext,
- pub reactions: HashMap,
-}
-
-impl Message {
- pub fn new(channel: usize, owner: usize, content: String) -> Self {
- let now = unix_epoch_timestamp();
-
- Self {
- id: Snowflake::new().to_string().parse::().unwrap(),
- channel,
- owner,
- created: now,
- edited: now,
- content,
- context: MessageContext,
- reactions: HashMap::new(),
- }
- }
-}
-
-#[derive(Clone, Serialize, Deserialize)]
-pub struct MessageContext;
-
-impl Default for MessageContext {
- fn default() -> Self {
- Self
- }
-}
-
-#[derive(Clone, Serialize, Deserialize)]
-pub struct MessageReaction {
- pub id: usize,
- pub created: usize,
- pub owner: usize,
- pub message: usize,
- pub emoji: String,
-}
-
-impl MessageReaction {
- /// Create a new [`MessageReaction`].
- pub fn new(owner: usize, message: usize, emoji: String) -> Self {
- Self {
- id: Snowflake::new().to_string().parse::().unwrap(),
- created: unix_epoch_timestamp(),
- owner,
- message,
- emoji,
- }
- }
-}
diff --git a/crates/core/src/model/communities_permissions.rs b/crates/core/src/model/communities_permissions.rs
index 1d8f0da..b61c1d2 100644
--- a/crates/core/src/model/communities_permissions.rs
+++ b/crates/core/src/model/communities_permissions.rs
@@ -18,7 +18,7 @@ bitflags! {
const MANAGE_PINS = 1 << 7;
const MANAGE_COMMUNITY = 1 << 8;
const MANAGE_QUESTIONS = 1 << 9;
- const MANAGE_CHANNELS = 1 << 10;
+ const UNUSED_0 = 1 << 10;
const MANAGE_MESSAGES = 1 << 11;
const MANAGE_EMOJIS = 1 << 12;
diff --git a/crates/core/src/model/mod.rs b/crates/core/src/model/mod.rs
index 75a133f..532408c 100644
--- a/crates/core/src/model/mod.rs
+++ b/crates/core/src/model/mod.rs
@@ -2,7 +2,6 @@ pub mod addr;
pub mod apps;
pub mod auth;
pub mod carp;
-pub mod channels;
pub mod communities;
pub mod communities_permissions;
pub mod economy;
diff --git a/crates/core/src/model/permissions.rs b/crates/core/src/model/permissions.rs
index 61ebb61..01e1193 100644
--- a/crates/core/src/model/permissions.rs
+++ b/crates/core/src/model/permissions.rs
@@ -30,7 +30,7 @@ bitflags! {
const SUPPORTER = 1 << 19;
const MANAGE_REQUESTS = 1 << 20;
const MANAGE_QUESTIONS = 1 << 21;
- const MANAGE_CHANNELS = 1 << 22;
+ const UNUSED_0 = 1 << 22;
const MANAGE_MESSAGES = 1 << 23;
const MANAGE_UPLOADS = 1 << 24;
const MANAGE_EMOJIS = 1 << 25;
diff --git a/example/tetratto.toml b/example/tetratto.toml
index 21b2932..df898a6 100644
--- a/example/tetratto.toml
+++ b/example/tetratto.toml
@@ -24,6 +24,7 @@ system_user = 211903918383300608
[service_hosts]
buckets = "http://localhost:8020"
littleweb = "http://localhost:4119"
+tawny = "http://localhost:8021"
[security]
registration_enabled = true