From 5dec98d698cae199cf610fc5da3b61c513b221a2 Mon Sep 17 00:00:00 2001 From: trisua Date: Sun, 1 Jun 2025 12:25:33 -0400 Subject: [PATCH] add: finish ui rewrite --- Cargo.lock | 8 +- crates/app/Cargo.toml | 2 +- crates/app/src/assets.rs | 253 +-- crates/app/src/langs/en-US.toml | 1 - crates/app/src/public/html/auth/base.html | 9 - crates/app/src/public/html/auth/base.lisp | 16 + .../app/src/public/html/auth/connection.html | 75 - .../app/src/public/html/auth/connection.lisp | 80 + crates/app/src/public/html/auth/login.html | 103 -- crates/app/src/public/html/auth/login.lisp | 121 ++ crates/app/src/public/html/auth/register.html | 120 -- crates/app/src/public/html/auth/register.lisp | 123 ++ .../public/html/chats/{app.html => app.lisp} | 443 +++-- .../app/src/public/html/chats/channels.html | 75 - .../app/src/public/html/chats/channels.lisp | 60 + crates/app/src/public/html/chats/message.html | 2 - crates/app/src/public/html/chats/message.lisp | 1 + crates/app/src/public/html/chats/stream.html | 55 - crates/app/src/public/html/chats/stream.lisp | 55 + .../app/src/public/html/communities/base.html | 340 ---- .../app/src/public/html/communities/base.lisp | 300 +++ .../public/html/communities/create_post.html | 432 ----- .../public/html/communities/create_post.lisp | 406 ++++ .../app/src/public/html/communities/feed.html | 45 - .../app/src/public/html/communities/feed.lisp | 27 + .../app/src/public/html/communities/list.html | 102 -- .../app/src/public/html/communities/list.lisp | 97 + .../src/public/html/communities/members.html | 49 - .../src/public/html/communities/members.lisp | 44 + .../src/public/html/communities/question.html | 111 -- .../src/public/html/communities/question.lisp | 99 + .../public/html/communities/questions.html | 30 - .../public/html/communities/questions.lisp | 21 + .../src/public/html/communities/search.html | 45 - .../src/public/html/communities/search.lisp | 44 + .../src/public/html/communities/settings.html | 964 ---------- .../src/public/html/communities/settings.lisp | 912 +++++++++ crates/app/src/public/html/components.html | 1493 --------------- crates/app/src/public/html/components.lisp | 1310 +++++++++++++ crates/app/src/public/html/macros.lisp | 3 +- .../src/public/html/misc/notifications.html | 86 - .../src/public/html/misc/notifications.lisp | 76 + crates/app/src/public/html/misc/requests.html | 259 --- crates/app/src/public/html/misc/requests.lisp | 252 +++ crates/app/src/public/html/mod/audit_log.html | 38 - crates/app/src/public/html/mod/audit_log.lisp | 37 + .../app/src/public/html/mod/file_report.html | 65 - .../app/src/public/html/mod/file_report.lisp | 63 + crates/app/src/public/html/mod/ip_bans.html | 87 - crates/app/src/public/html/mod/ip_bans.lisp | 86 + crates/app/src/public/html/mod/profile.html | 253 --- crates/app/src/public/html/mod/profile.lisp | 237 +++ crates/app/src/public/html/mod/reports.html | 81 - crates/app/src/public/html/mod/reports.lisp | 72 + crates/app/src/public/html/mod/stats.html | 31 - crates/app/src/public/html/mod/stats.lisp | 34 + crates/app/src/public/html/mod/warnings.html | 132 -- crates/app/src/public/html/mod/warnings.lisp | 121 ++ crates/app/src/public/html/post/likes.html | 92 - crates/app/src/public/html/post/likes.lisp | 85 + crates/app/src/public/html/post/post.html | 344 ---- crates/app/src/public/html/post/post.lisp | 321 ++++ crates/app/src/public/html/post/quotes.html | 74 - crates/app/src/public/html/post/quotes.lisp | 69 + crates/app/src/public/html/post/reposts.html | 93 - crates/app/src/public/html/post/reposts.lisp | 88 + .../app/src/public/html/profile/banned.html | 27 - .../app/src/public/html/profile/banned.lisp | 33 + crates/app/src/public/html/profile/base.html | 386 ---- crates/app/src/public/html/profile/base.lisp | 371 ++++ .../app/src/public/html/profile/blocked.html | 65 - .../app/src/public/html/profile/blocked.lisp | 70 + .../src/public/html/profile/followers.html | 27 - .../src/public/html/profile/followers.lisp | 24 + .../src/public/html/profile/following.html | 27 - .../src/public/html/profile/following.lisp | 24 + crates/app/src/public/html/profile/media.html | 42 - crates/app/src/public/html/profile/media.lisp | 28 + crates/app/src/public/html/profile/posts.html | 67 - crates/app/src/public/html/profile/posts.lisp | 46 + .../app/src/public/html/profile/private.html | 162 -- .../app/src/public/html/profile/private.lisp | 163 ++ .../app/src/public/html/profile/replies.html | 42 - .../app/src/public/html/profile/replies.lisp | 28 + .../app/src/public/html/profile/settings.html | 1632 ----------------- .../app/src/public/html/profile/settings.lisp | 1544 ++++++++++++++++ .../app/src/public/html/profile/warning.html | 37 - .../app/src/public/html/profile/warning.lisp | 39 + crates/app/src/public/html/root.lisp | 4 +- crates/app/src/public/html/stacks/list.html | 93 - crates/app/src/public/html/stacks/list.lisp | 93 + crates/app/src/public/html/stacks/manage.html | 325 ---- crates/app/src/public/html/stacks/manage.lisp | 316 ++++ crates/app/src/public/html/stacks/posts.html | 74 - crates/app/src/public/html/stacks/posts.lisp | 37 + crates/app/src/public/html/timelines/all.html | 43 - crates/app/src/public/html/timelines/all.lisp | 35 + .../public/html/timelines/all_questions.html | 18 - .../public/html/timelines/all_questions.lisp | 13 + .../src/public/html/timelines/following.html | 24 - .../src/public/html/timelines/following.lisp | 13 + .../html/timelines/following_questions.html | 18 - .../html/timelines/following_questions.lisp | 13 + .../app/src/public/html/timelines/home.html | 40 - .../app/src/public/html/timelines/home.lisp | 33 + .../public/html/timelines/home_questions.html | 18 - .../public/html/timelines/home_questions.lisp | 13 + .../src/public/html/timelines/popular.html | 24 - .../src/public/html/timelines/popular.lisp | 13 + .../html/timelines/popular_questions.html | 18 - .../html/timelines/popular_questions.lisp | 13 + .../app/src/public/html/timelines/search.html | 82 - .../app/src/public/html/timelines/search.lisp | 61 + crates/app/src/public/js/atto.js | 4 + crates/core/Cargo.toml | 2 +- crates/core/src/database/posts.rs | 8 +- crates/l10n/Cargo.toml | 2 +- crates/shared/Cargo.toml | 2 +- tools/html_to_lisp.html | 138 ++ 119 files changed, 8776 insertions(+), 9350 deletions(-) delete mode 100644 crates/app/src/public/html/auth/base.html create mode 100644 crates/app/src/public/html/auth/base.lisp delete mode 100644 crates/app/src/public/html/auth/connection.html create mode 100644 crates/app/src/public/html/auth/connection.lisp delete mode 100644 crates/app/src/public/html/auth/login.html create mode 100644 crates/app/src/public/html/auth/login.lisp delete mode 100644 crates/app/src/public/html/auth/register.html create mode 100644 crates/app/src/public/html/auth/register.lisp rename crates/app/src/public/html/chats/{app.html => app.lisp} (56%) delete mode 100644 crates/app/src/public/html/chats/channels.html create mode 100644 crates/app/src/public/html/chats/channels.lisp delete mode 100644 crates/app/src/public/html/chats/message.html create mode 100644 crates/app/src/public/html/chats/message.lisp delete mode 100644 crates/app/src/public/html/chats/stream.html create mode 100644 crates/app/src/public/html/chats/stream.lisp delete mode 100644 crates/app/src/public/html/communities/base.html create mode 100644 crates/app/src/public/html/communities/base.lisp delete mode 100644 crates/app/src/public/html/communities/create_post.html create mode 100644 crates/app/src/public/html/communities/create_post.lisp delete mode 100644 crates/app/src/public/html/communities/feed.html create mode 100644 crates/app/src/public/html/communities/feed.lisp delete mode 100644 crates/app/src/public/html/communities/list.html create mode 100644 crates/app/src/public/html/communities/list.lisp delete mode 100644 crates/app/src/public/html/communities/members.html create mode 100644 crates/app/src/public/html/communities/members.lisp delete mode 100644 crates/app/src/public/html/communities/question.html create mode 100644 crates/app/src/public/html/communities/question.lisp delete mode 100644 crates/app/src/public/html/communities/questions.html create mode 100644 crates/app/src/public/html/communities/questions.lisp delete mode 100644 crates/app/src/public/html/communities/search.html create mode 100644 crates/app/src/public/html/communities/search.lisp delete mode 100644 crates/app/src/public/html/communities/settings.html create mode 100644 crates/app/src/public/html/communities/settings.lisp delete mode 100644 crates/app/src/public/html/components.html create mode 100644 crates/app/src/public/html/components.lisp delete mode 100644 crates/app/src/public/html/misc/notifications.html create mode 100644 crates/app/src/public/html/misc/notifications.lisp delete mode 100644 crates/app/src/public/html/misc/requests.html create mode 100644 crates/app/src/public/html/misc/requests.lisp delete mode 100644 crates/app/src/public/html/mod/audit_log.html create mode 100644 crates/app/src/public/html/mod/audit_log.lisp delete mode 100644 crates/app/src/public/html/mod/file_report.html create mode 100644 crates/app/src/public/html/mod/file_report.lisp delete mode 100644 crates/app/src/public/html/mod/ip_bans.html create mode 100644 crates/app/src/public/html/mod/ip_bans.lisp delete mode 100644 crates/app/src/public/html/mod/profile.html create mode 100644 crates/app/src/public/html/mod/profile.lisp delete mode 100644 crates/app/src/public/html/mod/reports.html create mode 100644 crates/app/src/public/html/mod/reports.lisp delete mode 100644 crates/app/src/public/html/mod/stats.html create mode 100644 crates/app/src/public/html/mod/stats.lisp delete mode 100644 crates/app/src/public/html/mod/warnings.html create mode 100644 crates/app/src/public/html/mod/warnings.lisp delete mode 100644 crates/app/src/public/html/post/likes.html create mode 100644 crates/app/src/public/html/post/likes.lisp delete mode 100644 crates/app/src/public/html/post/post.html create mode 100644 crates/app/src/public/html/post/post.lisp delete mode 100644 crates/app/src/public/html/post/quotes.html create mode 100644 crates/app/src/public/html/post/quotes.lisp delete mode 100644 crates/app/src/public/html/post/reposts.html create mode 100644 crates/app/src/public/html/post/reposts.lisp delete mode 100644 crates/app/src/public/html/profile/banned.html create mode 100644 crates/app/src/public/html/profile/banned.lisp delete mode 100644 crates/app/src/public/html/profile/base.html create mode 100644 crates/app/src/public/html/profile/base.lisp delete mode 100644 crates/app/src/public/html/profile/blocked.html create mode 100644 crates/app/src/public/html/profile/blocked.lisp delete mode 100644 crates/app/src/public/html/profile/followers.html create mode 100644 crates/app/src/public/html/profile/followers.lisp delete mode 100644 crates/app/src/public/html/profile/following.html create mode 100644 crates/app/src/public/html/profile/following.lisp delete mode 100644 crates/app/src/public/html/profile/media.html create mode 100644 crates/app/src/public/html/profile/media.lisp delete mode 100644 crates/app/src/public/html/profile/posts.html create mode 100644 crates/app/src/public/html/profile/posts.lisp delete mode 100644 crates/app/src/public/html/profile/private.html create mode 100644 crates/app/src/public/html/profile/private.lisp delete mode 100644 crates/app/src/public/html/profile/replies.html create mode 100644 crates/app/src/public/html/profile/replies.lisp delete mode 100644 crates/app/src/public/html/profile/settings.html create mode 100644 crates/app/src/public/html/profile/settings.lisp delete mode 100644 crates/app/src/public/html/profile/warning.html create mode 100644 crates/app/src/public/html/profile/warning.lisp delete mode 100644 crates/app/src/public/html/stacks/list.html create mode 100644 crates/app/src/public/html/stacks/list.lisp delete mode 100644 crates/app/src/public/html/stacks/manage.html create mode 100644 crates/app/src/public/html/stacks/manage.lisp delete mode 100644 crates/app/src/public/html/stacks/posts.html create mode 100644 crates/app/src/public/html/stacks/posts.lisp delete mode 100644 crates/app/src/public/html/timelines/all.html create mode 100644 crates/app/src/public/html/timelines/all.lisp delete mode 100644 crates/app/src/public/html/timelines/all_questions.html create mode 100644 crates/app/src/public/html/timelines/all_questions.lisp delete mode 100644 crates/app/src/public/html/timelines/following.html create mode 100644 crates/app/src/public/html/timelines/following.lisp delete mode 100644 crates/app/src/public/html/timelines/following_questions.html create mode 100644 crates/app/src/public/html/timelines/following_questions.lisp delete mode 100644 crates/app/src/public/html/timelines/home.html create mode 100644 crates/app/src/public/html/timelines/home.lisp delete mode 100644 crates/app/src/public/html/timelines/home_questions.html create mode 100644 crates/app/src/public/html/timelines/home_questions.lisp delete mode 100644 crates/app/src/public/html/timelines/popular.html create mode 100644 crates/app/src/public/html/timelines/popular.lisp delete mode 100644 crates/app/src/public/html/timelines/popular_questions.html create mode 100644 crates/app/src/public/html/timelines/popular_questions.lisp delete mode 100644 crates/app/src/public/html/timelines/search.html create mode 100644 crates/app/src/public/html/timelines/search.lisp create mode 100644 tools/html_to_lisp.html diff --git a/Cargo.lock b/Cargo.lock index 24a871b..3ed31d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3275,7 +3275,7 @@ dependencies = [ [[package]] name = "tetratto" -version = "4.0.0" +version = "4.5.0" dependencies = [ "ammonia", "async-stripe", @@ -3307,7 +3307,7 @@ dependencies = [ [[package]] name = "tetratto-core" -version = "4.0.0" +version = "4.5.0" dependencies = [ "async-recursion", "base16ct", @@ -3332,7 +3332,7 @@ dependencies = [ [[package]] name = "tetratto-l10n" -version = "4.0.0" +version = "4.5.0" dependencies = [ "pathbufd", "serde", @@ -3341,7 +3341,7 @@ dependencies = [ [[package]] name = "tetratto-shared" -version = "4.0.0" +version = "4.5.0" dependencies = [ "ammonia", "chrono", diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 80eeaa4..396f908 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto" -version = "4.0.0" +version = "4.5.0" edition = "2024" [features] diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index 47bc41d..7a86bb3 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -8,6 +8,7 @@ use std::{ collections::HashMap, fs::{exists, read_to_string, write}, sync::LazyLock, + time::SystemTime, }; use tera::Context; use tetratto_core::{ @@ -41,76 +42,76 @@ pub const STREAMS_JS: &str = include_str!("./public/js/streams.js"); // html pub const ROOT: &str = include_str!("./public/html/root.lisp"); pub const MACROS: &str = include_str!("./public/html/macros.lisp"); -pub const COMPONENTS: &str = include_str!("./public/html/components.html"); +pub const COMPONENTS: &str = include_str!("./public/html/components.lisp"); pub const MISC_ERROR: &str = include_str!("./public/html/misc/error.lisp"); -pub const MISC_NOTIFICATIONS: &str = include_str!("./public/html/misc/notifications.html"); +pub const MISC_NOTIFICATIONS: &str = include_str!("./public/html/misc/notifications.lisp"); pub const MISC_MARKDOWN: &str = include_str!("./public/html/misc/markdown.lisp"); -pub const MISC_REQUESTS: &str = include_str!("./public/html/misc/requests.html"); +pub const MISC_REQUESTS: &str = include_str!("./public/html/misc/requests.lisp"); -pub const AUTH_BASE: &str = include_str!("./public/html/auth/base.html"); -pub const AUTH_LOGIN: &str = include_str!("./public/html/auth/login.html"); -pub const AUTH_REGISTER: &str = include_str!("./public/html/auth/register.html"); -pub const AUTH_CONNECTION: &str = include_str!("./public/html/auth/connection.html"); +pub const AUTH_BASE: &str = include_str!("./public/html/auth/base.lisp"); +pub const AUTH_LOGIN: &str = include_str!("./public/html/auth/login.lisp"); +pub const AUTH_REGISTER: &str = include_str!("./public/html/auth/register.lisp"); +pub const AUTH_CONNECTION: &str = include_str!("./public/html/auth/connection.lisp"); -pub const PROFILE_BASE: &str = include_str!("./public/html/profile/base.html"); -pub const PROFILE_POSTS: &str = include_str!("./public/html/profile/posts.html"); -pub const PROFILE_SETTINGS: &str = include_str!("./public/html/profile/settings.html"); -pub const PROFILE_FOLLOWING: &str = include_str!("./public/html/profile/following.html"); -pub const PROFILE_FOLLOWERS: &str = include_str!("./public/html/profile/followers.html"); -pub const PROFILE_WARNING: &str = include_str!("./public/html/profile/warning.html"); -pub const PROFILE_PRIVATE: &str = include_str!("./public/html/profile/private.html"); -pub const PROFILE_BLOCKED: &str = include_str!("./public/html/profile/blocked.html"); -pub const PROFILE_BANNED: &str = include_str!("./public/html/profile/banned.html"); -pub const PROFILE_REPLIES: &str = include_str!("./public/html/profile/replies.html"); -pub const PROFILE_MEDIA: &str = include_str!("./public/html/profile/media.html"); +pub const PROFILE_BASE: &str = include_str!("./public/html/profile/base.lisp"); +pub const PROFILE_POSTS: &str = include_str!("./public/html/profile/posts.lisp"); +pub const PROFILE_SETTINGS: &str = include_str!("./public/html/profile/settings.lisp"); +pub const PROFILE_FOLLOWING: &str = include_str!("./public/html/profile/following.lisp"); +pub const PROFILE_FOLLOWERS: &str = include_str!("./public/html/profile/followers.lisp"); +pub const PROFILE_WARNING: &str = include_str!("./public/html/profile/warning.lisp"); +pub const PROFILE_PRIVATE: &str = include_str!("./public/html/profile/private.lisp"); +pub const PROFILE_BLOCKED: &str = include_str!("./public/html/profile/blocked.lisp"); +pub const PROFILE_BANNED: &str = include_str!("./public/html/profile/banned.lisp"); +pub const PROFILE_REPLIES: &str = include_str!("./public/html/profile/replies.lisp"); +pub const PROFILE_MEDIA: &str = include_str!("./public/html/profile/media.lisp"); -pub const COMMUNITIES_LIST: &str = include_str!("./public/html/communities/list.html"); -pub const COMMUNITIES_BASE: &str = include_str!("./public/html/communities/base.html"); -pub const COMMUNITIES_FEED: &str = include_str!("./public/html/communities/feed.html"); -pub const COMMUNITIES_SETTINGS: &str = include_str!("./public/html/communities/settings.html"); -pub const COMMUNITIES_MEMBERS: &str = include_str!("./public/html/communities/members.html"); -pub const COMMUNITIES_SEARCH: &str = include_str!("./public/html/communities/search.html"); +pub const COMMUNITIES_LIST: &str = include_str!("./public/html/communities/list.lisp"); +pub const COMMUNITIES_BASE: &str = include_str!("./public/html/communities/base.lisp"); +pub const COMMUNITIES_FEED: &str = include_str!("./public/html/communities/feed.lisp"); +pub const COMMUNITIES_SETTINGS: &str = include_str!("./public/html/communities/settings.lisp"); +pub const COMMUNITIES_MEMBERS: &str = include_str!("./public/html/communities/members.lisp"); +pub const COMMUNITIES_SEARCH: &str = include_str!("./public/html/communities/search.lisp"); pub const COMMUNITIES_CREATE_POST: &str = - include_str!("./public/html/communities/create_post.html"); -pub const COMMUNITIES_QUESTION: &str = include_str!("./public/html/communities/question.html"); -pub const COMMUNITIES_QUESTIONS: &str = include_str!("./public/html/communities/questions.html"); + include_str!("./public/html/communities/create_post.lisp"); +pub const COMMUNITIES_QUESTION: &str = include_str!("./public/html/communities/question.lisp"); +pub const COMMUNITIES_QUESTIONS: &str = include_str!("./public/html/communities/questions.lisp"); -pub const POST_POST: &str = include_str!("./public/html/post/post.html"); -pub const POST_REPOSTS: &str = include_str!("./public/html/post/reposts.html"); -pub const POST_QUOTES: &str = include_str!("./public/html/post/quotes.html"); -pub const POST_LIKES: &str = include_str!("./public/html/post/likes.html"); +pub const POST_POST: &str = include_str!("./public/html/post/post.lisp"); +pub const POST_REPOSTS: &str = include_str!("./public/html/post/reposts.lisp"); +pub const POST_QUOTES: &str = include_str!("./public/html/post/quotes.lisp"); +pub const POST_LIKES: &str = include_str!("./public/html/post/likes.lisp"); -pub const TIMELINES_HOME: &str = include_str!("./public/html/timelines/home.html"); -pub const TIMELINES_POPULAR: &str = include_str!("./public/html/timelines/popular.html"); -pub const TIMELINES_FOLLOWING: &str = include_str!("./public/html/timelines/following.html"); -pub const TIMELINES_ALL: &str = include_str!("./public/html/timelines/all.html"); +pub const TIMELINES_HOME: &str = include_str!("./public/html/timelines/home.lisp"); +pub const TIMELINES_POPULAR: &str = include_str!("./public/html/timelines/popular.lisp"); +pub const TIMELINES_FOLLOWING: &str = include_str!("./public/html/timelines/following.lisp"); +pub const TIMELINES_ALL: &str = include_str!("./public/html/timelines/all.lisp"); pub const TIMELINES_HOME_QUESTIONS: &str = - include_str!("./public/html/timelines/home_questions.html"); + include_str!("./public/html/timelines/home_questions.lisp"); pub const TIMELINES_POPULAR_QUESTIONS: &str = - include_str!("./public/html/timelines/popular_questions.html"); + include_str!("./public/html/timelines/popular_questions.lisp"); pub const TIMELINES_FOLLOWING_QUESTIONS: &str = - include_str!("./public/html/timelines/following_questions.html"); + include_str!("./public/html/timelines/following_questions.lisp"); pub const TIMELINES_ALL_QUESTIONS: &str = - include_str!("./public/html/timelines/all_questions.html"); -pub const TIMELINES_SEARCH: &str = include_str!("./public/html/timelines/search.html"); + include_str!("./public/html/timelines/all_questions.lisp"); +pub const TIMELINES_SEARCH: &str = include_str!("./public/html/timelines/search.lisp"); -pub const MOD_AUDIT_LOG: &str = include_str!("./public/html/mod/audit_log.html"); -pub const MOD_REPORTS: &str = include_str!("./public/html/mod/reports.html"); -pub const MOD_FILE_REPORT: &str = include_str!("./public/html/mod/file_report.html"); -pub const MOD_IP_BANS: &str = include_str!("./public/html/mod/ip_bans.html"); -pub const MOD_PROFILE: &str = include_str!("./public/html/mod/profile.html"); -pub const MOD_WARNINGS: &str = include_str!("./public/html/mod/warnings.html"); -pub const MOD_STATS: &str = include_str!("./public/html/mod/stats.html"); +pub const MOD_AUDIT_LOG: &str = include_str!("./public/html/mod/audit_log.lisp"); +pub const MOD_REPORTS: &str = include_str!("./public/html/mod/reports.lisp"); +pub const MOD_FILE_REPORT: &str = include_str!("./public/html/mod/file_report.lisp"); +pub const MOD_IP_BANS: &str = include_str!("./public/html/mod/ip_bans.lisp"); +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.html"); -pub const CHATS_STREAM: &str = include_str!("./public/html/chats/stream.html"); -pub const CHATS_MESSAGE: &str = include_str!("./public/html/chats/message.html"); -pub const CHATS_CHANNELS: &str = include_str!("./public/html/chats/channels.html"); +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.html"); -pub const STACKS_POSTS: &str = include_str!("./public/html/stacks/posts.html"); -pub const STACKS_MANAGE: &str = include_str!("./public/html/stacks/manage.html"); +pub const STACKS_LIST: &str = include_str!("./public/html/stacks/list.lisp"); +pub const STACKS_POSTS: &str = include_str!("./public/html/stacks/posts.lisp"); +pub const STACKS_MANAGE: &str = include_str!("./public/html/stacks/manage.lisp"); // langs pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml"); @@ -143,13 +144,7 @@ pub(crate) async fn pull_icon(icon: &str, icons_dir: &str) { } println!("download icon: {icon}"); - let svg = reqwest::get(icon_url) - .await - .unwrap() - .text() - .await - .unwrap() - .replace("\n", ""); + let svg = reqwest::get(icon_url).await.unwrap().text().await.unwrap(); write(&file_path, &svg).unwrap(); writer.insert(icon.to_string(), svg); @@ -198,7 +193,10 @@ pub(crate) async fn replace_in_html( let mut input = if !lisp { input.to_string() } else { + let start = SystemTime::now(); let parsed = bberry::parse(input); + println!("parsed lisp in {}μs", start.elapsed().unwrap().as_micros()); + if let Some(plugins) = plugins { parsed.render(plugins) } else { @@ -221,7 +219,12 @@ pub(crate) async fn replace_in_html( Regex::new("(\\{\\{)\\s*(icon)\\s*(.*?)\\s*c\\((.*?)\\)\\s*(\\}\\})").unwrap(); for cap in icon_with_class.captures_iter(&input.clone()) { - let icon = &cap.get(3).unwrap().as_str().replace("\"", ""); + let cap_str = &cap.get(3).unwrap().as_str().replace("\"", ""); + let icon = &(if cap_str.contains(" }}") { + cap_str.split(" }}").next().unwrap().to_string() + } else { + cap_str.to_string() + }); pull_icon(icon, &config.dirs.icons).await; @@ -231,14 +234,26 @@ pub(crate) async fn replace_in_html( &format!(" PathBufD { write_template!(html_path->"root.html"(crate::assets::ROOT) --config=config --lisp plugins); write_template!(html_path->"macros.html"(crate::assets::MACROS) --config=config --lisp plugins); - write_template!(html_path->"components.html"(crate::assets::COMPONENTS) --config=config); + write_template!(html_path->"components.html"(crate::assets::COMPONENTS) --config=config --lisp plugins); write_template!(html_path->"misc/error.html"(crate::assets::MISC_ERROR) -d "misc" --config=config --lisp plugins); - write_template!(html_path->"misc/notifications.html"(crate::assets::MISC_NOTIFICATIONS) --config=config); + write_template!(html_path->"misc/notifications.html"(crate::assets::MISC_NOTIFICATIONS) --config=config --lisp plugins); write_template!(html_path->"misc/markdown.html"(crate::assets::MISC_MARKDOWN) --config=config --lisp plugins); - write_template!(html_path->"misc/requests.html"(crate::assets::MISC_REQUESTS) --config=config); + write_template!(html_path->"misc/requests.html"(crate::assets::MISC_REQUESTS) --config=config --lisp plugins); - write_template!(html_path->"auth/base.html"(crate::assets::AUTH_BASE) -d "auth" --config=config); - write_template!(html_path->"auth/login.html"(crate::assets::AUTH_LOGIN) --config=config); - write_template!(html_path->"auth/register.html"(crate::assets::AUTH_REGISTER) --config=config); - write_template!(html_path->"auth/connection.html"(crate::assets::AUTH_CONNECTION) --config=config); + write_template!(html_path->"auth/base.html"(crate::assets::AUTH_BASE) -d "auth" --config=config --lisp plugins); + write_template!(html_path->"auth/login.html"(crate::assets::AUTH_LOGIN) --config=config --lisp plugins); + write_template!(html_path->"auth/register.html"(crate::assets::AUTH_REGISTER) --config=config --lisp plugins); + write_template!(html_path->"auth/connection.html"(crate::assets::AUTH_CONNECTION) --config=config --lisp plugins); - write_template!(html_path->"profile/base.html"(crate::assets::PROFILE_BASE) -d "profile" --config=config); - write_template!(html_path->"profile/posts.html"(crate::assets::PROFILE_POSTS) --config=config); - write_template!(html_path->"profile/settings.html"(crate::assets::PROFILE_SETTINGS) --config=config); - write_template!(html_path->"profile/following.html"(crate::assets::PROFILE_FOLLOWING) --config=config); - write_template!(html_path->"profile/followers.html"(crate::assets::PROFILE_FOLLOWERS) --config=config); - write_template!(html_path->"profile/warning.html"(crate::assets::PROFILE_WARNING) --config=config); - write_template!(html_path->"profile/private.html"(crate::assets::PROFILE_PRIVATE) --config=config); - write_template!(html_path->"profile/blocked.html"(crate::assets::PROFILE_BLOCKED) --config=config); - write_template!(html_path->"profile/banned.html"(crate::assets::PROFILE_BANNED) --config=config); - write_template!(html_path->"profile/replies.html"(crate::assets::PROFILE_REPLIES) --config=config); - write_template!(html_path->"profile/media.html"(crate::assets::PROFILE_MEDIA) --config=config); + write_template!(html_path->"profile/base.html"(crate::assets::PROFILE_BASE) -d "profile" --config=config --lisp plugins); + write_template!(html_path->"profile/posts.html"(crate::assets::PROFILE_POSTS) --config=config --lisp plugins); + write_template!(html_path->"profile/settings.html"(crate::assets::PROFILE_SETTINGS) --config=config --lisp plugins); + write_template!(html_path->"profile/following.html"(crate::assets::PROFILE_FOLLOWING) --config=config --lisp plugins); + write_template!(html_path->"profile/followers.html"(crate::assets::PROFILE_FOLLOWERS) --config=config --lisp plugins); + write_template!(html_path->"profile/warning.html"(crate::assets::PROFILE_WARNING) --config=config --lisp plugins); + write_template!(html_path->"profile/private.html"(crate::assets::PROFILE_PRIVATE) --config=config --lisp plugins); + write_template!(html_path->"profile/blocked.html"(crate::assets::PROFILE_BLOCKED) --config=config --lisp plugins); + write_template!(html_path->"profile/banned.html"(crate::assets::PROFILE_BANNED) --config=config --lisp plugins); + write_template!(html_path->"profile/replies.html"(crate::assets::PROFILE_REPLIES) --config=config --lisp plugins); + write_template!(html_path->"profile/media.html"(crate::assets::PROFILE_MEDIA) --config=config --lisp plugins); - write_template!(html_path->"communities/list.html"(crate::assets::COMMUNITIES_LIST) -d "communities" --config=config); - write_template!(html_path->"communities/base.html"(crate::assets::COMMUNITIES_BASE) --config=config); - write_template!(html_path->"communities/feed.html"(crate::assets::COMMUNITIES_FEED) --config=config); - write_template!(html_path->"communities/settings.html"(crate::assets::COMMUNITIES_SETTINGS) --config=config); - write_template!(html_path->"communities/members.html"(crate::assets::COMMUNITIES_MEMBERS) --config=config); - write_template!(html_path->"communities/search.html"(crate::assets::COMMUNITIES_SEARCH) --config=config); - write_template!(html_path->"communities/create_post.html"(crate::assets::COMMUNITIES_CREATE_POST) --config=config); - write_template!(html_path->"communities/question.html"(crate::assets::COMMUNITIES_QUESTION) --config=config); - write_template!(html_path->"communities/questions.html"(crate::assets::COMMUNITIES_QUESTIONS) --config=config); + write_template!(html_path->"communities/list.html"(crate::assets::COMMUNITIES_LIST) -d "communities" --config=config --lisp plugins); + write_template!(html_path->"communities/base.html"(crate::assets::COMMUNITIES_BASE) --config=config --lisp plugins); + write_template!(html_path->"communities/feed.html"(crate::assets::COMMUNITIES_FEED) --config=config --lisp plugins); + write_template!(html_path->"communities/settings.html"(crate::assets::COMMUNITIES_SETTINGS) --config=config --lisp plugins); + write_template!(html_path->"communities/members.html"(crate::assets::COMMUNITIES_MEMBERS) --config=config --lisp plugins); + write_template!(html_path->"communities/search.html"(crate::assets::COMMUNITIES_SEARCH) --config=config --lisp plugins); + write_template!(html_path->"communities/create_post.html"(crate::assets::COMMUNITIES_CREATE_POST) --config=config --lisp plugins); + write_template!(html_path->"communities/question.html"(crate::assets::COMMUNITIES_QUESTION) --config=config --lisp plugins); + write_template!(html_path->"communities/questions.html"(crate::assets::COMMUNITIES_QUESTIONS) --config=config --lisp plugins); - write_template!(html_path->"post/post.html"(crate::assets::POST_POST) -d "post" --config=config); - write_template!(html_path->"post/reposts.html"(crate::assets::POST_REPOSTS) --config=config); - write_template!(html_path->"post/quotes.html"(crate::assets::POST_QUOTES) --config=config); - write_template!(html_path->"post/likes.html"(crate::assets::POST_LIKES) --config=config); + write_template!(html_path->"post/post.html"(crate::assets::POST_POST) -d "post" --config=config --lisp plugins); + write_template!(html_path->"post/reposts.html"(crate::assets::POST_REPOSTS) --config=config --lisp plugins); + write_template!(html_path->"post/quotes.html"(crate::assets::POST_QUOTES) --config=config --lisp plugins); + write_template!(html_path->"post/likes.html"(crate::assets::POST_LIKES) --config=config --lisp plugins); - write_template!(html_path->"timelines/home.html"(crate::assets::TIMELINES_HOME) -d "timelines" --config=config); - write_template!(html_path->"timelines/popular.html"(crate::assets::TIMELINES_POPULAR) --config=config); - write_template!(html_path->"timelines/following.html"(crate::assets::TIMELINES_FOLLOWING) --config=config); - write_template!(html_path->"timelines/all.html"(crate::assets::TIMELINES_ALL) --config=config); - write_template!(html_path->"timelines/home_questions.html"(crate::assets::TIMELINES_HOME_QUESTIONS) --config=config); - write_template!(html_path->"timelines/popular_questions.html"(crate::assets::TIMELINES_POPULAR_QUESTIONS) --config=config); - write_template!(html_path->"timelines/following_questions.html"(crate::assets::TIMELINES_FOLLOWING_QUESTIONS) --config=config); - write_template!(html_path->"timelines/all_questions.html"(crate::assets::TIMELINES_ALL_QUESTIONS) --config=config); - write_template!(html_path->"timelines/search.html"(crate::assets::TIMELINES_SEARCH) --config=config); + write_template!(html_path->"timelines/home.html"(crate::assets::TIMELINES_HOME) -d "timelines" --config=config --lisp plugins); + write_template!(html_path->"timelines/popular.html"(crate::assets::TIMELINES_POPULAR) --config=config --lisp plugins); + write_template!(html_path->"timelines/following.html"(crate::assets::TIMELINES_FOLLOWING) --config=config --lisp plugins); + write_template!(html_path->"timelines/all.html"(crate::assets::TIMELINES_ALL) --config=config --lisp plugins); + write_template!(html_path->"timelines/home_questions.html"(crate::assets::TIMELINES_HOME_QUESTIONS) --config=config --lisp plugins); + write_template!(html_path->"timelines/popular_questions.html"(crate::assets::TIMELINES_POPULAR_QUESTIONS) --config=config --lisp plugins); + write_template!(html_path->"timelines/following_questions.html"(crate::assets::TIMELINES_FOLLOWING_QUESTIONS) --config=config --lisp plugins); + write_template!(html_path->"timelines/all_questions.html"(crate::assets::TIMELINES_ALL_QUESTIONS) --config=config --lisp plugins); + write_template!(html_path->"timelines/search.html"(crate::assets::TIMELINES_SEARCH) --config=config --lisp plugins); - write_template!(html_path->"mod/audit_log.html"(crate::assets::MOD_AUDIT_LOG) -d "mod" --config=config); - write_template!(html_path->"mod/reports.html"(crate::assets::MOD_REPORTS) --config=config); - write_template!(html_path->"mod/file_report.html"(crate::assets::MOD_FILE_REPORT) --config=config); - write_template!(html_path->"mod/ip_bans.html"(crate::assets::MOD_IP_BANS) --config=config); - write_template!(html_path->"mod/profile.html"(crate::assets::MOD_PROFILE) --config=config); - write_template!(html_path->"mod/warnings.html"(crate::assets::MOD_WARNINGS) --config=config); - write_template!(html_path->"mod/stats.html"(crate::assets::MOD_STATS) --config=config); + write_template!(html_path->"mod/audit_log.html"(crate::assets::MOD_AUDIT_LOG) -d "mod" --config=config --lisp plugins); + write_template!(html_path->"mod/reports.html"(crate::assets::MOD_REPORTS) --config=config --lisp plugins); + write_template!(html_path->"mod/file_report.html"(crate::assets::MOD_FILE_REPORT) --config=config --lisp plugins); + write_template!(html_path->"mod/ip_bans.html"(crate::assets::MOD_IP_BANS) --config=config --lisp plugins); + write_template!(html_path->"mod/profile.html"(crate::assets::MOD_PROFILE) --config=config --lisp plugins); + 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); - write_template!(html_path->"chats/stream.html"(crate::assets::CHATS_STREAM) --config=config); - write_template!(html_path->"chats/message.html"(crate::assets::CHATS_MESSAGE) --config=config); - write_template!(html_path->"chats/channels.html"(crate::assets::CHATS_CHANNELS) --config=config); + 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); - write_template!(html_path->"stacks/posts.html"(crate::assets::STACKS_POSTS) --config=config); - write_template!(html_path->"stacks/manage.html"(crate::assets::STACKS_MANAGE) --config=config); + write_template!(html_path->"stacks/list.html"(crate::assets::STACKS_LIST) -d "stacks" --config=config --lisp plugins); + write_template!(html_path->"stacks/posts.html"(crate::assets::STACKS_POSTS) --config=config --lisp plugins); + write_template!(html_path->"stacks/manage.html"(crate::assets::STACKS_MANAGE) --config=config --lisp plugins); html_path } diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index 6cef894..d251b81 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -114,7 +114,6 @@ version = "1.0.0" "communities:label.edit_content" = "Edit content" "communities:label.repost" = "Repost" "communities:label.quote_post" = "Quote post" -"communities:label.expand_original" = "Expand original" "communities:label.search_results" = "Search results" "communities:label.query" = "Query" "communities:label.join_new" = "Join new" diff --git a/crates/app/src/public/html/auth/base.html b/crates/app/src/public/html/auth/base.html deleted file mode 100644 index 40ebd28..0000000 --- a/crates/app/src/public/html/auth/base.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends "root.html" %} {% block body %} -
-

{% block title %}{% endblock %}

-
- {% block content %}{% endblock %} -
- {% block footer %}{% endblock %} - -{% endblock %} diff --git a/crates/app/src/public/html/auth/base.lisp b/crates/app/src/public/html/auth/base.lisp new file mode 100644 index 0000000..c13c336 --- /dev/null +++ b/crates/app/src/public/html/auth/base.lisp @@ -0,0 +1,16 @@ +(text "{% extends \"root.html\" %} {% block body %}") +(main + ("class" "flex flex-col gap-2") + ("style" "max-width: 25rem") + (h2 + ("class" "w-full text-center") + ; block for title + (text "{% block title %}{% endblock %}")) + (div + ("class" "card w-full flex flex-col gap-4 justify-center align-center") + ; block for actual page content + (text "{% block content %}{% endblock %}")) + ; small footer block (for switching context) + (text "{% block footer %}{% endblock %}")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/auth/connection.html b/crates/app/src/public/html/auth/connection.html deleted file mode 100644 index fafa8c9..0000000 --- a/crates/app/src/public/html/auth/connection.html +++ /dev/null @@ -1,75 +0,0 @@ -{% extends "auth/base.html" %} {% block head %} -Connection -{% endblock %} {% block title %}Connection{% endblock %} {% block content %} -
Working...
- -{% if connection_type == "Spotify" and user and user.connections.Spotify and -config.connections.spotify_client_id %} - -{% elif connection_type == "LastFm" and user and user.connections.LastFm and -config.connections.last_fm_key %} - -{%- endif %} {% endblock %} diff --git a/crates/app/src/public/html/auth/connection.lisp b/crates/app/src/public/html/auth/connection.lisp new file mode 100644 index 0000000..905b215 --- /dev/null +++ b/crates/app/src/public/html/auth/connection.lisp @@ -0,0 +1,80 @@ +(text "{% extends \"auth/base.html\" %} {% block head %}") +(title + (text "Connection")) + +(text "{% endblock %} {% block title %}Connection{% endblock %} {% block content %}") +(div + ("class" "w-full flex-col gap-2") + ("id" "status") + ; display loading text because we have to wait for the data to update on remote + (b + (text "Working..."))) + +(text "{% if connection_type == \"Spotify\" and user and user.connections.Spotify and config.connections.spotify_client_id %}") +(script + (text "setTimeout(async () => { + const code = new URLSearchParams(window.location.search).get(\"code\"); + const client_id = \"{{ config.connections.spotify_client_id }}\"; + const verifier = \"{{ user.connections.Spotify[0].data.verifier }}\"; + + if (!code) { + alert(\"Connection failed (did not get code from Spotify)\"); + return; + } + + const [token, refresh_token, expires_in] = await trigger( + \"spotify::get_token\", + [client_id, verifier, code], + ); + + const profile = await trigger(\"spotify::profile\", [token]); + + const { message } = await trigger(\"connections::push_con_data\", [ + \"Spotify\", + { + token, + refresh_token, + expires_in: expires_in.toString(), + name: profile.display_name, + url: profile.external_urls.spotify, + }, + ]); + + document.getElementById(\"status\").innerHTML = + `${message}. You can now close this tab.`; + + setTimeout(() => { + window.location.href = \"/settings#/connections\"; + }, 500); + }, 150);")) + +(text "{% elif connection_type == \"LastFm\" and user and user.connections.LastFm and config.connections.last_fm_key %}") +(script + (text "setTimeout(async () => { + const token = new URLSearchParams(window.location.search).get(\"token\"); + const api_key = \"{{ config.connections.last_fm_key }}\"; + + if (!token) { + alert(\"Connection failed (did not get token from Last.fm)\"); + return; + } + + const res = await trigger(\"last_fm::get_session\", [token]); + const { message } = await trigger(\"connections::push_con_data\", [ + \"LastFm\", + { + session_token: res.session.key, + name: res.session.name, + url: `https://last.fm/user/${res.session.name}`, + }, + ]); + + document.getElementById(\"status\").innerHTML = + `${message}. You can now close this tab.`; + + setTimeout(() => { + window.location.href = \"/settings#/connections\"; + }, 500); + }, 1000);")) + +(text "{%- endif %} {% endblock %}") diff --git a/crates/app/src/public/html/auth/login.html b/crates/app/src/public/html/auth/login.html deleted file mode 100644 index 943676e..0000000 --- a/crates/app/src/public/html/auth/login.html +++ /dev/null @@ -1,103 +0,0 @@ -{% extends "auth/base.html" %} {% block head %} -Login -{% endblock %} {% block title %}Login{% endblock %} {% block content %} -
-
-
- - -
- -
- - -
-
- - - - -
- - -{% endblock %} {% block footer %} -Or, register -{% endblock %} diff --git a/crates/app/src/public/html/auth/login.lisp b/crates/app/src/public/html/auth/login.lisp new file mode 100644 index 0000000..82ce3b4 --- /dev/null +++ b/crates/app/src/public/html/auth/login.lisp @@ -0,0 +1,121 @@ +(text "{% extends \"auth/base.html\" %} {% block head %}") +(title + (text "Login")) + +(text "{% endblock %} {% block title %}Login{% endblock %} {% block content %}") +(form + ("class" "w-full flex flex-col gap-4") + ("onsubmit" "login(event)") + (div + ("id" "flow_1") + ("style" "display: contents") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "username") + (b + (text "Username"))) + (input + ("type" "text") + ("placeholder" "username") + ("required" "") + ("name" "username") + ("id" "username"))) + (div + ("class" "flex flex-col gap-1") + (label + ("for" "username") + (b + (text "Password"))) + (input + ("type" "password") + ("placeholder" "password") + ("required" "") + ("name" "password") + ("id" "password")))) + (div + ("id" "flow_2") + ("style" "display: none") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "totp") + (b + (text "TOTP code"))) + (input + ("type" "text") + ("placeholder" "totp code") + ("name" "totp") + ("id" "totp")))) + (button + (text "Submit"))) + +(script + (text "let flow_page = 1; + + function next_page() { + document.getElementById(`flow_${flow_page}`).style.display = \"none\"; + flow_page += 1; + document.getElementById(`flow_${flow_page}`).style.display = \"contents\"; + } + + async function login(e) { + e.preventDefault(); + + if (flow_page === 1) { + // check if we need TOTP + const res = await ( + await fetch( + `/api/v1/auth/user/${e.target.username.value}/totp/check`, + ) + ).json(); + + trigger(\"atto::toast\", [res.ok ? \"success\" : \"error\", res.message]); + + if (res.ok && res.payload) { + // user exists AND totp is required + return next_page(); + } + } + + fetch(\"/api/v1/auth/login\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + username: e.target.username.value, + password: e.target.password.value, + totp: e.target.totp.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + // update tokens + const new_tokens = ns(\"me\").LOGIN_ACCOUNT_TOKENS; + new_tokens[e.target.username.value] = res.message; + trigger(\"me::set_login_account_tokens\", [new_tokens]); + + // redirect + setTimeout(() => { + window.location.href = \"/\"; + }, 150); + } + }); + }")) + +(text "{% endblock %} {% block footer %}") +(span + ("class" "small w-full text-center") + (text "Or, ") + (a + ("href" "/auth/register") + (text "register"))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/auth/register.html b/crates/app/src/public/html/auth/register.html deleted file mode 100644 index 83c46fc..0000000 --- a/crates/app/src/public/html/auth/register.html +++ /dev/null @@ -1,120 +0,0 @@ -{% extends "auth/base.html" %} {% block head %} -Register -{% endblock %} {% block title %}Register{% endblock %} {% block content %} - - -
-
- - -
- -
- - -
- -
- -
-
- {{ icon "scroll-text" }} - Policies -
- -
- By continuing, you agree to the following policies: - - - -
- - -
-
-
- -
- -
- -
- - -{% endblock %} {% block footer %} -Or, login -{% endblock %} diff --git a/crates/app/src/public/html/auth/register.lisp b/crates/app/src/public/html/auth/register.lisp new file mode 100644 index 0000000..116cdcf --- /dev/null +++ b/crates/app/src/public/html/auth/register.lisp @@ -0,0 +1,123 @@ +(text "{% extends \"auth/base.html\" %} {% block head %}") +(title + (text "Register")) + +(text "{% endblock %} {% block title %}Register{% endblock %} {% block content %}") +(script + ("src" "https://challenges.cloudflare.com/turnstile/v0/api.js") + ("defer" "")) + +(form + ("class" "w-full flex flex-col gap-4") + ("onsubmit" "register(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "username") + (b + (text "Username"))) + (input + ("type" "text") + ("placeholder" "username") + ("required" "") + ("name" "username") + ("id" "username"))) + (div + ("class" "flex flex-col gap-1") + (label + ("for" "username") + (b + (text "Password"))) + (input + ("type" "password") + ("placeholder" "password") + ("required" "") + ("name" "password") + ("id" "password"))) + (hr) + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"scroll-text\" }}") + (b + (text "Policies"))) + (div + ("class" "card secondary flex flex-col gap-2") + (span + (text "By continuing, you agree to the following policies:")) + (ul + (li + (a + ("href" "{{ config.policies.terms_of_service }}") + (text "Terms of service"))) + (li + (a + ("href" "{{ config.policies.privacy }}") + (text "Privacy policy")))) + (div + ("class" "flex gap-2") + (input + ("type" "checkbox") + ("name" "policy_consent") + ("id" "policy_consent") + ("class" "w-content") + ("required" "")) + (label + ("for" "policy_consent") + (text "I agree"))))) + (div + ("class" "cf-turnstile") + ("data-sitekey" "{{ config.turnstile.site_key }}")) + (hr) + (button + (text "Submit"))) + +(script + (text "async function register(e) { + e.preventDefault(); + await trigger(\"atto::debounce\", [\"users::create\"]); + fetch(\"/api/v1/auth/register\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + username: e.target.username.value, + password: e.target.password.value, + policy_consent: e.target.policy_consent.checked, + captcha_response: e.target.querySelector( + \"[name=cf-turnstile-response]\", + ).value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + // update tokens + const new_tokens = ns(\"me\").LOGIN_ACCOUNT_TOKENS; + new_tokens[e.target.username.value] = res.message; + trigger(\"me::set_login_account_tokens\", [new_tokens]); + + // redirect + setTimeout(() => { + window.location.href = \"/\"; + }, 150); + } + }); + }")) + +(text "{% endblock %} {% block footer %}") +(span + ("class" "small w-full text-center") + (text "Or, ") + (a + ("href" "/auth/login") + (text "login"))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/chats/app.html b/crates/app/src/public/html/chats/app.lisp similarity index 56% rename from crates/app/src/public/html/chats/app.html rename to crates/app/src/public/html/chats/app.lisp index 5f96774..d60ef08 100644 --- a/crates/app/src/public/html/chats/app.html +++ b/crates/app/src/public/html/chats/app.lisp @@ -1,141 +1,117 @@ -{% extends "root.html" %} {% block head %} -Chats - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav(selected="chats", -hide_user_menu=true) }} - +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Chats - {{ config.name }}")) -
- - - - - {% if channel -%} -
- - -
- - - -
-
- {%- endif %} - - - - - - - - {% if selected_channel -%} - - {%- endif %} -
-{% endblock %} + }, 100);")) + (text "{%- endif %}")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/chats/channels.html b/crates/app/src/public/html/chats/channels.html deleted file mode 100644 index 2970d0e..0000000 --- a/crates/app/src/public/html/chats/channels.html +++ /dev/null @@ -1,75 +0,0 @@ -{%- import "components.html" as components -%} - -
- {% for channel in channels %} -
- - {{ icon "rss" }} - {{ channel.title }} - - - -
- {% endfor %} -
- - {% if selected_community == 0 and selected_channel -%} -
- {% for member in members %} {{ components::user_plate(user=member, - show_kick=user.id == channel.owner) }} {% endfor %} -
- {%- endif %} -
diff --git a/crates/app/src/public/html/chats/channels.lisp b/crates/app/src/public/html/chats/channels.lisp new file mode 100644 index 0000000..433d0a9 --- /dev/null +++ b/crates/app/src/public/html/chats/channels.lisp @@ -0,0 +1,60 @@ +(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 -%}quaternary{% 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 -%}quaternary{% else %}camo{%- endif %}") + ("onclick" "trigger('atto::hooks::dropdown', [event])") + ("exclude" "dropdown") + ("style" "width: 32px") + (text "{{ icon \"ellipsis\" }}")) + (div + ("class" "inner") + (text "{% if user.id == channel.owner -%} {% if selected_community == 0 %}") + (button + ("class" "quaternary small") + ("onclick" "add_member('{{ channel.id }}')") + (text "{{ icon \"user-plus\" }}") + (span + (text "{{ text \"chats:action.add_someone\" }}"))) + (text "{%- endif %}") + (button + ("class" "quaternary small") + ("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 "{% else %}") + (button + ("onclick" "kick_member('{{ channel.id }}', '{{ user.id }}')") + ("class" "red") + (text "{{ icon \"door-open\" }}") + (span + (text "{{ text \"chats:action.leave\" }}"))) + (text "{%- 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.html b/crates/app/src/public/html/chats/message.html deleted file mode 100644 index 30f2198..0000000 --- a/crates/app/src/public/html/chats/message.html +++ /dev/null @@ -1,2 +0,0 @@ -{%- import "components.html" as components -%} {{ components::message(user=user, -message=message, grouped=grouped) }} diff --git a/crates/app/src/public/html/chats/message.lisp b/crates/app/src/public/html/chats/message.lisp new file mode 100644 index 0000000..4d5946c --- /dev/null +++ b/crates/app/src/public/html/chats/message.lisp @@ -0,0 +1 @@ +(text "{%- import \"components.html\" as components -%} {{ components::message(user=user, message=message, grouped=grouped) }}") diff --git a/crates/app/src/public/html/chats/stream.html b/crates/app/src/public/html/chats/stream.html deleted file mode 100644 index 8bb1782..0000000 --- a/crates/app/src/public/html/chats/stream.html +++ /dev/null @@ -1,55 +0,0 @@ -{%- import "components.html" as components -%} - - -
- {% if page != 0 -%} -
- {{ text "chats:label.viewing_old_messages" }} - - {{ text "chats:label.go_back" }} - -
- {%- endif %} - - {% if message -%} -
- {{ text "chats:label.viewing_single_message" }} - - {{ text "chats:label.go_back" }} - -
- - {{ 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 -%} - - {%- endif %} -
- - -
diff --git a/crates/app/src/public/html/chats/stream.lisp b/crates/app/src/public/html/chats/stream.lisp new file mode 100644 index 0000000..d019e0c --- /dev/null +++ b/crates/app/src/public/html/chats/stream.lisp @@ -0,0 +1,55 @@ +(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 tertiary flex-wrap") + (b + (text "{{ text \"chats:label.viewing_old_messages\" }}")) + (a + ("href" "/chats/{{ community }}/{{ channel }}/_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 tertiary flex-wrap") + (b + (text "{{ text \"chats:label.viewing_single_message\" }}")) + (a + ("href" "/chats/{{ community }}/{{ channel }}?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 }}/_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 quaternary") + ("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/base.html b/crates/app/src/public/html/communities/base.html deleted file mode 100644 index f199159..0000000 --- a/crates/app/src/public/html/communities/base.html +++ /dev/null @@ -1,340 +0,0 @@ -{% extends "root.html" %} {% block head %} -{{ community.context.display_name }} - {{ config.name }} - - - - - - - - - - - - - - - -{% endblock %} {% block body %} {{ macros::nav() }} -
-
- {{ components::community_banner(id=community.id, community=community) }} - -
-
-
-
- {{ components::community_avatar(id=community.id, - community=community, size="72px") }} -
-
-

- - {% if community.context.display_name -%} - {{ community.context.display_name }} - {% else %} - {{ community.title }} - {%- endif %} - - {% if community.context.is_nsfw -%} - - {{ icon "square-asterisk" }} - - {%- endif %} -

- - {% if user -%} {% if user.id != community.owner - %} - - {%- endif %} {%- endif %} -
- - {{ community.title }} -
-
- - {% if user -%} -
- {% if not is_owner -%} {% if not is_joined -%} {% if not - is_pending %} - - - - {% else %} - - - - {%- endif %} {% else %} - - - - {{ icon "message-circle" }} - {{ text "communities:label.chats" }} - - - {% if user and can_post -%} - - {{ icon "plus" }} - {{ text "general:action.post" }} - - {%- endif %} - - - {%- endif %} {% else %} - - {{ icon "message-circle" }} - {{ text "communities:label.chats" }} - - - - {{ icon "plus" }} - {{ text "general:action.post" }} - - {%- endif %} {% if can_manage_community or is_manager - -%} - - {{ icon "settings" }} - {{ text "communities:action.configure" }} - - {%- endif %} -
- - {%- endif %} -
- -
-
- {{ community.context.description|markdown|safe }} -
- -
-
- ID - -
- -
- Created - {{ community.created }} -
- - - -
- Score -
- {{ community.likes - community.dislikes - }} - {% if user -%} -
- {{ components::likes(id=community.id, - asset_type="Community", - likes=community.likes, - dislikes=community.dislikes) }} -
- {%- endif %} -
-
-
-
-
- -
- {% if can_read -%} {% block content %}{% endblock %} {% else %} -
-
- {{ icon "frown" }} - {{ text "communities:label.not_allowed_to_read" - }} -
- -
- - {{ text "communities:label.might_need_to_join" }} - -
-
- {%- endif %} -
-
-
-
-{% endblock %} diff --git a/crates/app/src/public/html/communities/base.lisp b/crates/app/src/public/html/communities/base.lisp new file mode 100644 index 0000000..4cc0075 --- /dev/null +++ b/crates/app/src/public/html/communities/base.lisp @@ -0,0 +1,300 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "{{ community.context.display_name }} - {{ config.name }}")) + +(meta + ("name" "og:title") + ("content" "{{ community.title }}")) + +(meta + ("name" "description") + ("content" "View the \\\"{{ community.title }}\\\" community on {{ config.name }}!")) + +(meta + ("name" "og:description") + ("content" "View the \\\"{{ community.title }}\\\" community on {{ config.name }}!")) + +(meta + ("property" "og:type") + ("content" "profile")) + +(meta + ("property" "profile:username") + ("content" "{{ community.title }}")) + +(meta + ("name" "og:image") + ("content" "{{ config.host|safe }}/api/v1/communities/{{ community.id }}/avatar")) + +(meta + ("name" "twitter:image") + ("content" "{{ config.host|safe }}/api/v1/communities/{{ community.id }}/avatar")) + +(meta + ("name" "twitter:card") + ("content" "summary")) + +(meta + ("name" "twitter:title") + ("content" "{{ community.title }}")) + +(meta + ("name" "twitter:description") + ("content" "View the \\\"{{ community.title }}\\\" community on {{ config.name }}!")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(article + (div + ("class" "content_container flex flex-col gap-4") + (text "{{ components::community_banner(id=community.id, community=community) }}") + (div + ("class" "w-full flex gap-4 flex-collapse") + (div + ("class" "lhs flex flex-col gap-2 sm:w-full") + ("style" "width: 22rem; min-width: 22rem") + (div + ("class" "card-nest w-full") + (div + ("class" "card flex gap-2") + ("id" "community_avatar_and_name") + (text "{{ components::community_avatar(id=community.id, community=community, size=\"72px\") }}") + (div + ("class" "flex flex-col") + (div + ("class" "flex gap-2 items-center") + (h3 + ("id" "title") + ("class" "title name shorter flex gap-2") + (text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %} {% if community.context.is_nsfw -%}") + (span + ("title" "NSFW community") + ("class" "flex items-center") + ("style" "color: var(--color-primary)") + (text "{{ icon \"square-asterisk\" }}")) + (text "{%- endif %}")) + (text "{% if user -%} {% if user.id != community.owner %}") + (div + ("class" "dropdown") + (button + ("class" "camo small") + ("onclick" "trigger('atto::hooks::dropdown', [event])") + ("exclude" "dropdown") + (text "{{ icon \"ellipsis\" }}")) + (div + ("class" "inner") + (button + ("class" "red") + ("onclick" "trigger('me::report', ['{{ community.id }}', 'community'])") + (text "{{ icon \"flag\" }}") + (span + (text "{{ text \"general:action.report\" }}"))))) + (text "{%- endif %} {%- endif %}")) + (span + ("class" "fade") + (text "{{ community.title }}")))) + (text "{% if user -%}") + (div + ("class" "card flex gap-2 flex-wrap") + ("id" "join_or_leave") + (text "{% if not is_owner -%} {% if not is_joined -%} {% if not is_pending %}") + (button + ("class" "primary") + ("onclick" "join_community()") + (text "{{ icon \"circle-plus\" }}") + (span + (text "{{ text \"communities:action.join\" }}"))) + (script + (text "globalThis.join_community = () => { + fetch( + \"/api/v1/communities/{{ community.id }}/join\", + { + method: \"POST\", + }, + ) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + setTimeout(() => { + window.location.reload(); + }, 150); + }); + };")) + (text "{% else %}") + (button + ("class" "quaternary red") + ("onclick" "cancel_request()") + (text "{{ icon \"x\" }}") + (span + (text "{{ text \"communities:action.cancel_request\" }}"))) + (script + (text "globalThis.cancel_request = async () => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + fetch( + \"/api/v1/communities/{{ community.id }}/memberships/{{ user.id }}\", + { + method: \"DELETE\", + }, + ) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + setTimeout(() => { + window.location.reload(); + }, 150); + }); + };")) + (text "{%- endif %} {% else %}") + (button + ("class" "quaternary red") + ("onclick" "leave_community()") + (text "{{ icon \"circle-minus\" }}") + (span + (text "{{ text \"communities:action.leave\" }}"))) + (a + ("href" "/chats/{{ community.id }}/0") + ("class" "button quaternary") + (text "{{ icon \"message-circle\" }}") + (span + (text "{{ text \"communities:label.chats\" }}"))) + (text "{% if user and can_post -%}") + (a + ("href" "/communities/intents/post?community={{ community.id }}") + ("class" "button quaternary") + ("data-turbo" "false") + (text "{{ icon \"plus\" }}") + (span + (text "{{ text \"general:action.post\" }}"))) + (text "{%- endif %}") + (script + (text "globalThis.leave_community = async () => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + fetch( + \"/api/v1/communities/{{ community.id }}/memberships/{{ user.id }}\", + { + method: \"DELETE\", + }, + ) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + setTimeout(() => { + window.location.reload(); + }, 150); + }); + };")) + (text "{%- endif %} {% else %}") + (a + ("href" "/chats/{{ community.id }}/0") + ("class" "button quaternary") + (text "{{ icon \"message-circle\" }}") + (span + (text "{{ text \"communities:label.chats\" }}"))) + (a + ("href" "/communities/intents/post?community={{ community.id }}") + ("class" "button quaternary") + ("data-turbo" "false") + (text "{{ icon \"plus\" }}") + (span + (text "{{ text \"general:action.post\" }}"))) + (text "{%- endif %} {% if can_manage_community or is_manager -%}") + (a + ("href" "/community/{{ community.id }}/manage") + ("class" "button primary") + (text "{{ icon \"settings\" }}") + (span + (text "{{ text \"communities:action.configure\" }}"))) + (text "{%- endif %}")) + (text "{%- endif %}")) + (div + ("class" "card-nest flex flex-col") + (div + ("id" "bio") + ("class" "card small no_p_margin") + (text "{{ community.context.description|markdown|safe }}")) + (div + ("class" "card flex flex-col gap-2") + (div + ("class" "w-full flex justify-between items-center") + (span + ("class" "notification chip") + (text "ID")) + (button + ("title" "Copy") + ("onclick" "trigger('atto::copy_text', ['{{ community.id }}'])") + ("class" "camo small") + (text "{{ icon \"copy\" }}"))) + (div + ("class" "w-full flex justify-between items-center") + (span + ("class" "notification chip") + (text "Created")) + (span + ("class" "date") + (text "{{ community.created }}"))) + (div + ("class" "w-full flex justify-between items-center") + (span + ("class" "notification chip") + (text "Members")) + (a + ("href" "/community/{{ community.title }}/members") + (text "{{ community.member_count }}"))) + (div + ("class" "w-full flex justify-between items-center") + (span + ("class" "notification chip") + (text "Score")) + (div + ("class" "flex gap-2") + (b + (text "{{ community.likes - community.dislikes }}")) + (text "{% if user -%}") + (div + ("class" "flex gap-1 reactions_box") + ("hook" "check_reactions") + ("hook-arg:id" "{{ community.id }}") + (text "{{ components::likes(id=community.id, asset_type=\"Community\", likes=community.likes, dislikes=community.dislikes) }}")) + (text "{%- endif %}")))))) + (div + ("class" "rhs w-full") + (text "{% if can_read -%} {% block content %}{% endblock %} {% else %}") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"frown\" }}") + (b + (text "{{ text \"communities:label.not_allowed_to_read\" }}"))) + (div + ("class" "card") + (span + (text "{{ text \"communities:label.might_need_to_join\" }}")))) + (text "{%- endif %}"))))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/communities/create_post.html b/crates/app/src/public/html/communities/create_post.html deleted file mode 100644 index 74a442c..0000000 --- a/crates/app/src/public/html/communities/create_post.html +++ /dev/null @@ -1,432 +0,0 @@ -{% extends "root.html" %} {% block head %} -Create post - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
- {% if drafts|length > 0 -%} - - {%- endif %} - -
-
- - {{ icon "pen" }} - {{ text "communities:label.create_post" }} - - - -
- -
- {% if draft -%} - - {%- endif %} {% if quoting -%} - - {%- endif %} - -
-
- {{ components::avatar(username=user.id, size="32px", - selector_type="id") }} - - -
- -
-
- - -
- -
- -
- {{ components::create_post_options() }} - -
- {% if not quoting -%} {% if draft -%} - - {% else %} - - {%- endif %} {%- endif %} - - -
-
-
-
- - {% if not quoting -%} - - {% else %} - - {%- endif %} -
-
- - {% if drafts|length > 0 -%} - - - - {%- endif %} -
- - -{% endblock %} diff --git a/crates/app/src/public/html/communities/create_post.lisp b/crates/app/src/public/html/communities/create_post.lisp new file mode 100644 index 0000000..1f1204a --- /dev/null +++ b/crates/app/src/public/html/communities/create_post.lisp @@ -0,0 +1,406 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Create post - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (text "{% if drafts|length > 0 -%}") + (div + ("class" "pillmenu") + (a + ("href" "#/create") + ("data-tab-button" "create") + ("class" "active") + (text "{{ icon \"plus\" }}") + (span + (text "{{ text \"general:action.post\" }}"))) + (a + ("href" "#/drafts") + ("data-tab-button" "drafts") + (text "{{ icon \"notepad-text-dashed\" }}") + (span + (text "{{ text \"communities:label.drafts\" }}")))) + (text "{%- endif %}") + (div + ("class" "card-nest") + ("data-tab" "create") + (div + ("class" "card small flex items-center justify-between gap-2") + (span + ("class" "flex items-center gap-2") + (text "{{ icon \"pen\" }}") + (span + (text "{{ text \"communities:label.create_post\" }}"))) + (button + ("onclick" "cancel_create_post()") + ("class" "quaternary small red") + (text "{{ icon \"x\" }}") + (span + (text "{{ text \"dialog:action.cancel\" }}")))) + (div + ("class" "card tertiary flex flex-col gap-2") + (text "{% if draft -%}") + (div + ("class" "card secondary w-full flex items-center justify-between gap-2 small") + (a + ("class" "flex items-center gap-2 flush") + ("href" "#/drafts") + (text "{{ icon \"notepad-text-dashed\" }}") + (span + ("class" "date") + (text "{{ draft.created }}"))) + (div + ("class" "flex gap-2") + (a + ("href" "?") + ("class" "button quaternary small") + (text "{{ icon \"x\" }}") + (span + (text "{{ text \"dialog:action.cancel\" }}"))) + (button + ("class" "button quaternary red small") + ("onclick" "remove_draft('{{ draft.id }}')") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}"))))) + (text "{%- endif %} {% if quoting -%}") + (div + ("class" "card secondary w-full flex items-center justify-between gap-2 small") + (a + ("class" "flex items-center gap-2 flush") + ("href" "/post/{{ quoting[1].id }}") + (text "{{ icon \"quote\" }}") + (span + (text "{{ quoting[0].username }}'s post"))) + (a + ("href" "?") + ("class" "button quaternary small") + (text "{{ icon \"x\" }}") + (span + (text "{{ text \"dialog:action.cancel\" }}")))) + (text "{%- endif %}") + (div + ("class" "card-nest") + (div + ("class" "card small flex flex-row gap-2 items-center") + (text "{{ components::avatar(username=user.id, size=\"32px\", selector_type=\"id\") }}") + (select + ("id" "community_to_post_to") + ("onchange" "update_community_avatar(event)") + (option + ("value" "{{ config.town_square }}") + ("selected" "{% if not selected_community -%}true{% else %}false{%- endif %}") + (text "{{ text \"auth:link.my_profile\" }}")) + (text "{% for community in communities %}") + (option + ("value" "{{ community.id }}") + ("selected" "{% if selected_community == community.id -%}true{% else %}false{%- endif %}") + (text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}")) + (text "{% endfor %}"))) + (form + ("class" "card flex flex-col gap-2") + ("id" "create_form") + ("onsubmit" "create_post_from_form(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "content") + (text "{{ text \"communities:label.content\" }}")) + (textarea + ("type" "text") + ("name" "content") + ("id" "content") + ("placeholder" "content") + ("minlength" "2") + ("maxlength" "4096") + (text "{% if draft -%}{{ draft.content }}{%- endif %}"))) + (div + ("id" "files_list") + ("class" "flex gap-2 flex-wrap")) + (div + ("class" "flex justify-between gap-2") + (text "{{ components::create_post_options() }}") + (div + ("class" "flex gap-2") + (text "{% if not quoting -%} {% if draft -%}") + (button + ("class" "secondary small square") + ("title" "Save as Draft") + ("onclick" "update_draft('{{ draft.id }}')") + ("type" "button") + (text "{{ icon \"notepad-text-dashed\" }}")) + (text "{% else %}") + (button + ("class" "secondary small square") + ("title" "Save as Draft") + ("onclick" "create_draft()") + ("type" "button") + (text "{{ icon \"notepad-text-dashed\" }}")) + (text "{%- endif %} {%- endif %}") + (button + ("class" "primary") + (text "{{ text \"communities:action.create\" }}")))))) + (text "{% if not quoting -%}") + (script + (text "async function create_post_from_form(e) { + e.preventDefault(); + await trigger(\"atto::debounce\", [\"posts::create\"]); + + e.target + .querySelector(\"button.primary\") + .classList.add(\"hidden\"); + + // create body + const body = new FormData(); + + if (e.target.file_picker) { + for (const file of e.target.file_picker.files) { + body.append(file.name, file); + } + } + + body.append( + \"body\", + JSON.stringify({ + content: e.target.content.value, + community: document.getElementById( + \"community_to_post_to\", + ).selectedOptions[0].value, + }), + ); + + // ... + fetch(\"/api/v1/posts\", { + method: \"POST\", + body, + }) + .then((res) => res.json()) + .then(async (res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + // update settings + await update_settings_maybe(res.payload); + + // remove draft + // {% if draft -%} + if (\"{{ draft.id }}\") { + fetch(\"/api/v1/drafts/{{ draft.id }}\", { + method: \"DELETE\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + } + // {%- endif %} + + // ... + setTimeout(() => { + window.location.href = `/post/${res.payload}`; + }, 100); + } else { + e.target + .querySelector(\"button.primary\") + .classList.remove(\"hidden\"); + } + }); + } + + async function create_draft() { + const e = { + target: document.getElementById(\"create_form\"), + }; + + await trigger(\"atto::debounce\", [\"posts::create\"]); + + e.target + .querySelector(\"button.primary\") + .classList.add(\"hidden\"); + + fetch(\"/api/v1/drafts\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + content: e.target.content.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + setTimeout(() => { + window.location.href = `?from_draft=${res.payload}`; + }, 100); + } else { + e.target + .querySelector(\"button.primary\") + .classList.remove(\"hidden\"); + } + }); + } + + async function update_draft(id) { + const e = { + target: document.getElementById(\"create_form\"), + }; + + await trigger(\"atto::debounce\", [\"posts::create\"]); + + e.target + .querySelector(\"button.primary\") + .classList.add(\"hidden\"); + + fetch(`/api/v1/drafts/${id}`, { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + content: e.target.content.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (!res.ok) { + e.target + .querySelector(\"button.primary\") + .classList.remove(\"hidden\"); + } + }); + }")) + (text "{% else %}") + (script + (text "async function create_post_from_form(e) { + const id = await trigger(\"me::repost\", [ + \"{{ quoting[1].id }}\", + e.target.content.value, + document.getElementById(\"community_to_post_to\") + .selectedOptions[0].value, + false, + ]); + + // update settings + await update_settings_maybe(id); + + // redirect + setTimeout(() => { + window.location.href = `/post/${id}`; + }, 100); + }")) + (text "{%- endif %}"))) + (text "{% if drafts|length > 0 -%}") + (div + ("class" "card-nest tertiary hidden") + ("data-tab" "drafts") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"notepad-text-dashed\" }}") + (span + (text "{{ text \"communities:label.drafts\" }}"))) + (div + ("class" "card flex flex-col gap-2") + (text "{{ components::supporter_ad(body=\"Become a supporter to save infinite post drafts!\") }} {% for draft in drafts %}") + (div + ("class" "card-nest") + (div + ("class" "card small flex flex-col gap-2") + (span + ("class" "no_p_margin") + (text "{{ draft.content|markdown|safe }}")) + (span + ("class" "fade date") + (text "{{ draft.created }}"))) + (div + ("class" "card flex gap-2 secondary") + (a + ("href" "?from_draft={{ draft.id }}") + ("class" "button small") + (text "{{ icon \"pen-line\" }}") + (span + (text "{{ text \"communities:label.load\" }}"))) + (button + ("class" "button quaternary red small") + ("onclick" "remove_draft('{{ draft.id }}')") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}"))))) + (text "{% endfor %}"))) + (script + (text "async function remove_draft(id) { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you want to do this?\", + ])) + ) { + return; + } + + fetch(`/api/v1/drafts/${id}`, { + method: \"DELETE\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }")) + (text "{%- endif %}")) + +(script + (text "const town_square = \"{{ config.town_square }}\"; + const user_id = \"{{ user.id }}\"; + + function update_community_avatar(e) { + const element = e.target.parentElement.querySelector(\".avatar\"); + const id = e.target.selectedOptions[0].value; + + element.setAttribute(\"title\", id); + element.setAttribute(\"alt\", `${id}'s avatar`); + + if (id === town_square) { + element.src = `/api/v1/auth/user/${user_id}/avatar?selector_type=id`; + } else { + element.src = `/api/v1/communities/${id}/avatar`; + } + } + + setTimeout(() => { + update_community_avatar({ + target: document.getElementById(\"community_to_post_to\"), + }); + }, 150); + + async function cancel_create_post() { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this? Your post content will be lost.\", + ])) + ) { + return; + } + + window.history.back(); + }")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/communities/feed.html b/crates/app/src/public/html/communities/feed.html deleted file mode 100644 index 59df8b8..0000000 --- a/crates/app/src/public/html/communities/feed.html +++ /dev/null @@ -1,45 +0,0 @@ -{% import "components.html" as components %} {% extends "communities/base.html" -%} {% block content %} -
- {{ macros::community_nav(community=community, selected="posts") }} {% if - pinned|length != 0 %} -
-
- {{ icon "pin" }} - {{ text "communities:label.pinned" }} -
- -
- - {% for post in pinned %} - {% if post[0].context.repost and post[0].context.repost.reposting -%} - {{ components::repost(repost=post[2], post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} - {% else %} - {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} - {%- endif %} - {% endfor %} -
-
- {%- endif %} - -
-
- {{ icon "newspaper" }} - {{ text "communities:label.posts" }} -
- -
- - {% for post in feed %} - {% if post[0].context.repost and post[0].context.repost.reposting -%} - {{ components::repost(repost=post[2], post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} - {% else %} - {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} - {%- endif %} - {% endfor %} - - {{ components::pagination(page=page, items=feed|length) }} -
-
-
-{% endblock %} diff --git a/crates/app/src/public/html/communities/feed.lisp b/crates/app/src/public/html/communities/feed.lisp new file mode 100644 index 0000000..1801fc9 --- /dev/null +++ b/crates/app/src/public/html/communities/feed.lisp @@ -0,0 +1,27 @@ +(text "{% import \"components.html\" as components %} {% extends \"communities/base.html\" %} {% block content %}") +(div + ("class" "flex flex-col gap-4 w-full") + (text "{{ macros::community_nav(community=community, selected=\"posts\") }} {% if pinned|length != 0 %}") + (div + ("class" "card-nest") + (div + ("class" "card small flex gap-2 items-center") + (text "{{ icon \"pin\" }}") + (span + (text "{{ text \"communities:label.pinned\" }}"))) + (div + ("class" "card flex flex-col gap-4") + (text "{% for post in pinned %} {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[2], post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} {%- endif %} {% endfor %}"))) + (text "{%- endif %}") + (div + ("class" "card-nest") + (div + ("class" "card small flex gap-2 items-center") + (text "{{ icon \"newspaper\" }}") + (span + (text "{{ text \"communities:label.posts\" }}"))) + (div + ("class" "card flex flex-col gap-4") + (text "{% for post in feed %} {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[2], post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=feed|length) }}")))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/communities/list.html b/crates/app/src/public/html/communities/list.html deleted file mode 100644 index 79ea5de..0000000 --- a/crates/app/src/public/html/communities/list.html +++ /dev/null @@ -1,102 +0,0 @@ -{% extends "root.html" %} {% block head %} -My communities - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav(selected="communities") }} -
- {% if user -%} -
-
- {{ text "communities:label.create_new" }} -
- -
-
- - -
- - -
-
- - {% if list|length >= 4 -%} {{ components::supporter_ad(body="Become a - supporter to create up to 10 communities!") }} {%- endif %} {%- endif %} - -
-
-
- {{ icon "award" }} - {{ text "communities:label.my_communities" }} -
- - - {{ icon "search" }} - {{ text "communities:label.join_new" }} - -
- -
- {% for item in list %} {{ - components::community_listing_card(community=item) }} {% endfor %} -
-
- -
-
- {{ icon "trending-up" }} - {{ text "communities:label.popular_communities" }} -
- -
- {% for item in popular_list %} {{ - components::community_listing_card(community=item) }} {% endfor %} -
-
-
- - -{% endblock %} diff --git a/crates/app/src/public/html/communities/list.lisp b/crates/app/src/public/html/communities/list.lisp new file mode 100644 index 0000000..ecd8c04 --- /dev/null +++ b/crates/app/src/public/html/communities/list.lisp @@ -0,0 +1,97 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "My communities - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"communities\") }}") +(main + ("class" "flex flex-col gap-2") + (text "{% if user -%}") + (div + ("class" "card-nest") + (div + ("class" "card small") + (b + (text "{{ text \"communities:label.create_new\" }}"))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "create_community_from_form(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "title") + (text "{{ text \"communities:label.name\" }}")) + (input + ("type" "text") + ("name" "title") + ("id" "title") + ("placeholder" "name") + ("required" "") + ("minlength" "2") + ("maxlength" "32"))) + (button + ("class" "primary") + (text "{{ text \"communities:action.create\" }}")))) + (text "{% if list|length >= 4 -%} {{ components::supporter_ad(body=\"Become a supporter to create up to 10 communities!\") }} {%- endif %} {%- endif %}") + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center justify-between gap-2") + (div + ("class" "flex items-center gap-2") + (text "{{ icon \"award\" }}") + (span + (text "{{ text \"communities:label.my_communities\" }}"))) + (a + ("href" "/communities/search") + ("class" "button quaternary small") + (text "{{ icon \"search\" }}") + (span + (text "{{ text \"communities:label.join_new\" }}")))) + (div + ("class" "card flex flex-col gap-2") + (text "{% for item in list %} {{ components::community_listing_card(community=item) }} {% endfor %}"))) + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"trending-up\" }}") + (span + (text "{{ text \"communities:label.popular_communities\" }}"))) + (div + ("class" "card flex flex-col gap-2") + (text "{% for item in popular_list %} {{ components::community_listing_card(community=item) }} {% endfor %}")))) + +(script + (text "async function create_community_from_form(e) { + e.preventDefault(); + await trigger(\"atto::debounce\", [\"communities::create\"]); + + if (e.target.title.value.includes(\" \")) { + return alert(\"Cannot contain spaces!\"); + } + + fetch(\"/api/v1/communities\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + title: e.target.title.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + setTimeout(() => { + window.location.href = `/community/${res.payload}`; + }, 100); + } + }); + }")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/communities/members.html b/crates/app/src/public/html/communities/members.html deleted file mode 100644 index 73f8a37..0000000 --- a/crates/app/src/public/html/communities/members.html +++ /dev/null @@ -1,49 +0,0 @@ -{% import "components.html" as components %} {% extends "communities/base.html" -%} {% block content %} -
-
-
- {{ icon "users-round" }} - {{ text "communities:tab.members" }} -
- -
- {% if page == 0 -%} -
-
- {{ icon "crown" }} - Owner -
- - {{ components::user_card(user=owner) }} -
- {%- endif %} - - - {% for item in list %} -
-
- - Since - {{ item[0].created }} - - - {% if can_manage_roles -%} - - {{ icon "pencil" }} - {{ text "general:action.manage" }} - - {%- endif %} -
- - {{ components::user_card(user=item[1]) }} -
- {% endfor %} {{ components::pagination(page=page, items=list|length) - }} -
-
-
-{% endblock %} diff --git a/crates/app/src/public/html/communities/members.lisp b/crates/app/src/public/html/communities/members.lisp new file mode 100644 index 0000000..1056847 --- /dev/null +++ b/crates/app/src/public/html/communities/members.lisp @@ -0,0 +1,44 @@ +(text "{% import \"components.html\" as components %} {% extends \"communities/base.html\" %} {% block content %}") +(div + ("class" "flex flex-col gap-4 w-full") + (div + ("class" "card-nest") + (div + ("class" "card small flex gap-2 items-center") + (text "{{ icon \"users-round\" }}") + (span + (text "{{ text \"communities:tab.members\" }}"))) + (div + ("class" "card flex flex-col gap-4") + (text "{% if page == 0 -%}") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"crown\" }}") + (span + (text "Owner"))) + (text "{{ components::user_card(user=owner) }}")) + (text "{%- endif %}") + (text "{% for item in list %}") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center gap-2 justify-between") + (span + (text "Since") + (span + ("class" "date") + (text "{{ item[0].created }}"))) + (text "{% if can_manage_roles -%}") + (a + ("href" "/community/{{ community.id }}/manage?uid={{ item[1].id }}#/members") + ("class" "button small quaternary") + (text "{{ icon \"pencil\" }}") + (span + (text "{{ text \"general:action.manage\" }}"))) + (text "{%- endif %}")) + (text "{{ components::user_card(user=item[1]) }}")) + (text "{% endfor %} {{ components::pagination(page=page, items=list|length) }}")))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/communities/question.html b/crates/app/src/public/html/communities/question.html deleted file mode 100644 index 4eaeffe..0000000 --- a/crates/app/src/public/html/communities/question.html +++ /dev/null @@ -1,111 +0,0 @@ -{% extends "root.html" %} {% block head %} -Question - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
-
- {{ components::question(question=question, owner=owner) }} -
- - {% if user and (user.id == question.receiver or question.is_global) and not - has_answered %} -
-
- {{ icon "square-pen" }} - {{ text "requests:label.answer" }} -
- -
-
- - -
- -
- -
- {{ components::emoji_picker(element_id="content", - render_dialog=true) }} {% if is_supporter -%} {{ - components::file_picker(files_list_id="files_list") }} {% endif - %} - - -
-
-
- {%- endif %} - -
-
- {{ icon "newspaper" }} - {{ text "communities:label.replies" }} -
- -
- - {% for post in replies %} - {{ components::post(post=post[0], owner=post[1], question=false, secondary=true, show_community=false) }} - {% endfor %} - - {{ components::pagination(page=page, items=replies|length) }} -
-
-
- - -{% endblock %} diff --git a/crates/app/src/public/html/communities/question.lisp b/crates/app/src/public/html/communities/question.lisp new file mode 100644 index 0000000..2d6eefa --- /dev/null +++ b/crates/app/src/public/html/communities/question.lisp @@ -0,0 +1,99 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Question - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (div + ("style" "display: contents") + (text "{{ components::question(question=question, owner=owner) }}")) + (text "{% if user and (user.id == question.receiver or question.is_global) and not has_answered %}") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"square-pen\" }}") + (b + (text "{{ text \"requests:label.answer\" }}"))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "answer_question_from_form(event, '{{ question.id }}')") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "content") + (text "{{ text \"communities:label.content\" }}")) + (textarea + ("type" "text") + ("name" "content") + ("id" "content") + ("placeholder" "content") + ("required" "") + ("minlength" "2") + ("maxlength" "4096"))) + (div + ("id" "files_list") + ("class" "flex gap-2 flex-wrap")) + (div + ("class" "flex gap-2") + (text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {% endif %}") + (button + ("class" "primary") + (text "{{ text \"requests:label.answer\" }}"))))) + (text "{%- endif %}") + (div + ("class" "card-nest w-full") + ("data-tab" "replies") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"newspaper\" }}") + (span + (text "{{ text \"communities:label.replies\" }}"))) + (div + ("class" "card flex flex-col gap-4") + (text "{% for post in replies %} {{ components::post(post=post[0], owner=post[1], question=false, secondary=true, show_community=false) }} {% endfor %} {{ components::pagination(page=page, items=replies|length) }}")))) + +(script + (text "const community = \"{{ question.community }}\"; + window.answer_question_from_form = async (e, answering) => { + e.preventDefault(); + await trigger(\"atto::debounce\", [\"posts::create\"]); + + // create body + const body = new FormData(); + + if (e.target.file_picker) { + for (const file of e.target.file_picker.files) { + body.append(file.name, file); + } + } + + body.append( + \"body\", + JSON.stringify({ + content: e.target.content.value, + community: community ? community : \"{{ config.town_square }}\", + answering, + }), + ); + + // ... + fetch(\"/api/v1/posts\", { + method: \"POST\", + body, + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + window.location.reload(); + } + }); + };")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/communities/questions.html b/crates/app/src/public/html/communities/questions.html deleted file mode 100644 index babda27..0000000 --- a/crates/app/src/public/html/communities/questions.html +++ /dev/null @@ -1,30 +0,0 @@ -{% import "components.html" as components %} {% extends "communities/base.html" -%} {% block content %} -
- {{ macros::community_nav(community=community, selected="questions") }} - - - {% if user and can_post -%} -
- {{ components::create_question_form(community=community.id, - is_global=true) }} -
- {%- endif %} - -
-
- {{ icon "newspaper" }} - {{ text "communities:label.questions" }} -
- -
- - {% for question in feed %} - {{ components::global_question(question=question, can_manage_questions=can_manage_questions, show_community=false, secondary=true) }} - {% endfor %} - - {{ components::pagination(page=page, items=feed|length) }} -
-
-
-{% endblock %} diff --git a/crates/app/src/public/html/communities/questions.lisp b/crates/app/src/public/html/communities/questions.lisp new file mode 100644 index 0000000..b3e37a6 --- /dev/null +++ b/crates/app/src/public/html/communities/questions.lisp @@ -0,0 +1,21 @@ +(text "{% import \"components.html\" as components %} {% extends \"communities/base.html\" %} {% block content %}") +(div + ("class" "flex flex-col gap-4 w-full") + (text "{{ macros::community_nav(community=community, selected=\"questions\") }}") + (text "{% if user and can_post -%}") + (div + ("style" "display: contents") + (text "{{ components::create_question_form(community=community.id, is_global=true) }}")) + (text "{%- endif %}") + (div + ("class" "card-nest") + (div + ("class" "card small flex gap-2 items-center") + (text "{{ icon \"newspaper\" }}") + (span + (text "{{ text \"communities:label.questions\" }}"))) + (div + ("class" "card flex flex-col gap-4") + (text "{% for question in feed %} {{ components::global_question(question=question, can_manage_questions=can_manage_questions, show_community=false, secondary=true) }} {% endfor %} {{ components::pagination(page=page, items=feed|length) }}")))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/communities/search.html b/crates/app/src/public/html/communities/search.html deleted file mode 100644 index e69bddc..0000000 --- a/crates/app/src/public/html/communities/search.html +++ /dev/null @@ -1,45 +0,0 @@ -{% extends "root.html" %} {% block head %} -Search communities - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav(selected="communities") }} -
-
-
- {{ icon "search" }} - {{ text "general:link.search" }} -
- -
-
- - -
- - -
-
- -
-
- {{ icon "book-marked" }} - {{ text "communities:label.search_results" }} -
- - -
- {% for item in list %} - {{ components::community_listing_card(community=item) }} - {% endfor %} - - {{ components::pagination(page=page, items=list|length, key="&text=", value=text) }} -
-
-
-{% endblock %} diff --git a/crates/app/src/public/html/communities/search.lisp b/crates/app/src/public/html/communities/search.lisp new file mode 100644 index 0000000..642d214 --- /dev/null +++ b/crates/app/src/public/html/communities/search.lisp @@ -0,0 +1,44 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Search communities - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"communities\") }}") +(main + ("class" "flex flex-col gap-2") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"search\" }}") + (span + (text "{{ text \"general:link.search\" }}"))) + (form + ("class" "card flex flex-col gap-4") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "text") + (text "{{ text \"communities:label.query\" }}")) + (input + ("type" "text") + ("name" "text") + ("id" "text") + ("placeholder" "text") + ("required" "") + ("maxlength" "32") + ("value" "{{ text }}"))) + (button + ("class" "primary") + (text "{{ text \"dialog:action.continue\" }}")))) + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"book-marked\" }}") + (span + (text "{{ text \"communities:label.search_results\" }}"))) + (div + ("class" "card flex flex-col gap-4") + (text "{% for item in list %} {{ components::community_listing_card(community=item) }} {% endfor %} {{ components::pagination(page=page, items=list|length, key=\"&text=\", value=text) }}")))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/communities/settings.html b/crates/app/src/public/html/communities/settings.html deleted file mode 100644 index 54904e9..0000000 --- a/crates/app/src/public/html/communities/settings.html +++ /dev/null @@ -1,964 +0,0 @@ -{% extends "root.html" %} {% block head %} -Community settings - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
- - -
-
-
-
- Read access -
- -
- -
-
- -
-
- Join access -
- -
- -
-
- -
-
- Post permission -
- -
- -
-
- -
-
- {{ text "communities:label.change_title" }} -
- -
-
- - -
- - -
-
-
- -
-
- {{ icon "skull" }} - {{ text "communities:label.danger_zone" }} -
- -
- -
-
- -
- - - - {{ icon "arrow-left" }} - {{ text "general:action.back" }} - -
-
- - - - - - {% if can_manage_channels -%} - - - - {%- endif %} {% if can_manage_emojis -%} - - - - {%- endif %} -
- - - - - - - -{% endblock %} diff --git a/crates/app/src/public/html/communities/settings.lisp b/crates/app/src/public/html/communities/settings.lisp new file mode 100644 index 0000000..78d4349 --- /dev/null +++ b/crates/app/src/public/html/communities/settings.lisp @@ -0,0 +1,912 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Community settings - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (div + ("class" "pillmenu") + (a + ("href" "#/general") + ("data-tab-button" "general") + ("class" "active") + (text "{{ icon \"settings\" }}") + (span + (text "{{ text \"settings:tab.general\" }}"))) + (a + ("href" "#/images") + ("data-tab-button" "images") + (text "{{ icon \"image\" }}") + (span + (text "{{ text \"settings:tab.images\" }}"))) + (a + ("href" "#/members") + ("data-tab-button" "members") + (text "{{ icon \"users-round\" }}") + (span + (text "{{ text \"communities:tab.members\" }}"))) + (text "{% if can_manage_channels -%}") + (a + ("href" "#/channels") + ("data-tab-button" "channels") + (text "{{ icon \"rss\" }}") + (span + (text "{{ text \"communities:tab.channels\" }}"))) + (text "{%- endif %} {% if can_manage_emojis -%}") + (a + ("href" "#/emojis") + ("data-tab-button" "emojis") + (text "{{ icon \"smile\" }}") + (span + (text "{{ text \"communities:tab.emojis\" }}"))) + (text "{%- endif %}")) + (div + ("class" "w-full flex flex-col gap-2") + ("data-tab" "general") + (div + ("id" "manage_fields") + ("class" "card tertiary flex flex-col gap-2") + (div + ("class" "card-nest") + ("ui_ident" "read_access") + (div + ("class" "card small") + (b + (text "Read access"))) + (div + ("class" "card") + (select + ("onchange" "save_access(event, 'read')") + (option + ("value" "Everybody") + ("selected" "{% if community.read_access == 'Everybody' -%}true{% else %}false{%- endif %}") + (text "Everybody")) + (option + ("value" "Joined") + ("selected" "{% if community.read_access == 'Joined' -%}true{% else %}false{%- endif %}") + (text "Joined"))))) + (div + ("class" "card-nest") + ("ui_ident" "join_access") + (div + ("class" "card small") + (b + (text "Join access"))) + (div + ("class" "card") + (select + ("onchange" "save_access(event, 'join')") + (option + ("value" "Everybody") + ("selected" "{% if community.join_access == 'Everybody' -%}true{% else %}false{%- endif %}") + (text "Everybody")) + (option + ("value" "Request") + ("selected" "{% if community.join_access == 'Request' -%}true{% else %}false{%- endif %}") + (text "Request")) + (option + ("value" "Nobody") + ("selected" "{% if community.join_access == 'Nobody' -%}true{% else %}false{%- endif %}") + (text "Nobody"))))) + (div + ("class" "card-nest") + ("ui_ident" "write_access") + (div + ("class" "card small") + (b + (text "Post permission"))) + (div + ("class" "card") + (select + ("onchange" "save_access(event, 'write')") + (option + ("value" "Everybody") + ("selected" "{% if community.write_access == 'Everybody' -%}true{% else %}false{%- endif %}") + (text "Everybody")) + (option + ("value" "Joined") + ("selected" "{% if community.write_access == 'Joined' -%}true{% else %}false{%- endif %}") + (text "Joined")) + (option + ("value" "Owner") + ("selected" "{% if community.write_access == 'Owner' -%}true{% else %}false{%- endif %}") + (text "Owner only"))))) + (div + ("class" "card-nest") + ("ui_ident" "change_title") + (div + ("class" "card small") + (b + (text "{{ text \"communities:label.change_title\" }}"))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "change_title(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "new_title") + (text "{{ text \"communities:label.new_title\" }}")) + (input + ("type" "text") + ("name" "new_title") + ("id" "new_title") + ("placeholder" "new_title") + ("required" "") + ("minlength" "2"))) + (button + ("class" "primary") + (text "{{ icon \"check\" }}") + (span + (text "{{ text \"general:action.save\" }}")))))) + (div + ("class" "card-nest") + ("ui_ident" "danger_zone") + (div + ("class" "card small flex gap-1 items-center red") + (text "{{ icon \"skull\" }}") + (b + (text "{{ text \"communities:label.danger_zone\" }}"))) + (div + ("class" "card flex flex-wrap gap-2") + (button + ("class" "red quaternary") + ("onclick" "delete_community()") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"communities:label.delete_community\" }}"))))) + (div + ("class" "flex gap-2 flex-wrap") + (button + ("onclick" "save_context()") + (text "{{ icon \"check\" }}") + (span + (text "{{ text \"general:action.save\" }}"))) + (a + ("href" "/community/{{ community.title }}") + ("class" "button secondary") + (text "{{ icon \"arrow-left\" }}") + (span + (text "{{ text \"general:action.back\" }}"))))) + (div + ("class" "card tertiary w-full hidden flex flex-col gap-2") + ("data-tab" "images") + (div + ("class" "card-nest") + ("ui_ident" "change_avatar") + (div + ("class" "card small") + (b + (text "{{ text \"settings:label.change_avatar\" }}"))) + (form + ("class" "card flex gap-2 flex-row flex-wrap items-center") + ("method" "post") + ("enctype" "multipart/form-data") + ("onsubmit" "upload_avatar(event)") + (input + ("id" "avatar_file") + ("name" "file") + ("type" "file") + ("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif") + ("class" "w-content")) + (button + ("class" "primary") + (text "{{ icon \"check\" }}")))) + (div + ("class" "card-nest") + ("ui_ident" "change_banner") + (div + ("class" "card small") + (b + (text "{{ text \"settings:label.change_banner\" }}"))) + (form + ("class" "card flex flex-col gap-2") + ("method" "post") + ("enctype" "multipart/form-data") + ("onsubmit" "upload_banner(event)") + (div + ("class" "flex gap-2 flex-row flex-wrap items-center") + (input + ("id" "banner_file") + ("name" "file") + ("type" "file") + ("accept" "image/png,image/jpeg,image/avif,image/webp") + ("class" "w-content")) + (button + ("class" "primary") + (text "{{ icon \"check\" }}"))) + (span + ("class" "fade") + (text "Use an image of 1100x350px for the best results."))))) + (div + ("class" "card tertiary w-full hidden flex flex-col gap-2") + ("data-tab" "members") + (div + ("class" "card-nest") + (div + ("class" "card small") + (b + (text "{{ text \"communities:label.select_member\" }}"))) + (form + ("class" "card flex-col gap-2") + ("onsubmit" "select_user_from_form(event)") + (div + ("class" "flex flex-col gap-1") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "uid") + (text "{{ text \"communities:label.user_id\" }}")) + (input + ("type" "number") + ("name" "uid") + ("id" "uid") + ("placeholder" "user id") + ("required" "") + ("minlength" "18"))) + (button + ("class" "primary") + (text "{{ text \"communities:action.select\" }}"))))) + (div + ("class" "card flex flex-col gap-2 w-full") + ("id" "membership_info"))) + (text "{% if can_manage_channels -%}") + (div + ("class" "card tertiary 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") + (text "{{ text \"communities:label.name\" }}")) + (input + ("type" "text") + ("name" "title") + ("id" "title") + ("placeholder" "name") + ("required" "") + ("minlength" "2") + ("maxlength" "32"))) + (button + ("class" "primary") + (text "{{ 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 quaternary small") + ("onclick" "delete_channel('{{ channel.id }}')") + (text "{{ text \"general:action.delete\" }}")) + (button + ("class" "quaternary small") + ("onclick" "update_channel_position('{{ channel.id }}')") + (text "{{ text \"chats:action.move\" }}")) + (button + ("class" "quaternary 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 -%}") + (div + ("class" "card tertiary w-full hidden flex flex-col gap-2") + ("data-tab" "emojis") + (text "{{ components::supporter_ad(body=\"Become a supporter to upload GIF animated emojis!\") }}") + (div + ("class" "card-nest") + ("ui_ident" "change_banner") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"upload\" }}") + (b + (text "{{ text \"communities:label.upload\" }}"))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "upload_emoji(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "name") + (text "{{ text \"communities:label.name\" }}")) + (input + ("type" "text") + ("name" "name") + ("id" "name") + ("placeholder" "name") + ("required" "") + ("minlength" "2") + ("maxlength" "32"))) + (div + ("class" "flex flex-col gap-1") + (label + ("for" "file") + (text "{{ text \"communities:label.file\" }}")) + (input + ("id" "banner_file") + ("name" "file") + ("type" "file") + ("accept" "image/png,image/jpeg,image/avif,image/webp") + ("class" "w-full"))) + (button + (text "{{ text \"communities:action.create\" }}")) + (span + ("class" "fade") + (text "Emojis can be a maximum of 256 KiB, or 512x512px (width x + height).")))) + (text "{% for emoji in emojis %}") + (div + ("class" "card secondary flex flex-wrap gap-2 items-center justify-between") + (div + ("class" "flex gap-2 items-center") + (img + ("src" "/api/v1/communities/{{ community.id }}/emojis/{{ emoji.name }}") + ("alt" "{{ emoji.name }}") + ("class" "emoji") + ("loading" "lazy")) + (b + (text "{{ emoji.name }}"))) + (div + ("class" "flex gap-2") + (button + ("class" "quaternary small") + ("onclick" "rename_emoji('{{ emoji.id }}')") + (text "{{ icon \"pencil\" }}") + (span + (text "{{ text \"chats:action.rename\" }}"))) + (button + ("class" "quaternary small red") + ("onclick" "remove_emoji('{{ emoji.id }}')") + (text "{{ icon \"x\" }}") + (span + (text "{{ text \"stacks:label.remove\" }}"))))) + (text "{% endfor %}")) + (script + (text "globalThis.upload_emoji = (e) => { + e.preventDefault(); + e.target.querySelector(\"button\").style.display = \"none\"; + + fetch( + `/api/v1/communities/{{ community.id }}/emojis/${e.target.name.value}`, + { + method: \"POST\", + body: e.target.file.files[0], + }, + ) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + e.target.querySelector(\"button\").removeAttribute(\"style\"); + }); + + alert(\"Emoji upload in progress. Please wait!\"); + }; + + globalThis.rename_emoji = async (id) => { + const name = await trigger(\"atto::prompt\", [\"New emoji name:\"]); + + if (!name) { + return; + } + + fetch(`/api/v1/emojis_id/${id}/name`, { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + name, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.remove_emoji = async (id) => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this? This action is permanent.\", + ])) + ) { + return; + } + + fetch(`/api/v1/emojis_id/${id}`, { + method: \"DELETE\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + };")) + (text "{%- endif %}")) + +(script + (text "setTimeout(() => { + const element = document.getElementById(\"membership_info\"); + const ui = ns(\"ui\"); + + const uid = new URLSearchParams(window.location.search).get(\"uid\"); + if (uid) { + document.getElementById(\"uid\").value = uid; + } + + globalThis.update_user_role = async (uid, new_role) => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + fetch( + `/api/v1/communities/{{ community.id }}/memberships/${uid}/role`, + { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + role: Number.parseInt(new_role), + }), + }, + ) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.kick_user = async (uid, new_role) => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + fetch(`/api/v1/communities/{{ community.id }}/memberships/${uid}`, { + method: \"DELETE\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.transfer_ownership = async (uid) => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\n\nThis action is PERMANENT!\", + ])) + ) { + return; + } + + fetch(`/api/v1/communities/{{ community.id }}/transfer_ownership`, { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + user: uid, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.select_user_from_form = (e) => { + e.preventDefault(); + fetch( + `/api/v1/communities/{{ community.id }}/memberships/${e.target.uid.value}`, + ) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (!res.ok) { + return; + } + + // permissions manager + const get_permissions_html = trigger( + \"ui::generate_permissions_ui\", + [ + { + // https://trisuaso.github.io/tetratto/tetratto/model/communities_permissions/struct.CommunityPermission.html + DEFAULT: 1 << 0, + ADMINISTRATOR: 1 << 1, + MEMBER: 1 << 2, + MANAGE_POSTS: 1 << 3, + MANAGE_ROLES: 1 << 4, + BANNED: 1 << 5, + REQUESTED: 1 << 6, + MANAGE_PINS: 1 << 7, + MANAGE_COMMUNITY: 1 << 8, + MANAGE_QUESTIONS: 1 << 9, + MANAGE_CHANNELS: 1 << 10, + MANAGE_MESSAGES: 1 << 11, + MANAGE_EMOJIS: 1 << 12, + }, + ], + ); + + // ... + element.innerHTML = `
+ Open user profile + ${res.payload.role !== 33 ? `` : ``} + ${res.payload.role !== 65 ? `` : ``} + + +
+ +
+ ${get_permissions_html(res.payload.role, \"permissions\")} +
`; + + ui.refresh_container(element, [\"actions\", \"permissions\"]); + ui.generate_settings_ui( + element, + [ + [ + [\"role\", \"Permission level\"], + res.payload.role, + \"input\", + ], + ], + null, + { + role: (new_role) => { + const [matching, _] = + all_matching_permissions(new_role); + + document.getElementById( + \"permissions\", + ).innerHTML = get_permissions_html( + rebuild_role(matching), + \"permissions\", + ); + + return update_user_role( + e.target.uid.value, + new_role, + ); + }, + }, + ); + }); + }; + }, 250);")) + +(script + ("type" "application/json") + ("id" "settings_json") + (text "{{ community.context|json_encode()|safe }}")) + +(script + (text "setTimeout(() => { + const ui = ns(\"ui\"); + const settings = JSON.parse( + document.getElementById(\"settings_json\").innerHTML, + ); + + globalThis.upload_avatar = (e) => { + e.preventDefault(); + e.target.querySelector(\"button\").style.display = \"none\"; + + fetch(\"/api/v1/communities/{{ community.id }}/upload/avatar\", { + method: \"POST\", + body: e.target.file.files[0], + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + e.target.querySelector(\"button\").removeAttribute(\"style\"); + }); + + alert(\"Avatar upload in progress. Please wait!\"); + }; + + globalThis.upload_banner = (e) => { + e.preventDefault(); + e.target.querySelector(\"button\").style.display = \"none\"; + + fetch(\"/api/v1/communities/{{ community.id }}/upload/banner\", { + method: \"POST\", + body: e.target.file.files[0], + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + e.target.querySelector(\"button\").removeAttribute(\"style\"); + }); + + alert(\"Banner upload in progress. Please wait!\"); + }; + + globalThis.save_context = () => { + fetch(\"/api/v1/communities/{{ community.id }}/context\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + context: settings, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.save_access = (event, mode) => { + const selected = event.target.selectedOptions[0]; + fetch(`/api/v1/communities/{{ community.id }}/access/${mode}`, { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + access: selected.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.change_title = async (e) => { + e.preventDefault(); + + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + fetch(\"/api/v1/communities/{{ community.id }}/title\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + title: e.target.new_title.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.delete_community = async () => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this? This action is permanent.\", + ])) + ) { + return; + } + + fetch(`/api/v1/communities/{{ community.id }}`, { + method: \"DELETE\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + ui.refresh_container(document.getElementById(\"manage_fields\"), [ + \"read_access\", + \"join_access\", + \"write_access\", + \"change_title\", + \"change_avatar\", + \"change_banner\", + ]); + + ui.generate_settings_ui( + document.getElementById(\"manage_fields\"), + [ + [ + [\"display_name\", \"Display title\"], + \"{{ community.context.display_name }}\", + \"input\", + ], + [ + [\"description\", \"Description\"], + settings.description, + \"textarea\", + ], + [ + [\"is_nsfw\", \"Mark as NSFW\"], + \"{{ community.context.is_nsfw }}\", + \"checkbox\", + ], + [ + [ + \"enable_questions\", + \"Allow users to ask questions in this community\", + ], + \"{{ community.context.enable_questions }}\", + \"checkbox\", + ], + ], + settings, + ); + }, 250);")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/components.html b/crates/app/src/public/html/components.html deleted file mode 100644 index 938416b..0000000 --- a/crates/app/src/public/html/components.html +++ /dev/null @@ -1,1493 +0,0 @@ -{% macro avatar(username, size="24px", selector_type="username") -%} -@{{ username }} -{%- endmacro %} {% macro community_avatar(id, community=false, size="24px") -%} -{% if community -%} -{{ community.title }}'s avatar -{% else %} -{{ id }}'s avatar -{%- endif %} {%- endmacro %} {% macro banner(username, -border_radius="var(--radius)") -%} - -{%- endmacro %} {% macro community_banner(id, community=false) -%} {% if -community %} - -{% else %} - -{%- endif %} {%- endmacro %} {% macro community_listing_card(community) -%} - - {{ self::community_avatar(id=community.id, community=community, size="48px") - }} -
-

{{ community.context.display_name }}

- {{ community.member_count }} members -
-
-{%- endmacro %} {% macro username(user) -%} -
- {% if user.settings.display_name -%} {{ user.settings.display_name }} {% - else %} {{ user.username }} {%- endif %} -
-{%- endmacro %} {% macro likes(id, asset_type, likes=0, dislikes=0, -secondary=false) -%} - - -{% if not user or not user.settings.hide_dislikes -%} - -{%- endif %} {%- endmacro %} {% macro full_username(user) -%} -
- - {{ self::username(user=user) }} - - - {{ self::online_indicator(user=user) }} {% if user.is_verified -%} - - {{ icon "badge-check" }} - - {%- endif %} -
-{%- endmacro %} {% macro repost(repost, post, owner, secondary=false, -community=false, show_community=true, can_manage_post=false) -%} -
- {{ self::post(post=post, owner=owner, secondary=secondary, - community=community, show_community=show_community, - can_manage_post=can_manage_post, repost=repost, expect_repost=true) }} -
-{%- endmacro %} {% macro post(post, owner, question=false, secondary=false, -community=false, show_community=true, can_manage_post=false, repost=false, -expect_repost=false) -%} {% if community and show_community and community.id != -config.town_square or question %} -
- {% if question -%} {{ self::question(question=question[0], - owner=question[1], profile=owner) }} {% else %} - - {%- endif %} {%- endif %} -
-
- {% if not expect_repost -%} - - {{ self::avatar(username=owner.username, size="52px", - selector_type="username") }} - - {%- endif %} - -
-
- {% if expect_repost -%} - - {{ self::avatar(username=owner.username, size="24px", - selector_type="username") }} - - {%- endif %} - - {{ self::full_username(user=owner) }} - - {% if post.context.edited != 0 -%} -
- {{ post.context.edited }} - * -
- {% else %} - {{ post.created }} - {%- endif %} {% if post.context.is_nsfw -%} - - {{ icon "square-asterisk" }} - - {%- endif %} {% if post.context.repost and - post.context.repost.reposting %} - - {{ icon "repeat-2" }} - - {%- endif %} {% if post.community == config.town_square -%} - - {{ icon "user-round" }} - - {%- endif %} {% if post.is_deleted -%} - - {{ icon "trash-2" }} - - {%- endif %} -
- - {% if not post.context.content_warning -%} - - - {{ post.content|markdown|safe }} - - {% if expect_repost -%} - {% if repost -%} - {{ self::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false) }} - {% else %} -
- {{ icon "frown" }} - Could not find original post... -
- {%- endif %} - {%- endif %} -
- - {{ self::post_media(upload_ids=post.uploads) }} {% else %} -
- - {{ icon "triangle-alert" }} - {{ post.context.content_warning }} - - -
- - - {{ post.content|markdown|safe }} - - {% if expect_repost -%} - {% if repost -%} - {{ self::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false) }} - {% else %} -
- {{ icon "frown" }} - Could not find original post... -
- {%- endif %} - {%- endif %} -
- - {{ self::post_media(upload_ids=post.uploads) }} -
-
- {%- endif %} - -
- {% for tag in post.context.tags %} - #{{ tag }} - {% endfor %} -
-
-
- -
- {% if user -%} -
- - {% if post.context.reactions_enabled -%} - {% if post.content|length > 0 -%} - {{ self::likes(id=post.id, asset_type="Post", likes=post.likes, dislikes=post.dislikes) }} - {%- endif %} - {%- endif %} - - {% if post.context.repost and post.context.repost.reposting -%} - - {{ icon "expand" }} - - {%- endif %} -
- {% else %} -
- {%- endif %} - -
- - {{ icon "message-circle" }} - {{ post.comment_count }} - - - - {{ icon "external-link" }} - - - {% if user -%} - - {%- endif %} -
-
-
- {% if community and show_community and community.id != config.town_square or - question %} -
-{%- endif %} {%- endmacro %} {% macro post_media(upload_ids) -%} {% if -upload_ids|length > 0%} - -{%- endif %} {%- endmacro %} {% macro notification(notification) -%} -
-
- {% if not notification.read -%} - - - - {%- endif %} - {{ notification.title|markdown|safe }} -
- -
- {{ notification.content|markdown|safe }} - -
- {% if notification.read -%} - - {% else %} - - {%- endif %} - - -
-
-
-{%- endmacro %} {% macro user_card(user) -%} - -
- {{ self::banner(username=user.username, border_radius="0px") }} -
- -
- {{ self::avatar(username=user.username, size="48px") }} -
- {{ self::username(user=user) }} - {{ self::online_indicator(user=user) }} -
-
-
-{%- endmacro %} {% macro pagination(page=0, items=0, key="", value="") -%} -
- {% if page > 0 -%} - - {{ icon "arrow-left" }} - {{ text "general:link.previous" }} - - {% else %} -
- {%- endif %} {% if items != 0 -%} - - {{ text "general:link.next" }} - {{ icon "arrow-right"}} - - {%- endif %} -
-{%- endmacro %} {% macro online_indicator(user) -%} {% if not -user.settings.private_last_seen or is_helper %} -
-
- - - -
- -
- - - -
- -
- - - -
-
-{% else %} -
- - - -
-{%- endif %} {%- endmacro %} {% macro theme(user, theme_preference) -%} {% if -user %} {% if user.settings.theme_hue -%} - -{%- endif %} {% if user.settings.theme_sat -%} - -{%- endif %} {% if user.settings.theme_lit -%} - -{%- endif %} {% if theme_preference -%} - -{%- endif %} - -
- {{ self::theme_color(color=user.settings.theme_color_surface, css="color-surface") }} - {{ self::theme_color(color=user.settings.theme_color_text, css="color-text") }} - {{ self::theme_color(color=user.settings.theme_color_text_link, css="color-link") }} - - {{ self::theme_color(color=user.settings.theme_color_lowered, css="color-lowered") }} - {{ self::theme_color(color=user.settings.theme_color_text_lowered, css="color-text-lowered") }} - {{ self::theme_color(color=user.settings.theme_color_super_lowered, css="color-super-lowered") }} - - {{ self::theme_color(color=user.settings.theme_color_raised, css="color-raised") }} - {{ self::theme_color(color=user.settings.theme_color_text_raised, css="color-text-raised") }} - {{ self::theme_color(color=user.settings.theme_color_super_raised, css="color-super-raised") }} - - {{ self::theme_color(color=user.settings.theme_color_primary, css="color-primary") }} - {{ self::theme_color(color=user.settings.theme_color_text_primary, css="color-text-primary") }} - {{ self::theme_color(color=user.settings.theme_color_primary_lowered, css="color-primary-lowered") }} - - {{ self::theme_color(color=user.settings.theme_color_secondary, css="color-secondary") }} - {{ self::theme_color(color=user.settings.theme_color_text_secondary, css="color-text-secondary") }} - {{ self::theme_color(color=user.settings.theme_color_secondary_lowered, css="color-secondary-lowered") }} - - {% if user.permissions|has_supporter -%} - - {%- endif %} -
-{%- endif %} {%- endmacro %} {% macro theme_color(color, css) -%} {% if color --%} - - -{%- endif %} {%- endmacro %} {% macro question(question, owner, -show_community=true, secondary=false, profile=false) -%} -
- {% if owner.id == 0 -%} - - {% if profile and profile.settings.anonymous_avatar_url -%} - anonymous' avatar - {% else %} {{ self::avatar(username=owner.username, - selector_type="username", size="52px") }} {%- endif %} - - {% else %} - - {{ self::avatar(username=owner.username, selector_type="username", - size="52px") }} - - {%- endif %} - -
-
- - - {% if owner.id == 0 -%} - {% if profile and profile.settings.anonymous_username -%} - - {{ profile.settings.anonymous_username }} - - {{ icon "drama" }} - - - {% else %} - anonymous - {%- endif %} - {% else %} - {{ self::full_username(user=owner) }} - {%- endif %} - - - {{ question.created }} - - - {{ icon "message-circle-heart" }} - - - {% if question.context.is_nsfw -%} - - {{ icon "square-asterisk" }} - - {%- endif %} {% if question.community > 0 and show_community -%} - - {{ self::community_avatar(id=question.community, size="24px") }} - - {%- endif %} {% if question.is_global -%} - {{ question.answer_count }} answers - {%- endif %} -
- - {{ question.content|markdown|safe }} - -
-
-
-{%- endmacro %} {% macro create_question_form(receiver="0", community="", -header="", is_global=false) -%} -
-
- {{ icon "message-circle-heart" }} - - - {% if header -%} - {{ header|markdown|safe }} - {% else %} - {{ text "requests:label.ask_question" }} - {%- endif %} - -
- -
-
- - -
- - -
-
- - -{%- endmacro %} {% macro global_question(question, can_manage_questions=false, -secondary=false, show_community=true) -%} -
- {{ self::question(question=question[0], owner=question[1], - show_community=show_community) }} - -
-
- {{ self::likes(id=question[0].id, asset_type="Question", - likes=question[0].likes, dislikes=question[0].dislikes, - secondary=false) }} -
- -
- - {{ icon "external-link" }} {% if user -%} - {{ text "requests:label.answer" }} - {% else %} - {{ text "general:action.open" }} - {%- endif %} - - - {% if user -%} {% if can_manage_questions or is_helper or - question[1].id == user.id %} - - {%- endif %} {%- endif %} -
-
-
-{%- endmacro %} {% macro spotify_playing(state, size="60px") -%} {% if state and -state.data %} -
-
-
- Listening on - {{ icon "spotify" }} -
- - {{ state.data.timestamp }} -
- - -
-{%- endif %} {%- endmacro %} {% macro last_fm_playing(state, size="60px") -%} {% -if state and state.data %} -
-
-
- Listening on - {{ icon "last_fm" }} -
- - {{ state.data.timestamp }} -
- -
- - Track cover - - -
-
- {{ state.data.track }} -
- - {{ state.data.artist }} - - {% if state.data.duration_ms and state.data.duration_ms != "0" -%} - - {%- endif %} -
-
-
-{%- endif %} {%- endmacro %} {% macro connection_icon(key) -%} - -
- {% if key == "Spotify" -%} - {{ icon "spotify" }} - {% elif key == "LastFm" %} - {{ icon "last_fm" }} - {%- endif %} -
-{%- 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) -%} - -{%- endmacro %} {% macro message(user, message, can_manage_message=false, -grouped=false) -%} -
- {% if not grouped -%} - - {{ self::avatar(username=user.username, size="42px") }} - - {%- endif %} - -
- {% if not grouped -%} -
-
- {{ self::full_username(user=user) }} {% if message.edited != - message.created %} - {{ message.edited }}* - {% else %} - {{ message.created }} - {%- endif %} -
- - -
- {%- endif %} - -
- {{ message.content|markdown|safe }} - - {% if grouped -%} - - {%- endif %} -
-
-
-{%- endmacro %} {% macro user_menu() -%} -
- {{ user.username }} - - {{ icon "circle-user-round" }} - {{ text "auth:link.my_profile" }} - - - - {{ icon "settings" }} - {{ text "auth:link.settings" }} - - - {% if is_helper -%} - {{ text "general:label.mod" }} - - - {{ icon "scroll-text" }} - {{ text "general:link.audit_log" }} - - - - {{ icon "flag" }} - {{ text "general:link.reports" }} - - - - {{ icon "ban" }} - {{ text "general:link.ip_bans" }} - - - - {{ icon "chart-line" }} - {{ text "general:link.stats" }} - - {%- endif %} - - {{ config.name }} - - - {{ icon "code" }} - {{ text "general:link.source_code" }} - - - - -
- - - -
-{%- endmacro %} {% macro user_status(other_user) -%} {% if -other_user.settings.status %} -
- {{ other_user.settings.status }} - - - {% if (other_user.connections.LastFm[1].data and - other_user.connections.LastFm[1].data.track) or - (other_user.connections.Spotify[1].data and - other_user.connections.Spotify[1].data.track) %} {{ icon "music" }} {% endif - %} -
-{% elif other_user.connections.LastFm[0].data.name and -other_user.connections.LastFm[1].data and -other_user.connections.LastFm[1].data.track %} -
- {{ icon "music" }} - Listening to {{ other_user.connections.LastFm[1].data.artist - }} -
-{% elif other_user.connections.Spotify[0].data.name and -other_user.connections.Spotify[1].data and -other_user.connections.Spotify[1].data.track %} -
- {{ icon "music" }} - Listening to {{ other_user.connections.Spotify[1].data.artist - }} -
-{%- endif %} {%- endmacro %} {% macro user_plate(user, show_menu=false, -show_kick=false, secondary=false) -%} -
- - {{ self::avatar(username=user.username, size="42px", - selector_type="username") }} - - -
- {{ self::full_username(user=user) }} -
{{ self::user_status(other_user=user) }}
-
- - {% if show_menu -%} - - {% elif show_kick %} - - {%- endif %} -
-{%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false) -%} - - -{% if render_dialog -%} - -
- - - - - - -
-
- -
- -
-
-
-
-{%- endif %} {%- endmacro %} {% macro file_picker(files_list_id) -%} - - - - - - - -{%- endmacro %} {% macro supporter_ad(body="") -%} {% if config.stripe and not -is_supporter %} -
-
- {% if body -%} - {{ body }} - {% else %} - {{ text "general:label.supporter_motivation" }} - {%- endif %} - - - {{ icon "heart" }} - {{ text "general:action.become_supporter" }} - -
-
-{%- endif %} {%- endmacro %} {% macro create_post_options() -%} - -
- {{ 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 %} - - -
- - -
-
- -
- -
-
- -
- -
-
- - -
-
-{%- endmacro %} diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp new file mode 100644 index 0000000..1190070 --- /dev/null +++ b/crates/app/src/public/html/components.lisp @@ -0,0 +1,1310 @@ +(text "{% macro avatar(username, size=\"24px\", selector_type=\"username\") -%}") +(img + ("title" "{{ username }}'s avatar") + ("src" "/api/v1/auth/user/{{ username }}/avatar?selector_type={{ selector_type }}") + ("alt" "@{{ username }}") + ("class" "avatar shadow") + ("loading" "lazy") + ("style" "--size: {{ size }}")) + +(text "{%- endmacro %} {% macro community_avatar(id, community=false, size=\"24px\") -%} {% if community -%}") +(img + ("src" "/api/v1/communities/{{ id }}/avatar") + ("alt" "{{ community.title }}'s avatar") + ("class" "avatar shadow") + ("loading" "lazy") + ("style" "--size: {{ size }}")) + +(text "{% else %}") +(img + ("src" "/api/v1/communities/{{ id }}/avatar") + ("alt" "{{ id }}'s avatar") + ("class" "avatar shadow") + ("loading" "lazy") + ("style" "--size: {{ size }}")) + +(text "{%- endif %} {%- endmacro %} {% macro banner(username, border_radius=\"var(--radius)\") -%}") +(img + ("title" "{{ username }}'s banner") + ("src" "/api/v1/auth/user/{{ username }}/banner") + ("alt" "@{{ username }}'s banner") + ("class" "banner shadow w-full") + ("loading" "lazy") + ("style" "border-radius: {{ border_radius }};")) + +(text "{%- endmacro %} {% macro community_banner(id, community=false) -%} {% if community %}") +(img + ("src" "/api/v1/communities/{{ id }}/banner") + ("alt" "{{ community.title }}'s banner") + ("class" "banner shadow") + ("loading" "lazy")) + +(text "{% else %}") +(img + ("src" "/api/v1/communities/{{ id }}/banner") + ("alt" "{{ id }}'s banner") + ("class" "banner shadow") + ("loading" "lazy")) + +(text "{%- endif %} {%- endmacro %} {% macro community_listing_card(community) -%}") +(a + ("class" "card secondary w-full flex items-center gap-4") + ("href" "/community/{{ community.title }}") + (text "{{ self::community_avatar(id=community.id, community=community, size=\"48px\") }}") + (div + ("class" "flex flex-col") + (h3 + ("class" "name lg:long") + (text "{{ community.context.display_name }}")) + (span + ("class" "fade") + (b + (text "{{ community.member_count }} ")) + (text "members")))) + +(text "{%- endmacro %} {% macro username(user) -%}") +(div + ("style" "display: contents") + (text "{% if user.settings.display_name -%} {{ user.settings.display_name }} {% else %} {{ user.username }} {%- endif %}")) + +(text "{%- endmacro %} {% macro likes(id, asset_type, likes=0, dislikes=0, secondary=false) -%}") +(button + ("title" "Like") + ("class" "{% if secondary -%}quaternary{% else %}camo{%- endif %} small") + ("hook_element" "reaction.like") + ("onclick" "trigger('me::react', [event.target, '{{ id }}', '{{ asset_type }}', true])") + (text "{{ icon \"heart\" }} {% if likes > 0 -%}") + (span + (text "{{ likes }}")) + (text "{%- endif %}")) + +(text "{% if not user or not user.settings.hide_dislikes -%}") +(button + ("title" "Dislike") + ("class" "{% if secondary -%}quaternary{% else %}camo{%- endif %} small") + ("hook_element" "reaction.dislike") + ("onclick" "trigger('me::react', [event.target, '{{ id }}', '{{ asset_type }}', false])") + (text "{{ icon \"heart-crack\" }} {% if dislikes > 0 -%}") + (span + (text "{{ dislikes }}")) + (text "{%- endif %}")) + +(text "{%- endif %} {%- endmacro %} {% macro full_username(user) -%}") +(div + ("class" "flex items-center") + (a + ("href" "/@{{ user.username }}") + ("class" "flush") + ("style" "font-weight: 600") + ("target" "_top") + (text "{{ self::username(user=user) }}")) + (text "{{ self::online_indicator(user=user) }} {% if user.is_verified -%}") + (span + ("title" "Verified") + ("style" "color: var(--color-primary)") + ("class" "flex items-center") + (text "{{ icon \"badge-check\" }}")) + (text "{%- endif %}")) + +(text "{%- endmacro %} {% macro repost(repost, post, owner, secondary=false, community=false, show_community=true, can_manage_post=false) -%}") +(div + ("style" "display: contents") + (text "{{ self::post(post=post, owner=owner, secondary=secondary, community=community, show_community=show_community, can_manage_post=can_manage_post, repost=repost, expect_repost=true) }}")) + +(text "{%- endmacro %} {% macro post(post, owner, question=false, secondary=false, community=false, show_community=true, can_manage_post=false, repost=false, expect_repost=false) -%} {% if community and show_community and community.id != config.town_square or question %}") +(div + ("class" "card-nest") + (text "{% if question -%} {{ self::question(question=question[0], owner=question[1], profile=owner) }} {% else %}") + (div + ("class" "card small") + (a + ("href" "/api/v1/communities/find/{{ post.community }}") + ("class" "flush flex gap-1 items-center") + (text "{{ self::community_avatar(id=post.community, community=community) }}") + (b + (text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}")) + (text "{% if post.context.is_pinned or post.context.is_profile_pinned -%} {{ icon \"pin\" }} {%- endif %}"))) + (text "{%- endif %} {%- endif %}") + (div + ("class" "card flex flex-col gap-2 {% if secondary -%}secondary{%- endif %}") + ("id" "post:{{ post.id }}") + ("data-community" "{{ post.community }}") + ("data-ownsup" "{{ owner.permissions|has_supporter }}") + ("hook" "verify_emojis") + (div + ("class" "w-full flex gap-2") + (text "{% if not expect_repost -%}") + (a + ("href" "/@{{ owner.username }}") + (text "{{ self::avatar(username=owner.username, size=\"52px\", selector_type=\"username\") }}")) + (text "{%- endif %}") + (div + ("class" "flex flex-col w-full gap-1 post_right {% if expect_repost -%}repost{%- endif %}") + (div + ("class" "flex flex-wrap gap-2 items-center") + (text "{% if expect_repost -%}") + (a + ("href" "/@{{ owner.username }}") + (text "{{ self::avatar(username=owner.username, size=\"24px\", selector_type=\"username\") }}")) + (text "{%- endif %}") + (span + ("class" "name") + (text "{{ self::full_username(user=owner) }}")) + (text "{% if post.context.edited != 0 -%}") + (div + ("class" "flex") + (span + ("class" "fade date") + (text "{{ post.context.edited }}")) + (sup + ("title" "Edited") + (text "*"))) + (text "{% else %}") + (span + ("class" "fade date") + (text "{{ post.created }}")) + (text "{%- endif %} {% if post.context.is_nsfw -%}") + (span + ("title" "NSFW post") + ("class" "flex items-center") + ("style" "color: var(--color-primary)") + (text "{{ icon \"square-asterisk\" }}")) + (text "{%- endif %} {% if post.context.repost and post.context.repost.reposting %}") + (span + ("title" "Repost") + ("class" "flex items-center") + ("style" "color: var(--color-primary)") + (text "{{ icon \"repeat-2\" }}")) + (text "{%- endif %} {% if post.community == config.town_square -%}") + (span + ("title" "Posted to profile") + ("class" "flex items-center") + ("style" "color: var(--color-primary)") + (text "{{ icon \"user-round\" }}")) + (text "{%- endif %} {% if post.is_deleted -%}") + (span + ("title" "Deleted") + ("class" "flex items-center") + ("style" "color: var(--color-primary)") + (text "{{ icon \"trash-2\" }}")) + (text "{%- endif %}")) + (text "{% if not post.context.content_warning -%}") + (span + ("id" "post-content:{{ post.id }}") + ("class" "no_p_margin") + ("hook" "long") + (text "{{ post.content|markdown|safe }} {% if expect_repost -%} {% if repost -%} {{ self::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false) }} {% else %}") + (div + ("class" "card tertiary red flex items-center gap-2") + (text "{{ icon \"frown\" }}") + (span + (text "Could not find original post..."))) + (text "{%- endif %} {%- endif %}")) + (text "{{ self::post_media(upload_ids=post.uploads) }} {% else %}") + (details + (summary + ("class" "card flex gap-2 flex-wrap items-center tertiary red w-full") + (text "{{ icon \"triangle-alert\" }}") + (b + (text "{{ post.context.content_warning }}"))) + (div + ("class" "flex flex-col gap-2") + (span + ("id" "post-content:{{ post.id }}") + ("class" "no_p_margin") + ("hook" "long") + (text "{{ post.content|markdown|safe }} {% if expect_repost -%} {% if repost -%} {{ self::post(post=repost[1], owner=repost[0], secondary=not secondary, community=false, show_community=false, can_manage_post=false) }} {% else %}") + (div + ("class" "card tertiary red flex items-center gap-2") + (text "{{ icon \"frown\" }}") + (span + (text "Could not find original post..."))) + (text "{%- endif %} {%- endif %}")) + (text "{{ self::post_media(upload_ids=post.uploads) }}"))) + (text "{%- endif %}") + (div + ("class" "flex flex-wrap gap-2 fade") + (text "{% for tag in post.context.tags %}") + (a + ("href" "/@{{ owner.username }}?tag={{ tag }}") + ("class" "flush fade") + (text "#{{ tag }}")) + (text "{% endfor %}")))) + (div + ("class" "flex justify-between items-center gap-2 w-full") + (text "{% if user -%}") + (div + ("class" "flex gap-1 reactions_box") + ("hook" "check_reactions") + ("hook-arg:id" "{{ post.id }}") + (text "{% if post.context.reactions_enabled -%} {% if post.content|length > 0 -%} {{ self::likes(id=post.id, asset_type=\"Post\", likes=post.likes, dislikes=post.dislikes) }} {%- endif %} {%- endif %} {% if post.context.repost and post.context.repost.reposting -%}") + (a + ("href" "/post/{{ post.context.repost.reposting }}") + ("class" "button small camo") + (text "{{ icon \"expand\" }}")) + (text "{%- endif %}")) + (text "{% else %}") + (div) + (text "{%- endif %}") + (div + ("class" "flex gap-1 buttons_box") + (a + ("href" "/post/{{ post.id }}") + ("class" "button camo small") + (text "{{ icon \"message-circle\" }}") + (span + (text "{{ post.comment_count }}"))) + (a + ("href" "/post/{{ post.id }}") + ("class" "button camo small") + ("target" "_blank") + (text "{{ icon \"external-link\" }}")) + (text "{% if user -%}") + (div + ("class" "dropdown") + (button + ("class" "camo small") + ("onclick" "trigger('atto::hooks::dropdown', [event])") + ("exclude" "dropdown") + (text "{{ icon \"ellipsis\" }}")) + (div + ("class" "inner") + (text "{% if config.town_square and post.context.reposts_enabled %}") + (b + ("class" "title") + (text "{{ text \"general:label.share\" }}")) + (button + ("onclick" "trigger('me::repost', ['{{ post.id }}', '', '{{ config.town_square }}'])") + (text "{{ icon \"repeat-2\" }}") + (span + (text "{{ text \"communities:label.repost\" }}"))) + (a + ("class" "button") + ("href" "/communities/intents/post?quote={{ post.id }}") + (text "{{ icon \"quote\" }}") + (span + (text "{{ text \"communities:label.quote_post\" }}"))) + (text "{%- endif %} {% if user.id != post.owner -%}") + (b + ("class" "title") + (text "{{ text \"general:label.safety\" }}")) + (button + ("class" "red") + ("onclick" "trigger('me::report', ['{{ post.id }}', 'post'])") + (text "{{ icon \"flag\" }}") + (span + (text "{{ text \"general:action.report\" }}"))) + (text "{%- endif %} {% if (user.id == post.owner) or is_helper or can_manage_post %}") + (b + ("class" "title") + (text "{{ text \"general:action.manage\" }}")) + (text "{% if user.id == post.owner -%}") + (a + ("href" "/post/{{ post.id }}#/edit") + (text "{{ icon \"pen\" }}") + (span + (text "{{ text \"communities:label.edit_content\" }}"))) + (text "{%- endif %}") + (a + ("href" "/post/{{ post.id }}#/configure") + (text "{{ icon \"settings\" }}") + (span + (text "{{ text \"communities:action.configure\" }}"))) + (text "{% if not post.is_deleted -%}") + (button + ("class" "red") + ("onclick" "trigger('me::remove_post', ['{{ post.id }}'])") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}"))) + (text "{%- endif %} {% if is_helper and post.is_deleted -%}") + (button + ("class" "red") + ("onclick" "trigger('me::purge_post', ['{{ post.id }}'])") + (text "{{ icon \"trash-2\" }}") + (span + (text "{{ text \"general:action.purge\" }}"))) + (button + ("class" "green") + ("onclick" "trigger('me::restore_post', ['{{ post.id }}'])") + (text "{{ icon \"undo\" }}") + (span + (text "{{ text \"general:action.restore\" }}"))) + (text "{%- endif %} {%- endif %}"))) + (text "{%- endif %}")))) + (text "{% if community and show_community and community.id != config.town_square or question %}")) + +(text "{%- endif %} {%- endmacro %} {% macro post_media(upload_ids) -%} {% if upload_ids|length > 0%}") +(div + ("class" "media_gallery gap-2") + (text "{% for upload in upload_ids %}") + (img + ("src" "/api/v1/uploads/{{ upload }}") + ("alt" "Image upload") + ("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload }}'])")) + (text "{% endfor %}")) + +(text "{%- endif %} {%- endmacro %} {% macro notification(notification) -%}") +(div + ("class" "w-full card-nest") + (div + ("class" "card small notif_title flex items-center") + (text "{% if not notification.read -%}") + (svg + ("width" "24") + ("height" "24") + ("viewBox" "0 0 24 24") + ("style" "fill: var(--color-link)") + (circle + ("cx" "12") + ("cy" "12") + ("r" "6"))) + (text "{%- endif %}") + (b + ("class" "no_p_margin") + (text "{{ notification.title|markdown|safe }}"))) + (div + ("class" "card notif_content flex flex-col gap-2") + (span + ("class" "no_p_margin") + (text "{{ notification.content|markdown|safe }}")) + (div + ("class" "card secondary w-full flex flex-wrap gap-2") + (text "{% if notification.read -%}") + (button + ("class" "tertiary") + ("onclick" "trigger('me::update_notification_read_status', ['{{ notification.id }}', false])") + (text "{{ icon \"undo\" }}") + (span + (text "{{ text \"notifs:action.mark_as_unread\" }}"))) + (text "{% else %}") + (button + ("class" "green tertiary") + ("onclick" "trigger('me::update_notification_read_status', ['{{ notification.id }}', true])") + (text "{{ icon \"check\" }}") + (span + (text "{{ text \"notifs:action.mark_as_read\" }}"))) + (text "{%- endif %}") + (button + ("class" "red tertiary") + ("onclick" "trigger('me::remove_notification', ['{{ notification.id }}'])") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}")))))) + +(text "{%- endmacro %} {% macro user_card(user) -%}") +(a + ("class" "card-nest w-full") + ("href" "/@{{ user.username }}") + (div + ("class" "card small") + ("style" "padding: 0") + (text "{{ self::banner(username=user.username, border_radius=\"0px\") }}")) + (div + ("class" "card secondary flex items-center gap-4") + (text "{{ self::avatar(username=user.username, size=\"48px\") }}") + (div + ("class" "flex items-center") + (b + (text "{{ self::username(user=user) }}")) + (text "{{ self::online_indicator(user=user) }}")))) + +(text "{%- endmacro %} {% macro pagination(page=0, items=0, key=\"\", value=\"\") -%}") +(div + ("class" "flex justify-between gap-2 w-full") + (text "{% if page > 0 -%}") + (a + ("class" "button quaternary") + ("href" "?page={{ page - 1 }}{{ key }}{{ value }}") + (text "{{ icon \"arrow-left\" }}") + (span + (text "{{ text \"general:link.previous\" }}"))) + (text "{% else %}") + (div) + (text "{%- endif %} {% if items != 0 -%}") + (a + ("class" "button quaternary") + ("href" "?page={{ page + 1 }}{{ key }}{{ value }}") + (span + (text "{{ text \"general:link.next\" }}")) + (text "{{ icon \"arrow-right\" }}")) + (text "{%- endif %}")) + +(text "{%- endmacro %} {% macro online_indicator(user) -%} {% if not user.settings.private_last_seen or is_helper %}") +(div + ("class" "online_indicator") + ("style" "display: contents") + ("hook" "online_indicator") + ("hook-arg:last_seen" "{{ user.last_seen }}") + (div + ("style" "display: none") + ("hook_ui_ident" "online") + ("title" "Online") + (svg + ("width" "24") + ("height" "24") + ("viewBox" "0 0 24 24") + ("style" "fill: var(--color-green)") + (circle + ("cx" "12") + ("cy" "12") + ("r" "6")))) + (div + ("style" "display: none") + ("hook_ui_ident" "idle") + ("title" "Idle") + (svg + ("width" "24") + ("height" "24") + ("viewBox" "0 0 24 24") + ("style" "fill: var(--color-yellow)") + (circle + ("cx" "12") + ("cy" "12") + ("r" "6")))) + (div + ("style" "display: none") + ("hook_ui_ident" "offline") + ("title" "Offline") + (svg + ("width" "24") + ("height" "24") + ("viewBox" "0 0 24 24") + ("style" "fill: hsl(0, 0%, 50%)") + (circle + ("cx" "12") + ("cy" "12") + ("r" "6"))))) + +(text "{% else %}") +(div + ("title" "Offline") + ("style" "display: contents") + (svg + ("width" "24") + ("height" "24") + ("viewBox" "0 0 24 24") + ("style" "fill: hsl(0, 0%, 50%)") + (circle + ("cx" "12") + ("cy" "12") + ("r" "6")))) + +(text "{%- endif %} {%- endmacro %} {% macro theme(user, theme_preference) -%} {% if user %} {% if user.settings.theme_hue -%}") +(style + (text ":root, * { + --hue: {{ user.settings.theme_hue }} !important; + }")) + +(text "{%- endif %} {% if user.settings.theme_sat -%}") +(style + (text ":root, * { + --sat: {{ user.settings.theme_sat }} !important; + }")) + +(text "{%- endif %} {% if user.settings.theme_lit -%}") +(style + (text ":root, * { + --lit: {{ user.settings.theme_lit }} !important; + }")) + +(text "{%- endif %} {% if theme_preference -%}") +(script + (text "function match_user_theme() { + const pref = \"{{ theme_preference }}\".toLowerCase(); + + if (pref === \"auto\") { + return; + } + + document.documentElement.className = pref; + } + + setTimeout(() => { + match_user_theme(); + }, 150);")) + +(text "{%- endif %}") +(div + ("style" "display: none;") + (text "{{ self::theme_color(color=user.settings.theme_color_surface, css=\"color-surface\") }} {{ self::theme_color(color=user.settings.theme_color_text, css=\"color-text\") }} {{ self::theme_color(color=user.settings.theme_color_text_link, css=\"color-link\") }} {{ self::theme_color(color=user.settings.theme_color_lowered, css=\"color-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_text_lowered, css=\"color-text-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_super_lowered, css=\"color-super-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_raised, css=\"color-raised\") }} {{ self::theme_color(color=user.settings.theme_color_text_raised, css=\"color-text-raised\") }} {{ self::theme_color(color=user.settings.theme_color_super_raised, css=\"color-super-raised\") }} {{ self::theme_color(color=user.settings.theme_color_primary, css=\"color-primary\") }} {{ self::theme_color(color=user.settings.theme_color_text_primary, css=\"color-text-primary\") }} {{ self::theme_color(color=user.settings.theme_color_primary_lowered, css=\"color-primary-lowered\") }} {{ self::theme_color(color=user.settings.theme_color_secondary, css=\"color-secondary\") }} {{ self::theme_color(color=user.settings.theme_color_text_secondary, css=\"color-text-secondary\") }} {{ self::theme_color(color=user.settings.theme_color_secondary_lowered, css=\"color-secondary-lowered\") }} {% if user.permissions|has_supporter -%}") + (style + (text "{{ user.settings.theme_custom_css }}")) + (text "{%- endif %}")) + +(text "{%- endif %} {%- endmacro %} {% macro theme_color(color, css) -%} {% if color -%}") +(style + (text ":root, + * { + --{{ css }}: {{ color|color }} !important; + }")) + +(text "{%- endif %} {%- endmacro %} {% macro question(question, owner, show_community=true, secondary=false, profile=false) -%}") +(div + ("class" "card{% if secondary -%} secondary{%- endif %} flex gap-2") + (text "{% if owner.id == 0 -%}") + (span + (text "{% if profile and profile.settings.anonymous_avatar_url -%}") + (img + ("src" "/api/v1/util/proxy?url={{ profile.settings.anonymous_avatar_url }}") + ("alt" "anonymous' avatar") + ("class" "avatar shadow") + ("loading" "lazy") + ("style" "--size: 52px")) + (text "{% else %} {{ self::avatar(username=owner.username, selector_type=\"username\", size=\"52px\") }} {%- endif %}")) + (text "{% else %}") + (a + ("href" "/@{{ owner.username }}") + (text "{{ self::avatar(username=owner.username, selector_type=\"username\", size=\"52px\") }}")) + (text "{%- endif %}") + (div + ("class" "flex flex-col gap-1") + (div + ("class" "flex items-center gap-2 flex-wrap") + (span + ("class" "name") + (text "{% if owner.id == 0 -%} {% if profile and profile.settings.anonymous_username -%}") + (span + ("class" "flex items-center gap-2") + (b + (text "{{ profile.settings.anonymous_username }}")) + (span + ("title" "Anonymous user") + ("class" "flex items-center") + ("style" "color: var(--color-primary)") + (text "{{ icon \"drama\" }}"))) + (text "{% else %}") + (b + (text "anonymous")) + (text "{%- endif %} {% else %} {{ self::full_username(user=owner) }} {%- endif %}")) + (span + ("class" "date") + (text "{{ question.created }}")) + (span + ("title" "Question") + ("class" "flex items-center") + ("style" "color: var(--color-primary)") + (text "{{ icon \"message-circle-heart\" }}")) + (text "{% if question.context.is_nsfw -%}") + (span + ("title" "NSFW community") + ("class" "flex items-center") + ("style" "color: var(--color-primary)") + (text "{{ icon \"square-asterisk\" }}")) + (text "{%- endif %} {% if question.community > 0 and show_community -%}") + (a + ("href" "/api/v1/communities/find/{{ question.community }}") + ("class" "flex items-center") + (text "{{ self::community_avatar(id=question.community, size=\"24px\") }}")) + (text "{%- endif %} {% if question.is_global -%}") + (a + ("class" "notification chip") + ("href" "/question/{{ question.id }}") + (text "{{ question.answer_count }} answers")) + (text "{%- endif %}")) + (span + ("class" "no_p_margin") + ("style" "font-weight: 500") + (text "{{ question.content|markdown|safe }}")) + (div + ("class" "flex gap-2 items-center justify-between")))) + +(text "{%- endmacro %} {% macro create_question_form(receiver=\"0\", community=\"\", header=\"\", is_global=false) -%}") +(div + ("class" "card-nest") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"message-circle-heart\" }}") + (span + ("class" "no_p_margin") + (text "{% if header -%} {{ header|markdown|safe }} {% else %} {{ text \"requests:label.ask_question\" }} {%- endif %}"))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "create_question_from_form(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "content") + (text "{{ text \"communities:label.content\" }}")) + (textarea + ("type" "text") + ("name" "content") + ("id" "content") + ("placeholder" "content") + ("required" "") + ("minlength" "2") + ("maxlength" "4096"))) + (button + ("class" "primary") + (text "{{ text \"communities:action.create\" }}")))) + +(script + (text "async function create_question_from_form(e) { + e.preventDefault(); + await trigger(\"atto::debounce\", [\"questions::create\"]); + fetch(\"/api/v1/questions\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + content: e.target.content.value, + receiver: \"{{ receiver }}\", + community: \"{{ community }}\", + is_global: \"{{ is_global }}\" == \"true\", + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + e.target.reset(); + } + }); + }")) + +(text "{%- endmacro %} {% macro global_question(question, can_manage_questions=false, secondary=false, show_community=true) -%}") +(div + ("class" "card-nest") + (text "{{ self::question(question=question[0], owner=question[1], show_community=show_community) }}") + (div + ("class" "small card flex justify-between flex-wrap gap-2{% if secondary -%} secondary{%- endif %}") + (div + ("class" "flex gap-1 reactions_box") + ("hook" "check_reactions") + ("hook-arg:id" "{{ question[0].id }}") + (text "{{ self::likes(id=question[0].id, asset_type=\"Question\", likes=question[0].likes, dislikes=question[0].dislikes, secondary=false) }}")) + (div + ("class" "flex gap-1 buttons_box") + (a + ("href" "/question/{{ question[0].id }}") + ("class" "button small") + (text "{{ icon \"external-link\" }} {% if user -%}") + (span + (text "{{ text \"requests:label.answer\" }}")) + (text "{% else %}") + (span + (text "{{ text \"general:action.open\" }}")) + (text "{%- endif %}")) + (text "{% if user -%} {% if can_manage_questions or is_helper or question[1].id == user.id %}") + (div + ("class" "dropdown") + (button + ("class" "camo small") + ("onclick" "trigger('atto::hooks::dropdown', [event])") + ("exclude" "dropdown") + (text "{{ icon \"ellipsis\" }}")) + (div + ("class" "inner") + (button + ("class" "camo small red") + ("onclick" "trigger('me::remove_question', ['{{ question[0].id }}'])") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}"))))) + (text "{%- endif %} {%- endif %}")))) + +(text "{%- endmacro %} {% macro spotify_playing(state, size=\"60px\") -%} {% if state and state.data %}") +(div + ("class" "card-nest") + (div + ("class" "card flex items-center justify-between gap-2 small") + (div + ("class" "flex items-center gap-2") + (b + (text "Listening on")) + (text "{{ icon \"spotify\" }}")) + (span + ("class" "fade date short") + (text "{{ state.data.timestamp }}"))) + (div + ("class" "card secondary flex gap-2") + (a + ("href" "{{ state.external_urls.album }}") + (img + ("src" "{{ state.external_urls.album_img }}") + ("alt" "Album cover") + ("loading" "lazy") + ("class" "avatar") + ("style" "--size: {{ size }}"))) + (div + ("class" "flex flex-col") + (h5 + ("class" "w-full") + (a + ("href" "{{ state.external_urls.track }}") + ("class" "flush") + (text "{{ state.data.track }}"))) + (span + ("class" "fade") + (a + ("href" "{{ state.external_urls.artist }}") + ("class" "flush") + (text "{{ state.data.artist }}"))) + (span + ("hook" "spotify_time_text") + ("hook-arg:updated" "{{ state.data.timestamp }}") + ("hook-arg:progress" "{{ state.data.progress_ms }}") + ("hook-arg:duration" "{{ state.data.duration_ms }}") + ("hook-arg:display" "full"))))) + +(text "{%- endif %} {%- endmacro %} {% macro last_fm_playing(state, size=\"60px\") -%} {% if state and state.data %}") +(div + ("class" "card-nest") + (div + ("class" "card flex items-center justify-between gap-2 small") + (div + ("class" "flex items-center gap-2") + (b + (text "Listening on")) + (text "{{ icon \"last_fm\" }}")) + (span + ("class" "fade date short") + (text "{{ state.data.timestamp }}"))) + (div + ("class" "card secondary flex gap-2") + (a + ("href" "{{ state.external_urls.track }}") + (img + ("src" "{{ state.external_urls.track_img }}") + ("alt" "Track cover") + ("loading" "lazy") + ("class" "avatar") + ("style" "--size: {{ size }}"))) + (div + ("class" "flex flex-col") + (h5 + ("class" "w-full") + (a + ("href" "{{ state.external_urls.track }}") + ("class" "flush") + (text "{{ state.data.track }}"))) + (span + ("class" "fade") + (a + ("href" "{{ state.external_urls.artist }}") + ("class" "flush") + (text "{{ state.data.artist }}"))) + (text "{% if state.data.duration_ms and state.data.duration_ms != \"0\" -%}") + (span + ("hook" "spotify_time_text") + ("hook-arg:updated" "{{ state.data.timestamp }}") + ("hook-arg:progress" "25000") + ("hook-arg:duration" "{{ state.data.duration_ms }}") + ("hook-arg:display" "full")) + (text "{%- endif %}")))) + +(text "{%- endif %} {%- endmacro %} {% macro connection_icon(key) -%}") +(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") + (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(username=user.username, 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") + (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") + (span + ("class" "no_p_margin") + (text "{{ message.content|markdown|safe }}")) + (text "{% if grouped -%}") + (div + ("class" "hidden") + (text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}")) + (text "{%- endif %}")))) + +(text "{%- endmacro %} {% macro user_menu() -%}") +(div + ("class" "inner") + (b + ("class" "title") + (text "{{ user.username }}")) + (a + ("href" "/@{{ user.username }}") + (text "{{ icon \"circle-user-round\" }}") + (span + (text "{{ text \"auth:link.my_profile\" }}"))) + (a + ("href" "/settings") + (text "{{ icon \"settings\" }}") + (span + (text "{{ text \"auth:link.settings\" }}"))) + (text "{% if is_helper -%}") + (b + ("class" "title") + (text "{{ text \"general:label.mod\" }}")) + (a + ("href" "/mod_panel/audit_log") + (text "{{ icon \"scroll-text\" }}") + (span + (text "{{ text \"general:link.audit_log\" }}"))) + (a + ("href" "/mod_panel/reports") + (text "{{ icon \"flag\" }}") + (span + (text "{{ text \"general:link.reports\" }}"))) + (a + ("href" "/mod_panel/ip_bans") + (text "{{ icon \"ban\" }}") + (span + (text "{{ text \"general:link.ip_bans\" }}"))) + (a + ("href" "/mod_panel/stats") + (text "{{ icon \"chart-line\" }}") + (span + (text "{{ text \"general:link.stats\" }}"))) + (text "{%- endif %}") + (b + ("class" "title") + (text "{{ config.name }}")) + (a + ("href" "https://trisua.com/t/tetratto") + (text "{{ icon \"code\" }}") + (span + (text "{{ text \"general:link.source_code\" }}"))) + ; + ; {{ icon "book" }} + ; {{ text "general:link.reference" }} + ; + (div + ("class" "title")) + (button + ("onclick" "trigger('me::switch_account')") + (text "{{ icon \"ellipsis\" }}") + (span + (text "{{ text \"general:action.switch_account\" }}"))) + (button + ("class" "red") + ("onclick" "trigger('me::logout')") + (text "{{ icon \"log-out\" }}") + (span + (text "{{ text \"auth:action.logout\" }}")))) + +(text "{%- endmacro %} {% macro user_status(other_user) -%} {% if other_user.settings.status %}") +(div + ("class" "flex items-center gap-2") + (span + (text "{{ other_user.settings.status }}")) + ; connection icon + (text "{% if (other_user.connections.LastFm[1].data and other_user.connections.LastFm[1].data.track) or (other_user.connections.Spotify[1].data and other_user.connections.Spotify[1].data.track) %} {{ icon \"music\" }} {% endif %}")) + +(text "{% elif other_user.connections.LastFm[0].data.name and other_user.connections.LastFm[1].data and other_user.connections.LastFm[1].data.track %}") +(div + ("class" "flex items-center gap-2") + (text "{{ icon \"music\" }}") + (span + (b + (text "Listening to")) + (text "{{ other_user.connections.LastFm[1].data.artist }}"))) + +(text "{% elif other_user.connections.Spotify[0].data.name and other_user.connections.Spotify[1].data and other_user.connections.Spotify[1].data.track %}") +(div + ("class" "flex items-center gap-2") + (text "{{ icon \"music\" }}") + (span + (b + (text "Listening to")) + (text "{{ other_user.connections.Spotify[1].data.artist }}"))) + +(text "{%- endif %} {%- endmacro %} {% macro user_plate(user, show_menu=false, show_kick=false, secondary=false) -%}") +(div + ("class" "flex gap-2 items-center card tiny user_plate {% if secondary -%}secondary{%- endif %}") + (a + ("href" "/@{{ user.username }}") + (text "{{ self::avatar(username=user.username, size=\"42px\", selector_type=\"username\") }}")) + (div + ("class" "flex justify-center flex-col") + ("style" "{% if show_menu or show_kick -%}width: 60%{% else %}max-width: 150px{%- endif %}") + (text "{{ self::full_username(user=user) }}") + (div + ("class" "user_status") + (text "{{ self::user_status(other_user=user) }}"))) + (text "{% if show_menu -%}") + (div + ("class" "dropdown") + (button + ("class" "camo small square") + ("onclick" "trigger('atto::hooks::dropdown', [event])") + ("exclude" "dropdown") + (text "{{ icon \"settings\" }}")) + (text "{{ self::user_menu() }}")) + (text "{% elif show_kick %}") + (div + ("class" "dropdown") + ("style" "margin-left: auto") + (button + ("class" "camo small square") + ("onclick" "trigger('atto::hooks::dropdown', [event])") + ("exclude" "dropdown") + (text "{{ icon \"ellipsis\" }}")) + (div + ("class" "inner") + (button + ("class" "red") + ("onclick" "kick_member('{{ channel.id }}', '{{ user.id }}')") + (text "{{ icon \"x\" }}") + (span + (text "{{ text \"chats:action.kick_member\" }}"))))) + (text "{%- endif %}")) + +(text "{%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false) -%}") +(button + ("class" "button small square quaternary") + ("onclick" "window.EMOJI_PICKER_TEXT_ID = '{{ element_id }}'; document.getElementById('emoji_dialog').showModal()") + ("title" "Emojis") + ("type" "button") + (text "{{ icon \"smile-plus\" }}")) + +(text "{% if render_dialog -%}") +(dialog + ("id" "emoji_dialog") + (div + ("class" "inner flex flex-col gap-2") + (script + ("type" "module") + ("src" "https://unpkg.com/emoji-picker-element@1.22.8/index.js")) + (emoji-picker + ("style" " + --border-radius: var(--radius); + --background: var(--color-super-raised); + --input-border-radiFus: var(--radius); + --input-border-color: var(--color-primary); + --indicator-color: var(--color-primary); + --emoji-padding: 0.25rem; + box-shadow: 0 0 4px var(--color-shadow); + ") + ("class" "w-full")) + (script + (text "setTimeout(async () => { + document.querySelector(\"emoji-picker\").customEmoji = + await trigger(\"me::emojis\"); + + const style = document.createElement(\"style\"); + style.textContent = `.custom-emoji { border-radius: 4px !important; } .category { font-weight: 600; }`; + document + .querySelector(\"emoji-picker\") + .shadowRoot.appendChild(style); + }, 150); + + document + .querySelector(\"emoji-picker\") + .addEventListener(\"emoji-click\", async (event) => { + if (event.detail.skinTone > 0) { + document.getElementById( + window.EMOJI_PICKER_TEXT_ID, + ).value += event.detail.unicode; + + document.getElementById(\"emoji_dialog\").close(); + return; + } + + if (event.detail.unicode) { + document.getElementById( + window.EMOJI_PICKER_TEXT_ID, + ).value += ` :${await ( + await fetch(\"/api/v1/lookup_emoji\", { + method: \"POST\", + body: event.detail.unicode, + }) + ).text()}:`; + } else { + document.getElementById( + window.EMOJI_PICKER_TEXT_ID, + ).value += ` :${event.detail.emoji.shortcodes[0]}:`; + } + + document.getElementById(\"emoji_dialog\").close(); + });")) + (div + ("class" "flex justify-between") + (div) + (div + ("class" "flex gap-2") + (button + ("class" "bold red quaternary") + ("onclick" "document.getElementById('emoji_dialog').close()") + ("type" "button") + (text "{{ icon \"x\" }} {{ text \"dialog:action.close\" }}")))))) + +(text "{%- endif %} {%- endmacro %} {% macro file_picker(files_list_id) -%}") +(button + ("class" "button small square quaternary") + ("onclick" "pick_file()") + ("title" "Images") + ("type" "button") + (text "{{ icon \"image-up\" }}")) + +(input + ("type" "file") + ("multiple" "") + ("accept" "image/png,image/jpeg,image/avif,image/webp") + ("style" "display: none") + ("name" "file_picker")) + +(div + ("style" "display: none") + ("id" "file_template") + (text "{{ icon \"image\" }}") + (b + ("class" "name shorter") + ("style" "overflow-wrap: normal") + (text ".file_name"))) + +(script + (text "(() => { + const input = document.querySelector(\"input[name=file_picker]\"); + const element = document.getElementById(\"{{ files_list_id }}\"); + const template = document.getElementById(\"file_template\"); + + globalThis.pick_file = () => { + input.click(); + }; + + globalThis.render_file_picker_files = () => { + element.innerHTML = \"\"; + + let idx = 0; + for (const file of input.files) { + element.innerHTML += `
${template.innerHTML.replace( + \".file_name\", + file.name, + )}
`; + + idx += 1; + } + }; + + globalThis.remove_file = (idx) => { + const files = Array.from(input.files); + files.splice(idx - 1, 1); + + // update files + const list = new DataTransfer(); + + for (item of files) { + list.items.add(item); + } + + input.files = list.files; + + // render + render_file_picker_files(); + }; + + input.addEventListener(\"change\", () => { + render_file_picker_files(); + }); + })();")) + +(text "{%- endmacro %} {% macro supporter_ad(body=\"\") -%} {% if config.stripe and not is_supporter %}") +(div + ("class" "card w-full supporter_ad") + ("ui_ident" "supporter_ad") + ("onclick" "window.location.href = '/settings#/account/billing'") + (div + ("class" "card w-full flex flex-wrap items-center gap-2 justify-between") + (text "{% if body -%}") + (b + (text "{{ body }}")) + (text "{% else %}") + (b + (text "{{ text \"general:label.supporter_motivation\" }}")) + (text "{%- endif %}") + (a + ("href" "/settings#/account/billing") + ("class" "button small") + (text "{{ icon \"heart\" }}") + (span + (text "{{ text \"general:action.become_supporter\" }}"))))) + +(text "{%- endif %} {%- endmacro %} {% macro create_post_options() -%}") +(div + ("class" "flex gap-2") + (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 %}") + (button + ("class" "small square quaternary") + ("title" "More options") + ("onclick" "document.getElementById('post_options_dialog').showModal()") + ("type" "button") + (text "{{ icon \"ellipsis\" }}"))) + +(dialog + ("id" "post_options_dialog") + (div + ("class" "inner flex flex-col gap-2") + (div + ("id" "post_options") + ("class" "flex flex-col gap-2")) + (hr) + (div + ("class" "flex justify-between") + (div) + (div + ("class" "flex gap-2") + (button + ("class" "bold red quaternary") + ("onclick" "document.getElementById('post_options_dialog').close()") + ("type" "button") + (text "{{ icon \"x\" }} {{ text \"dialog:action.close\" }}")))) + (script + (text "setTimeout(() => { + window.POST_INITIAL_SETTINGS = { + comments_enabled: true, + reposts_enabled: true, + reactions_enabled: true, + is_nsfw: false, + content_warning: \"\", + tags: [], + }; + + window.BLANK_INITIAL_SETTINGS = JSON.stringify( + window.POST_INITIAL_SETTINGS, + ); + + const settings_fields = [ + [ + [ + \"comments_enabled\", + \"Allow people to comment on your post\", + ], + window.POST_INITIAL_SETTINGS.comments_enabled.toString(), + \"checkbox\", + ], + [ + [ + \"reposts_enabled\", + \"Allow people to repost/quote your post\", + ], + window.POST_INITIAL_SETTINGS.reposts_enabled.toString(), + \"checkbox\", + ], + [ + [ + \"reactions_enabled\", + \"Allow people to like/dislike your post\", + ], + window.POST_INITIAL_SETTINGS.reactions_enabled.toString(), + \"checkbox\", + ], + [ + [\"is_nsfw\", \"Hide from public timelines\"], + window.POST_INITIAL_SETTINGS.is_nsfw.toString(), + \"checkbox\", + ], + [ + [\"content_warning\", \"Content warning\"], + window.POST_INITIAL_SETTINGS.content_warning, + \"textarea\", + ], + [ + [\"tags\", \"Tags\"], + window.POST_INITIAL_SETTINGS.tags, + \"input\", + { + embed_html: + 'Tags should be separated by a comma.', + }, + ], + ]; + + document.getElementById(\"post_options\").innerHTML = \"\"; + trigger(\"ui::generate_settings_ui\", [ + document.getElementById(\"post_options\"), + settings_fields, + window.POST_INITIAL_SETTINGS, + { + tags: (new_tags) => { + window.POST_INITIAL_SETTINGS.tags = new_tags + .split(\",\") + .map((t) => t.trim()); + }, + }, + ]); + }, 250); + + globalThis.update_settings_maybe = async (id) => { + if ( + JSON.stringify(window.POST_INITIAL_SETTINGS) !== + window.BLANK_INITIAL_SETTINGS + ) { + await fetch(`/api/v1/posts/${id}/context`, { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + context: window.POST_INITIAL_SETTINGS, + }), + }); + } + };")))) + +(text "{%- endmacro %}") diff --git a/crates/app/src/public/html/macros.lisp b/crates/app/src/public/html/macros.lisp index 41f80e8..858ad66 100644 --- a/crates/app/src/public/html/macros.lisp +++ b/crates/app/src/public/html/macros.lisp @@ -130,7 +130,7 @@ (a ("href" "/") ("class" "{% if selected == 'all' -%}active{%- endif %}") - (icon (text "earch")) + (icon (text "earth")) (str (text "general:link.all"))) (text "{%- endif %}") @@ -198,7 +198,6 @@ (text "{% macro profile_nav(selected=\"\") -%}") (div ("class" "pillmenu") - (a ("href" "/@{{ profile.username }}") ("class" "{% if selected == 'posts' -%}active{%- endif %}") diff --git a/crates/app/src/public/html/misc/notifications.html b/crates/app/src/public/html/misc/notifications.html deleted file mode 100644 index 27caa27..0000000 --- a/crates/app/src/public/html/misc/notifications.html +++ /dev/null @@ -1,86 +0,0 @@ -{% extends "root.html" %} {% block head %} -Notifications - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav(selected="notifications") }} -
-
-
- - {{ icon "bell" }} - {{ text "notifs:label.notifications" }} - - -
- - - -
-
- -
- {% for notification in notifications %} {{ - components::notification(notification=notification) }} {% endfor %} -
-
-
- - -{% endblock %} diff --git a/crates/app/src/public/html/misc/notifications.lisp b/crates/app/src/public/html/misc/notifications.lisp new file mode 100644 index 0000000..dd08d58 --- /dev/null +++ b/crates/app/src/public/html/misc/notifications.lisp @@ -0,0 +1,76 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Notifications - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"notifications\") }}") +(main + ("class" "flex flex-col gap-2") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center justify-between gap-2") + (span + ("class" "flex items-center gap-2") + (text "{{ icon \"bell\" }}") + (span + (text "{{ text \"notifs:label.notifications\" }}"))) + (div + ("class" "flex gap-2") + (button + ("onclick" "trigger('me::clear_notifs')") + ("class" "small red quaternary") + (text "{{ icon \"bomb\" }}") + (span + (text "{{ text \"notifs:action.clear\" }}"))) + (div + ("class" "dropdown") + (button + ("class" "small quaternary") + ("onclick" "trigger('atto::hooks::dropdown', [event])") + ("exclude" "dropdown") + (text "{{ icon \"ellipsis\" }}")) + (div + ("class" "inner") + (button + ("onclick" "mark_all_as_read(true)") + (text "{{ icon \"bookmark-check\" }}") + (span + (text "{{ text \"notifs:label.mark_all_as_read\" }}"))) + (button + ("onclick" "mark_all_as_read(false)") + (text "{{ icon \"bookmark-x\" }}") + (span + (text "{{ text \"notifs:label.mark_all_as_unread\" }}"))))))) + (div + ("class" "card tertiary flex flex-col gap-4") + (text "{% for notification in notifications %} {{ components::notification(notification=notification) }} {% endfor %}")))) + +(script + (text "async function mark_all_as_read(read) { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you want to do this?\", + ])) + ) { + return; + } + + fetch(\"/api/v1/notifications/all/read_status\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + read, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/misc/requests.html b/crates/app/src/public/html/misc/requests.html deleted file mode 100644 index 2d0b77e..0000000 --- a/crates/app/src/public/html/misc/requests.html +++ /dev/null @@ -1,259 +0,0 @@ -{% extends "root.html" %} {% block head %} -Requests - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav(selected="requests") }} -
-
-
- - {{ icon "inbox" }} - {{ text "requests:label.requests" }} - - - -
- -
- {% for request in requests %} {% if request.action_type == - "CommunityJoin" %} -
-
- {{ icon "user-plus" }} - {{ text "requests:label.community_join_request" - }} -
- -
- - {{ icon "external-link" }} - {{ text "requests:label.review" }} - - - -
-
- {% elif request.action_type == "Follow" %} -
-
- {{ icon "user-plus" }} - {{ text "requests:label.user_follow_request" }} -
- -
- - {{ text "requests:label.user_follow_request_message" }} - - -
- - {{ icon "external-link" }} - {{ text "requests:action.view_profile" }} - - - - - -
-
-
- {%- endif %} {% endfor %} {% for question in questions %} - -
- {{ components::question(question=question[0], owner=question[1], profile=user) }} - -
-
- - -
- -
- -
- {{ components::create_post_options() }} - - - - -
-
-
- {% endfor %} -
-
-
- - -{% endblock %} diff --git a/crates/app/src/public/html/misc/requests.lisp b/crates/app/src/public/html/misc/requests.lisp new file mode 100644 index 0000000..4b97b53 --- /dev/null +++ b/crates/app/src/public/html/misc/requests.lisp @@ -0,0 +1,252 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Requests - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"requests\") }}") +(main + ("class" "flex flex-col gap-2") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center justify-between gap-2") + (span + ("class" "flex items-center gap-2") + (text "{{ icon \"inbox\" }}") + (span + (text "{{ text \"requests:label.requests\" }}"))) + (button + ("onclick" "clear_requests()") + ("class" "small red quaternary") + (text "{{ icon \"bomb\" }}") + (span + (text "{{ text \"notifs:action.clear\" }}")))) + (div + ("class" "card tertiary flex flex-col gap-4") + (text "{% for request in requests %} {% if request.action_type == \"CommunityJoin\" %}") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"user-plus\" }}") + (span + (text "{{ text \"requests:label.community_join_request\" }}"))) + (div + ("class" "card flex flex-wrap gap-2") + (a + ("href" "/community/{{ request.linked_asset }}/manage?uid={{ request.id }}#/members") + ("class" "button") + (text "{{ icon \"external-link\" }}") + (span + (text "{{ text \"requests:label.review\" }}"))) + (button + ("class" "quaternary red") + ("onclick" "remove_request('{{ request.id }}', '{{ request.linked_asset }}')") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}"))))) + (text "{% elif request.action_type == \"Follow\" %}") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"user-plus\" }}") + (span + (text "{{ text \"requests:label.user_follow_request\" }}"))) + (div + ("class" "card flex flex-col gap-2") + (span + (text "{{ text \"requests:label.user_follow_request_message\" }}")) + (div + ("class" "card flex w-full secondary gap-2") + (a + ("href" "/api/v1/auth/user/find/{{ request.id }}") + ("class" "button") + (text "{{ icon \"external-link\" }}") + (span + (text "{{ text \"requests:action.view_profile\" }}"))) + (button + ("class" "quaternary green") + ("onclick" "accept_follow_request(event, '{{ request.id }}')") + (text "{{ icon \"check\" }}") + (span + (text "{{ text \"general:action.accept\" }}"))) + (button + ("class" "quaternary red") + ("onclick" "remove_request('{{ request.id }}', '{{ request.linked_asset }}')") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}")))))) + (text "{%- endif %} {% endfor %} {% for question in questions %}") + (div + ("class" "card-nest") + (text "{{ components::question(question=question[0], owner=question[1], profile=user) }}") + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "answer_question_from_form(event, '{{ question[0].id }}')") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "content") + (text "{{ text \"communities:label.content\" }}")) + (textarea + ("type" "text") + ("name" "content") + ("id" "content") + ("placeholder" "content") + ("required" "") + ("minlength" "2") + ("maxlength" "4096"))) + (div + ("id" "files_list") + ("class" "flex gap-2 flex-wrap")) + (div + ("class" "flex flex-wrap w-full gap-2") + (text "{{ components::create_post_options() }}") + (button + ("class" "primary") + (text "{{ text \"requests:label.answer\" }}")) + (button + ("type" "button") + ("class" "red quaternary") + ("onclick" "trigger('me::remove_question', ['{{ question[0].id }}'])") + (text "{{ text \"general:action.delete\" }}")) + (button + ("type" "button") + ("class" "red quaternary") + ("onclick" "trigger('me::ip_block_question', ['{{ question[0].id }}'])") + (text "{{ text \"auth:action.ip_block\" }}"))))) + (text "{% endfor %}")))) + +(script + (text "async function remove_request(id, linked_asset) { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you want to do this?\", + ])) + ) { + return; + } + + fetch(`/api/v1/requests/${id}/${linked_asset}`, { + method: \"DELETE\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + } + + async function clear_requests() { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you want to do this?\", + ])) + ) { + return; + } + + fetch(\"/api/v1/requests/my\", { + method: \"DELETE\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + } + + window.answer_question_from_form = async (e, answering) => { + e.preventDefault(); + await trigger(\"atto::debounce\", [\"posts::create\"]); + + // create body + const body = new FormData(); + + if (e.target.file_picker) { + for (const file of e.target.file_picker.files) { + body.append(file.name, file); + } + } + + body.append( + \"body\", + JSON.stringify({ + content: e.target.content.value, + community: \"{{ config.town_square }}\", + answering, + }), + ); + + // ... + fetch(\"/api/v1/posts\", { + method: \"POST\", + + body, + }) + .then((res) => res.json()) + .then(async (res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + // update settings + await update_settings_maybe(res.payload); + + // ... + e.target.parentElement.remove(); + } + }); + }; + + globalThis.accept_follow_request = async (e, id) => { + await trigger(\"atto::debounce\", [\"users::follow\"]); + + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + fetch(`/api/v1/auth/user/${id}/follow/accept`, { + method: \"POST\", + }) + .then((res) => res.json()) + .then(async (res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + e.target.parentElement.parentElement.parentElement.parentElement.remove(); + + if ( + await trigger(\"atto::confirm\", [ + \"Would you like to follow this user back? This will allow them to view your profile.\", + ]) + ) { + fetch(`/api/v1/auth/user/${id}/follow`, { + method: \"POST\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + } + } + }); + };")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/mod/audit_log.html b/crates/app/src/public/html/mod/audit_log.html deleted file mode 100644 index 44cc81d..0000000 --- a/crates/app/src/public/html/mod/audit_log.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends "root.html" %} {% block head %} -Audit log - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
-
-
- {{ icon "scroll" }} - {{ text "general:link.audit_log" }} -
- -
- - {% for item in items %} - - {% endfor %} - - - {{ components::pagination(page=page, items=items|length) }} -
-
-
-{% endblock %} diff --git a/crates/app/src/public/html/mod/audit_log.lisp b/crates/app/src/public/html/mod/audit_log.lisp new file mode 100644 index 0000000..7a1ec78 --- /dev/null +++ b/crates/app/src/public/html/mod/audit_log.lisp @@ -0,0 +1,37 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Audit log - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"scroll\" }}") + (span + (text "{{ text \"general:link.audit_log\" }}"))) + (div + ("class" "card flex flex-col gap-2") + (text "{% for item in items %}") + (div + ("class" "card-nest") + (a + ("class" "card small flex items-center gap-2 flush") + ("href" "/api/v1/auth/user/find/{{ item.moderator }}") + (text "{{ components::avatar(username=item.moderator, selector_type=\"id\") }}") + (span + (text "{{ item.moderator }}")) + (span + ("class" "fade date") + (text "{{ item.created }}"))) + (div + ("class" "card secondary") + (span + ("class" "no_p_margin") + (text "{{ item.content|markdown|safe }}")))) + (text "{% endfor %}") + (text "{{ components::pagination(page=page, items=items|length) }}")))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/mod/file_report.html b/crates/app/src/public/html/mod/file_report.html deleted file mode 100644 index f51df64..0000000 --- a/crates/app/src/public/html/mod/file_report.html +++ /dev/null @@ -1,65 +0,0 @@ -{% extends "root.html" %} {% block head %} -File report - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
-
-
- {{ icon "flag" }} - {{ text "general:label.file_report" }} -
- -
-
- - -
- - -
-
-
- - -{% endblock %} diff --git a/crates/app/src/public/html/mod/file_report.lisp b/crates/app/src/public/html/mod/file_report.lisp new file mode 100644 index 0000000..39891a7 --- /dev/null +++ b/crates/app/src/public/html/mod/file_report.lisp @@ -0,0 +1,63 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "File report - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"flag\" }}") + (span + (text "{{ text \"general:label.file_report\" }}"))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "create_report_from_form(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "title") + (text "{{ text \"communities:label.content\" }}")) + (textarea + ("type" "text") + ("name" "content") + ("id" "content") + ("placeholder" "content") + ("required" "") + ("minlength" "16"))) + (button + ("class" "primary") + (text "{{ text \"communities:action.create\" }}"))))) + +(script + (text "function create_report_from_form(e) { + e.preventDefault(); + fetch(\"/api/v1/reports\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + content: e.target.content.value, + asset: \"{{ asset }}\", + asset_type: `{{ asset_type }}`, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + setTimeout(() => { + window.close(); + }, 150); + } + }); + }")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/mod/ip_bans.html b/crates/app/src/public/html/mod/ip_bans.html deleted file mode 100644 index 574964a..0000000 --- a/crates/app/src/public/html/mod/ip_bans.html +++ /dev/null @@ -1,87 +0,0 @@ -{% extends "root.html" %} {% block head %} -IP Bans - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
-
-
-
- {{ icon "ban" }} - {{ text "general:link.ip_bans" }} -
- - -
- -
- - {% for item in items %} -
- - - {{ components::avatar(username=item.moderator, selector_type="id") }} - {{ item.moderator }} - {{ item.created }} - - -
- {{ item.ip }} - {{ item.reason|markdown|safe }} - -
- -
-
-
- {% endfor %} - - - {{ components::pagination(page=page, items=items|length) }} -
-
-
- - -{% endblock %} diff --git a/crates/app/src/public/html/mod/ip_bans.lisp b/crates/app/src/public/html/mod/ip_bans.lisp new file mode 100644 index 0000000..1f12ba7 --- /dev/null +++ b/crates/app/src/public/html/mod/ip_bans.lisp @@ -0,0 +1,86 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "IP Bans - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center justify-between gap-2") + (div + ("class" "flex items-center gap-2") + (text "{{ icon \"ban\" }}") + (span + (text "{{ text \"general:link.ip_bans\" }}"))) + (button + ("onclick" "prompt_ban_ip()") + ("class" "quaternary small") + (text "{{ icon \"plus\" }}") + (span + (text "{{ text \"communities:action.create\" }}")))) + (div + ("class" "card flex flex-col gap-2") + (text "{% for item in items %}") + (div + ("class" "card-nest") + (a + ("class" "card small flex items-center gap-2 flush") + ("href" "/api/v1/auth/user/find/{{ item.moderator }}") + (text "{{ components::avatar(username=item.moderator, selector_type=\"id\") }}") + (span + (text "{{ item.moderator }}")) + (span + ("class" "fade date") + (text "{{ item.created }}"))) + (div + ("class" "card secondary flex flex-col gap-2") + (code + (text "{{ item.ip }}")) + (span + (text "{{ item.reason|markdown|safe }}")) + (div + ("class" "card w-full flex flex-wrap gap-2") + (button + ("onclick" "remove_ipban('{{ item.ip }}')") + ("class" "red quaternary") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}")))))) + (text "{% endfor %}") + (text "{{ components::pagination(page=page, items=items|length) }}")))) + +(script + (text "async function prompt_ban_ip() { + const ip = await trigger(\"atto::prompt\", [\"IP address (or prefix):\"]); + + if (!ip) { + return; + } + + trigger(\"atto::ban_ip\", [ip]); + } + + async function remove_ipban(ip) { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + fetch(`/api/v1/bans/${ip}`, { + method: \"DELETE\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/mod/profile.html b/crates/app/src/public/html/mod/profile.html deleted file mode 100644 index aa2e77f..0000000 --- a/crates/app/src/public/html/mod/profile.html +++ /dev/null @@ -1,253 +0,0 @@ -{% extends "root.html" %} {% block head %} -Manage profile - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
-
-
- {{ icon "shield" }} - {{ text "mod_panel:label.manage_profile" }} -
- -
-
-
- - {{ icon "settings" }} - View settings - - - - {{ icon "shield-alert" }} - View warnings - - - - - {% if profile.permissions != 131073 -%} - - {% else %} - - {%- endif %} -
-
- - -
-
- -
-
-
- {{ icon "blocks" }} - {{ text "mod_panel:label.permissions_level_builder" - }} -
- - -
- -
-
- - -
-{% endblock %} diff --git a/crates/app/src/public/html/mod/profile.lisp b/crates/app/src/public/html/mod/profile.lisp new file mode 100644 index 0000000..c8433cd --- /dev/null +++ b/crates/app/src/public/html/mod/profile.lisp @@ -0,0 +1,237 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Manage profile - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"shield\" }}") + (span + (text "{{ text \"mod_panel:label.manage_profile\" }}"))) + (div + ("class" "card tertiary") + (div + ("class" "flex flex-col gap-2") + ("id" "mod_options") + (div + ("class" "card w-full flex flex-wrap gap-2") + ("ui_ident" "actions") + (a + ("href" "/settings?username={{ profile.username }}") + ("class" "button quaternary") + (text "{{ icon \"settings\" }}") + (span + (text "View settings"))) + (a + ("href" "/mod_panel/profile/{{ profile.id }}/warnings") + ("class" "button quaternary") + (text "{{ icon \"shield-alert\" }}") + (span + (text "View warnings"))) + (button + ("class" "red quaternary") + ("onclick" "delete_account(event)") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"settings:label.delete_account\" }}"))) + (text "{% if profile.permissions != 131073 -%}") + (button + ("class" "red quaternary") + ("onclick" "update_user_role(131073)") + (text "Ban")) + (text "{% else %}") + (button + ("class" "quaternary") + ("onclick" "update_user_role(1)") + (text "Unban")) + (text "{%- endif %}"))) + (script + (text "setTimeout(() => { + const ui = ns(\"ui\"); + const element = document.getElementById(\"mod_options\"); + + async function profile_request(do_confirm, path, body) { + if (do_confirm) { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + } + + fetch(`/api/v1/auth/user/{{ profile.id }}/${path}`, { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify(body), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + } + + globalThis.delete_account = async (e) => { + e.preventDefault(); + + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + fetch(\"/api/v1/auth/user/{{ profile.id }}\", { + method: \"DELETE\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + password: \"\", + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.update_user_role = async (new_role) => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + fetch(`/api/v1/auth/user/{{ profile.id }}/role`, { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + role: Number.parseInt(new_role), + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + ui.refresh_container(element, [\"actions\"]); + + setTimeout(() => { + ui.refresh_container(element, [\"actions\"]); + + ui.generate_settings_ui( + element, + [ + [ + [\"is_verified\", \"Is verified\"], + \"{{ profile.is_verified }}\", + \"checkbox\", + ], + [ + [\"role\", \"Permission level\"], + \"{{ profile.permissions }}\", + \"input\", + ], + ], + null, + { + is_verified: (value) => { + profile_request(false, \"verified\", { + is_verified: value, + }); + }, + role: (new_role) => { + return update_user_role(new_role); + }, + }, + ); + }, 100); + }, 150);")))) + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center justify-between gap-2") + (div + ("class" "flex items-center gap-2") + (text "{{ icon \"blocks\" }}") + (span + (text "{{ text \"mod_panel:label.permissions_level_builder\" }}"))) + (button + ("class" "small quaternary") + ("onclick" "update_user_role(Number.parseInt(document.getElementById('role').value))") + (text "{{ icon \"check\" }}") + (span + (text "{{ text \"general:action.save\" }}")))) + (div + ("class" "card tertiary flex flex-col gap-2") + ("id" "permission_builder"))) + (script + (text "setTimeout(() => { + const get_permissions_html = trigger( + \"ui::generate_permissions_ui\", + [ + { + // https://trisuaso.github.io/tetratto/tetratto/model/permissions/struct.FinePermission.html + DEFAULT: 1 << 0, + ADMINISTRATOR: 1 << 1, + MANAGE_COMMUNITIES: 1 << 2, + MANAGE_POSTS: 1 << 3, + MANAGE_POST_REPLIES: 1 << 4, + MANAGE_USERS: 1 << 5, + MANAGE_BANS: 1 << 6, + MANAGE_WARNINGS: 1 << 7, + MANAGE_NOTIFICATIONS: 1 << 8, + VIEW_REPORTS: 1 << 9, + VIEW_AUDIT_LOG: 1 << 10, + MANAGE_MEMBERSHIPS: 1 << 11, + MANAGE_REACTIONS: 1 << 12, + MANAGE_FOLLOWS: 1 << 13, + MANAGE_VERIFIED: 1 << 14, + MANAGE_AUDITLOG: 1 << 15, + MANAGE_REPORTS: 1 << 16, + BANNED: 1 << 17, + INFINITE_COMMUNITIES: 1 << 18, + SUPPORTER: 1 << 19, + MANAGE_REQUESTS: 1 << 20, + MANAGE_QUESTIONS: 1 << 21, + MANAGE_CHANNELS: 1 << 22, + MANAGE_MESSAGES: 1 << 23, + MANAGE_UPLOADS: 1 << 24, + MANAGE_EMOJIS: 1 << 25, + MANAGE_STACKS: 1 << 26, + STAFF_BADGE: 1 << 27, + }, + ], + ); + + document.getElementById(\"permission_builder\").innerHTML = + get_permissions_html( + Number.parseInt(\"{{ profile.permissions }}\"), + \"permission_builder\", + ); + }, 250);"))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/mod/reports.html b/crates/app/src/public/html/mod/reports.html deleted file mode 100644 index 8793a56..0000000 --- a/crates/app/src/public/html/mod/reports.html +++ /dev/null @@ -1,81 +0,0 @@ -{% extends "root.html" %} {% block head %} -Reports - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
-
-
- {{ icon "flag" }} - {{ text "general:link.reports" }} -
- -
- - {% for item in items %} -
- - - {{ components::avatar(username=item.owner, selector_type="id") }} - {{ item.owner }} - {{ item.created }} - - -
- {{ item.content|markdown|safe }} - -
- - - -
-
-
- {% endfor %} - - - {{ components::pagination(page=page, items=items|length) }} -
-
-
- - -{% endblock %} diff --git a/crates/app/src/public/html/mod/reports.lisp b/crates/app/src/public/html/mod/reports.lisp new file mode 100644 index 0000000..c2c0a4f --- /dev/null +++ b/crates/app/src/public/html/mod/reports.lisp @@ -0,0 +1,72 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Reports - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"flag\" }}") + (span + (text "{{ text \"general:link.reports\" }}"))) + (div + ("class" "card flex flex-col gap-2") + (text "{% for item in items %}") + (div + ("class" "card-nest") + (a + ("class" "card small flex items-center gap-2 flush") + ("href" "/api/v1/auth/user/find/{{ item.owner }}") + (text "{{ components::avatar(username=item.owner, selector_type=\"id\") }}") + (span + (text "{{ item.owner }}")) + (span + ("class" "fade date") + (text "{{ item.created }}"))) + (div + ("class" "card secondary flex flex-col gap-2") + (span + ("class" "no_p_margin") + (text "{{ item.content|markdown|safe }}")) + (div + ("class" "card w-full flex flex-wrap gap-2") + (button + ("onclick" "open_reported_content('{{ item.asset }}', '{{ item.asset_type }}')") + (text "{{ icon \"external-link\" }}") + (span + (text "{{ text \"mod_panel:label.open_reported_content\" }}"))) + (button + ("onclick" "remove_report('{{ item.id }}')") + ("class" "red quaternary") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}")))))) + (text "{% endfor %}") + (text "{{ components::pagination(page=page, items=items|length) }}")))) + +(script + (text "function open_reported_content(asset, asset_type) { + if (asset_type === \"Post\") { + window.open(`/post/${asset}`); + } else if (asset_type === \"Community\") { + window.open(`/community/${asset}`); + } + } + + function remove_report(id) { + fetch(`/api/v1/reports/${id}`, { + method: \"DELETE\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/mod/stats.html b/crates/app/src/public/html/mod/stats.html deleted file mode 100644 index 869c3fc..0000000 --- a/crates/app/src/public/html/mod/stats.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends "root.html" %} {% block head %} -Server stats - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
-
-
- {{ icon "chart-line" }} - {{ text "general:link.stats" }} -
- -
-
    -
  • - Active user streams: - {{ active_users }} -
  • - -
  • - Active chat subscriptions: - {{ active_users_chats }} -
  • - -
  • - Socket tasks: - {{ (active_users_chats + active_users) * 3 }} -
  • -
-
-
-
-{% endblock %} diff --git a/crates/app/src/public/html/mod/stats.lisp b/crates/app/src/public/html/mod/stats.lisp new file mode 100644 index 0000000..aea04e8 --- /dev/null +++ b/crates/app/src/public/html/mod/stats.lisp @@ -0,0 +1,34 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Server stats - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"chart-line\" }}") + (span + (text "{{ text \"general:link.stats\" }}"))) + (div + ("class" "card flex flex-col gap-2") + (ul + (li + (b + (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 "{% endblock %}") diff --git a/crates/app/src/public/html/mod/warnings.html b/crates/app/src/public/html/mod/warnings.html deleted file mode 100644 index 2e4aed6..0000000 --- a/crates/app/src/public/html/mod/warnings.html +++ /dev/null @@ -1,132 +0,0 @@ -{% extends "root.html" %} {% block head %} -User warnings - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
-
-
- - {{ icon "gavel" }} - {{ text "mod_panel:label.create_warning" }} - - - - {{ icon "x" }} - {{ text "dialog:action.cancel" }} - -
- -
-
- - -
- - -
-
- -
-
- - {{ icon "message-circle-warning" }} - {{ text "mod_panel:label.warnings" }} - -
- -
- {% for item in items %} -
- - -
- {{ item.content|markdown|safe }} -
-
- {% endfor %} - - - {{ components::pagination(page=page, items=items|length) }} -
-
-
- - -{% endblock %} diff --git a/crates/app/src/public/html/mod/warnings.lisp b/crates/app/src/public/html/mod/warnings.lisp new file mode 100644 index 0000000..5d66ad3 --- /dev/null +++ b/crates/app/src/public/html/mod/warnings.lisp @@ -0,0 +1,121 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "User warnings - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center justify-between gap-2") + (span + ("class" "flex items-center gap-2") + (text "{{ icon \"gavel\" }}") + (span + (text "{{ text \"mod_panel:label.create_warning\" }}"))) + (a + ("href" "/mod_panel/profile/{{ profile.id }}") + ("class" "button quaternary small red") + (text "{{ icon \"x\" }}") + (span + (text "{{ text \"dialog:action.cancel\" }}")))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "create_warning_from_form(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "content") + (text "{{ text \"communities:label.content\" }}")) + (textarea + ("type" "text") + ("name" "content") + ("id" "content") + ("placeholder" "content") + ("required" "") + ("minlength" "2") + ("maxlength" "4096"))) + (button + ("class" "primary") + (text "{{ text \"communities:action.create\" }}")))) + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center justify-between gap-2") + (span + ("class" "flex items-center gap-2") + (text "{{ icon \"message-circle-warning\" }}") + (span + (text "{{ text \"mod_panel:label.warnings\" }}")))) + (div + ("class" "card flex flex-col gap-4") + (text "{% for item in items %}") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center justify-between gap-2") + (a + ("class" "flex items-center gap-2 flush") + ("href" "/api/v1/auth/user/find/{{ item.moderator }}") + ("title" "Moderator") + (text "{{ components::avatar(username=item.moderator, selector_type=\"id\") }}") + (span + (text "{{ item.moderator }}")) + (span + ("class" "fade date") + (text "{{ item.created }}"))) + (button + ("class" "small quaternary red") + ("onclick" "remove_warning('{{ item.id }}')") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}")))) + (div + ("class" "card secondary flex flex-col gap-2") + (span + ("class" "no_p_margin") + (text "{{ item.content|markdown|safe }}")))) + (text "{% endfor %}") + (text "{{ components::pagination(page=page, items=items|length) }}")))) + +(script + (text "async function create_warning_from_form(e) { + e.preventDefault(); + await trigger(\"atto::debounce\", [\"warnings::create\"]); + fetch(\"/api/v1/warnings/{{ profile.id }}\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + content: e.target.content.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + e.target.reset(); + } + }); + } + + function remove_warning(id) { + fetch(`/api/v1/warnings/${id}`, { + method: \"DELETE\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/post/likes.html b/crates/app/src/public/html/post/likes.html deleted file mode 100644 index 18ff3e1..0000000 --- a/crates/app/src/public/html/post/likes.html +++ /dev/null @@ -1,92 +0,0 @@ -{% extends "root.html" %} {% block head %} -Post quotes - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
- {% if post.replying_to -%} - - {{ icon "arrow-up" }} - {{ text "communities:action.continue_thread" }} - - {%- endif %} - - -
- {% if post.context.repost and post.context.repost.reposting -%} - {{ components::repost(repost=reposting, post=post, owner=owner, community=community, show_community=true, can_manage_post=can_manage_posts) }} - {% else %} - {{ components::post(post=post, owner=owner, question=question, community=community, show_community=true, can_manage_post=can_manage_posts) }} - {%- endif %} -
- - - - {% if (user and user.id == post.owner) or can_manage_posts -%} - - {%- endif %} - -
-
- {{ icon "heart" }} - {{ text "communities:label.likes" }} -
- -
- - {% for tu in list %} - {% set reaction = tu[0] %} - {% set user = tu[1] %} -
- {{ components::user_plate(user=user, secondary=true) }} -
- {% endfor %} {{ components::pagination(page=page, items=list|length) - }} -
-
-
- - -{% endblock %} diff --git a/crates/app/src/public/html/post/likes.lisp b/crates/app/src/public/html/post/likes.lisp new file mode 100644 index 0000000..1e13aff --- /dev/null +++ b/crates/app/src/public/html/post/likes.lisp @@ -0,0 +1,85 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Post likes - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (text "{% if post.replying_to -%}") + (a + ("href" "/post/{{ post.replying_to }}") + ("class" "button") + (text "{{ icon \"arrow-up\" }}") + (span + (text "{{ text \"communities:action.continue_thread\" }}"))) + (text "{%- endif %}") + (div + ("style" "display: contents;") + (text "{% if post.context.repost and post.context.repost.reposting -%} {{ components::repost(repost=reposting, post=post, owner=owner, community=community, show_community=true, can_manage_post=can_manage_posts) }} {% else %} {{ components::post(post=post, owner=owner, question=question, community=community, show_community=true, can_manage_post=can_manage_posts) }} {%- endif %}")) + (div + ("class" "pillmenu") + (a + ("href" "/post/{{ post.id }}#/replies") + (text "{{ icon \"newspaper\" }}") + (span + (text "{{ text \"communities:label.replies\" }}"))) + (a + ("href" "/post/{{ post.id }}/reposts") + (text "{{ icon \"repeat-2\" }}") + (span + (text "{{ text \"communities:label.reposts\" }}"))) + (a + ("href" "/post/{{ post.id }}/reposts?quotes=true") + (text "{{ icon \"quote\" }}") + (span + (text "{{ text \"communities:label.quotes\" }}")))) + (text "{% if (user and user.id == post.owner) or can_manage_posts -%}") + (div + ("class" "pillmenu") + (text "{% if user and user.id == post.owner -%}") + (a + ("href" "/post/{{ post.id }}#/edit") + (text "{{ icon \"pen\" }}") + (span + (text "{{ text \"communities:label.edit_content\" }}"))) + (text "{%- endif %}") + (a + ("href" "/post/{{ post.id }}/likes") + ("class" "active") + (text "{{ icon \"heart\" }}") + (span + (text "{{ text \"communities:label.likes\" }}"))) + (a + ("href" "/post/{{ post.id }}#/configure") + (text "{{ icon \"settings\" }}") + (span + (text "{{ text \"communities:action.configure\" }}")))) + (text "{%- endif %}") + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"heart\" }}") + (span + (text "{{ text \"communities:label.likes\" }}"))) + (div + ("class" "card flex flex-wrap gap-4 flex-collapse") + (text "{% for tu in list %} {% set reaction = tu[0] %} {% set user = tu[1] %}") + (div + ("style" "display: contents") + ("title" "{% if reaction.is_like %}Like{% else %}Dislike{% endif %}") + (text "{{ components::user_plate(user=user, secondary=true) }}")) + (text "{% endfor %} {{ components::pagination(page=page, items=list|length) }}")))) + +(style + (text ".user_plate { + width: calc(50% - 0.5rem); + } + + @media screen and (max-width: 900px) { + .user_plate { + width: 100%; + } + }")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/post/post.html b/crates/app/src/public/html/post/post.html deleted file mode 100644 index 964bea8..0000000 --- a/crates/app/src/public/html/post/post.html +++ /dev/null @@ -1,344 +0,0 @@ -{% extends "root.html" %} {% block head %} -Post - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
- {% if post.replying_to -%} - - {{ icon "arrow-up" }} - {{ text "communities:action.continue_thread" }} - - {%- endif %} - - -
- {% if post.context.repost and post.context.repost.reposting -%} - {{ components::repost(repost=reposting, post=post, owner=owner, community=community, show_community=true, can_manage_post=can_manage_posts) }} - {% else %} - {{ components::post(post=post, owner=owner, question=question, community=community, show_community=true, can_manage_post=can_manage_posts) }} - {%- endif %} -
- - {% if user and post.context.comments_enabled -%} -
-
- {{ text "communities:label.create_reply" }} -
- -
-
- - -
- -
- -
- {{ components::emoji_picker(element_id="content", - render_dialog=true) }} {% if is_supporter -%} {{ - components::file_picker(files_list_id="files_list") }} {% endif - %} - - -
-
-
- {%- endif %} - - - {% if (user and user.id == post.owner) or can_manage_posts -%} - - {%- endif %} - - - - {% if user and user.id == post.owner -%} - - - - {%- endif %} - -
-
- {{ icon "newspaper" }} - {{ text "communities:label.replies" }} -
- -
- - {% for post in replies %} - {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false) }} - {% endfor %} - - {{ components::pagination(page=page, items=replies|length) }} -
-
-
- - -{% endblock %} diff --git a/crates/app/src/public/html/post/post.lisp b/crates/app/src/public/html/post/post.lisp new file mode 100644 index 0000000..ca0c675 --- /dev/null +++ b/crates/app/src/public/html/post/post.lisp @@ -0,0 +1,321 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Post - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (text "{% if post.replying_to -%}") + (a + ("href" "/post/{{ post.replying_to }}") + ("class" "button") + (text "{{ icon \"arrow-up\" }}") + (span + (text "{{ text \"communities:action.continue_thread\" }}"))) + (text "{%- endif %}") + (div + ("style" "display: contents;") + (text "{% if post.context.repost and post.context.repost.reposting -%} {{ components::repost(repost=reposting, post=post, owner=owner, community=community, show_community=true, can_manage_post=can_manage_posts) }} {% else %} {{ components::post(post=post, owner=owner, question=question, community=community, show_community=true, can_manage_post=can_manage_posts) }} {%- endif %}")) + (text "{% if user and post.context.comments_enabled -%}") + (div + ("class" "card-nest") + (div + ("class" "card small") + (b + (text "{{ text \"communities:label.create_reply\" }}"))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "create_reply_from_form(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "content") + (text "{{ text \"communities:label.content\" }}")) + (textarea + ("type" "text") + ("name" "content") + ("id" "content") + ("placeholder" "content") + ("required" "") + ("minlength" "2") + ("maxlength" "4096"))) + (div + ("id" "files_list") + ("class" "flex gap-2 flex-wrap")) + (div + ("class" "flex gap-2") + (text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {% endif %}") + (button + ("class" "primary") + (text "{{ text \"communities:action.create\" }}"))))) + (text "{%- endif %}") + (div + ("class" "pillmenu") + (a + ("href" "#/replies") + ("data-tab-button" "replies") + ("class" "active") + (text "{{ icon \"newspaper\" }}") + (span + (text "{{ text \"communities:label.replies\" }}"))) + (a + ("href" "/post/{{ post.id }}/reposts") + (text "{{ icon \"repeat-2\" }}") + (span + (text "{{ text \"communities:label.reposts\" }}"))) + (a + ("href" "/post/{{ post.id }}/reposts?quotes=true") + (text "{{ icon \"quote\" }}") + (span + (text "{{ text \"communities:label.quotes\" }}")))) + (text "{% if (user and user.id == post.owner) or can_manage_posts -%}") + (div + ("class" "pillmenu") + (text "{% if user and user.id == post.owner -%}") + (a + ("href" "/post/{{ post.id }}#/edit") + (text "{{ icon \"pen\" }}") + (span + (text "{{ text \"communities:label.edit_content\" }}"))) + (text "{%- endif %}") + (a + ("href" "/post/{{ post.id }}/likes") + (text "{{ icon \"heart\" }}") + (span + (text "{{ text \"communities:label.likes\" }}"))) + (a + ("href" "#/configure") + ("data-tab-button" "configure") + (text "{{ icon \"settings\" }}") + (span + (text "{{ text \"communities:action.configure\" }}")))) + (text "{%- endif %}") + (div + ("class" "flex flex-col gap-2 hidden") + ("data-tab" "configure") + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"settings\" }}") + (span + (text "{{ text \"communities:action.configure\" }}"))) + (div + ("class" "card tertiary flex flex-col gap-4") + ("id" "post_context"))) + (button + ("onclick" "save_context()") + (text "{{ icon \"check\" }}") + (span + (text "{{ text \"general:action.save\" }}"))) + (script + (text "setTimeout(() => { + const ui = ns(\"ui\"); + const element = document.getElementById(\"post_context\"); + const settings = JSON.parse(\"{{ post_context_serde|safe }}\"); + + globalThis.save_context = () => { + fetch(\"/api/v1/posts/{{ post.id }}/context\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + context: settings, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + ui.refresh_container(element, []); + + const can_manage_pins = \"{{ can_manage_pins }}\" === \"true\"; + const is_owner = + \"{{ user and user.id == post.owner }}\" === \"true\"; + + const settings_fields = [ + [ + [ + \"comments_enabled\", + \"Allow people to comment on your post\", + ], + \"{{ post.context.comments_enabled }}\", + \"checkbox\", + ], + [ + [ + \"reposts_enabled\", + \"Allow people to repost/quote your post\", + ], + \"{{ post.context.reposts_enabled }}\", + \"checkbox\", + ], + [ + [ + \"reactions_enabled\", + \"Allow people to like/dislike your post\", + ], + \"{{ post.context.reactions_enabled }}\", + \"checkbox\", + ], + [ + [\"is_nsfw\", \"Hide from public timelines\"], + \"{{ community.context.is_nsfw }}\", + \"checkbox\", + ], + [ + [\"content_warning\", \"Content warning\"], + settings.content_warning, + \"textarea\", + ], + [ + [\"tags\", \"Tags\"], + settings.tags.join(\", \"), + \"input\", + { + embed_html: + 'Tags should be separated by a comma.', + }, + ], + ]; + + if (can_manage_pins) { + settings_fields.push([ + [\"is_pinned\", \"Pinned to community wall\"], + \"{{ post.context.is_pinned }}\", + \"checkbox\", + ]); + } + + if (is_owner) { + settings_fields.push([ + [\"is_profile_pinned\", \"Pinned to your profile\"], + \"{{ post.context.is_profile_pinned }}\", + \"checkbox\", + ]); + } + + ui.generate_settings_ui(element, settings_fields, settings, { + tags: (new_tags) => { + settings.tags = new_tags + .split(\",\") + .map((t) => t.trim()); + }, + }); + }, 250);"))) + (text "{% if user and user.id == post.owner -%}") + (div + ("class" "card-nest w-full hidden") + ("data-tab" "edit") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"pen\" }}") + (span + (text "{{ text \"communities:label.edit_content\" }}"))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "edit_post_from_form(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "content") + (text "{{ text \"communities:label.content\" }}")) + (textarea + ("type" "text") + ("name" "new_content") + ("id" "new_content") + ("placeholder" "content") + ("required" "") + ("minlength" "2") + ("maxlength" "4096") + (text "{{ post.content }}"))) + (div + ("class" "flex gap-2") + (text "{{ components::emoji_picker(element_id=\"new_content\", render_dialog=false) }}") + (button + ("class" "primary") + (text "{{ text \"general:action.save\" }}"))))) + (script + (text "async function edit_post_from_form(e) { + e.preventDefault(); + await trigger(\"atto::debounce\", [\"posts::edit\"]); + fetch(\"/api/v1/posts/{{ post.id }}/content\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + content: e.target.new_content.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }")) + (text "{%- endif %}") + (div + ("class" "card-nest w-full") + ("data-tab" "replies") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"newspaper\" }}") + (span + (text "{{ text \"communities:label.replies\" }}"))) + (div + ("class" "card flex flex-col gap-4") + (text "{% for post in replies %} {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false) }} {% endfor %} {{ components::pagination(page=page, items=replies|length) }}")))) + +(script + (text "async function create_reply_from_form(e) { + e.preventDefault(); + await trigger(\"atto::debounce\", [\"posts::create\"]); + + // create body + const body = new FormData(); + + if (e.target.file_picker) { + for (const file of e.target.file_picker.files) { + body.append(file.name, file); + } + } + + body.append( + \"body\", + JSON.stringify({ + content: e.target.content.value, + community: \"{{ community.id }}\", + replying_to: \"{{ post.id }}\", + }), + ); + + // ... + fetch(\"/api/v1/posts\", { + method: \"POST\", + body, + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + setTimeout(() => { + window.location.href = `/post/${res.payload}`; + }, 100); + } + }); + }")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/post/quotes.html b/crates/app/src/public/html/post/quotes.html deleted file mode 100644 index 139dbad..0000000 --- a/crates/app/src/public/html/post/quotes.html +++ /dev/null @@ -1,74 +0,0 @@ -{% extends "root.html" %} {% block head %} -Post quotes - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
- {% if post.replying_to -%} - - {{ icon "arrow-up" }} - {{ text "communities:action.continue_thread" }} - - {%- endif %} - - -
- {% if post.context.repost and post.context.repost.reposting -%} - {{ components::repost(repost=reposting, post=post, owner=owner, community=community, show_community=true, can_manage_post=can_manage_posts) }} - {% else %} - {{ components::post(post=post, owner=owner, question=question, community=community, show_community=true, can_manage_post=can_manage_posts) }} - {%- endif %} -
- - - - {% if (user and user.id == post.owner) or can_manage_posts -%} - - {%- endif %} - -
-
- {{ icon "quote" }} - {{ text "communities:label.quotes" }} -
- -
- - {% for post in list %} - {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false) }} - {% endfor %} - - {{ components::pagination(page=page, items=list|length, key="quotes", value="true") }} -
-
-
-{% endblock %} diff --git a/crates/app/src/public/html/post/quotes.lisp b/crates/app/src/public/html/post/quotes.lisp new file mode 100644 index 0000000..44c4ecc --- /dev/null +++ b/crates/app/src/public/html/post/quotes.lisp @@ -0,0 +1,69 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Post quotes - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (text "{% if post.replying_to -%}") + (a + ("href" "/post/{{ post.replying_to }}") + ("class" "button") + (text "{{ icon \"arrow-up\" }}") + (span + (text "{{ text \"communities:action.continue_thread\" }}"))) + (text "{%- endif %}") + (div + ("style" "display: contents;") + (text "{% if post.context.repost and post.context.repost.reposting -%} {{ components::repost(repost=reposting, post=post, owner=owner, community=community, show_community=true, can_manage_post=can_manage_posts) }} {% else %} {{ components::post(post=post, owner=owner, question=question, community=community, show_community=true, can_manage_post=can_manage_posts) }} {%- endif %}")) + (div + ("class" "pillmenu") + (a + ("href" "/post/{{ post.id }}#/replies") + (text "{{ icon \"newspaper\" }}") + (span + (text "{{ text \"communities:label.replies\" }}"))) + (a + ("href" "/post/{{ post.id }}/reposts") + (text "{{ icon \"repeat-2\" }}") + (span + (text "{{ text \"communities:label.reposts\" }}"))) + (a + ("href" "/post/{{ post.id }}/reposts?quotes=true") + ("class" "active") + (text "{{ icon \"quote\" }}") + (span + (text "{{ text \"communities:label.quotes\" }}")))) + (text "{% if (user and user.id == post.owner) or can_manage_posts -%}") + (div + ("class" "pillmenu") + (text "{% if user and user.id == post.owner -%}") + (a + ("href" "/post/{{ post.id }}#/edit") + (text "{{ icon \"pen\" }}") + (span + (text "{{ text \"communities:label.edit_content\" }}"))) + (text "{%- endif %}") + (a + ("href" "/post/{{ post.id }}/likes") + (text "{{ icon \"heart\" }}") + (span + (text "{{ text \"communities:label.likes\" }}"))) + (a + ("href" "/post/{{ post.id }}#/configure") + (text "{{ icon \"settings\" }}") + (span + (text "{{ text \"communities:action.configure\" }}")))) + (text "{%- endif %}") + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"quote\" }}") + (span + (text "{{ text \"communities:label.quotes\" }}"))) + (div + ("class" "card flex flex-col gap-4") + (text "{% for post in list %} {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false) }} {% endfor %} {{ components::pagination(page=page, items=list|length, key=\"quotes\", value=\"true\") }}")))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/post/reposts.html b/crates/app/src/public/html/post/reposts.html deleted file mode 100644 index 255b516..0000000 --- a/crates/app/src/public/html/post/reposts.html +++ /dev/null @@ -1,93 +0,0 @@ -{% extends "root.html" %} {% block head %} -Post reposts - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
- {% if post.replying_to -%} - - {{ icon "arrow-up" }} - {{ text "communities:action.continue_thread" }} - - {%- endif %} - - -
- {% if post.context.repost and post.context.repost.reposting -%} - {{ components::repost(repost=reposting, post=post, owner=owner, community=community, show_community=true, can_manage_post=can_manage_posts) }} - {% else %} - {{ components::post(post=post, owner=owner, question=question, community=community, show_community=true, can_manage_post=can_manage_posts) }} - {%- endif %} -
- - - - {% if (user and user.id == post.owner) or can_manage_posts -%} - - {%- endif %} - -
-
- {{ icon "repeat-2" }} - {{ text "communities:label.reposts" }} -
- -
- {% for post in list %} - - {% endfor %} {{ components::pagination(page=page, items=list|length, - key="quotes", value="false") }} -
-
-
-{% endblock %} diff --git a/crates/app/src/public/html/post/reposts.lisp b/crates/app/src/public/html/post/reposts.lisp new file mode 100644 index 0000000..f47a8b4 --- /dev/null +++ b/crates/app/src/public/html/post/reposts.lisp @@ -0,0 +1,88 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Post reposts - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (text "{% if post.replying_to -%}") + (a + ("href" "/post/{{ post.replying_to }}") + ("class" "button") + (text "{{ icon \"arrow-up\" }}") + (span + (text "{{ text \"communities:action.continue_thread\" }}"))) + (text "{%- endif %}") + (div + ("style" "display: contents;") + (text "{% if post.context.repost and post.context.repost.reposting -%} {{ components::repost(repost=reposting, post=post, owner=owner, community=community, show_community=true, can_manage_post=can_manage_posts) }} {% else %} {{ components::post(post=post, owner=owner, question=question, community=community, show_community=true, can_manage_post=can_manage_posts) }} {%- endif %}")) + (div + ("class" "pillmenu") + (a + ("href" "/post/{{ post.id }}#/replies") + (text "{{ icon \"newspaper\" }}") + (span + (text "{{ text \"communities:label.replies\" }}"))) + (a + ("href" "/post/{{ post.id }}/reposts") + ("class" "active") + (text "{{ icon \"repeat-2\" }}") + (span + (text "{{ text \"communities:label.reposts\" }}"))) + (a + ("href" "/post/{{ post.id }}/reposts?quotes=true") + (text "{{ icon \"quote\" }}") + (span + (text "{{ text \"communities:label.quotes\" }}")))) + (text "{% if (user and user.id == post.owner) or can_manage_posts -%}") + (div + ("class" "pillmenu") + (text "{% if user and user.id == post.owner -%}") + (a + ("href" "/post/{{ post.id }}#/edit") + (text "{{ icon \"pen\" }}") + (span + (text "{{ text \"communities:label.edit_content\" }}"))) + (text "{%- endif %}") + (a + ("href" "/post/{{ post.id }}/likes") + (text "{{ icon \"heart\" }}") + (span + (text "{{ text \"communities:label.likes\" }}"))) + (a + ("href" "/post/{{ post.id }}#/configure") + (text "{{ icon \"settings\" }}") + (span + (text "{{ text \"communities:action.configure\" }}")))) + (text "{%- endif %}") + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"repeat-2\" }}") + (span + (text "{{ text \"communities:label.reposts\" }}"))) + (div + ("class" "card flex flex-col gap-4") + (text "{% for post in list %}") + (div + ("class" "card-nest") + (div + ("class" "card flex items-center gap-2") + (a + ("href" "/@{{ post[1].username }}") + (text "{{ components::avatar(username=post[1].username, size=\"24px\", selector_type=\"username\") }}")) + (div + ("class" "name") + (text "{{ components::full_username(user=post[1]) }}"))) + (div + ("class" "card flex items-center gap-2 flex-wrap secondary") + (a + ("href" "/post/{{ post[0].id }}") + ("class" "quaternary small button") + (text "{{ icon \"external-link\" }}") + (span + (text "{{ text \"general:action.open\" }}"))))) + (text "{% endfor %} {{ components::pagination(page=page, items=list|length, key=\"quotes\", value=\"false\") }}")))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/profile/banned.html b/crates/app/src/public/html/profile/banned.html deleted file mode 100644 index aec1122..0000000 --- a/crates/app/src/public/html/profile/banned.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends "root.html" %} {% block head %} -{{ profile.username }} (banned) - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
-
-
-
- {{ components::avatar(username=profile.username, size="24px") }} - {{ profile.username }} -
- - {{ text "auth:label.banned" }} -
- -
- {{ text "auth:label.banned_message" }} - - -
-
-
-{% endblock %} diff --git a/crates/app/src/public/html/profile/banned.lisp b/crates/app/src/public/html/profile/banned.lisp new file mode 100644 index 0000000..60fd50c --- /dev/null +++ b/crates/app/src/public/html/profile/banned.lisp @@ -0,0 +1,33 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "{{ profile.username }} (banned) - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center justify-between gap-2") + (div + ("class" "flex items-center gap-2") + (text "{{ components::avatar(username=profile.username, size=\"24px\") }}") + (span + (text "{{ profile.username }}"))) + (b + ("class" "notification chip") + (text "{{ text \"auth:label.banned\" }}"))) + (div + ("class" "card flex flex-col gap-2") + (span + (text "{{ text \"auth:label.banned_message\" }}")) + (div + ("class" "card w-full secondary flex gap-2") + (a + ("href" "/") + ("class" "button red quaternary") + (text "{{ icon \"x\" }}") + (span + (text "{{ text \"general:action.back\" }}"))))))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/profile/base.html b/crates/app/src/public/html/profile/base.html deleted file mode 100644 index e293f53..0000000 --- a/crates/app/src/public/html/profile/base.html +++ /dev/null @@ -1,386 +0,0 @@ -{% extends "root.html" %} {% block head %} -{{ profile.username }} - {{ config.name }} - - - - - - - - - - - - - - - -{% endblock %} {% block body %} {{ macros::nav() }} -
-
- {{ components::banner(username=profile.username) }} - -
-
-
-
- {{ - components::avatar(username=profile.username,size="72px") - }} -
- -

- {{ components::username(user=profile) }} - - {% if profile.is_verified -%} - - {{ icon "badge-check" }} - - {%- endif %} - - {% if profile.permissions|has_supporter -%} - - {{ icon "star" }} - - {%- endif %} - - {% if profile.permissions|has_staff_badge -%} - - {{ icon "shield-user" }} - - {%- endif %} - - {% if profile.permissions|has_banned -%} - - {{ icon "shield-ban" }} - - {%- endif %} -

- - {{ profile.username }} -
-
- -
- {% if profile.settings.status -%} -

{{ profile.settings.status }}

- {%- endif %} - - - - {% if is_following_you -%} - - {{ icon "heart" }} - Follows you - - {%- endif %} -
-
- -
-
- {{ profile.settings.biography|markdown|safe }} -
- -
- -
- {% if profile.connections.Spotify and profile.connections.Spotify[0].data.name -%} - {{ components::spotify_playing(state=profile.connections.Spotify[1]) }} - {% elif profile.connections.LastFm and profile.connections.LastFm[0].data.name %} - {{ components::last_fm_playing(state=profile.connections.LastFm[1]) }} - {%- endif %} -
- -
- ID - -
- -
- Joined - {{ profile.created }} -
- -
- Posts - {{ profile.post_count }} -
- - {% if not profile.settings.private_last_seen or is_self - or is_helper %} -
- Last seen - -
- {{ components::online_indicator(user=profile) }} - - {{ profile.last_seen }} - -
-
- {%- endif %} -
-
- - {% if not is_self and user -%} -
-
- {{ text "auth:label.relationship" }} -
- -
- {% if not is_blocking -%} - - - - - - {% else %} - - {%- endif %} {% if not user.settings.private_chats or - is_following_you %} - - {%- endif %} {% if is_helper -%} - - {{ icon "shield" }} - {{ text "general:action.manage" }} - - {%- endif %} - - -
-
- {%- endif %} {% if not profile.settings.private_communities or - is_self or is_helper %} -
-
- {{ icon "users-round" }} - {{ text "auth:label.joined_communities" }} -
- - -
- {%- endif %} - -
- {% for key, value in profile.connections %} {% if - value[0].data.name and value[0].show_on_profile %} - -
- {{ components::connection_icon(key=key) }} - {{ value[0].data.name }} -
- - -
- {%- endif %} {% endfor %} -
-
- -
- {% block content %}{% endblock %} -
-
-
-
-{% if not is_self and profile.settings.warning -%} - -{%- endif %} {% if not use_user_theme -%} {{ components::theme(user=profile, -theme_preference=profile.settings.profile_theme) }} {%- endif %} {% endblock %} diff --git a/crates/app/src/public/html/profile/base.lisp b/crates/app/src/public/html/profile/base.lisp new file mode 100644 index 0000000..7b86141 --- /dev/null +++ b/crates/app/src/public/html/profile/base.lisp @@ -0,0 +1,371 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "{{ profile.username }} - {{ config.name }}")) + +(meta + ("name" "og:title") + ("content" "{{ profile.username }}")) + +(meta + ("name" "description") + ("content" "View @{{ profile.username }}'s profile on {{ config.name }}!")) + +(meta + ("name" "og:description") + ("content" "View @{{ profile.username }}'s profile on {{ config.name }}!")) + +(meta + ("property" "og:type") + ("content" "profile")) + +(meta + ("property" "profile:username") + ("content" "{{ profile.username }}")) + +(meta + ("name" "og:image") + ("content" "{{ config.host|safe }}/api/v1/auth/user/{{ profile.username }}/avatar?selector_type=username")) + +(meta + ("name" "twitter:image") + ("content" "{{ config.host|safe }}/api/v1/auth/user/{{ profile.username }}/avatar?selector_type=username")) + +(meta + ("name" "twitter:card") + ("content" "summary")) + +(meta + ("name" "twitter:title") + ("content" "{{ profile.username }}")) + +(meta + ("name" "twitter:description") + ("content" "View @{{ profile.username }}'s profile on {{ config.name }}!")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(article + (div + ("class" "content_container flex flex-col gap-4") + (text "{{ components::banner(username=profile.username) }}") + (div + ("class" "w-full flex gap-4 flex-collapse") + (div + ("class" "lhs flex flex-col gap-2 sm:w-full") + ("style" "width: 22rem; min-width: 22rem") + (div + ("class" "card-nest w-full") + (div + ("class" "card flex gap-2") + ("id" "user_avatar_and_name") + (text "{{ components::avatar(username=profile.username,size=\"72px\") }}") + (div + ("class" "flex flex-col") + (h3 + ("id" "username") + ("class" "username flex items-center gap-2 flex-wrap w-full") + (span + ("class" "name shorter") + (text "{{ components::username(user=profile) }}")) + (text "{% if profile.is_verified -%}") + (span + ("title" "Verified") + ("style" "color: var(--color-primary);") + ("class" "flex items-center") + (text "{{ icon \"badge-check\" }}")) + (text "{%- endif %} {% if profile.permissions|has_supporter -%}") + (span + ("title" "Supporter") + ("style" "color: var(--color-primary);") + ("class" "flex items-center") + (text "{{ icon \"star\" }}")) + (text "{%- endif %} {% if profile.permissions|has_staff_badge -%}") + (span + ("title" "Staff") + ("style" "color: var(--color-primary);") + ("class" "flex items-center") + (text "{{ icon \"shield-user\" }}")) + (text "{%- endif %} {% if profile.permissions|has_banned -%}") + (span + ("title" "Banned") + ("style" "color: var(--color-primary);") + ("class" "flex items-center") + (text "{{ icon \"shield-ban\" }}")) + (text "{%- endif %}")) + (span + ("class" "fade") + (text "{{ profile.username }}")))) + (div + ("class" "card flex flex-col items-center gap-2") + ("id" "social") + (text "{% if profile.settings.status -%}") + (p + (text "{{ profile.settings.status }}")) + (text "{%- endif %}") + (div + ("class" "w-full flex") + (a + ("href" "/@{{ profile.username }}/followers") + ("class" "w-full flex justify-center items-center gap-2") + (h4 + (text "{{ profile.follower_count }}")) + (span + (text "{{ text \"auth:label.followers\" }}"))) + (a + ("href" "/@{{ profile.username }}/following") + ("class" "w-full flex justify-center items-center gap-2") + (h4 + (text "{{ profile.following_count }}")) + (span + (text "{{ text \"auth:label.following\" }}")))) + (text "{% if is_following_you -%}") + (b + ("class" "notification chip w-content flex items-center gap-2") + (text "{{ icon \"heart\" }}") + (span + (text "Follows you"))) + (text "{%- endif %}"))) + (div + ("class" "card-nest flex flex-col") + (div + ("id" "bio") + ("class" "card small no_p_margin") + (text "{{ profile.settings.biography|markdown|safe }}")) + (div + ("class" "card flex flex-col gap-2") + (div + ("style" "display: contents;") + (text "{% if profile.connections.Spotify and profile.connections.Spotify[0].data.name -%} {{ components::spotify_playing(state=profile.connections.Spotify[1]) }} {% elif profile.connections.LastFm and profile.connections.LastFm[0].data.name %} {{ components::last_fm_playing(state=profile.connections.LastFm[1]) }} {%- endif %}")) + (div + ("class" "w-full flex justify-between items-center") + (span + ("class" "notification chip") + (text "ID")) + (button + ("title" "Copy") + ("onclick" "trigger('atto::copy_text', ['{{ profile.id }}'])") + ("class" "camo small") + (text "{{ icon \"copy\" }}"))) + (div + ("class" "w-full flex justify-between items-center") + (span + ("class" "notification chip") + (text "Joined")) + (span + ("class" "date") + (text "{{ profile.created }}"))) + (div + ("class" "w-full flex justify-between items-center") + (span + ("class" "notification chip") + (text "Posts")) + (span + (text "{{ profile.post_count }}"))) + (text "{% if not profile.settings.private_last_seen or is_self or is_helper %}") + (div + ("class" "w-full flex justify-between items-center") + (span + ("class" "notification chip") + (text "Last seen")) + (div + ("class" "flex") + (text "{{ components::online_indicator(user=profile) }}") + (span + ("class" "date") + (text "{{ profile.last_seen }}")))) + (text "{%- endif %}"))) + (text "{% if not is_self and user -%}") + (div + ("class" "card-nest") + (div + ("class" "card small") + (b + (text "{{ text \"auth:label.relationship\" }}"))) + (div + ("class" "card flex gap-2 flex-wrap") + (text "{% if not is_blocking -%}") + (button + ("onclick" "toggle_follow_user(event)") + ("class" "{% if is_following %} hidden{% endif %}") + ("atto_tag" "user.follow") + (text "{{ icon \"user-plus\" }}") + (span + (text "{{ text \"auth:action.follow\" }}"))) + (button + ("onclick" "toggle_follow_user(event)") + ("class" "quaternary red{% if not is_following %} hidden{% endif %}") + ("atto_tag" "user.unfollow") + (text "{{ icon \"user-minus\" }}") + (span + (text "{{ text \"auth:action.unfollow\" }}"))) + (button + ("onclick" "toggle_block_user()") + ("class" "quaternary red") + (text "{{ icon \"shield\" }}") + (span + (text "{{ text \"auth:action.block\" }}"))) + (text "{% else %}") + (button + ("onclick" "toggle_block_user()") + ("class" "quaternary red") + (text "{{ icon \"shield-off\" }}") + (span + (text "{{ text \"auth:action.unblock\" }}"))) + (text "{%- endif %} {% if not user.settings.private_chats or is_following_you %}") + (button + ("onclick" "create_group_chat()") + ("class" "quaternary") + (text "{{ icon \"message-circle\" }}") + (span + (text "{{ text \"auth:action.message\" }}"))) + (text "{%- endif %} {% if is_helper -%}") + (a + ("href" "/mod_panel/profile/{{ profile.id }}") + ("class" "button quaternary") + (text "{{ icon \"shield\" }}") + (span + (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.toggle_follow_user = async (e) => { + await trigger(\"atto::debounce\", [ + \"users::follow\", + ]); + + fetch( + \"/api/v1/auth/user/{{ profile.id }}/follow\", + { + method: \"POST\", + }, + ) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if ( + e.target.getAttribute( + \"atto_tag\", + ) === \"user.follow\" + ) { + document + .querySelector( + '[atto_tag=\"user.follow\"]', + ) + .classList.add(\"hidden\"); + document + .querySelector( + '[atto_tag=\"user.unfollow\"]', + ) + .classList.remove(\"hidden\"); + } else { + document + .querySelector( + '[atto_tag=\"user.unfollow\"]', + ) + .classList.add(\"hidden\"); + document + .querySelector( + '[atto_tag=\"user.follow\"]', + ) + .classList.remove(\"hidden\"); + } + }); + }; + + globalThis.toggle_block_user = async () => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + fetch( + \"/api/v1/auth/user/{{ profile.id }}/block\", + { + method: \"POST\", + }, + ) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + };")))) + (text "{%- endif %} {% if not profile.settings.private_communities or is_self or is_helper %}") + (div + ("class" "card-nest") + (div + ("class" "card small flex gap-2 items-center") + (text "{{ icon \"users-round\" }}") + (span + (text "{{ text \"auth:label.joined_communities\" }}"))) + (div + ("class" "card flex flex-wrap gap-2") + (text "{% for community in communities %}") + (a + ("href" "/community/{{ community.title }}") + (text "{{ components::community_avatar(id=community.id, community=community, size=\"48px\") }}")) + (text "{% endfor %}"))) + (text "{%- endif %}") + (div + ("class" "flex flex-col gap-2") + ("id" "connections") + (text "{% for key, value in profile.connections %} {% if value[0].data.name and value[0].show_on_profile %}") + (a + ("class" "card small flush flex items-center justify-between gap-2") + ("href" "{{ components::connection_url(key=key, value=value) }}") + (div + ("class" "flex items-center gap-2") + (text "{{ components::connection_icon(key=key) }}") + (b + (text "{{ value[0].data.name }}"))) + (button + ("class" "camo small") + (text "{{ icon \"external-link\" }}"))) + (text "{%- endif %} {% endfor %}"))) + (div + ("class" "rhs w-full flex flex-col gap-4") + (text "{% block content %}{% endblock %}"))))) + +(text "{% if not is_self and profile.settings.warning -%}") +(script + (text "setTimeout(() => { + // check for warning + trigger(\"warnings::open\", [ + \"{{ profile.id }}\", + \"{{ warning_hash }}\", + \"?warning=true\", + ]); + }, 150);")) + +(text "{%- endif %} {% if not use_user_theme -%} {{ components::theme(user=profile, theme_preference=profile.settings.profile_theme) }} {%- endif %} {% endblock %}") diff --git a/crates/app/src/public/html/profile/blocked.html b/crates/app/src/public/html/profile/blocked.html deleted file mode 100644 index 15014d1..0000000 --- a/crates/app/src/public/html/profile/blocked.html +++ /dev/null @@ -1,65 +0,0 @@ -{% extends "root.html" %} {% block head %} -{{ profile.username }} (blocked) - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
-
-
-
- {{ components::avatar(username=profile.username, size="24px") }} - {{ profile.username }} -
- - {{ text "auth:label.blocked_profile" }} -
- -
- {{ text "auth:label.blocked_profile_message" }} - -
- {% if user -%} {% if not is_blocking -%} - - {% else %} - - {%- endif %} - - - {%- endif %} - - - {{ icon "x" }} - {{ text "general:action.back" }} - -
-
-
-
-{% endblock %} diff --git a/crates/app/src/public/html/profile/blocked.lisp b/crates/app/src/public/html/profile/blocked.lisp new file mode 100644 index 0000000..3f3edd7 --- /dev/null +++ b/crates/app/src/public/html/profile/blocked.lisp @@ -0,0 +1,70 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "{{ profile.username }} (blocked) - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center justify-between gap-2") + (div + ("class" "flex items-center gap-2") + (text "{{ components::avatar(username=profile.username, size=\"24px\") }}") + (span + (text "{{ profile.username }}"))) + (b + ("class" "notification chip") + (text "{{ text \"auth:label.blocked_profile\" }}"))) + (div + ("class" "card flex flex-col gap-2") + (span + (text "{{ text \"auth:label.blocked_profile_message\" }}")) + (div + ("class" "card w-full secondary flex gap-2") + (text "{% if user -%} {% if not is_blocking -%}") + (button + ("onclick" "toggle_block_user()") + ("class" "quaternary red") + (text "{{ icon \"shield\" }}") + (span + (text "{{ text \"auth:action.block\" }}"))) + (text "{% else %}") + (button + ("onclick" "toggle_block_user()") + ("class" "quaternary red") + (text "{{ icon \"shield-off\" }}") + (span + (text "{{ text \"auth:action.unblock\" }}"))) + (text "{%- endif %}") + (script + (text "globalThis.toggle_block_user = async () => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + fetch(\"/api/v1/auth/user/{{ profile.id }}/block\", { + method: \"POST\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + };")) + (text "{%- endif %}") + (a + ("href" "/") + ("class" "button red quaternary") + (text "{{ icon \"x\" }}") + (span + (text "{{ text \"general:action.back\" }}"))))))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/profile/followers.html b/crates/app/src/public/html/profile/followers.html deleted file mode 100644 index 1dba539..0000000 --- a/crates/app/src/public/html/profile/followers.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends "profile/base.html" %} {% block content %} -
-
- {{ icon "users-round" }} - {{ text "auth:label.followers" }} -
- -
- - {% for item in list %} - {{ components::user_plate(user=item[1], secondary=true) }} - {% endfor %} {{ components::pagination(page=page, items=list|length) }} -
-
- - -{% endblock %} diff --git a/crates/app/src/public/html/profile/followers.lisp b/crates/app/src/public/html/profile/followers.lisp new file mode 100644 index 0000000..c12c7a9 --- /dev/null +++ b/crates/app/src/public/html/profile/followers.lisp @@ -0,0 +1,24 @@ +(text "{% extends \"profile/base.html\" %} {% block content %}") +(div + ("class" "card-nest") + (div + ("class" "card small flex gap-2 items-center") + (text "{{ icon \"users-round\" }}") + (span + (text "{{ text \"auth:label.followers\" }}"))) + (div + ("class" "card flex flex-wrap gap-4 flex-collapse") + (text "{% for item in list %} {{ components::user_plate(user=item[1], secondary=true) }} {% endfor %} {{ components::pagination(page=page, items=list|length) }}"))) + +(style + (text ".user_plate { + width: calc(50% - 0.5rem); + } + + @media screen and (max-width: 900px) { + .user_plate { + width: 100%; + } + }")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/profile/following.html b/crates/app/src/public/html/profile/following.html deleted file mode 100644 index dca3ae2..0000000 --- a/crates/app/src/public/html/profile/following.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends "profile/base.html" %} {% block content %} -
-
- {{ icon "users-round" }} - {{ text "auth:label.following" }} -
- -
- - {% for item in list %} - {{ components::user_plate(user=item[1], secondary=true) }} - {% endfor %} {{ components::pagination(page=page, items=list|length) }} -
-
- - -{% endblock %} diff --git a/crates/app/src/public/html/profile/following.lisp b/crates/app/src/public/html/profile/following.lisp new file mode 100644 index 0000000..1cfb0e8 --- /dev/null +++ b/crates/app/src/public/html/profile/following.lisp @@ -0,0 +1,24 @@ +(text "{% extends \"profile/base.html\" %} {% block content %}") +(div + ("class" "card-nest") + (div + ("class" "card small flex gap-2 items-center") + (text "{{ icon \"users-round\" }}") + (span + (text "{{ text \"auth:label.following\" }}"))) + (div + ("class" "card flex flex-wrap gap-4 flex-collapse") + (text "{% for item in list %} {{ components::user_plate(user=item[1], secondary=true) }} {% endfor %} {{ components::pagination(page=page, items=list|length) }}"))) + +(style + (text ".user_plate { + width: calc(50% - 0.5rem); + } + + @media screen and (max-width: 900px) { + .user_plate { + width: 100%; + } + }")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/profile/media.html b/crates/app/src/public/html/profile/media.html deleted file mode 100644 index 5429e02..0000000 --- a/crates/app/src/public/html/profile/media.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "profile/base.html" %} {% block content %} {% if -profile.settings.enable_questions and (user or -profile.settings.allow_anonymous_questions) %} -
- {{ components::create_question_form(receiver=profile.id, - header=profile.settings.motivational_header) }} -
-{%- endif %} {{ macros::profile_nav(selected="media") }} -
-
-
- {{ icon "clock" }} - {{ text "auth:label.recent_posts_with_media" }} -
- - {% if user -%} - - {{ icon "search" }} - {{ text "general:link.search" }} - - {%- endif %} -
- -
- - {% 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) }} - {%- endif %} - {%- endif %} - {% endfor %} - - {{ components::pagination(page=page, items=posts|length) }} -
-
-{% endblock %} diff --git a/crates/app/src/public/html/profile/media.lisp b/crates/app/src/public/html/profile/media.lisp new file mode 100644 index 0000000..a9387cc --- /dev/null +++ b/crates/app/src/public/html/profile/media.lisp @@ -0,0 +1,28 @@ +(text "{% extends \"profile/base.html\" %} {% block content %} {% if profile.settings.enable_questions and (user or profile.settings.allow_anonymous_questions) %}") +(div + ("style" "display: contents") + (text "{{ components::create_question_form(receiver=profile.id, header=profile.settings.motivational_header) }}")) + +(text "{%- endif %} {{ macros::profile_nav(selected=\"media\") }}") +(div + ("class" "card-nest") + (div + ("class" "card small flex gap-2 justify-between items-center") + (div + ("class" "flex gap-2 items-center") + (text "{{ icon \"clock\" }}") + (span + (text "{{ text \"auth:label.recent_posts_with_media\" }}"))) + (text "{% if user -%}") + (a + ("href" "/search?profile={{ profile.id }}") + ("class" "button quaternary small") + (text "{{ icon \"search\" }}") + (span + (text "{{ text \"general:link.search\" }}"))) + (text "{%- endif %}")) + (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) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=posts|length) }}"))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/profile/posts.html b/crates/app/src/public/html/profile/posts.html deleted file mode 100644 index 4b396c5..0000000 --- a/crates/app/src/public/html/profile/posts.html +++ /dev/null @@ -1,67 +0,0 @@ -{% extends "profile/base.html" %} {% block content %} {% if -profile.settings.enable_questions and (user or -profile.settings.allow_anonymous_questions) %} -
- {{ components::create_question_form(receiver=profile.id, - header=profile.settings.motivational_header) }} -
-{%- endif %} {% if not tag and pinned|length != 0 -%} -
-
- {{ icon "pin" }} - {{ text "communities:label.pinned" }} -
- -
- - {% for post in pinned %} - {% 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) }} - {%- endif %} - {%- endif %} - {% endfor %} -
-
-{%- endif %} {{ macros::profile_nav(selected="posts") }} -
-
-
- {% if not tag -%} {{ icon "clock" }} - {{ text "auth:label.recent_posts" }} - {% else %} {{ icon "tag" }} - {{ text "auth:label.recent_with_tag" }}: {{ tag }} - {%- endif %} -
- - {% if user -%} - - {{ icon "search" }} - {{ text "general:link.search" }} - - {%- endif %} -
- -
- - {% 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) }} - {%- endif %} - {%- endif %} - {% endfor %} - - {{ components::pagination(page=page, items=posts|length, key="&tag=", value=tag) }} -
-
-{% endblock %} diff --git a/crates/app/src/public/html/profile/posts.lisp b/crates/app/src/public/html/profile/posts.lisp new file mode 100644 index 0000000..83dba21 --- /dev/null +++ b/crates/app/src/public/html/profile/posts.lisp @@ -0,0 +1,46 @@ +(text "{% extends \"profile/base.html\" %} {% block content %} {% if profile.settings.enable_questions and (user or profile.settings.allow_anonymous_questions) %}") +(div + ("style" "display: contents") + (text "{{ components::create_question_form(receiver=profile.id, header=profile.settings.motivational_header) }}")) + +(text "{%- endif %} {% if not tag and pinned|length != 0 -%}") +(div + ("class" "card-nest") + (div + ("class" "card small flex gap-2 items-center") + (text "{{ icon \"pin\" }}") + (span + (text "{{ text \"communities:label.pinned\" }}"))) + (div + ("class" "card flex flex-col gap-4") + (text "{% for post in pinned %} {% 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) }} {%- endif %} {%- endif %} {% endfor %}"))) + +(text "{%- endif %} {{ macros::profile_nav(selected=\"posts\") }}") +(div + ("class" "card-nest") + (div + ("class" "card small flex gap-2 justify-between items-center") + (div + ("class" "flex gap-2 items-center") + (text "{% if not tag -%} {{ icon \"clock\" }}") + (span + (text "{{ text \"auth:label.recent_posts\" }}")) + (text "{% else %} {{ icon \"tag\" }}") + (span + (text "{{ text \"auth:label.recent_with_tag\" }}:") + (b + (text "{{ tag }}"))) + (text "{%- endif %}")) + (text "{% if user -%}") + (a + ("href" "/search?profile={{ profile.id }}") + ("class" "button quaternary small") + (text "{{ icon \"search\" }}") + (span + (text "{{ text \"general:link.search\" }}"))) + (text "{%- endif %}")) + (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) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=posts|length, key=\"&tag=\", value=tag) }}"))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/profile/private.html b/crates/app/src/public/html/profile/private.html deleted file mode 100644 index c89e2a8..0000000 --- a/crates/app/src/public/html/profile/private.html +++ /dev/null @@ -1,162 +0,0 @@ -{% extends "root.html" %} {% block head %} -{{ profile.username }} (private profile) - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
-
-
-
- {{ components::avatar(username=profile.username, size="24px") }} - {{ profile.username }} -
- - {{ text "auth:label.private_profile" }} -
- -
- {{ text "auth:label.private_profile_message" }} - -
- {% if user -%} {% if not is_following -%} - - - - {% else %} - - {%- endif %} {% if not is_blocking -%} - - {% else %} - - {%- endif %} - - - {%- endif %} - - - {{ icon "x" }} - {{ text "general:action.back" }} - -
-
-
-
-{% endblock %} diff --git a/crates/app/src/public/html/profile/private.lisp b/crates/app/src/public/html/profile/private.lisp new file mode 100644 index 0000000..12c528d --- /dev/null +++ b/crates/app/src/public/html/profile/private.lisp @@ -0,0 +1,163 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "{{ profile.username }} (private profile) - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center justify-between gap-2") + (div + ("class" "flex items-center gap-2") + (text "{{ components::avatar(username=profile.username, size=\"24px\") }}") + (span + (text "{{ profile.username }}"))) + (b + ("class" "notification chip") + (text "{{ text \"auth:label.private_profile\" }}"))) + (div + ("class" "card flex flex-col gap-2") + (span + (text "{{ text \"auth:label.private_profile_message\" }}")) + (div + ("class" "card w-full secondary flex gap-2") + (text "{% if user -%} {% if not is_following -%}") + (button + ("onclick" "toggle_follow_user(event)") + ("class" "{% if follow_requested -%} hidden{%- endif %}") + ("atto_tag" "user.follow_request") + (text "{{ icon \"user-plus\" }}") + (span + (text "{{ text \"auth:action.request_to_follow\" }}"))) + (button + ("onclick" "cancel_follow_user(event)") + ("class" "quaternary red{% if not follow_requested -%} hidden{%- endif %}") + ("atto_tag" "user.cancel_request") + (text "{{ icon \"user-minus\" }}") + (span + (text "{{ text \"auth:action.cancel_follow_request\" }}"))) + (text "{% else %}") + (button + ("onclick" "toggle_follow_user(event)") + ("class" "quaternary red") + ("atto_tag" "user.unfollow") + (text "{{ icon \"user-minus\" }}") + (span + (text "{{ text \"auth:action.unfollow\" }}"))) + (text "{%- endif %} {% if not is_blocking -%}") + (button + ("onclick" "toggle_block_user()") + ("class" "quaternary red") + (text "{{ icon \"shield\" }}") + (span + (text "{{ text \"auth:action.block\" }}"))) + (text "{% else %}") + (button + ("onclick" "toggle_block_user()") + ("class" "quaternary red") + (text "{{ icon \"shield-off\" }}") + (span + (text "{{ text \"auth:action.unblock\" }}"))) + (text "{%- endif %}") + (script + (text "globalThis.toggle_follow_user = async (e) => { + await trigger(\"atto::debounce\", [\"users::follow\"]); + fetch(\"/api/v1/auth/user/{{ profile.id }}/follow\", { + method: \"POST\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if ( + e.target.getAttribute(\"atto_tag\") === + \"user.follow_request\" + ) { + document + .querySelector( + '[atto_tag=\"user.follow_request\"]', + ) + .classList.add(\"hidden\"); + + document + .querySelector( + '[atto_tag=\"user.cancel_request\"]', + ) + .classList.remove(\"hidden\"); + } else { + window.location.reload(); + } + }); + }; + + globalThis.cancel_follow_user = async (e) => { + await trigger(\"atto::debounce\", [\"users::follow\"]); + + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + fetch( + \"/api/v1/auth/user/{{ profile.id }}/follow/cancel\", + { + method: \"POST\", + }, + ) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + document + .querySelector( + '[atto_tag=\"user.cancel_request\"]', + ) + .classList.add(\"hidden\"); + document + .querySelector( + '[atto_tag=\"user.follow_request\"]', + ) + .classList.remove(\"hidden\"); + }); + }; + + globalThis.toggle_block_user = async () => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + fetch(\"/api/v1/auth/user/{{ profile.id }}/block\", { + method: \"POST\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + };")) + (text "{%- endif %}") + (a + ("href" "/") + ("class" "button red quaternary") + (text "{{ icon \"x\" }}") + (span + (text "{{ text \"general:action.back\" }}"))))))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/profile/replies.html b/crates/app/src/public/html/profile/replies.html deleted file mode 100644 index 7b167a0..0000000 --- a/crates/app/src/public/html/profile/replies.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "profile/base.html" %} {% block content %} {% if -profile.settings.enable_questions and (user or -profile.settings.allow_anonymous_questions) %} -
- {{ components::create_question_form(receiver=profile.id, - header=profile.settings.motivational_header) }} -
-{%- endif %} {{ macros::profile_nav(selected="replies") }} -
-
-
- {{ icon "clock" }} - {{ text "auth:label.recent_replies" }} -
- - {% if user -%} - - {{ icon "search" }} - {{ text "general:link.search" }} - - {%- endif %} -
- -
- - {% 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) }} - {%- endif %} - {%- endif %} - {% endfor %} - - {{ components::pagination(page=page, items=posts|length) }} -
-
-{% endblock %} diff --git a/crates/app/src/public/html/profile/replies.lisp b/crates/app/src/public/html/profile/replies.lisp new file mode 100644 index 0000000..bb2c9c3 --- /dev/null +++ b/crates/app/src/public/html/profile/replies.lisp @@ -0,0 +1,28 @@ +(text "{% extends \"profile/base.html\" %} {% block content %} {% if profile.settings.enable_questions and (user or profile.settings.allow_anonymous_questions) %}") +(div + ("style" "display: contents") + (text "{{ components::create_question_form(receiver=profile.id, header=profile.settings.motivational_header) }}")) + +(text "{%- endif %} {{ macros::profile_nav(selected=\"replies\") }}") +(div + ("class" "card-nest") + (div + ("class" "card small flex gap-2 justify-between items-center") + (div + ("class" "flex gap-2 items-center") + (text "{{ icon \"clock\" }}") + (span + (text "{{ text \"auth:label.recent_replies\" }}"))) + (text "{% if user -%}") + (a + ("href" "/search?profile={{ profile.id }}") + ("class" "button quaternary small") + (text "{{ icon \"search\" }}") + (span + (text "{{ text \"general:link.search\" }}"))) + (text "{%- endif %}")) + (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) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=posts|length) }}"))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/profile/settings.html b/crates/app/src/public/html/profile/settings.html deleted file mode 100644 index 54f010c..0000000 --- a/crates/app/src/public/html/profile/settings.html +++ /dev/null @@ -1,1632 +0,0 @@ -{% extends "root.html" %} {% block head %} -Settings - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
- {% if profile.id != user.id -%} -
- {{ icon "skull" }} - Editing other user's settings! Please be careful. -
- {%- endif %} - - - -
-
- - -
-
- Home timeline -
- -
- - - This represents the timeline the home button takes you - to. -
-
- -
-
- Notifications -
- -
- - Notifications require you to keep {{ config.name }} - open in your browser for real-time updates. This setting - does not sync across browsers. -
-
- - - -
-
- {{ text "settings:label.change_username" }} -
- -
-
- - -
- - -
-
-
- -
-
- {{ icon "skull" }} - {{ text "settings:label.delete_account" }} -
- -
-
- - -
- - -
-
- - -
- - - - - - - - - - - - - - - - - - - - - - - -
-{% endblock %} diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp new file mode 100644 index 0000000..8e091a6 --- /dev/null +++ b/crates/app/src/public/html/profile/settings.lisp @@ -0,0 +1,1544 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Settings - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (text "{% if profile.id != user.id -%}") + (div + ("class" "card w-full red flex gap-2 items-center") + (text "{{ icon \"skull\" }}") + (b + (text "Editing other user's settings! Please be careful."))) + (text "{%- endif %}") + (div + ("class" "pillmenu") + (a + ("data-tab-button" "account") + ("class" "active") + ("href" "#/account") + (text "{{ icon \"smile\" }}") + (span + (text "{{ text \"settings:tab.account\" }}"))) + (a + ("data-tab-button" "profile") + ("href" "#/profile") + (text "{{ icon \"user-round\" }}") + (span + (text "{{ text \"settings:tab.profile\" }}"))) + (a + ("data-tab-button" "theme") + ("href" "#/theme") + (text "{{ icon \"paint-bucket\" }}") + (span + (text "{{ text \"settings:tab.theme\" }}"))) + (a + ("data-tab-button" "sessions") + ("href" "#/sessions") + (text "{{ icon \"cookie\" }}") + (span + (text "{{ text \"settings:tab.sessions\" }}"))) + (a + ("data-tab-button" "connections") + ("href" "#/connections") + (text "{{ icon \"cable\" }}") + (span + (text "{{ text \"settings:tab.connections\" }}")))) + (div + ("class" "w-full flex flex-col gap-2") + ("data-tab" "account") + (div + ("class" "card tertiary flex flex-col gap-2") + ("id" "account_settings") + (div + ("class" "pillmenu") + ("ui_ident" "account_settings_tabs") + (a + ("data-tab-button" "account/security") + ("href" "#/account/security") + (text "{{ icon \"user-lock\" }}") + (span + (text "{{ text \"settings:tab.security\" }}"))) + (a + ("data-tab-button" "account/following") + ("href" "#/account/following") + (text "{{ icon \"rss\" }}") + (span + (text "{{ text \"auth:label.following\" }}"))) + (a + ("data-tab-button" "account/blocks") + ("href" "#/account/blocks") + (text "{{ icon \"shield\" }}") + (span + (text "{{ text \"settings:tab.blocks\" }}"))) + (a + ("data-tab-button" "account/uploads") + ("href" "?page=0#/account/uploads") + (text "{{ icon \"image-up\" }}") + (span + (text "{{ text \"settings:tab.uploads\" }}"))) + (text "{% if config.stripe -%}") + (a + ("data-tab-button" "account/billing") + ("href" "#/account/billing") + (text "{{ icon \"credit-card\" }}") + (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" "set_setting_field('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 %}") + (option + ("value" "{\\\"Stack\\\":\\\"{{ stack.id }}\\\"}") + ("selected" "{% if home is ending_with(stack.id|as_str) -%}true{% else %}false{%- endif %}") + (text "{{ stack.name }} (stack)")) + (text "{% endfor %}")) + (span + ("class" "fade") + (text "This represents the timeline the home button takes you + to.")))) + (div + ("class" "card-nest desktop") + ("ui_ident" "notifications") + (div + ("class" "card small") + (b + (text "Notifications"))) + (div + ("class" "card flex flex-col gap-2") + (button + ("id" "notifications_button")) + (span + ("class" "fade") + (text "Notifications require you to keep {{ config.name }} + open in your browser for real-time updates. This setting + does not sync across browsers.")))) + (script + (text "setTimeout(() => { + trigger(\"me::notifications_button\", [ + document.getElementById(\"notifications_button\"), + ]); + }, 150);")) + (div + ("class" "card-nest") + ("ui_ident" "change_username") + (div + ("class" "card small") + (b + (text "{{ text \"settings:label.change_username\" }}"))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "change_username(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "new_username") + (text "{{ text \"settings:label.new_username\" }}")) + (input + ("type" "text") + ("name" "new_username") + ("id" "new_username") + ("placeholder" "new_username") + ("required" "") + ("minlength" "2"))) + (button + ("class" "primary") + (text "{{ icon \"check\" }}") + (span + (text "{{ text \"general:action.save\" }}")))))) + (div + ("class" "card-nest") + ("ui_ident" "delete_account") + (div + ("class" "card small flex items-center gap-2 red") + (text "{{ icon \"skull\" }}") + (b + (text "{{ text \"settings:label.delete_account\" }}"))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "delete_account(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "current_password") + (text "{{ text \"settings:label.current_password\" }}")) + (input + ("type" "password") + ("name" "current_password") + ("id" "current_password") + ("placeholder" "current_password") + ("required" "") + ("minlength" "6") + ("autocomplete" "off"))) + (button + ("class" "primary") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}"))))) + (button + ("onclick" "save_settings()") + ("id" "save_button") + (text "{{ icon \"check\" }}") + (span + (text "{{ text \"general:action.save\" }}")))) + (div + ("class" "w-full flex flex-col gap-2 hidden") + ("data-tab" "account/security") + (div + ("class" "card tertiary flex flex-col gap-2") + (a + ("href" "#/account") + ("class" "button secondary") + (text "{{ icon \"arrow-left\" }}") + (span + (text "{{ text \"general:action.back\" }}"))) + (div + ("class" "card-nest") + (div + ("class" "card flex items-center gap-2 small") + (text "{{ icon \"user-lock\" }}") + (span + (text "{{ text \"settings:tab.security\" }}"))) + (div + ("class" "card flex flex-col gap-2 secondary") + (div + ("class" "card-nest") + ("ui_ident" "two_factor_authentication") + (div + ("class" "card small") + (b + (text "{{ text \"settings:label.two_factor_authentication\" }}"))) + (div + ("class" "card flex flex-col gap-2") + (text "{% if profile.totp|length == 0 -%}") + (div + ("id" "totp_stuff") + ("style" "display: none") + (span + (text "Scan this QR code in a TOTP authenticator + app (like Google Authenticator):")) + (img + ("id" "totp_qr") + ("style" "max-width: 250px")) + (span + (text "TOTP secret (do NOT share):")) + (pre + ("id" "totp_secret")) + (span + (text "Recovery codes (STORE SAFELY, these can + only be viewed once):")) + (pre + ("id" "totp_recovery_codes"))) + (button + ("class" "quaternary green") + ("onclick" "enable_totp(event)") + (text "Enable TOTP 2FA")) + (text "{% else %}") + (pre + ("id" "totp_recovery_codes") + ("style" "display: none")) + (div + ("class" "flex gap-2 flex-wrap") + (button + ("class" "quaternary red") + ("onclick" "refresh_totp_codes(event)") + (text "Refresh recovery codes")) + (button + ("class" "quaternary red") + ("onclick" "disable_totp(event)") + (text "Disable TOTP 2FA"))) + (text "{%- endif %}"))) + (div + ("class" "card-nest") + ("ui_ident" "change_password") + (div + ("class" "card small") + (b + (text "{{ text \"settings:label.change_password\" }}"))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "change_password(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "current_password") + (text "{{ text \"settings:label.current_password\" }}")) + (input + ("type" "password") + ("name" "current_password") + ("id" "current_password") + ("placeholder" "current_password") + ("required" "") + ("minlength" "6") + ("autocomplete" "off"))) + (div + ("class" "flex flex-col gap-1") + (label + ("for" "new_password") + (text "{{ text \"settings:label.new_password\" }}")) + (input + ("type" "password") + ("name" "new_password") + ("id" "new_password") + ("placeholder" "new_password") + ("required" "") + ("minlength" "6") + ("autocomplete" "off"))) + (button + ("class" "primary") + (text "{{ icon \"check\" }}") + (span + (text "{{ text \"general:action.save\" }}"))))))))) + (div + ("class" "w-full flex flex-col gap-2 hidden") + ("data-tab" "account/following") + (div + ("class" "card tertiary flex flex-col gap-2") + (a + ("href" "#/account") + ("class" "button secondary") + (text "{{ icon \"arrow-left\" }}") + (span + (text "{{ text \"general:action.back\" }}"))) + (div + ("class" "card-nest") + (div + ("class" "card flex items-center gap-2 small") + (text "{{ icon \"rss\" }}") + (span + (text "{{ text \"auth:label.following\" }}"))) + (div + ("class" "card flex flex-col gap-2") + (text "{% for userfollow in following %} {% set user = userfollow[1] %}") + (div + ("class" "card secondary flex flex-wrap gap-2 items-center justify-between") + (div + ("class" "flex gap-2") + (text "{{ components::avatar(username=user.username) }} {{ components::full_username(user=user) }}")) + (div + ("class" "flex gap-2") + (button + ("class" "quaternary red small") + ("onclick" "toggle_follow_user('{{ user.id }}')") + (text "{{ icon \"user-minus\" }}") + (span + (text "{{ text \"auth:action.unfollow\" }}"))) + (a + ("href" "/@{{ user.username }}") + ("class" "button quaternary small") + (text "{{ icon \"external-link\" }}") + (span + (text "{{ text \"requests:action.view_profile\" }}"))))) + (text "{% endfor %}")))) + (script + (text "globalThis.toggle_follow_user = async (uid) => { + await trigger(\"atto::debounce\", [\"users::follow\"]); + + fetch(`/api/v1/auth/user/${uid}/follow`, { + method: \"POST\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + };"))) + (div + ("class" "w-full flex flex-col gap-2 hidden") + ("data-tab" "account/blocks") + (div + ("class" "card tertiary flex flex-col gap-2") + (a + ("href" "#/account") + ("class" "button secondary") + (text "{{ icon \"arrow-left\" }}") + (span + (text "{{ text \"general:action.back\" }}"))) + (div + ("class" "card-nest") + (div + ("class" "card flex items-center gap-2 small") + (text "{{ icon \"users-round\" }}") + (span + (text "{{ text \"settings:label.users\" }}"))) + (div + ("class" "card flex flex-col gap-2") + (text "{% for user in blocks %}") + (div + ("class" "card secondary flex flex-wrap gap-2 items-center justify-between") + (div + ("class" "flex gap-2") + (text "{{ components::avatar(username=user.username) }} {{ components::full_username(user=user) }}")) + (a + ("href" "/@{{ user.username }}") + ("class" "button quaternary small") + (text "{{ icon \"external-link\" }}") + (span + (text "{{ text \"requests:action.view_profile\" }}")))) + (text "{% endfor %}"))))) + (div + ("class" "w-full flex flex-col gap-2 hidden") + ("data-tab" "account/uploads") + (div + ("class" "card tertiary flex flex-col gap-2") + (a + ("href" "#/account") + ("class" "button secondary") + (text "{{ icon \"arrow-left\" }}") + (span + (text "{{ text \"general:action.back\" }}"))) + (div + ("class" "card-nest") + (div + ("class" "card flex items-center gap-2 small") + (text "{{ icon \"image-up\" }}") + (span + (text "{{ text \"settings:tab.uploads\" }}"))) + (div + ("class" "card flex flex-col gap-2 secondary") + (text "{{ components::supporter_ad(body=\"Become a supporter to upload images directly to posts!\") }} {% for upload in uploads %}") + (div + ("class" "card flex flex-wrap gap-2 items-center justify-between") + (div + ("class" "flex gap-2 items-center") + ("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload.id }}'])") + ("style" "cursor: pointer") + (text "{{ icon \"file-image\" }}") + (b + (span + ("class" "date") + (text "{{ upload.created }}")) + (text "({{ upload.what }})"))) + (div + ("class" "flex gap-2") + (button + ("class" "quaternary small") + ("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload.id }}'])") + (text "{{ icon \"view\" }}") + (span + (text "{{ text \"general:action.view\" }}"))) + (button + ("class" "quaternary small red") + ("onclick" "remove_upload('{{ upload.id }}')") + (text "{{ icon \"x\" }}") + (span + (text "{{ text \"stacks:label.remove\" }}"))))) + (text "{% endfor %} {{ components::pagination(page=page, items=uploads|length, key=\"#/account/uploads\") }}") + (script + (text "globalThis.remove_upload = async (id) => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this? This action is permanent.\", + ])) + ) { + return; + } + + fetch(`/api/v1/uploads/${id}`, { + method: \"DELETE\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + };")))))) + (div + ("class" "w-full flex flex-col gap-2 hidden") + ("data-tab" "account/billing") + (div + ("class" "card tertiary flex flex-col gap-2") + (a + ("href" "#/account") + ("class" "button secondary") + (text "{{ icon \"arrow-left\" }}") + (span + (text "{{ text \"general:action.back\" }}"))) + (div + ("class" "card-nest") + (div + ("class" "card flex items-center gap-2 small") + (text "{{ icon \"credit-card\" }}") + (span + (text "{{ text \"settings:tab.billing\" }}"))) + (div + ("class" "card flex flex-col gap-2 secondary") + (text "{% if config.stripe -%}") + (div + ("class" "card-nest") + ("ui_ident" "supporter_card") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"star\" }}") + (b + (text "Supporter status"))) + (div + ("class" "card flex flex-col gap-2") + (text "{% if is_supporter -%}") + (p + (text "You") + (b + (text "are")) + (text "a supporter! Thank you for all + that you do. You can manage your billing + information below.") + (b + (text "Please use your email address you supplied + when paying to login to the billing + portal."))) + (a + ("href" "{{ config.stripe.billing_portal_url }}") + ("class" "button quaternary") + ("target" "_blank") + (text "Manage billing")) + (text "{% else %}") + (p + (text "You're") + (b + (text "not")) + (text "currently a supporter! No + pressure, but it helps us do some pretty cool + things! As a supporter, you'll get:")) + (ul + ("style" "margin-bottom: 1rem") + (li + (text "Vanity badge on profile")) + (li + (text "No more supporter ads (duh)")) + (li + (text "Ability to upload gif avatars/banners")) + (li + (text "Be an admin/owner of up to 10 communities")) + (li + (text "Use custom CSS on your profile")) + (li + (text "Ability to use community emojis outside of + their community")) + (li + (text "Ability to upload and use gif emojis")) + (li + (text "Create infinite stack timelines")) + (li + (text "Ability to upload images to posts")) + (li + (text "Save infinite post drafts")) + (li + (text "Ability to search through all posts"))) + (a + ("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}") + ("class" "button") + ("target" "_blank") + (text "Become a supporter")) + (span + ("class" "fade") + (text "Please use your") + (b + (text "real email")) + (text "when + completing payment. It is required to manage + your billing settings.")) + (text "{%- endif %}"))) + (text "{%- endif %}"))))) + (div + ("class" "w-full hidden flex flex-col gap-2") + ("data-tab" "profile") + (div + ("class" "card tertiary flex flex-col gap-2") + ("id" "profile_settings") + (text "{{ components::supporter_ad(body=\"Become a supporter to upload GIF images!\") }}") + (div + ("class" "card-nest") + ("ui_ident" "change_avatar") + (div + ("class" "card small") + (b + (text "{{ text \"settings:label.change_avatar\" }}"))) + (form + ("class" "card flex gap-2 flex-row flex-wrap items-center") + ("method" "post") + ("enctype" "multipart/form-data") + ("onsubmit" "upload_avatar(event)") + (div + ("class" "flex gap-2 flex-row flex-wrap items-center") + (input + ("id" "avatar_file") + ("name" "file") + ("type" "file") + ("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif") + ("class" "w-content")) + (button + ("class" "primary") + (text "{{ icon \"check\" }}"))) + (span + ("class" "fade") + (text "Images must be less than 8 MB large. Animated GIFs are + only supported for supporter users. GIFs can be at most + 2 MB large.")))) + (div + ("class" "card-nest") + ("ui_ident" "change_banner") + (div + ("class" "card small") + (b + (text "{{ text \"settings:label.change_banner\" }}"))) + (form + ("class" "card flex flex-col gap-2") + ("method" "post") + ("enctype" "multipart/form-data") + ("onsubmit" "upload_banner(event)") + (div + ("class" "flex gap-2 flex-row flex-wrap items-center") + (input + ("id" "banner_file") + ("name" "file") + ("type" "file") + ("accept" "image/png,image/jpeg,image/avif,image/webp") + ("class" "w-content")) + (button + ("class" "primary") + (text "{{ icon \"check\" }}"))) + (span + ("class" "fade") + (text "Use an image of 1100x350px for the best results."))))) + (button + ("onclick" "save_settings()") + ("id" "save_button") + (text "{{ icon \"check\" }}") + (span + (text "{{ text \"general:action.save\" }}")))) + (div + ("class" "card w-full tertiary hidden flex flex-col gap-2") + ("data-tab" "sessions") + (text "{% for token in profile.tokens %}") + (div + ("class" "card w-full flex justify-between flex-collapse gap-2") + (div + ("class" "flex flex-col gap-1") + (b + ("style" " + width: 200px; + overflow: hidden; + text-overflow: ellipsis; + ") + (text "{{ token[1] }}")) + (text "{% if is_helper -%}") + (span + ("class" "flex gap-2 items-center") + (span + ("class" "fade") + (a + ("href" "/api/v1/auth/user/find_by_ip/{{ token[0] }}") + (code + (text "{{ token[0] }}"))))) + (text "{% else %}") + (span + ("class" "fade") + (code + (text "{{ token[0] }}"))) + (text "{%- endif %}") + (span + ("class" "fade date") + (text "{{ token[2] }}"))) + (button + ("class" "quaternary red") + ("onclick" "remove_token('{{ token[1] }}')") + (text "{{ text \"general:action.delete\" }}"))) + (text "{% endfor %}")) + (div + ("class" "w-full hidden flex flex-col gap-2") + ("data-tab" "theme") + (div + ("class" "card tertiary flex flex-col gap-2") + ("id" "theme_settings") + (text "{% if failing_color_keys|length > 0 -%}") + (div + ("class" "card flex flex-col gap-2") + ("style" "background: white; color: black") + ("ui_ident" "awful_contrast") + (div + ("class" "flex gap-2 items-center") + (span + ("class" "desktop") + ("style" "display: contents") + (text "{{ icon \"contrast\" }}")) + (b + (text "Some of your custom colors fail contrast checks:"))) + (ul + (text "{% for key in failing_color_keys %}") + (li + (text "{{ key[0] }}") + (b + (text "{{ key[1] }} < 4.5"))) + (text "{% endfor %}"))) + (text "{%- endif %}") + (div + ("class" "card w-full flex flex-wrap gap-2") + ("ui_ident" "import_export") + (button + ("class" "primary") + ("onclick" "import_theme_settings()") + (text "{{ icon \"upload\" }}") + (span + (text "{{ text \"settings:label.import\" }}"))) + (button + ("class" "secondary") + ("onclick" "export_theme_settings()") + (text "{{ icon \"download\" }}") + (span + (text "{{ text \"settings:label.export\" }}")))) + (text "{{ components::supporter_ad(body=\"Become a supporter to add custom CSS!\") }}") + (div + ("class" "card-nest") + ("ui_ident" "theme_preference") + (div + ("class" "card small") + (b + (text "Theme preference"))) + (div + ("class" "card") + (select + ("onchange" "set_setting_field('theme_preference', event.target.selectedOptions[0].value)") + (option + ("value" "Auto") + ("selected" "{% if user.settings.theme_preference == 'Auto' -%}true{% else %}false{%- endif %}") + (text "Auto")) + (option + ("value" "Light") + ("selected" "{% if user.settings.theme_preference == 'Light' -%}true{% else %}false{%- endif %}") + (text "Light")) + (option + ("value" "Dark") + ("selected" "{% if user.settings.theme_preference == 'Dark' -%}true{% else %}false{%- endif %}") + (text "Dark"))) + (span + ("class" "fade") + (text "This represents your local site theme.")))) + (div + ("class" "card-nest") + ("ui_ident" "profile_theme") + (div + ("class" "card small") + (b + (text "Profile theme base"))) + (div + ("class" "card") + (select + ("onchange" "set_setting_field('profile_theme', event.target.selectedOptions[0].value)") + (option + ("value" "Auto") + ("selected" "{% if user.settings.profile_theme == 'Auto' -%}true{% else %}false{%- endif %}") + (text "Auto")) + (option + ("value" "Light") + ("selected" "{% if user.settings.profile_theme == 'Light' -%}true{% else %}false{%- endif %}") + (text "Light")) + (option + ("value" "Dark") + ("selected" "{% if user.settings.profile_theme == 'Dark' -%}true{% else %}false{%- endif %}") + (text "Dark"))) + (span + ("class" "fade") + (text "This represents the site theme shown to users viewing + your profile."))))) + (button + ("onclick" "save_settings()") + ("id" "save_button") + (text "{{ icon \"check\" }}") + (span + (text "{{ text \"general:action.save\" }}")))) + (div + ("class" "card w-full tertiary hidden flex flex-col gap-2") + ("data-tab" "connections") + (div + ("class" "card w-full flex flex-wrap gap-2") + (text "{% if config.connections.spotify_client_id and not profile.connections.Spotify %}") + (button + ("class" "quaternary") + ("onclick" "trigger('spotify::create_connection', ['{{ config.connections.spotify_client_id }}'])") + (text "{{ icon \"spotify\" }}") + (span + (text "Spotify"))) + (text "{%- endif %} {% if config.connections.last_fm_key and not profile.connections.LastFm %}") + (button + ("class" "quaternary") + ("onclick" "trigger('last_fm::create_connection', ['{{ config.connections.last_fm_key }}'])") + (text "{{ icon \"last_fm\" }}") + (span + (text "Last.fm"))) + (text "{%- endif %}")) + (text "{% for key, value in profile.connections %}") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center gap-2") + (text "{{ components::connection_icon(key=key) }}") + (b + ("class" "flex items-center gap-2") + (text "{% if value[0].data.name -%}") + (span + (text "{{ value[0].data.name }}")) + (span + ("style" "display: contents;") + ("title" "Verified connection") + (text "{{ icon \"badge-check\" }}")) + (text "{% else %}") + (span + (text "{{ key }}")) + (span + ("style" "display: contents;") + (text "{{ icon \"badge-alert\" }}")) + (text "{%- endif %}"))) + (div + ("class" "card flex flex-col gap-2") + (button + ("class" "quaternary red small") + ("onclick" "trigger('connections::delete', ['{{ key }}'])") + (text "{{ text \"general:action.delete\" }}")) + (label + ("for" "{{ key }}-shown") + ("class" "flex items-center gap-2") + (input + ("type" "checkbox") + ("checked" "{% if value[0].show_on_profile -%}true{% else %}false{%- endif %}") + ("id" "{{ key }}-shown") + ("onchange" "trigger('connections::push_con_shown', ['{{ key }}', event.target.checked])") + ("class" "w-content")) + (span + (text "Shown on profile"))))) + (text "{% endfor %}")) + (script + ("type" "application/json") + ("id" "settings_json") + (text "{{ profile.settings|json_encode()|safe }}")) + (script + (text "setTimeout(() => { + const ui = ns(\"ui\"); + const settings = JSON.parse( + document.getElementById(\"settings_json\").innerHTML, + ); + let tokens = JSON.parse(\"{{ user_tokens_serde|safe }}\"); + + globalThis.remove_token = async (id) => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + // reconstruct tokens (but without the token with the given id) + const new_tokens = []; + + for (const token of tokens) { + if (token[1] === id) { + continue; + } + + new_tokens.push(token); + } + + tokens = new_tokens; + + // send request to save + fetch(\"/api/v1/auth/user/{{ profile.id }}/tokens\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify(tokens), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.save_settings = () => { + fetch(\"/api/v1/auth/user/{{ profile.id }}/settings\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify(settings), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.change_password = (e) => { + e.preventDefault(); + fetch(\"/api/v1/auth/user/{{ profile.id }}/password\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + from: e.target.current_password.value, + to: e.target.new_password.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.change_username = async (e) => { + e.preventDefault(); + + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + fetch(\"/api/v1/auth/user/{{ profile.id }}/username\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + to: e.target.new_username.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.delete_account = async (e) => { + e.preventDefault(); + + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + fetch(\"/api/v1/auth/user/{{ profile.id }}\", { + method: \"DELETE\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + password: e.target.current_password.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.upload_avatar = (e) => { + e.preventDefault(); + e.target.querySelector(\"button\").style.display = \"none\"; + + fetch(\"/api/v1/auth/upload/avatar\", { + method: \"POST\", + body: e.target.file.files[0], + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + e.target + .querySelector(\"button\") + .removeAttribute(\"style\"); + }); + + alert(\"Avatar upload in progress. Please wait!\"); + }; + + globalThis.upload_banner = (e) => { + e.preventDefault(); + e.target.querySelector(\"button\").style.display = \"none\"; + + fetch(\"/api/v1/auth/upload/banner\", { + method: \"POST\", + body: e.target.file.files[0], + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + e.target + .querySelector(\"button\") + .removeAttribute(\"style\"); + }); + + alert(\"Banner upload in progress. Please wait!\"); + }; + + globalThis.enable_totp = async (event) => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you want to do this? You must have access to your TOTP codes to disable TOTP.\", + ])) + ) { + return; + } + + fetch(\"/api/v1/auth/user/{{ user.id }}/totp\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + const [secret, qr, recovery_codes] = res.payload; + + document.getElementById(\"totp_secret\").innerText = + secret; + document.getElementById(\"totp_qr\").src = + `data:image/png;base64,${qr}`; + document.getElementById( + \"totp_recovery_codes\", + ).innerText = recovery_codes.join(\"\n\"); + + document.getElementById(\"totp_stuff\").style.display = + \"contents\"; + event.target.remove(); + }); + }; + + globalThis.disable_totp = async (event) => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you want to do this?\", + ])) + ) { + return; + } + + const totp_code = await trigger(\"atto::prompt\", [\"TOTP code:\"]); + + if (!totp_code) { + return; + } + + fetch(\"/api/v1/auth/user/{{ profile.id }}/totp\", { + method: \"DELETE\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ totp: totp_code }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + event.target.remove(); + }); + }; + + globalThis.refresh_totp_codes = async (event) => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you want to do this? The old codes will no longer work.\", + ])) + ) { + return; + } + + const totp_code = await trigger(\"atto::prompt\", [\"TOTP code:\"]); + + if (!totp_code) { + return; + } + + fetch(\"/api/v1/auth/user/{{ profile.id }}/totp/codes\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ totp: totp_code }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + document.getElementById( + \"totp_recovery_codes\", + ).innerText = res.payload.join(\"\n\"); + document.getElementById( + \"totp_recovery_codes\", + ).style.display = \"block\"; + + event.target.remove(); + }); + }; + + const account_settings = + document.getElementById(\"account_settings\"); + const profile_settings = + document.getElementById(\"profile_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\", + ]); + ui.refresh_container(profile_settings, [ + \"supporter_ad\", + \"change_avatar\", + \"change_banner\", + ]); + ui.refresh_container(theme_settings, [ + \"supporter_ad\", + \"awful_contrast\", + \"import_export\", + \"theme_preference\", + \"profile_theme\", + ]); + + ui.generate_settings_ui( + account_settings, + [ + [ + [\"display_name\", \"Display name\"], + \"{{ profile.settings.display_name }}\", + \"input\", + ], + [ + [\"biography\", \"Biography\"], + settings.biography, + \"textarea\", + ], + [[\"status\", \"Status\"], settings.status, \"textarea\"], + [ + [\"warning\", \"Profile warning\"], + settings.warning, + \"textarea\", + ], + ], + settings, + ); + + ui.generate_settings_ui( + profile_settings, + [ + [[], \"Privacy\", \"title\"], + [ + [ + \"require_account\", + \"Require an account to view my profile\", + ], + \"{{ profile.settings.require_account }}\", + \"checkbox\", + ], + [ + [ + \"private_profile\", + \"Only allow users I'm following to view my profile\", + ], + \"{{ profile.settings.private_profile }}\", + \"checkbox\", + ], + [ + [ + \"private_chats\", + \"Only allow users I'm following to add me to chats\", + ], + \"{{ profile.settings.private_chats }}\", + \"checkbox\", + ], + [ + [ + \"private_communities\", + \"Keep my joined communities private\", + ], + \"{{ profile.settings.private_communities }}\", + \"checkbox\", + ], + [ + [\"private_last_seen\", \"Keep my last seen time private\"], + \"{{ profile.settings.private_last_seen }}\", + \"checkbox\", + ], + [[], \"Questions\", \"title\"], + [ + [ + \"enable_questions\", + \"Allow users to ask you questions\", + ], + \"{{ profile.settings.enable_questions }}\", + \"checkbox\", + ], + [ + [ + \"allow_anonymous_questions\", + \"Allow anonymous questions\", + ], + \"{{ profile.settings.allow_anonymous_questions }}\", + \"checkbox\", + ], + [ + [\"motivational_header\", \"Motivational header\"], + settings.motivational_header, + \"input\", + ], + [[], \"Anonymous\", \"title\"], + [ + [\"anonymous_username\", \"Anonymous username\"], + settings.anonymous_username, + \"input\", + ], + [ + [\"anonymous_avatar_url\", \"Anonymous avatar URL\"], + settings.anonymous_avatar_url, + \"input\", + ], + [[], \"Misc\", \"title\"], + [ + [\"hide_dislikes\", \"Hide post dislikes\"], + \"{{ profile.settings.hide_dislikes }}\", + \"checkbox\", + ], + [ + [], + \"Hides dislikes on all posts. Users can still dislike your posts, you just won't be able to see it.\", + \"text\", + ], + ], + settings, + ); + + const can_use_custom_css = + \"{{ user.permissions|has_supporter }}\" === \"true\"; + + const theme_settings_ui_json = [ + [ + [ + \"disable_other_themes\", + \"Disable the profile theme of other users\", + ], + \"{{ profile.settings.disable_other_themes }}\", + \"checkbox\", + ], + [[], \"Theme builder\", \"title\"], + [ + [], + \"Allow the site to build the theme for you given a base hue, saturation, and lightness. Scroll down to the next section to manually build the theme.\", + \"text\", + ], + [ + [\"theme_hue\", \"Theme hue (integer 0-255)\"], + \"{{ profile.settings.theme_hue }}\", + \"input\", + ], + [ + [\"theme_sat\", \"Theme sat (percentage 0%-100%)\"], + \"{{ profile.settings.theme_sat }}\", + \"input\", + ], + [ + [\"theme_lit\", \"Theme lit (percentage 0%-100%)\"], + \"{{ profile.settings.theme_lit }}\", + \"input\", + ], + [[], \"Manual theme builder\", \"title\"], + [[], \"Override individual colors.\", \"text\"], + // surface + [ + [\"theme_color_surface\", \"Surface\"], + \"{{ profile.settings.theme_color_surface }}\", + \"color\", + { + description: \"Page background.\", + }, + ], + [ + [\"theme_color_text\", \"Text\"], + \"{{ profile.settings.theme_color_text }}\", + \"color\", + { + description: + \"Text on elements with the surface background.\", + }, + ], + [ + [\"theme_color_text_link\", \"Links\"], + \"{{ profile.settings.theme_color_text_link }}\", + \"color\", + { + description: \"Links on all elements.\", + }, + ], + // lowered + [[], \"\", \"divider\"], + [ + [\"theme_color_lowered\", \"Lowered\"], + \"{{ profile.settings.theme_color_lowered }}\", + \"color\", + { + description: + \"Some cards, buttons, or anything else with a darker background color than the surface.\", + }, + ], + [ + [\"theme_color_text_lowered\", \"Text\"], + \"{{ profile.settings.theme_color_text_lowered }}\", + \"color\", + { + description: + \"Text on elements with the lowered backgrounds.\", + }, + ], + [ + [\"theme_color_super_lowered\", \"Super lowered\"], + \"{{ profile.settings.theme_color_super_lowered }}\", + \"color\", + { + description: \"Borders.\", + }, + ], + // raised + [[], \"\", \"divider\"], + [ + [\"theme_color_raised\", \"Raised\"], + \"{{ profile.settings.theme_color_raised }}\", + \"color\", + { + description: + \"Some cards, buttons, or anything else with a lighter background color than the surface.\", + }, + ], + [ + [\"theme_color_text_raised\", \"Text\"], + \"{{ profile.settings.theme_color_text_raised }}\", + \"color\", + { + description: + \"Text on elements with the raised backgrounds.\", + }, + ], + [ + [\"theme_color_super_raised\", \"Super raised\"], + \"{{ profile.settings.theme_color_super_raised }}\", + \"color\", + { + description: \"Some borders.\", + }, + ], + // primary + [[], \"\", \"divider\"], + [ + [\"theme_color_primary\", \"Primary\"], + \"{{ profile.settings.theme_color_primary }}\", + \"color\", + { + description: + \"Primary color; navigation bar, some buttons, etc.\", + }, + ], + [ + [\"theme_color_text_primary\", \"Text\"], + \"{{ profile.settings.theme_color_text_primary }}\", + \"color\", + { + description: + \"Text on elements with the primary backgrounds.\", + }, + ], + [ + [\"theme_color_primary_lowered\", \"Lowered\"], + \"{{ profile.settings.theme_color_primary_lowered }}\", + \"color\", + { + description: \"Hover state for primary buttons.\", + }, + ], + // secondary + [[], \"\", \"divider\"], + [ + [\"theme_color_secondary\", \"Secondary\"], + \"{{ profile.settings.theme_color_secondary }}\", + \"color\", + { + description: \"Secondary color.\", + }, + ], + [ + [\"theme_color_text_secondary\", \"Text\"], + \"{{ profile.settings.theme_color_text_secondary }}\", + \"color\", + { + description: + \"Text on elements with the secondary backgrounds.\", + }, + ], + [ + [\"theme_color_secondary_lowered\", \"Lowered\"], + \"{{ profile.settings.theme_color_secondary_lowered }}\", + \"color\", + { + description: \"Hover state for secondary buttons.\", + }, + ], + ]; + + if (can_use_custom_css) { + theme_settings_ui_json.push([[], \"Advanced\", \"title\"]); + theme_settings_ui_json.push([ + [\"theme_custom_css\", \"Custom CSS\"], + settings.theme_custom_css, + \"textarea\", + { + embed_html: + 'Custom CSS input embedded into your theme.', + }, + ]); + } + + ui.generate_settings_ui( + theme_settings, + theme_settings_ui_json, + settings, + ); + + globalThis.import_theme_settings = () => { + const input = document.createElement(\"input\"); + input.type = \"file\"; + input.accept = \"application/json\"; + document.body.appendChild(input); + + input.addEventListener(\"change\", async (e) => { + const json = JSON.parse(await e.target.files[0].text()); + + for (const setting of Object.entries(json)) { + settings[setting[0]] = setting[1]; + } + + input.remove(); + save_settings(); + + setTimeout(() => { + window.location.reload(); + }, 150); + }); + + input.click(); + }; + + globalThis.export_theme_settings = () => { + const theme_settings = { + profile_theme: settings.profile_theme, + }; + + for (const setting of Object.entries(settings)) { + if (setting[0].startsWith(\"theme_\")) { + theme_settings[setting[0]] = setting[1]; + } + } + + const blob = new Blob( + [JSON.stringify(theme_settings, null, 4)], + { + type: \"appliction/json\", + }, + ); + const url = URL.createObjectURL(blob); + + const anchor = document.createElement(\"a\"); + anchor.href = url; + anchor.setAttribute(\"download\", \"theme.json\"); + + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + }; + });"))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/profile/warning.html b/crates/app/src/public/html/profile/warning.html deleted file mode 100644 index 1647721..0000000 --- a/crates/app/src/public/html/profile/warning.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends "root.html" %} {% block head %} -{{ profile.username }} (warning) - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
-
-
-
- {{ components::avatar(username=profile.username, size="24px") }} - {{ profile.username }} -
- - {{ text "auth:label.before_you_view" }} -
- -
- {{ profile.settings.warning|markdown|safe }} -
- - - - {{ icon "x" }} - {{ text "general:action.back" }} - -
-
-
-
-{% endblock %} diff --git a/crates/app/src/public/html/profile/warning.lisp b/crates/app/src/public/html/profile/warning.lisp new file mode 100644 index 0000000..89d3824 --- /dev/null +++ b/crates/app/src/public/html/profile/warning.lisp @@ -0,0 +1,39 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "{{ profile.username }} (warning) - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center justify-between gap-2") + (div + ("class" "flex items-center gap-2") + (text "{{ components::avatar(username=profile.username, size=\"24px\") }}") + (span + (text "{{ profile.username }}"))) + (b + ("class" "notification chip") + (text "{{ text \"auth:label.before_you_view\" }}"))) + (div + ("class" "card flex flex-col gap-2") + (span + ("class" "no_p_margin") + (text "{{ profile.settings.warning|markdown|safe }}")) + (div + ("class" "card w-full secondary flex gap-2") + (button + ("onclick" "trigger('warnings::accept', ['{{ profile.id }}', '{{ warning_hash }}'])") + (text "{{ icon \"check\" }}") + (span + (text "{{ text \"dialog:action.continue\" }}"))) + (a + ("href" "/") + ("class" "button red quaternary") + (text "{{ icon \"x\" }}") + (span + (text "{{ text \"general:action.back\" }}"))))))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/root.lisp b/crates/app/src/public/html/root.lisp index 4366dc9..d576077 100644 --- a/crates/app/src/public/html/root.lisp +++ b/crates/app/src/public/html/root.lisp @@ -10,7 +10,7 @@ (meta ("http-equiv" "content-security-policy") ("content" "default-src 'self' blob: *.spotify.com musicbrainz.org; img-src * data:; media-src *; font-src *; style-src 'unsafe-inline' 'self' blob: *; script-src 'self' 'unsafe-inline' blob: *; object-src 'self' blob: *; upgrade-insecure-requests; connect-src * localhost; frame-src 'self' blob: data: *")) (link ("rel" "icon") ("href" "/public/favicon.svg")) - (link ("rel" "stylesheet") ("href" "/css/style.css"))f + (link ("rel" "stylesheet") ("href" "/css/style.css")) (text "{% if user -%} -{% endblock %} diff --git a/crates/app/src/public/html/stacks/list.lisp b/crates/app/src/public/html/stacks/list.lisp new file mode 100644 index 0000000..2cf6fe8 --- /dev/null +++ b/crates/app/src/public/html/stacks/list.lisp @@ -0,0 +1,93 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "My stacks - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (text "{{ macros::timelines_nav(selected=\"stacks\") }} {% if user -%}") + (div + ("class" "card-nest") + (div + ("class" "card small") + (b + (text "{{ text \"stacks:label.create_new\" }}"))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "create_stack_from_form(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "name") + (text "{{ text \"communities:label.name\" }}")) + (input + ("type" "text") + ("name" "name") + ("id" "name") + ("placeholder" "name") + ("required" "") + ("minlength" "2") + ("maxlength" "32"))) + (button + ("class" "primary") + (text "{{ text \"communities:action.create\" }}")))) + (text "{%- endif %}") + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center justify-between gap-2") + (div + ("class" "flex items-center gap-2") + (text "{{ icon \"award\" }}") + (span + (text "{{ text \"stacks:label.my_stacks\" }}")))) + (div + ("class" "card flex flex-col gap-2") + (text "{% for item in list %}") + (a + ("href" "/stacks/{{ item.id }}") + ("class" "card secondary flex flex-col gap-2") + (div + ("class" "flex items-center gap-2") + (text "{{ icon \"list\" }}") + (b + (text "{{ item.name }}"))) + (span + (text "Created") + (span + ("class" "date") + (text "{{ item.created }}")) + (text "; {{ + item.privacy }}; {{ item.users|length }} users"))) + (text "{% endfor %}")))) + +(script + (text "async function create_stack_from_form(e) { + e.preventDefault(); + await trigger(\"atto::debounce\", [\"stacks::create\"]); + + fetch(\"/api/v1/stacks\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + name: e.target.name.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + setTimeout(() => { + window.location.href = `/stacks/${res.payload}`; + }, 100); + } + }); + }")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/stacks/manage.html b/crates/app/src/public/html/stacks/manage.html deleted file mode 100644 index 7f7cf47..0000000 --- a/crates/app/src/public/html/stacks/manage.html +++ /dev/null @@ -1,325 +0,0 @@ -{% extends "root.html" %} {% block head %} -Stack settings - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
- - -
-
-
-
- Privacy -
- -
- -
-
- -
-
- Mode -
- -
- -
-
- -
-
- Sort -
- -
- -
-
- -
-
- {{ text "stacks:label.change_name" }} -
- -
-
- - -
- - -
-
-
- -
-
- {{ icon "skull" }} - {{ text "communities:label.danger_zone" }} -
- -
- -
-
-
- - - - -
- - -{% endblock %} diff --git a/crates/app/src/public/html/stacks/manage.lisp b/crates/app/src/public/html/stacks/manage.lisp new file mode 100644 index 0000000..8891ec9 --- /dev/null +++ b/crates/app/src/public/html/stacks/manage.lisp @@ -0,0 +1,316 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Stack settings - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (div + ("class" "pillmenu") + (a + ("href" "#/general") + ("data-tab-button" "general") + ("class" "active") + (text "{{ icon \"settings\" }}") + (span + (text "{{ text \"stacks:tab.general\" }}"))) + (a + ("href" "#/users") + ("data-tab-button" "users") + (text "{{ icon \"users\" }}") + (span + (text "{{ text \"stacks:tab.users\" }}")))) + (div + ("class" "w-full flex flex-col gap-2") + ("data-tab" "general") + (div + ("id" "manage_fields") + ("class" "card tertiary flex flex-col gap-2") + (div + ("class" "card-nest") + ("ui_ident" "privacy") + (div + ("class" "card small") + (b + (text "Privacy"))) + (div + ("class" "card") + (select + ("onchange" "save_privacy(event)") + (option + ("value" "Private") + ("selected" "{% if stack.privacy == 'Private' -%}true{% else %}false{%- endif %}") + (text "Private")) + (option + ("value" "Public") + ("selected" "{% if stack.privacy == 'Public' -%}true{% else %}false{%- endif %}") + (text "Public"))))) + (div + ("class" "card-nest") + ("ui_ident" "mode") + (div + ("class" "card small") + (b + (text "Mode"))) + (div + ("class" "card") + (select + ("onchange" "save_mode(event)") + (option + ("value" "Include") + ("selected" "{% if stack.mode == 'Include' -%}true{% else %}false{%- endif %}") + (text "Include")) + (option + ("value" "Exclude") + ("selected" "{% if stack.mode == 'Exclude' -%}true{% else %}false{%- endif %}") + (text "Exclude"))))) + (div + ("class" "card-nest") + ("ui_ident" "sort") + (div + ("class" "card small") + (b + (text "Sort"))) + (div + ("class" "card") + (select + ("onchange" "save_sort(event)") + (option + ("value" "Created") + ("selected" "{% if stack.sort == 'Created' -%}true{% else %}false{%- endif %}") + (text "Created")) + (option + ("value" "Likes") + ("selected" "{% if stack.sort == 'Likes' -%}true{% else %}false{%- endif %}") + (text "Likes"))))) + (div + ("class" "card-nest") + ("ui_ident" "change_name") + (div + ("class" "card small") + (b + (text "{{ text \"stacks:label.change_name\" }}"))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "change_name(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "new_title") + (text "{{ text \"communities:label.name\" }}")) + (input + ("type" "text") + ("name" "name") + ("id" "name") + ("placeholder" "name") + ("required" "") + ("minlength" "2"))) + (button + ("class" "primary") + (text "{{ icon \"check\" }}") + (span + (text "{{ text \"general:action.save\" }}")))))) + (div + ("class" "card-nest") + ("ui_ident" "danger_zone") + (div + ("class" "card small flex gap-1 items-center red") + (text "{{ icon \"skull\" }}") + (b + (text "{{ text \"communities:label.danger_zone\" }}"))) + (div + ("class" "card flex flex-wrap gap-2") + (button + ("class" "red quaternary") + ("onclick" "delete_stack()") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}")))))) + (div + ("class" "card w-full flex flex-col gap-2 hidden") + ("data-tab" "users") + (button + ("onclick" "add_user()") + (text "{{ icon \"plus\" }}") + (span + (text "{{ text \"stacks:label.add_user\" }}"))) + (text "{% for user in users %}") + (div + ("class" "card secondary flex flex-wrap gap-2 items-center justify-between") + (div + ("class" "flex gap-2") + (text "{{ components::avatar(username=user.username) }} {{ components::full_username(user=user) }}")) + (button + ("class" "quaternary small red") + ("onclick" "remove_user('{{ user.username }}')") + (text "{{ icon \"x\" }}") + (span + (text "{{ text \"stacks:label.remove\" }}")))) + (text "{% endfor %}")) + (div + ("class" "flex gap-2 flex-wrap") + (a + ("href" "/stacks/{{ stack.id }}") + ("class" "button secondary") + (text "{{ icon \"arrow-left\" }}") + (span + (text "{{ text \"general:action.back\" }}"))))) + +(script + (text "globalThis.add_user = async () => { + await trigger(\"atto::debounce\", [\"stacks::add_user\"]); + const username = await trigger(\"atto::prompt\", [\"Username:\"]); + + if (!username) { + return; + } + + fetch(`/api/v1/stacks/{{ stack.id }}/users`, { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + username, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.remove_user = async (username) => { + await trigger(\"atto::debounce\", [\"stacks::remove_user\"]); + fetch(`/api/v1/stacks/{{ stack.id }}/users`, { + method: \"DELETE\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + username, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.save_privacy = (event, mode) => { + const selected = event.target.selectedOptions[0]; + fetch(`/api/v1/stacks/{{ stack.id }}/privacy`, { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + privacy: selected.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.save_mode = (event, mode) => { + const selected = event.target.selectedOptions[0]; + fetch(`/api/v1/stacks/{{ stack.id }}/mode`, { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + mode: selected.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.save_sort = (event, mode) => { + const selected = event.target.selectedOptions[0]; + fetch(`/api/v1/stacks/{{ stack.id }}/sort`, { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + sort: selected.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.change_name = async (e) => { + e.preventDefault(); + + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this?\", + ])) + ) { + return; + } + + fetch(\"/api/v1/stacks/{{ stack.id }}/name\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + name: e.target.name.value, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + }; + + globalThis.delete_stack = async () => { + if ( + !(await trigger(\"atto::confirm\", [ + \"Are you sure you would like to do this? This action is permanent.\", + ])) + ) { + return; + } + + fetch(\"/api/v1/stacks/{{ stack.id }}\", { + method: \"DELETE\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + }); + };")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/stacks/posts.html b/crates/app/src/public/html/stacks/posts.html deleted file mode 100644 index ffed706..0000000 --- a/crates/app/src/public/html/stacks/posts.html +++ /dev/null @@ -1,74 +0,0 @@ -{% extends "root.html" %} {% block head %} -{{ stack.name }} - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
- {{ macros::timelines_nav(selected="stacks") }} -
-
-
- {{ icon "list" }} - {{ stack.name }} -
- - {% if user and user.id == stack.owner -%} - - {{ icon "pencil" }} - {{ text "general:action.manage" }} - - {%- endif %} -
- - -
- {% if list|length == 0 -%} -

No posts yet! Maybe add a user to this stack!

- {%- endif %} - - {% for post in list %} - {% 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) }} - {% else %} - {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2]) }} - {%- endif %} - {%- endif %} - {% endfor %} - - {{ components::pagination(page=page, items=list|length) }} -
-
-
- - -{% endblock %} diff --git a/crates/app/src/public/html/stacks/posts.lisp b/crates/app/src/public/html/stacks/posts.lisp new file mode 100644 index 0000000..eb86635 --- /dev/null +++ b/crates/app/src/public/html/stacks/posts.lisp @@ -0,0 +1,37 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "{{ stack.name }} - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (text "{{ macros::timelines_nav(selected=\"stacks\") }}") + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center justify-between gap-2") + (div + ("class" "flex items-center gap-2") + (text "{{ icon \"list\" }}") + (span + (text "{{ stack.name }}"))) + (text "{% if user and user.id == stack.owner -%}") + (a + ("href" "/stacks/{{ stack.id }}/manage") + ("class" "button quaternary small") + (text "{{ icon \"pencil\" }}") + (span + (text "{{ text \"general:action.manage\" }}"))) + (text "{%- endif %}")) + (div + ("class" "card w-full flex flex-col gap-2") + (text "{% if list|length == 0 -%}") + (p + (text "No posts yet! Maybe ") + (a + ("href" "/stacks/{{ stack.id }}/manage#/users") + (text "add a user to this stack")) + (text "!")) + (text "{%- endif %} {% for post in list %} {% 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) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2]) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=list|length) }}")))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/all.html b/crates/app/src/public/html/timelines/all.html deleted file mode 100644 index 55658e1..0000000 --- a/crates/app/src/public/html/timelines/all.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends "root.html" %} {% block head %} -Latest posts - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
- {{ macros::timelines_nav(selected="all") }} {{ - macros::timelines_secondary_nav(posts="/all", questions="/all/questions") }} - {% if not user -%} -
-
- {{ icon "heart" }} - {{ text "general:label.better_with_account" }} -
- - -
- {%- endif %} - - -
- {% for post in list %} - {% 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) }} - {% else %} - {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2]) }} - {%- endif %} - {%- endif %} - {% endfor %} - - {{ components::pagination(page=page, items=list|length) }} -
-
-{% endblock %} diff --git a/crates/app/src/public/html/timelines/all.lisp b/crates/app/src/public/html/timelines/all.lisp new file mode 100644 index 0000000..84c6589 --- /dev/null +++ b/crates/app/src/public/html/timelines/all.lisp @@ -0,0 +1,35 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Latest posts - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (text "{{ macros::timelines_nav(selected=\"all\") }} {{ macros::timelines_secondary_nav(posts=\"/all\", questions=\"/all/questions\") }} {% if not user -%}") + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center gap-2") + (text "{{ icon \"heart\" }}") + (b + (text "{{ text \"general:label.better_with_account\" }}"))) + (div + ("class" "card flex gap-2") + (a + ("href" "/auth/login") + ("class" "button") + (text "{{ icon \"log-in\" }}") + (span + (text "{{ text \"auth:action.login\" }}"))) + (a + ("href" "/auth/register") + ("class" "button secondary") + (text "{{ icon \"user-plus\" }}") + (span + (text "{{ text \"auth:action.register\" }}"))))) + (text "{%- endif %}") + (div + ("class" "card w-full flex flex-col gap-2") + (text "{% for post in list %} {% 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) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2]) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=list|length) }}"))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/all_questions.html b/crates/app/src/public/html/timelines/all_questions.html deleted file mode 100644 index e95db77..0000000 --- a/crates/app/src/public/html/timelines/all_questions.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "root.html" %} {% block head %} -Latest questions - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
- {{ macros::timelines_nav(selected="all") }} {{ - macros::timelines_secondary_nav(posts="/all", questions="/all/questions", - selected="questions") }} - - -
- {% for question in list %} - {{ components::global_question(question=question, can_manage_questions=false, secondary=true) }} - {% endfor %} - - {{ components::pagination(page=page, items=list|length) }} -
-
-{% endblock %} diff --git a/crates/app/src/public/html/timelines/all_questions.lisp b/crates/app/src/public/html/timelines/all_questions.lisp new file mode 100644 index 0000000..4425f11 --- /dev/null +++ b/crates/app/src/public/html/timelines/all_questions.lisp @@ -0,0 +1,13 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Latest questions - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (text "{{ macros::timelines_nav(selected=\"all\") }} {{ macros::timelines_secondary_nav(posts=\"/all\", questions=\"/all/questions\", selected=\"questions\") }}") + (div + ("class" "card w-full flex flex-col gap-2") + (text "{% for question in list %} {{ components::global_question(question=question, can_manage_questions=false, secondary=true) }} {% endfor %} {{ components::pagination(page=page, items=list|length) }}"))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/following.html b/crates/app/src/public/html/timelines/following.html deleted file mode 100644 index 00b9123..0000000 --- a/crates/app/src/public/html/timelines/following.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "root.html" %} {% block head %} -Following - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
- {{ macros::timelines_nav(selected="following") }} {{ - macros::timelines_secondary_nav(posts="/following", - questions="/following/questions") }} - - -
- {% for post in list %} - {% 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) }} - {% else %} - {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2]) }} - {%- endif %} - {%- endif %} - {% endfor %} - - {{ components::pagination(page=page, items=list|length) }} -
-
-{% endblock %} diff --git a/crates/app/src/public/html/timelines/following.lisp b/crates/app/src/public/html/timelines/following.lisp new file mode 100644 index 0000000..852dca9 --- /dev/null +++ b/crates/app/src/public/html/timelines/following.lisp @@ -0,0 +1,13 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Following - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (text "{{ macros::timelines_nav(selected=\"following\") }} {{ macros::timelines_secondary_nav(posts=\"/following\", questions=\"/following/questions\") }}") + (div + ("class" "card w-full flex flex-col gap-2") + (text "{% for post in list %} {% 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) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2]) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=list|length) }}"))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/following_questions.html b/crates/app/src/public/html/timelines/following_questions.html deleted file mode 100644 index ff4e994..0000000 --- a/crates/app/src/public/html/timelines/following_questions.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "root.html" %} {% block head %} -Following (questions) - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
- {{ macros::timelines_nav(selected="following") }} {{ - macros::timelines_secondary_nav(posts="/following", - questions="/following/questions", selected="questions") }} - - -
- {% for question in list %} - {{ components::global_question(question=question, can_manage_questions=false, secondary=true) }} - {% endfor %} - - {{ components::pagination(page=page, items=list|length) }} -
-
-{% endblock %} diff --git a/crates/app/src/public/html/timelines/following_questions.lisp b/crates/app/src/public/html/timelines/following_questions.lisp new file mode 100644 index 0000000..d2adf28 --- /dev/null +++ b/crates/app/src/public/html/timelines/following_questions.lisp @@ -0,0 +1,13 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Following (questions) - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (text "{{ macros::timelines_nav(selected=\"following\") }} {{ macros::timelines_secondary_nav(posts=\"/following\", questions=\"/following/questions\", selected=\"questions\") }}") + (div + ("class" "card w-full flex flex-col gap-2") + (text "{% for question in list %} {{ components::global_question(question=question, can_manage_questions=false, secondary=true) }} {% endfor %} {{ components::pagination(page=page, items=list|length) }}"))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/home.html b/crates/app/src/public/html/timelines/home.html deleted file mode 100644 index 920a561..0000000 --- a/crates/app/src/public/html/timelines/home.html +++ /dev/null @@ -1,40 +0,0 @@ -{% extends "root.html" %} {% block head %} -{{ config.name }} -{% endblock %} {% block body %} {{ macros::nav(selected="home") }} -
- - {{ macros::timelines_nav(selected="home") }} {{ - macros::timelines_secondary_nav(posts="/", questions="/questions") }} - - {% if list|length == 0 and page == 0 -%} -
-
- ✨ Welcome to {{ config.name }}! -
- -
-

Join some communities to populate your home timeline!

-

- You can get started by - searching for a community to join! -

-
-
- {% else %} - -
- {% for post in list %} - {% 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) }} - {% else %} - {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2]) }} - {%- endif %} - {% endfor %} - - {{ components::pagination(page=page, items=list|length) }} -
- {%- endif %} -
-{% endblock %} diff --git a/crates/app/src/public/html/timelines/home.lisp b/crates/app/src/public/html/timelines/home.lisp new file mode 100644 index 0000000..ed7a66a --- /dev/null +++ b/crates/app/src/public/html/timelines/home.lisp @@ -0,0 +1,33 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "{{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"home\") }}") +(main + ("class" "flex flex-col gap-2") + (text "{{ macros::timelines_nav(selected=\"home\") }} {{ macros::timelines_secondary_nav(posts=\"/\", questions=\"/questions\") }} {% if list|length == 0 and page == 0 -%}") + (div + ("class" "card-nest") + (div + ("class" "card small") + (b + (text "✨ Welcome to ") + (i + (text "{{ config.name }}")) + (text "!"))) + (div + ("class" "card no_p_margin") + (p + (text "Join some communities to populate your home timeline!")) + (p + (text "You can get started by ") + (a + ("href" "/communities/search") + (text "searching for a community to join!"))))) + (text "{% else %}") + (div + ("class" "card w-full flex flex-col gap-2") + (text "{% for post in list %} {% 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) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2]) }} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=list|length) }}")) + (text "{%- endif %}")) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/home_questions.html b/crates/app/src/public/html/timelines/home_questions.html deleted file mode 100644 index 0f5199d..0000000 --- a/crates/app/src/public/html/timelines/home_questions.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "root.html" %} {% block head %} -From my communities (questions) - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
- {{ macros::timelines_nav(selected="home") }} {{ - macros::timelines_secondary_nav(posts="/", questions="/questions", - selected="questions") }} - - -
- {% for question in list %} - {{ components::global_question(question=question, can_manage_questions=false, secondary=true) }} - {% endfor %} - - {{ components::pagination(page=page, items=list|length) }} -
-
-{% endblock %} diff --git a/crates/app/src/public/html/timelines/home_questions.lisp b/crates/app/src/public/html/timelines/home_questions.lisp new file mode 100644 index 0000000..3406d22 --- /dev/null +++ b/crates/app/src/public/html/timelines/home_questions.lisp @@ -0,0 +1,13 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "From my communities (questions) - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (text "{{ macros::timelines_nav(selected=\"home\") }} {{ macros::timelines_secondary_nav(posts=\"/\", questions=\"/questions\", selected=\"questions\") }}") + (div + ("class" "card w-full flex flex-col gap-2") + (text "{% for question in list %} {{ components::global_question(question=question, can_manage_questions=false, secondary=true) }} {% endfor %} {{ components::pagination(page=page, items=list|length) }}"))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/popular.html b/crates/app/src/public/html/timelines/popular.html deleted file mode 100644 index 745adc2..0000000 --- a/crates/app/src/public/html/timelines/popular.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "root.html" %} {% block head %} -Popular - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav(selected="popular") }} -
- {{ macros::timelines_nav(selected="popular") }} {{ - macros::timelines_secondary_nav(posts="/popular", - questions="/popular/questions") }} - - -
- {% for post in list %} - {% 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) }} - {% else %} - {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2]) }} - {%- endif %} - {%- endif %} - {% endfor %} - - {{ components::pagination(page=page, items=list|length) }} -
-
-{% endblock %} diff --git a/crates/app/src/public/html/timelines/popular.lisp b/crates/app/src/public/html/timelines/popular.lisp new file mode 100644 index 0000000..10eda3c --- /dev/null +++ b/crates/app/src/public/html/timelines/popular.lisp @@ -0,0 +1,13 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Popular - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"popular\") }}") +(main + ("class" "flex flex-col gap-2") + (text "{{ macros::timelines_nav(selected=\"popular\") }} {{ macros::timelines_secondary_nav(posts=\"/popular\", questions=\"/popular/questions\") }}") + (div + ("class" "card w-full flex flex-col gap-2") + (text "{% for post in list %} {% 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) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2]) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=list|length) }}"))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/popular_questions.html b/crates/app/src/public/html/timelines/popular_questions.html deleted file mode 100644 index 65e17e9..0000000 --- a/crates/app/src/public/html/timelines/popular_questions.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "root.html" %} {% block head %} -Popular (questions) - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
- {{ macros::timelines_nav(selected="popular") }} {{ - macros::timelines_secondary_nav(posts="/popular", - questions="/popular/questions", selected="questions") }} - - -
- {% for question in list %} - {{ components::global_question(question=question, can_manage_questions=false, secondary=true) }} - {% endfor %} - - {{ components::pagination(page=page, items=list|length) }} -
-
-{% endblock %} diff --git a/crates/app/src/public/html/timelines/popular_questions.lisp b/crates/app/src/public/html/timelines/popular_questions.lisp new file mode 100644 index 0000000..293ad85 --- /dev/null +++ b/crates/app/src/public/html/timelines/popular_questions.lisp @@ -0,0 +1,13 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Popular (questions) - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (text "{{ macros::timelines_nav(selected=\"popular\") }} {{ macros::timelines_secondary_nav(posts=\"/popular\", questions=\"/popular/questions\", selected=\"questions\") }}") + (div + ("class" "card w-full flex flex-col gap-2") + (text "{% for question in list %} {{ components::global_question(question=question, can_manage_questions=false, secondary=true) }} {% endfor %} {{ components::pagination(page=page, items=list|length) }}"))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/search.html b/crates/app/src/public/html/timelines/search.html deleted file mode 100644 index 4970970..0000000 --- a/crates/app/src/public/html/timelines/search.html +++ /dev/null @@ -1,82 +0,0 @@ -{% extends "root.html" %} {% block head %} -Search - {{ config.name }} -{% endblock %} {% block body %} {{ macros::nav() }} -
- {{ macros::timelines_nav(selected="search") }} -
-
-
- {{ icon "search" }} {% if not profile -%} - {{ text "general:link.search" }} - {% else %} - {{ components::full_username(user=profile) }} - {%- endif %} -
-
- -
- {% if not profile and not user.permissions|has_supporter -%} {{ - components::supporter_ad(body="Become a supporter for full-site - search!") }} {% else %} -
-
- - - {% if profile -%} - - {%- endif %} - -
- - - {% if config.manuals.search_help -%} - - {{ icon "circle-help" }} - - {%- endif %} -
-
-
- {%- endif %} - - - {% for post in list %} - {% 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) }} - {% else %} - {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2]) }} - {%- endif %} - {%- endif %} - {% endfor %} - - {% if profile -%} - {{ components::pagination(page=page, items=list|length, key="&profile=" ~ profile.id, value="&query=" ~ query) }} - {% else %} - {{ components::pagination(page=page, items=list|length, key="&query=" ~ query) }} - {%- endif %} -
-
-
-{% endblock %} diff --git a/crates/app/src/public/html/timelines/search.lisp b/crates/app/src/public/html/timelines/search.lisp new file mode 100644 index 0000000..649e179 --- /dev/null +++ b/crates/app/src/public/html/timelines/search.lisp @@ -0,0 +1,61 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Search - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav() }}") +(main + ("class" "flex flex-col gap-2") + (text "{{ macros::timelines_nav(selected=\"search\") }}") + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center justify-between gap-2") + (div + ("class" "flex items-center gap-2") + (text "{{ icon \"search\" }} {% if not profile -%}") + (span + (text "{{ text \"general:link.search\" }}")) + (text "{% else %}") + (span + (text "{{ components::full_username(user=profile) }}")) + (text "{%- endif %}"))) + (div + ("class" "card w-full flex flex-col gap-2") + (text "{% if not profile and not user.permissions|has_supporter -%} {{ components::supporter_ad(body=\"Become a supporter for full-site search!\") }} {% else %}") + (form + ("class" "flex flex-col gap-2") + (div + ("class" "flex flex-row gap-2") + (input + ("type" "text") + ("name" "query") + ("id" "query") + ("required" "") + ("value" "{{ query }}") + ("placeholder" "{% if profile -%}Search {{ profile.username }}'s posts{% else %}Search all posts{%- endif %}") + ("autocomplete" "off")) + (text "{% if profile -%}") + (input + ("type" "text") + ("class" "hidden") + ("value" "{{ profile.id }}") + ("name" "profile") + ("id" "profile")) + (text "{%- endif %}") + (div + ("class" "flex gap-2 flex-row") + (button + ("class" "small square") + (text "{{ icon \"search\" }}")) + (text "{% if config.manuals.search_help -%}") + (a + ("class" "button small square secondary") + ("title" "Search help") + ("href" "{{ config.manuals.search_help }}") + ("target" "_blank") + (text "{{ icon \"circle-help\" }}")) + (text "{%- endif %}")))) + (text "{%- endif %}") + (text "{% for post in list %} {% 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) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2]) }} {%- endif %} {%- endif %} {% endfor %} {% if profile -%} {{ components::pagination(page=page, items=list|length, key=\"&profile=\" ~ profile.id, value=\"&query=\" ~ query) }} {% else %} {{ components::pagination(page=page, items=list|length, key=\"&query=\" ~ query) }} {%- endif %}")))) + +(text "{% endblock %}") diff --git a/crates/app/src/public/js/atto.js b/crates/app/src/public/js/atto.js index 3dc1966..2cd1100 100644 --- a/crates/app/src/public/js/atto.js +++ b/crates/app/src/public/js/atto.js @@ -80,6 +80,10 @@ media_theme_pref(); element.removeAttribute("selected"); } + for (const element of document.querySelectorAll('[checked="false"]')) { + element.removeAttribute("checked"); + } + for (const element of document.querySelectorAll('[selected="true"]')) { element.parentElement.value = element.value; } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index b231cde..c8636c9 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-core" -version = "4.0.0" +version = "4.5.0" edition = "2024" [features] diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index 6dbf96b..b7fc362 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -280,14 +280,11 @@ impl DataManager { continue; } - if ua1.id != ua.id { + if ua1.id != ua.id && !ua1.permissions.check(FinePermission::MANAGE_POSTS) { if let Some(is_following) = seen_user_follow_statuses.get(&(ua.id, ua1.id)) { - if !is_following - && ua.id != ua1.id - && !ua1.permissions.check(FinePermission::MANAGE_POSTS) - { + if !is_following && ua.id != ua1.id { private_post_replying!(post, replying_posts, ua1, self); } } else { @@ -295,7 +292,6 @@ impl DataManager { .get_userfollow_by_initiator_receiver(ua.id, ua1.id) .await .is_err() - && !ua1.permissions.check(FinePermission::MANAGE_POSTS) && ua.id != ua1.id { // post owner is not following us diff --git a/crates/l10n/Cargo.toml b/crates/l10n/Cargo.toml index 16b655c..98312a8 100644 --- a/crates/l10n/Cargo.toml +++ b/crates/l10n/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-l10n" -version = "4.0.0" +version = "4.5.0" edition = "2024" authors.workspace = true repository.workspace = true diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index 53212b8..c161003 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-shared" -version = "4.0.0" +version = "4.5.0" edition = "2024" authors.workspace = true repository.workspace = true diff --git a/tools/html_to_lisp.html b/tools/html_to_lisp.html new file mode 100644 index 0000000..53546c5 --- /dev/null +++ b/tools/html_to_lisp.html @@ -0,0 +1,138 @@ + + + + + + Document + + + + +
+
+ Data + + +
+ +
+ +
+ Output + + +
+ +
+ +
+ +
+ +
+ + + +