From a6140f7c8c0c7beaa2ade8307a989431ca3959de Mon Sep 17 00:00:00 2001 From: trisua Date: Mon, 9 Jun 2025 16:45:36 -0400 Subject: [PATCH] add: forges ui TODO: forges tickets feed, posts open/closed state --- Cargo.lock | 58 +- crates/app/Cargo.toml | 11 +- crates/app/src/assets.rs | 12 + crates/app/src/langs/en-US.toml | 5 + crates/app/src/public/css/root.css | 344 ++++++++++ crates/app/src/public/css/style.css | 620 +----------------- crates/app/src/public/css/utility.css | 213 ++++++ crates/app/src/public/html/body.lisp | 305 +++++++++ crates/app/src/public/html/chats/app.lisp | 12 +- .../app/src/public/html/communities/base.lisp | 224 +------ .../app/src/public/html/communities/list.lisp | 4 - .../src/public/html/communities/settings.lisp | 89 +-- crates/app/src/public/html/components.lisp | 218 +++++- crates/app/src/public/html/forge/base.lisp | 83 +++ crates/app/src/public/html/forge/home.lisp | 80 +++ crates/app/src/public/html/forge/info.lisp | 6 + crates/app/src/public/html/macros.lisp | 20 +- crates/app/src/public/html/post/likes.lisp | 2 +- .../src/public/html/profile/followers.lisp | 2 +- .../src/public/html/profile/following.lisp | 2 +- .../app/src/public/html/profile/settings.lisp | 6 +- crates/app/src/public/html/root.lisp | 306 +-------- crates/app/src/routes/api/v1/auth/profile.rs | 2 +- .../src/routes/api/v1/channels/messages.rs | 3 +- .../routes/api/v1/communities/communities.rs | 23 +- crates/app/src/routes/api/v1/mod.rs | 2 + crates/app/src/routes/assets.rs | 2 + crates/app/src/routes/mod.rs | 2 + crates/app/src/routes/pages/communities.rs | 42 +- crates/app/src/routes/pages/forge.rs | 126 ++++ crates/app/src/routes/pages/mod.rs | 5 + crates/core/Cargo.toml | 6 +- crates/core/src/database/auth.rs | 7 +- crates/core/src/database/communities.rs | 31 +- crates/core/src/database/messages.rs | 3 +- crates/core/src/database/posts.rs | 26 +- crates/core/src/model/communities.rs | 26 +- crates/l10n/Cargo.toml | 2 +- sql_changes/communities_is_forge.sql | 2 + sql_changes/communities_post_count.sql | 2 + 40 files changed, 1664 insertions(+), 1270 deletions(-) create mode 100644 crates/app/src/public/css/root.css create mode 100644 crates/app/src/public/css/utility.css create mode 100644 crates/app/src/public/html/body.lisp create mode 100644 crates/app/src/public/html/forge/base.lisp create mode 100644 crates/app/src/public/html/forge/home.lisp create mode 100644 crates/app/src/public/html/forge/info.lisp create mode 100644 crates/app/src/routes/pages/forge.rs create mode 100644 sql_changes/communities_is_forge.sql create mode 100644 sql_changes/communities_post_count.sql diff --git a/Cargo.lock b/Cargo.lock index 49ea63b..cb439e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1109,9 +1109,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "foldhash", ] @@ -2027,9 +2027,9 @@ dependencies = [ [[package]] name = "oiseau" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8082a9c49b3d0cf132dcfdeba5b3c8a21c3a21a98623fc1f8571e6fc3956ce38" +checksum = "99b097052e28781d560587373845626a85460969a55d180fc418aecd58f6fef3" dependencies = [ "bb8-postgres", "redis", @@ -2662,9 +2662,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.18" +version = "0.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5" +checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119" dependencies = [ "base64 0.22.1", "bytes", @@ -2957,9 +2957,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -3061,9 +3061,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smart-default" @@ -3302,7 +3302,6 @@ dependencies = [ "image", "mime_guess", "pathbufd", - "redis", "regex", "reqwest", "serde", @@ -3582,9 +3581,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -3594,18 +3593,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.26" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", @@ -3617,9 +3616,9 @@ dependencies = [ [[package]] name = "toml_write" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "totp-rs" @@ -3656,12 +3655,13 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.4" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "bitflags 2.9.1", "bytes", + "futures-core", "futures-util", "http 1.3.1", "http-body 1.0.1", @@ -4147,9 +4147,9 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "whoami" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" dependencies = [ "redox_syscall", "wasite", @@ -4445,9 +4445,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.7" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] @@ -4493,18 +4493,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 3f0e132..6464be0 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [features] postgres = ["tetratto-core/postgres"] sqlite = ["tetratto-core/sqlite"] -redis = ["tetratto-core/redis", "dep:redis"] +redis = ["tetratto-core/redis"] default = ["sqlite", "redis"] [dependencies] @@ -15,7 +15,7 @@ serde = { version = "1.0.219", features = ["derive"] } tera = "1.20.0" tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } -tower-http = { version = "0.6.4", features = ["trace", "fs", "catch-panic"] } +tower-http = { version = "0.6.6", features = ["trace", "fs", "catch-panic"] } axum = { version = "0.8.4", features = ["macros", "ws"] } tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] } axum-extra = { version = "0.10.1", features = ["cookie", "multipart"] } @@ -27,18 +27,13 @@ tetratto-core = { path = "../core", features = [ tetratto-l10n = { path = "../l10n" } image = "0.25.6" -reqwest = { version = "0.12.18", features = ["json", "stream"] } +reqwest = { version = "0.12.19", features = ["json", "stream"] } regex = "1.11.1" serde_json = "1.0.140" mime_guess = "2.0.5" cf-turnstile = "0.2.0" contrasted = "0.1.3" futures-util = "0.3.31" - -redis = { version = "0.31.0", features = [ - "aio", - "tokio-comp", -], optional = true } async-stripe = { version = "0.41.0", features = [ "events", "checkout", diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index 6747948..a63d795 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -31,6 +31,8 @@ pub const FAVICON: &str = include_str!("./public/images/favicon.svg"); // css pub const STYLE_CSS: &str = include_str!("./public/css/style.css"); +pub const ROOT_CSS: &str = include_str!("./public/css/root.css"); +pub const UTILITY_CSS: &str = include_str!("./public/css/utility.css"); // js pub const LOADER_JS: &str = include_str!("./public/js/loader.js"); @@ -39,6 +41,7 @@ pub const ME_JS: &str = include_str!("./public/js/me.js"); pub const STREAMS_JS: &str = include_str!("./public/js/streams.js"); // html +pub const BODY: &str = include_str!("./public/html/body.lisp"); 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.lisp"); @@ -113,6 +116,10 @@ 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"); +pub const FORGE_HOME: &str = include_str!("./public/html/forge/home.lisp"); +pub const FORGE_BASE: &str = include_str!("./public/html/forge/base.lisp"); +pub const FORGE_INFO: &str = include_str!("./public/html/forge/info.lisp"); + // langs pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml"); @@ -312,6 +319,7 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD { let mut plugins = lisp_plugins(); let html_path = PathBufD::current().join(&config.dirs.templates); + write_template!(html_path->"body.html"(crate::assets::BODY) --config=config --lisp plugins); 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 --lisp plugins); @@ -381,6 +389,10 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD { 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); + write_template!(html_path->"forge/home.html"(crate::assets::FORGE_HOME) -d "forge" --config=config --lisp plugins); + write_template!(html_path->"forge/base.html"(crate::assets::FORGE_BASE) --config=config --lisp plugins); + write_template!(html_path->"forge/info.html"(crate::assets::FORGE_INFO) --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 4d92c56..52f56e7 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -203,3 +203,8 @@ version = "1.0.0" "stacks:tab.users" = "Users" "stacks:label.add_user" = "Add user" "stacks:label.remove" = "Remove" + +"forge:label.my_forges" = "My forges" +"forge:label.create_new" = "Create new forge" +"forge:tab.info" = "Info" +"forge:tab.tickets" = "Tickets" diff --git a/crates/app/src/public/css/root.css b/crates/app/src/public/css/root.css new file mode 100644 index 0000000..6e349be --- /dev/null +++ b/crates/app/src/public/css/root.css @@ -0,0 +1,344 @@ +@import url("utility.css"); + +:root { + color-scheme: light dark; + --hue: 16; + --sat: 6%; + --lit: 0%; + --color-surface: hsl(var(--hue), var(--sat), calc(97% - var(--lit))); + --color-lowered: hsl(var(--hue), var(--sat), calc(94% - var(--lit))); + --color-raised: hsl(var(--hue), var(--sat), calc(99% - var(--lit))); + --color-super-lowered: hsl(var(--hue), var(--sat), calc(85% - var(--lit))); + --color-super-raised: hsl(var(--hue), var(--sat), calc(100% - var(--lit))); + --color-text: hsl(0, 0%, 0%); + --color-text-raised: var(--color-text); + --color-text-lowered: var(--color-text); + + --color-primary: hsl(330, 18%, 26%); + --color-primary-lowered: hsl(330, 18%, 21%); + --color-text-primary: hsl(0, 0%, 100%); + + --color-secondary: hsl(6, 18%, 66%); + --color-secondary-lowered: hsl(6, 18%, 61%); + --color-text-secondary: hsl(0, 0%, 0%); + + --color-link: #2949b2; + --color-shadow: rgba(0, 0, 0, 0.08); + --color-red: hsl(0, 84%, 40%); + --color-green: hsl(100, 84%, 20%); + --color-yellow: hsl(41, 63%, 75%); + --radius: 6px; + --circle: 360px; + --shadow-x-offset: 0; + --shadow-y-offset: 0.125rem; + --shadow-size: var(--pad-1); + + --pad-1: 0.25rem; + --pad-2: 0.5rem; + --pad-3: 0.75rem; + --pad-4: 1rem; +} + +.dark, +.dark * { + --hue: 266; + --sat: 14%; + --lit: 12%; + --color-surface: hsl(var(--hue), var(--sat), calc(0% + var(--lit))); + --color-lowered: hsl(var(--hue), var(--sat), calc(6% + var(--lit))); + --color-raised: hsl(var(--hue), var(--sat), calc(2% + var(--lit))); + --color-super-lowered: hsl(var(--hue), var(--sat), calc(12% + var(--lit))); + --color-super-raised: hsl(var(--hue), var(--sat), calc(4% + var(--lit))); + --color-text: hsl(0, 0%, 95%); + + --color-primary: hsl(331, 18%, 74%); + --color-primary-lowered: hsl(331, 18%, 69%); + --color-text-primary: hsl(0, 0%, 0%); + + --color-secondary: hsl(6, 18%, 34%); + --color-secondary-lowered: hsl(6, 18%, 29%); + --color-text-secondary: hsl(0, 0%, 100%); + + --color-link: #93c5fd; + --color-red: hsl(0, 94%, 82%); + --color-green: hsl(100, 94%, 82%); + --color-yellow: hsl(41, 63%, 65%); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body { + line-height: 1.5; + letter-spacing: 0.15px; + font-family: + "Inter", "Poppins", "Roboto", ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; + color: var(--color-text); + background: var(--color-surface); + overflow: auto auto; + height: 100dvh; + scroll-behavior: smooth; + overflow-x: hidden; +} + +main { + width: 80ch; + margin: 0 auto; + padding: var(--pad-3) var(--pad-4); +} + +article { + margin: var(--pad-4) 0; +} + +@media screen and (max-width: 900px) { + main, + article, + nav, + header, + footer { + width: 100%; + } + + article { + margin-top: 0; + } + + main { + padding: 0; + } + + body .card:not(.card *):not(#stream *):not(.user_plate), + body .pillmenu:not(.card *) > a, + body .card-nest:not(.card *) > .card, + body .banner { + border-radius: 0 !important; + } +} + +.content_container { + margin: 0 auto; + width: 100%; +} + +@media screen and (min-width: 500px) { + .content_container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .content_container { + max-width: 720px; + } +} + +@media (min-width: 900px) { + .content_container { + max-width: 960px; + } + + @media (min-width: 1200px) { + article { + padding: 0; + } + + .content_container { + max-width: 1100px; + } + } +} + +video { + max-width: 100%; + border-radius: var(--radius); +} + +/* typo */ +p { + margin-bottom: var(--pad-4); +} + +.no_p_margin p:last-child { + margin-bottom: 0; +} + +.post_content pre, +.post_content h1, +.post_content h2, +.post_content h3 { + max-width: calc(100% - 39px - var(--pad-2)); +} + +.post_right:not(.repost) { + max-width: calc(100% - 52px); +} + +.rhs { + width: 100% !important; +} + +.name { + max-width: 250px; + overflow: hidden; + /* overflow-wrap: break-word; */ + overflow-wrap: anywhere; + text-overflow: ellipsis; +} + +@media screen and (min-width: 901px) { + .name.shorter { + max-width: 200px; + } + + .name.lg\:long { + max-width: unset; + } + + .rhs { + width: calc(100% - 23rem) !important; + } +} + +ul, +ol { + margin-left: var(--pad-4); +} + +pre, +code { + font-family: "Jetbrains Mono", "Fire Code", monospace; + width: 100%; + max-width: 100%; + overflow: auto; + background: var(--color-lowered); + border-radius: var(--radius); + padding: var(--pad-1); + font-size: 0.8rem; +} + +pre { + padding: var(--pad-4); +} + +svg.icon { + stroke: currentColor; + width: 18px; + width: 1em; + height: 1em; +} + +svg.icon.filled { + fill: currentColor; +} + +button svg { + pointer-events: none; +} + +hr { + border-top: solid 1px var(--color-super-lowered) !important; + border-left: 0; + border-bottom: 0; + border-right: 0; +} + +hr.margin { + margin: var(--pad-4) 0; +} + +p, +li, +span, +code { + max-width: 100%; + overflow-wrap: normal; + text-wrap: pretty; + word-wrap: break-word; + overflow-wrap: anywhere; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + font-weight: 700; + width: -moz-max-content; + width: max-content; + position: relative; + max-width: 100%; +} + +h1 { + font-size: 2rem; +} + +h2 { + font-size: 1.75rem; +} + +h3 { + font-size: 1.5rem; +} + +h4 { + font-size: 1.25rem; +} + +h5 { + font-size: var(--pad-4); +} + +h6 { + font-size: var(--pad-3); +} + +a { + text-decoration: none; + color: var(--color-link); +} + +a.flush { + color: inherit; +} + +a:hover { + text-decoration: underline; +} + +.text-small { + font-size: 14px; +} + +img { + display: inline; + max-width: 100%; + vertical-align: middle; +} + +img.cover { + object-fit: cover; +} + +img.fill { + object-fit: fill; +} + +img.contain { + object-fit: contain; +} + +img.emoji { + width: 1em; + height: 1em; + aspect-ratio: 1 / 1; +} diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css index 6588f0e..26960fb 100644 --- a/crates/app/src/public/css/style.css +++ b/crates/app/src/public/css/style.css @@ -1,340 +1,4 @@ -:root { - color-scheme: light dark; - --hue: 16; - --sat: 6%; - --lit: 0%; - --color-surface: hsl(var(--hue), var(--sat), calc(97% - var(--lit))); - --color-lowered: hsl(var(--hue), var(--sat), calc(94% - var(--lit))); - --color-raised: hsl(var(--hue), var(--sat), calc(99% - var(--lit))); - --color-super-lowered: hsl(var(--hue), var(--sat), calc(85% - var(--lit))); - --color-super-raised: hsl(var(--hue), var(--sat), calc(100% - var(--lit))); - --color-text: hsl(0, 0%, 0%); - --color-text-raised: var(--color-text); - --color-text-lowered: var(--color-text); - - --color-primary: hsl(330, 18%, 26%); - --color-primary-lowered: hsl(330, 18%, 21%); - --color-text-primary: hsl(0, 0%, 100%); - - --color-secondary: hsl(6, 18%, 66%); - --color-secondary-lowered: hsl(6, 18%, 61%); - --color-text-secondary: hsl(0, 0%, 0%); - - --color-link: #2949b2; - --color-shadow: rgba(0, 0, 0, 0.08); - --color-red: hsl(0, 84%, 40%); - --color-green: hsl(100, 84%, 20%); - --color-yellow: hsl(41, 63%, 75%); - --radius: 6px; - --circle: 360px; - --shadow-x-offset: 0; - --shadow-y-offset: 0.125rem; - --shadow-size: 0.25rem; -} - -.dark, -.dark * { - --hue: 266; - --sat: 14%; - --lit: 12%; - --color-surface: hsl(var(--hue), var(--sat), calc(0% + var(--lit))); - --color-lowered: hsl(var(--hue), var(--sat), calc(6% + var(--lit))); - --color-raised: hsl(var(--hue), var(--sat), calc(2% + var(--lit))); - --color-super-lowered: hsl(var(--hue), var(--sat), calc(12% + var(--lit))); - --color-super-raised: hsl(var(--hue), var(--sat), calc(4% + var(--lit))); - --color-text: hsl(0, 0%, 95%); - - --color-primary: hsl(331, 18%, 74%); - --color-primary-lowered: hsl(331, 18%, 69%); - --color-text-primary: hsl(0, 0%, 0%); - - --color-secondary: hsl(6, 18%, 34%); - --color-secondary-lowered: hsl(6, 18%, 29%); - --color-text-secondary: hsl(0, 0%, 100%); - - --color-link: #93c5fd; - --color-red: hsl(0, 94%, 82%); - --color-green: hsl(100, 94%, 82%); - --color-yellow: hsl(41, 63%, 65%); -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -html, -body { - line-height: 1.5; - letter-spacing: 0.15px; - font-family: - "Inter", "Poppins", "Roboto", ui-sans-serif, system-ui, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", - "Noto Color Emoji"; - color: var(--color-text); - background: var(--color-surface); - overflow: auto auto; - height: 100dvh; - scroll-behavior: smooth; - overflow-x: hidden; -} - -main { - width: 80ch; - margin: 0 auto; - padding: 0.75rem 1rem; -} - -article { - margin: 1rem 0; -} - -@media screen and (max-width: 900px) { - main, - article, - nav, - header, - footer { - width: 100%; - } - - article { - margin-top: 0; - } - - main { - padding: 0; - } - - body .card:not(.card *):not(#stream *):not(.user_plate), - body .pillmenu:not(.card *) > a, - body .card-nest:not(.card *) > .card, - body .banner { - border-radius: 0 !important; - } -} - -.content_container { - margin: 0 auto; - width: 100%; -} - -@media screen and (min-width: 500px) { - .content_container { - max-width: 540px; - } -} - -@media (min-width: 768px) { - .content_container { - max-width: 720px; - } -} - -@media (min-width: 900px) { - .content_container { - max-width: 960px; - } - - @media (min-width: 1200px) { - article { - padding: 0; - } - - .content_container { - max-width: 1100px; - } - } -} - -video { - max-width: 100%; - border-radius: var(--radius); -} - -/* typo */ -p { - margin-bottom: 1rem; -} - -.no_p_margin p:last-child { - margin-bottom: 0; -} - -.post_content pre, -.post_content h1, -.post_content h2, -.post_content h3 { - max-width: calc(100% - 39px - 0.5rem); -} - -.post_right:not(.repost) { - max-width: calc(100% - 52px); -} - -.rhs { - width: 100% !important; -} - -.name { - max-width: 250px; - overflow: hidden; - /* overflow-wrap: break-word; */ - overflow-wrap: anywhere; - text-overflow: ellipsis; -} - -@media screen and (min-width: 901px) { - .name.shorter { - max-width: 200px; - } - - .name.lg\:long { - max-width: unset; - } - - .rhs { - width: calc(100% - 23rem) !important; - } -} - -ul, -ol { - margin-left: 1rem; -} - -pre, -code { - font-family: "Jetbrains Mono", "Fire Code", monospace; - width: 100%; - max-width: 100%; - overflow: auto; - background: var(--color-lowered); - border-radius: var(--radius); - padding: 0.25rem; - font-size: 0.8rem; -} - -pre { - padding: 1rem; -} - -svg.icon { - stroke: currentColor; - width: 18px; - width: 1em; - height: 1em; -} - -svg.icon.filled { - fill: currentColor; -} - -button svg { - pointer-events: none; -} - -hr { - border-top: solid 1px var(--color-super-lowered) !important; - border-left: 0; - border-bottom: 0; - border-right: 0; -} - -hr.margin { - margin: 1rem 0; -} - -p, -li, -span, -code { - max-width: 100%; - overflow-wrap: normal; - text-wrap: pretty; - word-wrap: break-word; - overflow-wrap: anywhere; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - margin: 0; - font-weight: 700; - width: -moz-max-content; - width: max-content; - position: relative; - max-width: 100%; -} - -h1 { - font-size: 2rem; -} - -h2 { - font-size: 1.75rem; -} - -h3 { - font-size: 1.5rem; -} - -h4 { - font-size: 1.25rem; -} - -h5 { - font-size: 1rem; -} - -h6 { - font-size: 0.75rem; -} - -a { - text-decoration: none; - color: var(--color-link); -} - -a.flush { - color: inherit; -} - -a:hover { - text-decoration: underline; -} - -.text-small { - font-size: 14px; -} - -img { - display: inline; - max-width: 100%; - vertical-align: middle; -} - -img.cover { - object-fit: cover; -} - -img.fill { - object-fit: fill; -} - -img.contain { - object-fit: contain; -} - -img.emoji { - width: 1em; - height: 1em; - aspect-ratio: 1 / 1; -} +@import url("root.css"); .media_gallery { display: grid; @@ -380,8 +44,8 @@ img.emoji { } .lightbox_exit { - top: 1rem; - right: 1rem; + top: var(--pad-4); + right: var(--pad-4); position: absolute; } @@ -504,7 +168,7 @@ table ol { /* card */ .card { - padding: 1rem; + padding: var(--pad-4); background: var(--color-raised); color: var(--color-text-raised); box-shadow: var(--shadow-x-offset) var(--shadow-y-offset) var(--shadow-size) @@ -513,11 +177,11 @@ table ol { } .card.small { - padding: 0.5rem 1rem; + padding: var(--pad-2) var(--pad-4); } .card.tiny { - padding: 0.5rem; + padding: var(--pad-2); } .card.secondary { @@ -591,13 +255,13 @@ button, transition: background 0.15s; width: max-content; height: 32px; - padding: 0.25rem 1rem; + padding: var(--pad-1) var(--pad-4); border-radius: var(--radius); cursor: pointer; display: flex; justify-content: center; align-items: center; - gap: 0.25rem; + gap: var(--pad-1); font-size: 0.9rem; text-decoration: none !important; user-select: none; @@ -610,7 +274,7 @@ button, button.small, .button.small { /* min-height: max-content; */ - padding: 0.25rem 0.5rem; + padding: var(--pad-1) var(--pad-2); height: 24px; font-size: 16px; } @@ -704,7 +368,7 @@ button.camo:hover, input, textarea, select { - padding: 0.35rem 0.75rem; + padding: 0.35rem var(--pad-3); border-radius: var(--radius); border: solid 1px var(--color-super-lowered); outline: none; @@ -752,7 +416,7 @@ select:focus { .pillmenu a { text-decoration: none; - padding: 0.5rem 1rem; + padding: var(--pad-2) var(--pad-4); width: 100%; color: var(--color-text-raised); background: var(--color-super-raised); @@ -760,7 +424,7 @@ select:focus { display: flex; align-items: center; justify-content: center; - gap: 0.5rem; + gap: var(--pad-2); flex-wrap: wrap; position: relative; } @@ -887,7 +551,7 @@ select:focus { color: var(--color-text-primary); font-weight: 600; border-radius: var(--circle); - padding: 0.05rem 0.75rem; + padding: 0.05rem var(--pad-3); } /* nav */ @@ -902,7 +566,7 @@ nav { position: sticky; top: 0; z-index: 6374; - padding: 0.25rem 0.5rem; + padding: var(--pad-1) var(--pad-2); transition: opacity 0.15s; font-size: 16px; } @@ -922,7 +586,7 @@ nav button:not(.inner *), nav .button:not(.inner *) { border-radius: var(--radius); color: inherit; - padding: 0.75rem 0.75rem; + padding: var(--pad-3) var(--pad-3); background: transparent; text-decoration: none; position: relative; @@ -953,7 +617,7 @@ nav .button:not(.title):not(.active):hover { @media screen and (max-width: 900px) { nav { - padding: 0.5rem 0.25rem; + padding: var(--pad-2) var(--pad-1); margin-bottom: 0; backdrop-filter: none; bottom: 0; @@ -1024,7 +688,7 @@ dialog { } dialog .inner { - padding: 1rem; + padding: var(--pad-4); width: 25rem; max-width: 100%; } @@ -1067,7 +731,7 @@ dialog::backdrop { max-width: 100dvw; max-height: 80dvh; overflow: auto; - padding: 0.5rem 0; + padding: var(--pad-2) 0; box-shadow: 0 0 8px 2px var(--color-shadow); } @@ -1087,7 +751,7 @@ dialog::backdrop { } .dropdown .inner .title { - padding: 0.25rem var(--horizontal-padding); + padding: var(--pad-1) var(--horizontal-padding); font-size: 13px; opacity: 50%; color: var(--color-text-raised); @@ -1099,19 +763,19 @@ dialog::backdrop { } .dropdown .inner .title:not(:first-of-type) { - padding-top: 0.5rem; + padding-top: var(--pad-2); } .dropdown .inner a, .dropdown .inner button { width: 100%; - padding: 0.25rem var(--horizontal-padding); + padding: var(--pad-1) var(--horizontal-padding); transition: none !important; text-decoration: none; display: flex; align-items: center; justify-content: flex-start; - gap: 0.5rem; + gap: var(--pad-2); color: var(--color-text-raised); box-shadow: none !important; background: transparent; @@ -1163,25 +827,25 @@ dialog::backdrop { display: flex; flex-direction: column; align-items: flex-end; - gap: 0.25rem; + gap: var(--pad-1); position: fixed; - bottom: 0.5rem; - right: 0.5rem; + bottom: var(--pad-2); + right: var(--pad-2); z-index: 6880; - width: calc(100% - 1rem); + width: calc(100% - var(--pad-4)); pointer-events: none; } .toast { box-shadow: 0 0 8px var(--color-shadow); width: max-content; - max-width: calc(100dvw - 1rem); + max-width: calc(100dvw - var(--pad-4)); border-radius: var(--radius); - padding: 0.75rem 1rem; + padding: var(--pad-3) var(--pad-4); animation: popin ease-in-out 1 0.15s running; display: flex; justify-content: space-between; - gap: 1rem; + gap: var(--pad-4); } .toast.success { @@ -1275,7 +939,7 @@ dialog::backdrop { position: absolute; content: "Show full content"; border-radius: var(--radius); - padding: 0.25rem 0.75rem; + padding: var(--pad-1) var(--pad-3); background: var(--color-primary); font-weight: 600; bottom: 20px; @@ -1306,20 +970,15 @@ dialog::backdrop { } } -/* turbo */ -.turbo-progress-bar { - background: var(--color-primary); -} - /* details */ details summary { display: flex; align-items: center; - gap: 0.25rem; + gap: var(--pad-1); transition: background 0.15s; cursor: pointer; width: max-content; - padding: 0.25rem 0.75rem; + padding: var(--pad-1) var(--pad-3); border-radius: var(--radius); background: var(--color-lowered); } @@ -1336,7 +995,7 @@ details[open] summary { position: relative; color: var(--color-primary); background: var(--color-super-lowered); - margin-bottom: 0.25rem; + margin-bottom: var(--pad-1); } details[open] summary::after { @@ -1365,7 +1024,7 @@ details.accordion summary { background: var(--background); border: solid 1px var(--color-super-lowered); border-radius: var(--radius); - padding: 0.75rem 1rem; + padding: var(--pad-3) var(--pad-4); margin: 0; width: 100%; user-select: none; @@ -1386,219 +1045,10 @@ details.accordion[open] summary { details.accordion .inner { background: var(--background); - padding: 0.75rem 1rem; + padding: var(--pad-3) var(--pad-4); border-radius: var(--radius); border-top-left-radius: 0; border-top-right-radius: 0; border: solid 1px var(--color-super-lowered); border-top: none; } - -/* utility */ -.flex { - display: flex; -} - -.flex-col { - flex-direction: column; -} - -.flex-rev-col { - flex-direction: column-reverse; -} - -.flex-row { - flex-direction: row !important; -} - -.flex-rev-row { - flex-direction: row-reverse; -} - -.flex-wrap { - flex-wrap: wrap; -} - -.justify-center { - justify-content: center; -} - -.justify-between { - justify-content: space-between; -} - -.justify-right { - justify-content: right; -} - -.justify-start { - justify-content: flex-start; -} - -.items-center { - align-items: center; -} - -.gap-1 { - gap: 0.25rem; -} - -.gap-2 { - gap: 0.5rem; -} - -.gap-4 { - gap: 1rem; -} - -.gap-8 { - gap: 1.25rem; -} - -.mobile { - display: none !important; -} - -@media screen and (max-width: 650px) { - .desktop { - display: none !important; - } - - .mobile { - display: flex !important; - } -} - -@media screen and (max-width: 900px) { - .flex-collapse { - flex-direction: column !important; - } - - .sm\:static { - position: static !important; - } - - .mobile.flex { - display: flex !important; - } - - .sm\:w-full { - width: 100% !important; - min-width: 100% !important; - } - - .sm\:mt-2 { - margin-top: 2rem !important; - } - - .sm\:items-start { - align-items: flex-start !important; - } - - .sm\:contents { - display: contents !important; - } -} - -.shadow { - box-shadow: 0 0 8px var(--color-shadow); -} - -.shadow-md { - box-shadow: 0 8px 16px var(--color-shadow); -} - -.round-sm { - border-radius: calc(var(--radius) / 2) !important; -} - -.round { - border-radius: var(--radius) !important; -} - -.round-md { - border-radius: calc(var(--radius) * 2) !important; -} - -.round-lg { - border-radius: calc(var(--radius) * 4) !important; -} - -.w-full { - width: 100% !important; -} - -.w-content { - width: max-content !important; -} - -.bold { - font-weight: 600; -} - -[disabled="fully"] { - opacity: 75%; - pointer-events: visible; - cursor: not-allowed; - user-select: none; -} - -.fade, -.CodeMirror-placeholder { - opacity: 75%; - transition: opacity 0.15s; -} - -.ff-inherit { - font-family: inherit; -} - -.fs-md { - font-size: 12px; -} - -[align="center"], -.text-center { - text-align: center; -} - -[align="right"], -.text-right { - text-align: right; -} - -.red { - color: var(--color-red) !important; -} - -.green { - color: var(--color-green) !important; -} - -.hidden { - display: none; -} - -align { - width: 100%; - display: block; -} - -align.center { - text-align: center; -} - -align.right { - text-align: right; -} - -/* lhs, rhs */ -.rhs { - width: calc(100% - 23rem) !important; -} - -@media screen and (max-width: 900px) { - .rhs { - width: 100% !important; - } -} diff --git a/crates/app/src/public/css/utility.css b/crates/app/src/public/css/utility.css new file mode 100644 index 0000000..8b84a98 --- /dev/null +++ b/crates/app/src/public/css/utility.css @@ -0,0 +1,213 @@ +/* utility */ +.flex { + display: flex; +} + +.flex-col { + flex-direction: column; +} + +.flex-rev-col { + flex-direction: column-reverse; +} + +.flex-row { + flex-direction: row !important; +} + +.flex-rev-row { + flex-direction: row-reverse; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.justify-right { + justify-content: right; +} + +.justify-start { + justify-content: flex-start; +} + +.items-center { + align-items: center; +} + +.gap-1 { + gap: var(--pad-1); +} + +.gap-2 { + gap: var(--pad-2); +} + +.gap-4 { + gap: var(--pad-4); +} + +.gap-8 { + gap: 1.25rem; +} + +.mobile { + display: none !important; +} + +@media screen and (max-width: 650px) { + .desktop { + display: none !important; + } + + .mobile { + display: flex !important; + } +} + +@media screen and (max-width: 900px) { + .flex-collapse { + flex-direction: column !important; + } + + .sm\:static { + position: static !important; + } + + .mobile.flex { + display: flex !important; + } + + .sm\:w-full { + width: 100% !important; + min-width: 100% !important; + } + + .sm\:mt-2 { + margin-top: 2rem !important; + } + + .sm\:items-start { + align-items: flex-start !important; + } + + .sm\:contents { + display: contents !important; + } +} + +.shadow { + box-shadow: 0 0 8px var(--color-shadow); +} + +.shadow-md { + box-shadow: 0 8px 16px var(--color-shadow); +} + +.round-sm { + border-radius: calc(var(--radius) / 2) !important; +} + +.round { + border-radius: var(--radius) !important; +} + +.round-md { + border-radius: calc(var(--radius) * 2) !important; +} + +.round-lg { + border-radius: calc(var(--radius) * 4) !important; +} + +.w-full { + width: 100% !important; +} + +.w-content { + width: max-content !important; +} + +.bold { + font-weight: 600; +} + +[disabled="fully"] { + opacity: 75%; + pointer-events: visible; + cursor: not-allowed; + user-select: none; +} + +.fade, +.CodeMirror-placeholder { + opacity: 75%; + transition: opacity 0.15s; +} + +.ff-inherit { + font-family: inherit; +} + +.fs-md { + font-size: 12px; +} + +[align="center"], +.text-center { + text-align: center; +} + +[align="right"], +.text-right { + text-align: right; +} + +.red { + color: var(--color-red) !important; +} + +.green { + color: var(--color-green) !important; +} + +.hidden { + display: none !important; +} + +align { + width: 100%; + display: block; +} + +align.center { + text-align: center; +} + +align.right { + text-align: right; +} + +/* lhs, rhs */ +.rhs { + width: calc(100% - 23rem) !important; +} + +@media screen and (max-width: 900px) { + .rhs { + width: 100% !important; + } +} + +/* turbo */ +.turbo-progress-bar { + background: var(--color-primary); +} diff --git a/crates/app/src/public/html/body.lisp b/crates/app/src/public/html/body.lisp new file mode 100644 index 0000000..e4e15c5 --- /dev/null +++ b/crates/app/src/public/html/body.lisp @@ -0,0 +1,305 @@ +(div ("id" "toast_zone")) + +; random js +(text "") + +(text "{% if user -%} + +{%- endif %}") + +; dialogs +(dialog + ("id" "link_filter") + (div + ("class" "inner flex flex-col gap-2") + + ; warning stuff + (p (text "Pressing continue will bring you to the following URL:")) + (pre (code ("id" "link_filter_url"))) + (p (text "Are sure you want to go there?")) + + (hr ("class" "margin")) + (div + ("class" "flex gap-2") + + (a + ("class" "button primary") + ("id" "link_filter_continue") + ("rel" "noopener noreferrer") + ("target" "_blank") + ("onclick", "document.getElementById('link_filter').close()") + (icon (text "external-link")) + (str (text "dialog:action.continue"))) + + (button + ("class" "secondary") + ("type" "button") + ("onclick", "document.getElementById('link_filter').close()") + (icon (text "x")) + (str (text "dialog:action.cancel")))))) + +(dialog + ("id" "web_api_prompt") + (div + ("class" "inner flex flex-col gap-2") + (form + ("class" "flex gap-2 flex-col") + ("onsubmit" "event.preventDefault()") + (label ("for" "prompt") ("id" "web_api_prompt:msg")) + (input ("id" "prompt") ("name" "prompt")) + + (div + ("class" "flex justify-between") + (div null?) + (div + ("class" "flex gap-2") + (button + ("class" "primary bold circle") + ("onclick", "globalThis.web_api_prompt_submit(document.getElementById('prompt').value); document.getElementById('prompt').value = ''") + ("type" "button") + (icon (text "check")) + (str (text "dialog:action.okay"))) + + (button + ("class" "bold red camo") + ("onclick", "globalThis.web_api_prompt_submit('')") + ("type" "button") + (icon (text "x")) + (str (text "dialog:action.cancel")))))))) + +(dialog + ("id" "web_api_prompt_long") + (div + ("class" "inner flex flex-col gap-2") + (form + ("class" "flex gap-2 flex-col") + ("onsubmit" "event.preventDefault()") + (label ("for" "prompt_long") ("id" "web_api_prompt_long:msg")) + (input ("id" "prompt_long") ("name" "prompt_long")) + + (div + ("class" "flex justify-between") + (div null?) + (div + ("class" "flex gap-2") + (button + ("class" "primary bold circle") + ("onclick", "globalThis.web_api_prompt_long_submit(document.getElementById('prompt_long').value); document.getElementById('prompt_long').value = ''") + ("type" "button") + (icon (text "check")) + (str (text "dialog:action.okay"))) + + (button + ("class" "bold red camo") + ("onclick", "globalThis.web_api_prompt_long_submit('')") + ("type" "button") + (icon (text "x")) + (str (text "dialog:action.cancel")))))))) + +(dialog + ("id" "web_api_confirm") + (div + ("class" "inner flex flex-col gap-2") + (form + ("class" "flex gap-2 flex-col") + ("onsubmit" "event.preventDefault()") + (span ("id" "web_api_confirm:msg")) + + (div + ("class" "flex justify-between") + (div null?) + (div + ("class" "flex gap-2") + (button + ("class" "primary bold circle") + ("onclick", "globalThis.web_api_confirm_submit(true)") + ("type" "button") + (icon (text "check")) + (str (text "dialog:action.yes"))) + + (button + ("class" "bold red camo") + ("onclick", "globalThis.web_api_confirm_submit(false)") + ("type" "button") + (icon (text "x")) + (str (text "dialog:action.no")))))))) + +(div + ("class" "lightbox hidden") + ("id" "lightbox") + (button + ("class" "lightbox_exit small square quaternary red") + ("onclick" "trigger('ui::lightbox_close')") + (icon (text "x"))) + + (a + ("href" "") + ("id" "lightbox_img_a") + ("target" "_blank") + (img ("id" "lightbox_img") ("loading" "lazy")))) + +; tokens dialog +(text "{% if user -%}") +(dialog + ("id" "tokens_dialog") + (div + ("class" "inner flex flex-col gap-2") + (form + ("class" "flex gap-2 flex-col") + ("onsubmit" "event.preventDefault()") + (div ("id" "tokens") ("style" "display: contents")) + + (div + ("class" "flex justify-between") + (a + ("href" "/auth/login") + ("class" "button") + ("data-turbo", "false") + (icon (text "plus")) + (span (str (text "general:action.add_account")))) + + (button + ("class" "quaternary") + ("onclick" "document.getElementById('tokens_dialog').close()") + ("type" "button") + (icon (text "check"))))))) + +; user scripts +(text "{%- endif %} {% if user and use_user_theme -%} {{ components::theme(user=user, theme_preference=user.settings.theme_preference) }} + +{%- endif %} {% if user and user.connections.Spotify and config.connections.spotify_client_id and user.connections.Spotify[0].data.token and user.connections.Spotify[0].data.refresh_token %} + +{% elif user and user.connections.LastFm and config.connections.last_fm_key and user.connections.LastFm[0].data.session_token %} + +{%- endif %}") diff --git a/crates/app/src/public/html/chats/app.lisp b/crates/app/src/public/html/chats/app.lisp index d60ef08..ddee672 100644 --- a/crates/app/src/public/html/chats/app.lisp +++ b/crates/app/src/public/html/chats/app.lisp @@ -89,7 +89,7 @@ (div ("class" "w-full flex flex-col gap-2") ("id" "stream") - ("style" "padding: 1rem") + ("style" "padding: var(--pad-4)") (turbo-frame ("id" "stream_body_frame") ("src" "/chats/{{ selected_community }}/{{ selected_channel }}/_stream?page={{ page }}&message={{ message }}")) @@ -222,7 +222,7 @@ } .chats_nav button svg { - margin-right: 1rem; + margin-right: var(--pad-4); } .sidebar { @@ -238,7 +238,7 @@ } .sidebar .title:not(.dropdown *) { - padding: 1rem; + padding: var(--pad-4); border-bottom: solid 1px var(--color-super-lowered); } @@ -272,7 +272,7 @@ } .message.grouped { - padding: 0.25rem 1rem 0.25rem calc(1rem + 0.5rem + 42px); + padding: var(--pad-1) var(--pad-4) var(--pad-1) calc(var(--pad-4) + var(--pad-2) + 42px); } turbo-frame { @@ -284,7 +284,7 @@ } .members_list_half { - padding-top: 1rem; + padding-top: var(--pad-4); border-top: solid 1px var(--color-super-lowered); } @@ -303,7 +303,7 @@ } .message.grouped { - padding: 0.25rem 1rem 0.25rem calc(1rem + 0.5rem + 31px); + padding: var(--pad-1) var(--pad-4) var(--pad-1) calc(var(--pad-4) + var(--pad-2) + 31px); } body:not(.sidebars_shown) .sidebar { diff --git a/crates/app/src/public/html/communities/base.lisp b/crates/app/src/public/html/communities/base.lisp index b1aa4b3..04bbd68 100644 --- a/crates/app/src/public/html/communities/base.lisp +++ b/crates/app/src/public/html/communities/base.lisp @@ -71,226 +71,12 @@ ("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 %}")) + (text "{%- 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 %}"))) - - (text "{% if user and user.id != community.owner -%}") - (hr) - (div - ("class" "flex flex-wrap gap-2 w-full fade") - (a - ("class" "red") - ("href" "javascript:trigger('me::report', ['{{ community.id }}', 'community'])") - (text "({{ lang[\"general:action.report\"]|lower }})"))) - (text "{%- endif %}")))) + (text "{{ components::community_actions(community=community) }}")) + (text "{{ components::community_info(community=community) }}")) (div ("class" "rhs w-full") (text "{% if can_read -%} {% block content %}{% endblock %} {% else %}") @@ -307,4 +93,8 @@ (text "{{ text \"communities:label.might_need_to_join\" }}")))) (text "{%- endif %}"))))) +(text "{% if community.is_forge and not allow_for_forges %}") +(script + (text "window.location.pathname = window.location.pathname.replace(\"/community\", \"/forge\")")) +(text "{% endif %}") (text "{% endblock %}") diff --git a/crates/app/src/public/html/communities/list.lisp b/crates/app/src/public/html/communities/list.lisp index ecd8c04..229cecb 100644 --- a/crates/app/src/public/html/communities/list.lisp +++ b/crates/app/src/public/html/communities/list.lisp @@ -66,10 +66,6 @@ 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: { diff --git a/crates/app/src/public/html/communities/settings.lisp b/crates/app/src/public/html/communities/settings.lisp index 4fc15d0..16c7581 100644 --- a/crates/app/src/public/html/communities/settings.lisp +++ b/crates/app/src/public/html/communities/settings.lisp @@ -896,49 +896,56 @@ \"change_banner\", ]); + const settings_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\", + ] + ]; + + // {% if not community.is_forge -%} + settings_fields.push([ + [ + \"enable_questions\", + \"Allow users to ask questions in this community\", + ], + \"{{ community.context.enable_questions }}\", + \"checkbox\", + ]); + + settings_fields.push([ + [ + \"enable_titles\", + \"Allow users to attach a title to their posts\", + ], + \"{{ community.context.enable_titles }}\", + \"checkbox\", + ]); + + settings_fields.push([ + [ + \"require_titles\", + \"Require users to attach a title to their posts\", + ], + \"{{ community.context.require_titles }}\", + \"checkbox\", + ]); + // {%- endif %} + 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\", - ], - [ - [ - \"enable_titles\", - \"Allow users to attach a title to their posts\", - ], - \"{{ community.context.enable_titles }}\", - \"checkbox\", - ], - [ - [ - \"require_titles\", - \"Require users to attach a title to their posts\", - ], - \"{{ community.context.require_titles }}\", - \"checkbox\", - ], - ], + settings_fields, settings, ); }, 250);")) diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp index 827bef7..1e482d0 100644 --- a/crates/app/src/public/html/components.lisp +++ b/crates/app/src/public/html/components.lisp @@ -49,13 +49,18 @@ (text "{%- endif %} {%- endmacro %} {% macro community_listing_card(community) -%}") (a ("class" "card secondary w-full flex items-center gap-4") - ("href" "/community/{{ community.title }}") + ("href" "{% if community.is_forge -%}/forge/{{ community.title }}{% else %}/community/{{ community.title }}{%- endif %}") (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 }}")) + (div + ("class" "flex gap-2 items-center") + (text "{% if community.is_forge -%}") + (icon (text "anvil")) + (text "{%- endif %}") + (h3 + ("class" "name lg:long") + (text "{{ community.context.display_name }}"))) (span ("class" "fade") (b @@ -1085,7 +1090,7 @@ --input-border-radiFus: var(--radius); --input-border-color: var(--color-primary); --indicator-color: var(--color-primary); - --emoji-padding: 0.25rem; + --emoji-padding: var(--pad-1); box-shadow: 0 0 4px var(--color-shadow); ") ("class" "w-full")) @@ -1538,3 +1543,206 @@ ("data-expires" "{{ poll[0].expires }}"))) (text "{%- endif %}"))) (text "{%- endmacro %}") + +(text "{% macro community_info(community) %}") +(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 "Posts")) + (a + ("href" "/community/{{ community.title }}") + (text "{{ community.post_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 %}"))))) +(text "{% endmacro %}") + +(text "{% macro community_actions(community) -%}") +(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 %}") +(text "{%- endmacro %}") diff --git a/crates/app/src/public/html/forge/base.lisp b/crates/app/src/public/html/forge/base.lisp new file mode 100644 index 0000000..646babd --- /dev/null +++ b/crates/app/src/public/html/forge/base.lisp @@ -0,0 +1,83 @@ +; this is essentially the same as `communities/base.lisp`, but it has some minor +; changes to be more github-like instead of retrospring-like +(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() }}") +(main + (div + ("class" "content_container flex flex-col gap-4") + (div + ("class" "card-nest") + (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 %}"))) + (span + ("class" "fade") + (text "{{ community.title }}")))) + + (text "{{ components::community_actions(community=community) }}")) + + (text "{% block content %}{% endblock %}"))) + +(text "{% if not community.is_forge %}") +(script + (text "window.location.pathname = window.location.pathname.replace(\"/forge\", \"/community\")")) +(text "{% endif %}") +(text "{% endblock %}") diff --git a/crates/app/src/public/html/forge/home.lisp b/crates/app/src/public/html/forge/home.lisp new file mode 100644 index 0000000..3132000 --- /dev/null +++ b/crates/app/src/public/html/forge/home.lisp @@ -0,0 +1,80 @@ +(text "{% extends \"root.html\" %} {% block head %}") +(title + (text "Forge - {{ config.name }}")) + +(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") +(main + ("class" "flex flex-col gap-2") + ; create new + (text "{% if user.permissions|has_supporter -%}") + (div + ("class" "card-nest") + (div + ("class" "card small") + (b + (text "{{ text \"forge: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 "{% else %}") + (text "{{ components::supporter_ad(body=\"Become a supporter to create forges!\") }}") + (text "{%- endif %}") + + ; forge listing + (div + ("class" "card-nest") + (div + ("class" "card small flex items-center gap-2") + (icon (text "anvil")) + (str (text "forge:label.my_forges"))) + + (div + ("class" "card flex flex-col gap-2") + (text "{% for item in 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\"]); + + fetch(\"/api/v1/communities\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + title: e.target.title.value, + forge: true + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + setTimeout(() => { + window.location.href = `/forge/${res.payload}`; + }, 100); + } + }); + }")) +(text "{% endblock %}") diff --git a/crates/app/src/public/html/forge/info.lisp b/crates/app/src/public/html/forge/info.lisp new file mode 100644 index 0000000..4019546 --- /dev/null +++ b/crates/app/src/public/html/forge/info.lisp @@ -0,0 +1,6 @@ +(text "{% extends \"forge/base.html\" %} {% block content %}") +(div + ("class" "flex flex-col gap-4 w-full") + (text "{{ macros::forge_nav(community=community, selected=\"info\") }}") + (text "{{ components::community_info(community=community) }}")) +(text "{% endblock %}") diff --git a/crates/app/src/public/html/macros.lisp b/crates/app/src/public/html/macros.lisp index 66bf692..70fcfdb 100644 --- a/crates/app/src/public/html/macros.lisp +++ b/crates/app/src/public/html/macros.lisp @@ -1,7 +1,7 @@ (text "{% macro nav(selected=\"\", show_lhs=true, hide_user_menu=false) -%}") (nav (div - ("class" "content_container") + ("class" "content_container flex justify-between") (div ("class" "flex nav_side") (a @@ -72,7 +72,7 @@ ("class" "flex-row title") ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exlude" "dropdown") - ("style" "gap: 0.25rem !important") + ("style" "gap: var(--pad-1) !important") (text "{{ components::avatar(username=user.username, size=\"24px\") }}") (icon_class (text "chevron-down") (text "dropdown-arrow"))) @@ -221,3 +221,19 @@ (str (text "auth:label.outbox"))) (text "{%- endif %} {%- endif %}")) (text "{%- endmacro %}") + +(text "{% macro forge_nav(community, selected=\"\") -%}") +(div + ("class" "pillmenu") + (a + ("href" "/forge/{{ community.title }}") + ("class" "{% if selected == 'info' -%}active{%- endif %}") + (icon (text "info")) + (str (text "forge:tab.info"))) + + (a + ("href" "/forge/{{ community.title }}/tickets") + ("class" "{% if selected == 'tickets' -%}active{%- endif %}") + (icon (text "circle-dot")) + (str (text "forge:tab.tickets")))) +(text "{%- endmacro %}") diff --git a/crates/app/src/public/html/post/likes.lisp b/crates/app/src/public/html/post/likes.lisp index 576b942..6e414aa 100644 --- a/crates/app/src/public/html/post/likes.lisp +++ b/crates/app/src/public/html/post/likes.lisp @@ -73,7 +73,7 @@ (style (text ".user_plate { - width: calc(50% - 0.5rem); + width: calc(50% - var(--pad-2)); } @media screen and (max-width: 900px) { diff --git a/crates/app/src/public/html/profile/followers.lisp b/crates/app/src/public/html/profile/followers.lisp index c12c7a9..e35811b 100644 --- a/crates/app/src/public/html/profile/followers.lisp +++ b/crates/app/src/public/html/profile/followers.lisp @@ -12,7 +12,7 @@ (style (text ".user_plate { - width: calc(50% - 0.5rem); + width: calc(50% - var(--pad-2)); } @media screen and (max-width: 900px) { diff --git a/crates/app/src/public/html/profile/following.lisp b/crates/app/src/public/html/profile/following.lisp index 1cfb0e8..7912052 100644 --- a/crates/app/src/public/html/profile/following.lisp +++ b/crates/app/src/public/html/profile/following.lisp @@ -12,7 +12,7 @@ (style (text ".user_plate { - width: calc(50% - 0.5rem); + width: calc(50% - var(--pad-2)); } @media screen and (max-width: 900px) { diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp index 6324eb1..ce5b532 100644 --- a/crates/app/src/public/html/profile/settings.lisp +++ b/crates/app/src/public/html/profile/settings.lisp @@ -546,7 +546,7 @@ pressure, but it helps us do some pretty cool things! As a supporter, you'll get:")) (ul - ("style" "margin-bottom: 1rem") + ("style" "margin-bottom: var(--pad-4)") (li (text "Vanity badge on profile")) (li @@ -569,7 +569,9 @@ (li (text "Save infinite post drafts")) (li - (text "Ability to search through all posts"))) + (text "Ability to search through all posts")) + (li + (text "Ability to create forges"))) (a ("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}") ("class" "button") diff --git a/crates/app/src/public/html/root.lisp b/crates/app/src/public/html/root.lisp index 4438bcd..c7867b1 100644 --- a/crates/app/src/public/html/root.lisp +++ b/crates/app/src/public/html/root.lisp @@ -55,8 +55,6 @@ (text "{% block head %}{% endblock %}")) (body - (div ("id" "toast_zone")) - (div ("id" "page") (text "{% if user and user.id == 0 -%}") @@ -78,306 +76,4 @@ (text "{% else %} {% block body %}{% endblock %} {%- endif %}") (text "")) - ; random js - (text "") - - (text "{% if user -%} - - {%- endif %}") - - ; dialogs - (dialog - ("id" "link_filter") - (div - ("class" "inner flex flex-col gap-2") - - ; warning stuff - (p (text "Pressing continue will bring you to the following URL:")) - (pre (code ("id" "link_filter_url"))) - (p (text "Are sure you want to go there?")) - - (hr ("class" "margin")) - (div - ("class" "flex gap-2") - - (a - ("class" "button primary") - ("id" "link_filter_continue") - ("rel" "noopener noreferrer") - ("target" "_blank") - ("onclick", "document.getElementById('link_filter').close()") - (icon (text "external-link")) - (str (text "dialog:action.continue"))) - - (button - ("class" "secondary") - ("type" "button") - ("onclick", "document.getElementById('link_filter').close()") - (icon (text "x")) - (str (text "dialog:action.cancel")))))) - - (dialog - ("id" "web_api_prompt") - (div - ("class" "inner flex flex-col gap-2") - (form - ("class" "flex gap-2 flex-col") - ("onsubmit" "event.preventDefault()") - (label ("for" "prompt") ("id" "web_api_prompt:msg")) - (input ("id" "prompt") ("name" "prompt")) - - (div - ("class" "flex justify-between") - (div null?) - (div - ("class" "flex gap-2") - (button - ("class" "primary bold circle") - ("onclick", "globalThis.web_api_prompt_submit(document.getElementById('prompt').value); document.getElementById('prompt').value = ''") - ("type" "button") - (icon (text "check")) - (str (text "dialog:action.okay"))) - - (button - ("class" "bold red camo") - ("onclick", "globalThis.web_api_prompt_submit('')") - ("type" "button") - (icon (text "x")) - (str (text "dialog:action.cancel")))))))) - - (dialog - ("id" "web_api_prompt_long") - (div - ("class" "inner flex flex-col gap-2") - (form - ("class" "flex gap-2 flex-col") - ("onsubmit" "event.preventDefault()") - (label ("for" "prompt_long") ("id" "web_api_prompt_long:msg")) - (input ("id" "prompt_long") ("name" "prompt_long")) - - (div - ("class" "flex justify-between") - (div null?) - (div - ("class" "flex gap-2") - (button - ("class" "primary bold circle") - ("onclick", "globalThis.web_api_prompt_long_submit(document.getElementById('prompt_long').value); document.getElementById('prompt_long').value = ''") - ("type" "button") - (icon (text "check")) - (str (text "dialog:action.okay"))) - - (button - ("class" "bold red camo") - ("onclick", "globalThis.web_api_prompt_long_submit('')") - ("type" "button") - (icon (text "x")) - (str (text "dialog:action.cancel")))))))) - - (dialog - ("id" "web_api_confirm") - (div - ("class" "inner flex flex-col gap-2") - (form - ("class" "flex gap-2 flex-col") - ("onsubmit" "event.preventDefault()") - (span ("id" "web_api_confirm:msg")) - - (div - ("class" "flex justify-between") - (div null?) - (div - ("class" "flex gap-2") - (button - ("class" "primary bold circle") - ("onclick", "globalThis.web_api_confirm_submit(true)") - ("type" "button") - (icon (text "check")) - (str (text "dialog:action.yes"))) - - (button - ("class" "bold red camo") - ("onclick", "globalThis.web_api_confirm_submit(false)") - ("type" "button") - (icon (text "x")) - (str (text "dialog:action.no")))))))) - - (div - ("class" "lightbox hidden") - ("id" "lightbox") - (button - ("class" "lightbox_exit small square quaternary red") - ("onclick" "trigger('ui::lightbox_close')") - (icon (text "x"))) - - (a - ("href" "") - ("id" "lightbox_img_a") - ("target" "_blank") - (img ("id" "lightbox_img") ("loading" "lazy")))) - - ; tokens dialog - (text "{% if user -%}") - (dialog - ("id" "tokens_dialog") - (div - ("class" "inner flex flex-col gap-2") - (form - ("class" "flex gap-2 flex-col") - ("onsubmit" "event.preventDefault()") - (div ("id" "tokens") ("style" "display: contents")) - - (div - ("class" "flex justify-between") - (a - ("href" "/auth/login") - ("class" "button") - ("data-turbo", "false") - (icon (text "plus")) - (span (str (text "general:action.add_account")))) - - (button - ("class" "quaternary") - ("onclick" "document.getElementById('tokens_dialog').close()") - ("type" "button") - (icon (text "check"))))))) - - ; user scripts - (text "{%- endif %} {% if user and use_user_theme -%} {{ components::theme(user=user, theme_preference=user.settings.theme_preference) }} - - {%- endif %} {% if user and user.connections.Spotify and config.connections.spotify_client_id and user.connections.Spotify[0].data.token and user.connections.Spotify[0].data.refresh_token %} - - {% elif user and user.connections.LastFm and config.connections.last_fm_key and user.connections.LastFm[0].data.session_token %} - - {%- endif %}"))) + (text "{% include \"body.html\" %}"))) diff --git a/crates/app/src/routes/api/v1/auth/profile.rs b/crates/app/src/routes/api/v1/auth/profile.rs index 75b94d4..f7f88d9 100644 --- a/crates/app/src/routes/api/v1/auth/profile.rs +++ b/crates/app/src/routes/api/v1/auth/profile.rs @@ -29,7 +29,7 @@ use tetratto_core::{ }; #[cfg(feature = "redis")] -use redis::Commands; +use tetratto_core::cache::redis::Commands; use tetratto_shared::hash; pub async fn redirect_from_id( diff --git a/crates/app/src/routes/api/v1/channels/messages.rs b/crates/app/src/routes/api/v1/channels/messages.rs index 2f736b2..1cee397 100644 --- a/crates/app/src/routes/api/v1/channels/messages.rs +++ b/crates/app/src/routes/api/v1/channels/messages.rs @@ -1,5 +1,4 @@ use std::{collections::HashMap, time::Duration}; -use redis::Commands; use axum::{ extract::{ ws::{Message as WsMessage, WebSocket, WebSocketUpgrade}, @@ -10,7 +9,7 @@ use axum::{ }; use axum_extra::extract::CookieJar; use tetratto_core::{ - cache::Cache, + cache::{Cache, redis::Commands}, model::{ auth::User, channels::Message, diff --git a/crates/app/src/routes/api/v1/communities/communities.rs b/crates/app/src/routes/api/v1/communities/communities.rs index ac50939..adda953 100644 --- a/crates/app/src/routes/api/v1/communities/communities.rs +++ b/crates/app/src/routes/api/v1/communities/communities.rs @@ -49,10 +49,15 @@ pub async fn create_request( None => return Json(Error::NotAllowed.into()), }; - match data - .create_community(Community::new(req.title, user.id)) - .await - { + let mut c = Community::new(req.title, user.id); + + if req.forge { + c.is_forge = true; + c.context.enable_titles = true; + c.context.require_titles = true; + } + + match data.create_community(c).await { Ok(id) => Json(ApiReturn { ok: true, message: "Community created".to_string(), @@ -117,6 +122,16 @@ pub async fn update_context_request( None => return Json(Error::NotAllowed.into()), }; + // check lengths + if req.context.display_name.len() > 32 { + return Json(Error::DataTooLong("display name".to_string()).into()); + } + + if req.context.description.len() > 2_usize.pow(14) { + return Json(Error::DataTooLong("description".to_string()).into()); + } + + // ... match data.update_community_context(id, &user, req.context).await { Ok(_) => Json(ApiReturn { ok: true, diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 8101b0c..9cdfd5d 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -404,6 +404,8 @@ pub struct RegisterProps { #[derive(Deserialize)] pub struct CreateCommunity { pub title: String, + #[serde(default)] + pub forge: bool, } #[derive(Deserialize)] diff --git a/crates/app/src/routes/assets.rs b/crates/app/src/routes/assets.rs index 59bcd97..82a759e 100644 --- a/crates/app/src/routes/assets.rs +++ b/crates/app/src/routes/assets.rs @@ -10,6 +10,8 @@ macro_rules! serve_asset { serve_asset!(favicon_request: FAVICON("image/svg+xml")); serve_asset!(style_css_request: STYLE_CSS("text/css")); +serve_asset!(root_css_request: ROOT_CSS("text/css")); +serve_asset!(utility_css_request: UTILITY_CSS("text/css")); serve_asset!(loader_js_request: LOADER_JS("text/javascript")); serve_asset!(atto_js_request: ATTO_JS("text/javascript")); diff --git a/crates/app/src/routes/mod.rs b/crates/app/src/routes/mod.rs index 84b5f46..43e75b9 100644 --- a/crates/app/src/routes/mod.rs +++ b/crates/app/src/routes/mod.rs @@ -12,6 +12,8 @@ pub fn routes(config: &Config) -> Router { Router::new() // assets .route("/css/style.css", get(assets::style_css_request)) + .route("/css/root.css", get(assets::root_css_request)) + .route("/css/utility.css", get(assets::utility_css_request)) .route("/js/loader.js", get(assets::loader_js_request)) .route("/js/atto.js", get(assets::atto_js_request)) .route("/js/me.js", get(assets::me_js_request)) diff --git a/crates/app/src/routes/pages/communities.rs b/crates/app/src/routes/pages/communities.rs index 946e47a..9d7605d 100644 --- a/crates/app/src/routes/pages/communities.rs +++ b/crates/app/src/routes/pages/communities.rs @@ -11,14 +11,12 @@ use axum_extra::extract::CookieJar; use serde::Deserialize; use tera::Context; use tetratto_core::model::{ - auth::User, - communities::{Community, CommunityReadAccess}, - communities_permissions::CommunityPermission, - permissions::FinePermission, - Error, + auth::User, communities::Community, communities_permissions::CommunityPermission, + permissions::FinePermission, Error, }; -macro_rules! check_permissions { +#[macro_export] +macro_rules! check_community_permissions { ($community:ident, $jar:ident, $data:ident, $user:ident) => {{ let mut is_member: bool = false; let mut can_manage_pins: bool = false; @@ -54,7 +52,7 @@ macro_rules! check_permissions { } match $community.read_access { - CommunityReadAccess::Joined => { + tetratto_core::model::communities::CommunityReadAccess::Joined => { if !can_manage_communities { if !is_member { (false, can_manage_pins) @@ -70,6 +68,7 @@ macro_rules! check_permissions { }}; } +#[macro_export] macro_rules! community_context_bools { ($data:ident, $user:ident, $community:ident) => {{ let membership = if let Some(ref ua) = $user { @@ -98,7 +97,9 @@ macro_rules! community_context_bools { }; let is_pending = if let Some(ref membership) = membership { - membership.role.check(CommunityPermission::REQUESTED) + membership.role.check( + tetratto_core::model::communities_permissions::CommunityPermission::REQUESTED, + ) } else { false }; @@ -110,25 +111,27 @@ macro_rules! community_context_bools { }; let can_manage_posts = if let Some(ref membership) = membership { - membership.role.check(CommunityPermission::MANAGE_POSTS) + membership.role.check( + tetratto_core::model::communities_permissions::CommunityPermission::MANAGE_POSTS, + ) } else { false }; let can_manage_community = if let Some(ref membership) = membership { - membership.role.check(CommunityPermission::MANAGE_COMMUNITY) + membership.role.check(tetratto_core::model::communities_permissions::CommunityPermission::MANAGE_COMMUNITY) } else { false }; let can_manage_roles = if let Some(ref membership) = membership { - membership.role.check(CommunityPermission::MANAGE_ROLES) + membership.role.check(tetratto_core::model::communities_permissions::CommunityPermission::MANAGE_ROLES) } else { false }; let can_manage_questions = if let Some(ref membership) = membership { - membership.role.check(CommunityPermission::MANAGE_QUESTIONS) + membership.role.check(tetratto_core::model::communities_permissions::CommunityPermission::MANAGE_QUESTIONS) } else { false }; @@ -389,7 +392,7 @@ pub async fn feed_request( } // check permissions - let (can_read, _) = check_permissions!(community, jar, data, user); + let (can_read, _) = check_community_permissions!(community, jar, data, user); // ... let ignore_users = crate::ignore_users_gen!(user, data); @@ -487,7 +490,7 @@ pub async fn questions_request( } // check permissions - let (can_read, _) = check_permissions!(community, jar, data, user); + let (can_read, _) = check_community_permissions!(community, jar, data, user); // ... let ignore_users = crate::ignore_users_gen!(user, data); @@ -724,7 +727,7 @@ pub async fn post_request( }; // check permissions - let (can_read, can_manage_pins) = check_permissions!(community, jar, data, user); + let (can_read, can_manage_pins) = check_community_permissions!(community, jar, data, user); if !can_read { return Err(Html( @@ -838,7 +841,7 @@ pub async fn reposts_request( }; // check permissions - let (can_read, _) = check_permissions!(community, jar, data, user); + let (can_read, _) = check_community_permissions!(community, jar, data, user); if !can_read { return Err(Html( @@ -989,7 +992,7 @@ pub async fn likes_request( }; // check permissions - let (can_read, _) = check_permissions!(community, jar, data, ua); + let (can_read, _) = check_community_permissions!(community, jar, data, ua); if !can_read { return Err(Html( @@ -1092,7 +1095,7 @@ pub async fn members_request( } // check permissions - let (can_read, _) = check_permissions!(community, jar, data, user); + let (can_read, _) = check_community_permissions!(community, jar, data, user); // ... let list = match data @@ -1128,6 +1131,7 @@ pub async fn members_request( can_manage_questions, ) = community_context_bools!(data, user, community); + context.insert("allow_for_forges", &true); context.insert("list", &list); context.insert("page", &props.page); context.insert("owner", &owner); @@ -1187,7 +1191,7 @@ pub async fn question_request( }; // check permissions - let (can_read, _) = check_permissions!(community, jar, data, user); + let (can_read, _) = check_community_permissions!(community, jar, data, user); if !can_read && !is_sender { return Err(Html( diff --git a/crates/app/src/routes/pages/forge.rs b/crates/app/src/routes/pages/forge.rs new file mode 100644 index 0000000..40de554 --- /dev/null +++ b/crates/app/src/routes/pages/forge.rs @@ -0,0 +1,126 @@ +use super::{communities::community_context, render_error}; +use crate::{ + assets::initial_context, check_community_permissions, community_context_bools, get_lang, + get_user_from_token, State, +}; +use axum::{ + response::{Html, IntoResponse}, + extract::Path, + Extension, +}; +use axum_extra::extract::CookieJar; +use tetratto_core::model::{communities::Community, Error}; + +/// `/forges` +pub async fn home_request(jar: CookieJar, Extension(data): Extension) -> impl IntoResponse { + let data = data.read().await; + let user = match get_user_from_token!(jar, data.0) { + Some(ua) => ua, + None => { + return Err(Html( + render_error(Error::NotAllowed, &jar, &data, &None).await, + )); + } + }; + + let list = match data.0.get_memberships_by_owner(user.id).await { + Ok(p) => p, + Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), + }; + + let mut communities: Vec = Vec::new(); + for membership in &list { + match data + .0 + .get_community_by_id_no_void(membership.community) + .await + { + Ok(c) => { + if c.is_forge { + communities.push(c) + } else { + // we only want to show forges here + continue; + } + } + Err(_) => { + // delete membership; community doesn't exist + if let Err(e) = data.0.delete_membership(membership.id, &user).await { + return Err(Html(render_error(e, &jar, &data, &Some(user)).await)); + } + + continue; + } + } + } + + let lang = get_lang!(jar, data.0); + let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; + context.insert("list", &communities); + + // return + Ok(Html(data.1.render("forge/home.html", &context).unwrap())) +} + +/// `/forge/{title}` +pub async fn info_request( + jar: CookieJar, + Path(title): Path, + Extension(data): Extension, +) -> impl IntoResponse { + let data = data.read().await; + let user = get_user_from_token!(jar, data.0); + + let community = match data.0.get_community_by_title(&title.to_lowercase()).await { + Ok(ua) => ua, + Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), + }; + + if community.id == 0 { + // don't show page for void community + return Err(Html( + render_error( + Error::GeneralNotFound("community".to_string()), + &jar, + &data, + &user, + ) + .await, + )); + } + + // check permissions + let (can_read, _) = check_community_permissions!(community, jar, data, user); + + // init context + let lang = get_lang!(jar, data.0); + let mut context = initial_context(&data.0.0.0, lang, &user).await; + + let ( + is_owner, + is_joined, + is_pending, + can_post, + can_manage_posts, + can_manage_community, + can_manage_roles, + can_manage_questions, + ) = community_context_bools!(data, user, community); + + community_context( + &mut context, + &community, + is_owner, + is_joined, + is_pending, + can_post, + can_read, + can_manage_posts, + can_manage_community, + can_manage_roles, + can_manage_questions, + ); + + // return + Ok(Html(data.1.render("forge/info.html", &context).unwrap())) +} diff --git a/crates/app/src/routes/pages/mod.rs b/crates/app/src/routes/pages/mod.rs index 654a950..d0ed2c0 100644 --- a/crates/app/src/routes/pages/mod.rs +++ b/crates/app/src/routes/pages/mod.rs @@ -1,5 +1,6 @@ pub mod auth; pub mod communities; +pub mod forge; pub mod misc; pub mod mod_panel; pub mod profile; @@ -110,6 +111,10 @@ pub fn routes() -> Router { "/chats/{community}/{channel}/_channels", get(chats::channels_request), ) + // forge + .route("/forges", get(forge::home_request)) + .route("/forge/{title}", get(forge::info_request)) + .route("/forge/{title}/members", get(communities::members_request)) // stacks .route("/stacks", get(stacks::list_request)) .route("/stacks/{id}", get(stacks::posts_request)) diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index b4fac35..e998103 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -12,12 +12,12 @@ default = ["sqlite", "redis"] [dependencies] pathbufd = "0.1.4" serde = { version = "1.0.219", features = ["derive"] } -toml = "0.8.22" +toml = "0.8.23" tetratto-shared = { path = "../shared" } tetratto-l10n = { path = "../l10n" } serde_json = "1.0.140" totp-rs = { version = "5.7.0", features = ["qr", "gen_secret"] } -reqwest = { version = "0.12.18", features = ["json"] } +reqwest = { version = "0.12.19", features = ["json"] } bitflags = "2.9.1" async-recursion = "1.1.1" md-5 = "0.10.6" @@ -25,4 +25,4 @@ base16ct = { version = "0.2.0", features = ["alloc"] } base64 = "0.22.1" emojis = "0.6.4" regex = "1.11.1" -oiseau = { version = "0.1.0", default-features = false } +oiseau = { version = "0.1.2", default-features = false } diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index 122308a..76ed2d3 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -10,7 +10,10 @@ use crate::model::{ }; use pathbufd::PathBufD; use std::fs::{exists, remove_file}; -use tetratto_shared::hash::{hash_salted, salt}; +use tetratto_shared::{ + hash::{hash_salted, salt}, + unix_epoch_timestamp, +}; use crate::{auto_method, DataManager}; #[cfg(feature = "sqlite")] @@ -18,8 +21,6 @@ use oiseau::SqliteRow; #[cfg(feature = "postgres")] use oiseau::PostgresRow; -#[cfg(feature = "postgres")] -use tetratto_shared::unix_epoch_timestamp; use oiseau::{execute, get, query_row, params}; diff --git a/crates/core/src/database/communities.rs b/crates/core/src/database/communities.rs index 670b803..b5db29a 100644 --- a/crates/core/src/database/communities.rs +++ b/crates/core/src/database/communities.rs @@ -40,8 +40,10 @@ impl DataManager { // likes likes: get!(x->8(i32)) as isize, dislikes: get!(x->9(i32)) as isize, - // counts + // ... member_count: get!(x->10(i32)) as usize, + is_forge: get!(x->11(i32)) as i8 == 1, + post_count: get!(x->12(i32)) as usize, } } @@ -51,7 +53,10 @@ impl DataManager { } if let Some(cached) = self.0.1.get(format!("atto.community:{}", id)).await { - return Ok(serde_json::from_str(&cached).unwrap()); + match serde_json::from_str(&cached) { + Ok(c) => return Ok(c), + Err(_) => self.0.1.remove(format!("atto.community:{}", id)).await, + }; } let conn = match self.0.connect().await { @@ -89,7 +94,10 @@ impl DataManager { } if let Some(cached) = self.0.1.get(format!("atto.community:{}", id)).await { - return Ok(serde_json::from_str(&cached).unwrap()); + match serde_json::from_str(&cached) { + Ok(c) => return Ok(c), + Err(_) => self.0.1.remove(format!("atto.community:{}", id)).await, + }; } let conn = match self.0.connect().await { @@ -218,7 +226,7 @@ impl DataManager { return Err(Error::MiscError("This title cannot be used".to_string())); } - let regex = regex::RegexBuilder::new(r"[^\w_\-\.!]+") + let regex = regex::RegexBuilder::new(NAME_REGEX) .multi_line(true) .build() .unwrap(); @@ -259,6 +267,12 @@ impl DataManager { } } + // check is_forge + // only supporters can CREATE forge communities... anybody can contribute to them + if data.is_forge && !owner.permissions.check(FinePermission::SUPPORTER) { + return Err(Error::RequiresSupporter); + } + // make sure community doesn't already exist with title if self .get_community_by_title_no_void(&data.title.to_lowercase()) @@ -276,7 +290,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO communities VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", + "INSERT INTO communities VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", params![ &(data.id as i64), &(data.created as i64), @@ -288,7 +302,9 @@ impl DataManager { &serde_json::to_string(&data.join_access).unwrap().as_str(), &0_i32, &0_i32, - &1_i32 + &1_i32, + &{ if data.is_forge { 1 } else { 0 } }, + &0_i32, ] ); @@ -532,4 +548,7 @@ impl DataManager { auto_method!(incr_community_member_count()@get_community_by_id_no_void -> "UPDATE communities SET member_count = member_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --incr); auto_method!(decr_community_member_count()@get_community_by_id_no_void -> "UPDATE communities SET member_count = member_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --decr=member_count); + + auto_method!(incr_community_post_count()@get_community_by_id_no_void -> "UPDATE communities SET post_count = post_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --incr); + auto_method!(decr_community_post_count()@get_community_by_id_no_void -> "UPDATE communities SET post_count = post_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --decr=post_count); } diff --git a/crates/core/src/database/messages.rs b/crates/core/src/database/messages.rs index df889be..17ec914 100644 --- a/crates/core/src/database/messages.rs +++ b/crates/core/src/database/messages.rs @@ -8,6 +8,7 @@ use crate::model::{ communities_permissions::CommunityPermission, channels::Message, }; use serde::Serialize; +use tetratto_shared::unix_epoch_timestamp; use crate::{auto_method, DataManager}; #[cfg(feature = "redis")] @@ -18,8 +19,6 @@ use oiseau::SqliteRow; #[cfg(feature = "postgres")] use oiseau::PostgresRow; -#[cfg(feature = "postgres")] -use tetratto_shared::unix_epoch_timestamp; use oiseau::{execute, get, query_rows, params}; diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index 8418361..f3e483d 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -872,7 +872,7 @@ impl DataManager { let res = query_row!( &conn, - "SELECT * FROM posts WHERE context LIKE $1 AND owner = $2 LIMIT 1", + "SELECT * FROM posts WHERE context LIKE $1 AND owner = $2 AND is_deleted = 0 LIMIT 1", params![&format!("%\"answering\":{question}%"), &(owner as i64),], |x| { Ok(Self::get_post_from_row(x)) } ); @@ -1494,6 +1494,9 @@ impl DataManager { // increase user post count self.incr_user_post_count(data.owner).await?; + // increase community post count + self.incr_community_post_count(data.community).await?; + // return Ok(data.id) } @@ -1546,6 +1549,13 @@ impl DataManager { self.decr_user_post_count(y.owner).await?; } + // decr community post count + let community = self.get_community_by_id_no_void(y.community).await?; + + if community.post_count > 0 { + self.decr_community_post_count(y.community).await?; + } + // decr question answer count if y.context.answering != 0 { let question = self.get_question_by_id(y.context.answering).await?; @@ -1622,12 +1632,19 @@ impl DataManager { self.decr_user_post_count(y.owner).await?; } + // decr community post count + let community = self.get_community_by_id_no_void(y.community).await?; + + if community.post_count > 0 { + self.decr_community_post_count(y.community).await?; + } + // decr question answer count if y.context.answering != 0 { let question = self.get_question_by_id(y.context.answering).await?; if question.is_global { - self.incr_question_answer_count(y.context.answering).await?; + self.decr_question_answer_count(y.context.answering).await?; } } @@ -1644,12 +1661,15 @@ impl DataManager { // incr user post count self.incr_user_post_count(y.owner).await?; + // incr community post count + self.incr_community_post_count(y.community).await?; + // incr question answer count if y.context.answering != 0 { let question = self.get_question_by_id(y.context.answering).await?; if question.is_global { - self.decr_question_answer_count(y.context.answering).await?; + self.incr_question_answer_count(y.context.answering).await?; } } diff --git a/crates/core/src/model/communities.rs b/crates/core/src/model/communities.rs index efc96aa..ecf8ded 100644 --- a/crates/core/src/model/communities.rs +++ b/crates/core/src/model/communities.rs @@ -22,8 +22,10 @@ pub struct Community { // likes pub likes: isize, pub dislikes: isize, - // counts + // ... pub member_count: usize, + pub is_forge: bool, + pub post_count: usize, } impl Community { @@ -44,6 +46,8 @@ impl Community { likes: 0, dislikes: 0, member_count: 0, + is_forge: false, + post_count: 0, } } @@ -62,6 +66,8 @@ impl Community { likes: 0, dislikes: 0, member_count: 0, + is_forge: false, + post_count: 0, } } } @@ -84,9 +90,6 @@ pub struct CommunityContext { /// `enable_titles` is required for this setting to work. #[serde(default)] pub require_titles: bool, - /// The community's layout in the UI. - #[serde(default)] - pub layout: CommunityLayout, } /// Who can read a [`Community`]. @@ -140,21 +143,6 @@ impl Default for CommunityJoinAccess { } } -/// The layout of the [`Community`]'s UI. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum CommunityLayout { - /// The classic timeline-like layout. - Classic, - /// A GitHub-esque bug tracker layout. - BugTracker, -} - -impl Default for CommunityLayout { - fn default() -> Self { - Self::Classic - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CommunityMembership { pub id: usize, diff --git a/crates/l10n/Cargo.toml b/crates/l10n/Cargo.toml index b882fa4..e8a5b4f 100644 --- a/crates/l10n/Cargo.toml +++ b/crates/l10n/Cargo.toml @@ -9,4 +9,4 @@ license.workspace = true [dependencies] pathbufd = "0.1.4" serde = { version = "1.0.219", features = ["derive"] } -toml = "0.8.22" +toml = "0.8.23" diff --git a/sql_changes/communities_is_forge.sql b/sql_changes/communities_is_forge.sql new file mode 100644 index 0000000..a4fe564 --- /dev/null +++ b/sql_changes/communities_is_forge.sql @@ -0,0 +1,2 @@ +ALTER TABLE communities +ADD COLUMN is_forge INT NOT NULL DEFAULT 0; diff --git a/sql_changes/communities_post_count.sql b/sql_changes/communities_post_count.sql new file mode 100644 index 0000000..fdb188c --- /dev/null +++ b/sql_changes/communities_post_count.sql @@ -0,0 +1,2 @@ +ALTER TABLE communities +ADD COLUMN post_count INT NOT NULL DEFAULT 0;