From 6413ed09fb6732121ad3baeae77cbff3b3b34927 Mon Sep 17 00:00:00 2001 From: trisua Date: Sat, 29 Mar 2025 22:27:57 -0400 Subject: [PATCH] add: community settings ui TODO: add community read/write access settings TODO: add profile settings TODO: profile following in ui TODO: community joining and membership management in ui --- Cargo.lock | 489 ++++++++++++++++++ crates/app/Cargo.toml | 1 + crates/app/src/langs/en-US.toml | 1 + crates/app/src/main.rs | 16 +- crates/app/src/public/css/style.css | 9 +- .../app/src/public/html/communities/base.html | 91 +++- crates/app/src/public/html/components.html | 44 +- crates/app/src/public/html/profile/base.html | 2 +- crates/app/src/public/js/atto.js | 61 ++- crates/app/src/public/js/me.js | 43 ++ crates/app/src/routes/api/v1/mod.rs | 2 +- crates/app/src/routes/api/v1/reactions.rs | 30 +- crates/app/src/routes/pages/communities.rs | 9 + crates/core/src/database/communities.rs | 6 +- crates/core/src/database/posts.rs | 4 +- crates/core/src/database/reactions.rs | 42 +- crates/core/src/model/reactions.rs | 4 +- crates/shared/Cargo.toml | 2 + crates/shared/src/lib.rs | 1 + crates/shared/src/markdown.rs | 44 ++ 20 files changed, 855 insertions(+), 46 deletions(-) create mode 100644 crates/shared/src/markdown.rs diff --git a/Cargo.lock b/Cargo.lock index c1ec6c8..4d51907 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,19 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" +[[package]] +name = "ammonia" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab99eae5ee58501ab236beb6f20f6ca39be615267b014899c89b2f0bc18a459" +dependencies = [ + "html5ever", + "maplit", + "once_cell", + "tendril", + "url", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -47,6 +60,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.97" @@ -261,6 +324,30 @@ dependencies = [ "tokio-postgres", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit_field" version = "0.10.2" @@ -294,6 +381,31 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bon" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65268237be94042665b92034f979c42d431d2fd998b49809543afe3e66abad1c" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803c95b2ecf650eb10b5f87dda6b9f6a1b758cee53245e2b7b825c9b3803a443" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "bstr" version = "1.11.3" @@ -340,6 +452,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "caseless" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8" +dependencies = [ + "unicode-normalization", +] + [[package]] name = "cc" version = "1.2.16" @@ -403,12 +524,59 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "clap" +version = "4.5.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "color_quant" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "combine" version = "4.6.7" @@ -419,6 +587,25 @@ dependencies = [ "memchr", ] +[[package]] +name = "comrak" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5afa2702ef2fecc5bd7ca605f37e875a6be3fc8138c4633e711a945b70351550" +dependencies = [ + "bon", + "caseless", + "clap", + "entities", + "memchr", + "shell-words", + "slug", + "syntect", + "typed-arena", + "unicode_categories", + "xdg", +] + [[package]] name = "cookie" version = "0.18.1" @@ -505,6 +692,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.4.0" @@ -557,6 +779,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "entities" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" + [[package]] name = "equivalent" version = "1.0.2" @@ -606,6 +834,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -667,6 +905,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -862,6 +1110,20 @@ dependencies = [ "digest", ] +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "http" version = "1.3.1" @@ -1137,6 +1399,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1240,6 +1508,12 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.12.1" @@ -1324,6 +1598,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.9.3" @@ -1361,6 +1641,32 @@ dependencies = [ "imgref", ] +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1582,6 +1888,28 @@ version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +[[package]] +name = "onig" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +dependencies = [ + "bitflags 1.3.2", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "openssl" version = "0.10.71" @@ -1786,6 +2114,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plist" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + [[package]] name = "png" version = "0.17.16" @@ -1843,6 +2184,22 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -1886,6 +2243,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.40" @@ -2385,6 +2751,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" @@ -2459,6 +2831,31 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -2470,6 +2867,12 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -2507,6 +2910,29 @@ dependencies = [ "syn", ] +[[package]] +name = "syntect" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "fancy-regex", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax 0.8.5", + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", + "walkdir", + "yaml-rust", +] + [[package]] name = "system-configuration" version = "0.6.1" @@ -2560,6 +2986,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "tera" version = "1.20.0" @@ -2582,6 +3019,16 @@ dependencies = [ "unic-segment", ] +[[package]] +name = "terminal_size" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "tetratto" version = "0.1.0" @@ -2593,6 +3040,7 @@ dependencies = [ "regex", "reqwest", "serde", + "serde_json", "tera", "tetratto-core", "tetratto-l10n", @@ -2633,7 +3081,9 @@ dependencies = [ name = "tetratto-shared" version = "0.1.0" dependencies = [ + "ammonia", "chrono", + "comrak", "hex_fmt", "num-bigint", "rand 0.9.0", @@ -3001,6 +3451,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typenum" version = "1.18.0" @@ -3096,6 +3552,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "untrusted" version = "0.9.0" @@ -3113,6 +3575,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf16_iter" version = "1.0.5" @@ -3125,6 +3593,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.16.0" @@ -3558,6 +4032,21 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 9c7d5db..186c44a 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -25,3 +25,4 @@ tetratto-l10n = { path = "../l10n" } image = "0.25.5" reqwest = "0.12.15" regex = "1.11.1" +serde_json = "1.0.140" diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index 7c45d97..bff8a76 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -11,6 +11,7 @@ version = "1.0.0" "dialog:action.cancel" = "Cancel" "dialog:action.yes" = "Yes" "dialog:action.no" = "No" +"dialog:action.save_and_close" = "Save and close" "auth:action.login" = "Login" "auth:action.register" = "Register" diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index b9cd5b0..29def3a 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -7,15 +7,19 @@ use assets::{init_dirs, write_assets}; pub use tetratto_core::*; use axum::{Extension, Router}; -use tera::Tera; +use tera::{Tera, Value}; use tower_http::trace::{self, TraceLayer}; use tracing::{Level, info}; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use tokio::sync::RwLock; pub(crate) type State = Arc>; +fn render_markdown(value: &Value, _: &HashMap) -> tera::Result { + Ok(tetratto_shared::markdown::render_markdown(&value.as_str().unwrap()).into()) +} + #[tokio::main] async fn main() { tracing_subscriber::fmt() @@ -33,12 +37,12 @@ async fn main() { let database = DataManager::new(config.clone()).await.unwrap(); database.init().await.unwrap(); + let mut tera = Tera::new(&format!("{html_path}/**/*")).unwrap(); + tera.register_filter("markdown", render_markdown); + let app = Router::new() .merge(routes::routes(&config)) - .layer(Extension(Arc::new(RwLock::new(( - database, - Tera::new(&format!("{html_path}/**/*")).unwrap(), - ))))) + .layer(Extension(Arc::new(RwLock::new((database, tera))))) .layer( TraceLayer::new_for_http() .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css index 647a7dd..4bc2a12 100644 --- a/crates/app/src/public/css/style.css +++ b/crates/app/src/public/css/style.css @@ -152,7 +152,14 @@ button svg { } hr { - border-top: 1px var(--color-super-lowered); + border-top: solid 1px var(--color-super-lowered) !important; + border-left: 0; + border-bottom: 0; + border-right: 0; +} + +hr.margin { + margin: 1rem 0; } p, diff --git a/crates/app/src/public/html/communities/base.html b/crates/app/src/public/html/communities/base.html index 765fd94..5bd84fd 100644 --- a/crates/app/src/public/html/communities/base.html +++ b/crates/app/src/public/html/communities/base.html @@ -25,6 +25,18 @@ {{ community.title }} + + {% if user %} +
+ {{ components::likes(id=community.id, + asset_type="Community", likes=community.likes, + dislikes=community.dislikes) }} +
+ {% endif %} @@ -41,15 +53,86 @@ {{ text "communities:action.leave" }} {% endif %} {% else %} - {{ icon "settings" }} {{ text "communities:action.configure" }} - + + + +
+
+ +
+ + +
+
+ + {% endif %} {% endif %} @@ -57,7 +140,7 @@
- {{ community.context.description }} + {{ community.context.description|markdown|safe }}
diff --git a/crates/app/src/public/html/components.html b/crates/app/src/public/html/components.html index fbdc98c..c9bd5bd 100644 --- a/crates/app/src/public/html/components.html +++ b/crates/app/src/public/html/components.html @@ -38,6 +38,28 @@ {% if user.settings.display_name %} {{ user.settings.display_name }} {% else %} {{ user.username }} {% endif %}
+{%- endmacro %} {% macro likes(id, asset_type, likes=0, dislikes=0) -%} + + + {%- endmacro %} {% macro post(post, owner, secondary=false, community=false, show_community=true) -%}
@@ -68,21 +90,23 @@ show_community=true) -%} {% endif %}
- {{ post.content }} + {{ post.content|markdown|safe }}
-
- {% if user %} - - - {% endif %} + {% if user %} +
+ {{ components::likes(id=post.id, asset_type="Post", + likes=post.likes, dislikes=post.dislikes) }}
+ {% endif %}
diff --git a/crates/app/src/public/html/profile/base.html b/crates/app/src/public/html/profile/base.html index 502dd31..af16cf7 100644 --- a/crates/app/src/public/html/profile/base.html +++ b/crates/app/src/public/html/profile/base.html @@ -53,7 +53,7 @@
- {{ profile.settings.biography }} + {{ profile.settings.biography|markdown|safe }}
diff --git a/crates/app/src/public/js/atto.js b/crates/app/src/public/js/atto.js index 4499a88..e2823e6 100644 --- a/crates/app/src/public/js/atto.js +++ b/crates/app/src/public/js/atto.js @@ -455,21 +455,33 @@ media_theme_pref(); self.define("hooks::check_reactions", async ({ $ }) => { const observer = $.offload_work_to_client_when_in_view( async (element) => { + const like = element.querySelector( + '[hook_element="reaction.like"]', + ); + + const dislike = element.querySelector( + '[hook_element="reaction.dislike"]', + ); + const reaction = await ( await fetch( `/api/v1/reactions/${element.getAttribute("hook-arg:id")}`, ) ).json(); - if (reaction.success) { - element.classList.add("green"); - element.querySelector("svg").classList.add("filled"); + if (reaction.ok) { + if (reaction.payload.is_like) { + like.classList.add("green"); + like.querySelector("svg").classList.add("filled"); + } else { + dislike.classList.add("red"); + } } }, ); for (const element of Array.from( - document.querySelectorAll("[hook=check_reaction]") || [], + document.querySelectorAll("[hook=check_reactions]") || [], )) { observer.observe(element); } @@ -619,3 +631,44 @@ media_theme_pref(); } }); })(); + +// ui ns +(() => { + const self = reg_ns("ui"); + + self.define("render_settings_ui_field", (_, into_element, option) => { + into_element.innerHTML += `
+
+ ${option.label.replaceAll("_", " ")} +
+ +
+ <${option.input_element_type || "input"} + type="text" + onchange="window.set_setting_field('${option.key}', event.target.value)" + placeholder="${option.key}" + ${option.input_element_type === "input" ? `value="${option.value}"/>` : ">"} +${option.input_element_type === "textarea" ? `${option.value}` : ""} +
+
`; + }); + + self.define( + "generate_settings_ui", + ({ $ }, into_element, options, settings_ref) => { + for (const option of options) { + $.render_settings_ui_field(into_element, { + key: Array.isArray(option[0]) ? option[0][0] : option[0], + label: Array.isArray(option[0]) ? option[0][1] : option[0], + value: option[1], + input_element_type: option[2], + }); + } + + window.set_setting_field = (key, value) => { + settings_ref[key] = value; + console.log("update", key); + }; + }, + ); +})(); diff --git a/crates/app/src/public/js/me.js b/crates/app/src/public/js/me.js index e729fbb..8a8fcec 100644 --- a/crates/app/src/public/js/me.js +++ b/crates/app/src/public/js/me.js @@ -48,4 +48,47 @@ ]); }); }); + + self.define("react", async (_, element, asset, asset_type, is_like) => { + fetch("/api/v1/reactions", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + asset, + asset_type, + is_like, + }), + }) + .then((res) => res.json()) + .then((res) => { + trigger("atto::toast", [ + res.ok ? "success" : "error", + res.message, + ]); + + if (res.ok) { + const like = element.parentElement.querySelector( + '[hook_element="reaction.like"]', + ); + + const dislike = element.parentElement.querySelector( + '[hook_element="reaction.dislike"]', + ); + + if (is_like) { + like.classList.add("green"); + like.querySelector("svg").classList.add("filled"); + + dislike.classList.remove("red"); + } else { + dislike.classList.add("red"); + + like.classList.remove("green"); + like.querySelector("svg").classList.remove("filled"); + } + } + }); + }); })(); diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 3365b79..09ae7d7 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -173,7 +173,7 @@ pub struct UpdatePostContext { #[derive(Deserialize)] pub struct CreateReaction { - pub asset: usize, + pub asset: String, pub asset_type: AssetType, pub is_like: bool, } diff --git a/crates/app/src/routes/api/v1/reactions.rs b/crates/app/src/routes/api/v1/reactions.rs index 915c972..dcc4767 100644 --- a/crates/app/src/routes/api/v1/reactions.rs +++ b/crates/app/src/routes/api/v1/reactions.rs @@ -36,10 +36,34 @@ pub async fn create_request( None => return Json(Error::NotAllowed.into()), }; + let asset_id = match req.asset.parse::() { + Ok(n) => n, + Err(e) => return Json(Error::MiscError(e.to_string()).into()), + }; + + // check for existing reaction + if let Ok(r) = data.get_reaction_by_owner_asset(user.id, asset_id).await { + match data.delete_reaction(r.id, &user).await { + Ok(_) => { + // if we're trying to create a reaction of a DIFFERENT TYPE, then + // we don't need to return here + if r.is_like == req.is_like { + return Json(ApiReturn { + ok: true, + message: "Reaction removed".to_string(), + payload: (), + }); + } + } + Err(e) => return Json(e.into()), + }; + } + + // create reaction match data .create_reaction(Reaction::new( user.id, - req.asset, + asset_id, req.asset_type, req.is_like, )) @@ -50,7 +74,7 @@ pub async fn create_request( message: "Reaction created".to_string(), payload: (), }), - Err(e) => return Json(e.into()), + Err(e) => Json(e.into()), } } @@ -70,7 +94,7 @@ pub async fn delete_request( Err(e) => return Json(e.into()), }; - match data.delete_reaction(reaction.id, user).await { + match data.delete_reaction(reaction.id, &user).await { Ok(_) => Json(ApiReturn { ok: true, message: "Reaction deleted".to_string(), diff --git a/crates/app/src/routes/pages/communities.rs b/crates/app/src/routes/pages/communities.rs index fac4131..456c1da 100644 --- a/crates/app/src/routes/pages/communities.rs +++ b/crates/app/src/routes/pages/communities.rs @@ -80,6 +80,15 @@ pub fn community_context( context.insert("community", &community); context.insert("is_owner", &is_owner); context.insert("is_joined", &is_joined); + + if is_owner { + context.insert( + "community_context_serde", + &serde_json::to_string(&community.context) + .unwrap() + .replace("\"", "\\\""), + ); + } } /// `/community/{title}` diff --git a/crates/core/src/database/communities.rs b/crates/core/src/database/communities.rs index 7997acb..66344e9 100644 --- a/crates/core/src/database/communities.rs +++ b/crates/core/src/database/communities.rs @@ -116,14 +116,14 @@ impl DataManager { auto_method!(delete_community()@get_community_by_id:MANAGE_COMMUNITIES -> "DELETE communities pages WHERE id = $1" --cache-key-tmpl=cache_clear_community); auto_method!(update_community_title(String)@get_community_by_id:MANAGE_COMMUNITIES -> "UPDATE communities SET title = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_community); - auto_method!(update_community_context(CommunityContext)@get_community_by_id:MANAGE_COMMUNITIES -> "UPDATE communities SET prompt = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community); + auto_method!(update_community_context(CommunityContext)@get_community_by_id:MANAGE_COMMUNITIES -> "UPDATE communities SET context = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community); auto_method!(update_community_read_access(CommunityReadAccess)@get_community_by_id:MANAGE_COMMUNITIES -> "UPDATE communities SET read_access = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community); auto_method!(update_community_write_access(CommunityWriteAccess)@get_community_by_id:MANAGE_COMMUNITIES -> "UPDATE communities SET write_access = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community); auto_method!(incr_community_likes()@get_community_by_id -> "UPDATE communities SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --incr); - auto_method!(incr_community_dislikes()@get_community_by_id -> "UPDATE communities SET likes = dislikes + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --incr); + auto_method!(incr_community_dislikes()@get_community_by_id -> "UPDATE communities SET dislikes = dislikes + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --incr); auto_method!(decr_community_likes()@get_community_by_id -> "UPDATE communities SET likes = likes - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --decr); - auto_method!(decr_community_dislikes()@get_community_by_id -> "UPDATE communities SET likes = dislikes - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --decr); + auto_method!(decr_community_dislikes()@get_community_by_id -> "UPDATE communities SET dislikes = dislikes - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --decr); auto_method!(incr_community_member_count()@get_community_by_id -> "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 -> "UPDATE communities SET member_count = member_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --decr); diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index 02956e8..fa253e9 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -287,9 +287,9 @@ impl DataManager { auto_method!(update_post_context(PostContext)@get_post_by_id:MANAGE_POSTS -> "UPDATE posts SET context = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.post:{}"); auto_method!(incr_post_likes() -> "UPDATE posts SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr); - auto_method!(incr_post_dislikes() -> "UPDATE posts SET likes = dislikes + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr); + auto_method!(incr_post_dislikes() -> "UPDATE posts SET dislikes = dislikes + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr); auto_method!(decr_post_likes() -> "UPDATE posts SET likes = likes - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr); - auto_method!(decr_post_dislikes() -> "UPDATE posts SET likes = dislikes - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr); + auto_method!(decr_post_dislikes() -> "UPDATE posts SET dislikes = dislikes - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr); auto_method!(incr_post_comments() -> "UPDATE posts SET comment_count = comment_count + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr); auto_method!(decr_post_comments() -> "UPDATE posts SET comment_count = comment_count - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr); diff --git a/crates/core/src/database/reactions.rs b/crates/core/src/database/reactions.rs index d59cd76..e14b103 100644 --- a/crates/core/src/database/reactions.rs +++ b/crates/core/src/database/reactions.rs @@ -86,13 +86,25 @@ impl DataManager { // incr corresponding match data.asset_type { - AssetType::Journal => { - if let Err(e) = self.incr_community_likes(data.id).await { + AssetType::Community => { + if let Err(e) = { + if data.is_like { + self.incr_community_likes(data.asset).await + } else { + self.incr_community_dislikes(data.asset).await + } + } { return Err(e); } } - AssetType::JournalEntry => { - if let Err(e) = self.incr_post_likes(data.id).await { + AssetType::Post => { + if let Err(e) = { + if data.is_like { + self.incr_post_likes(data.asset).await + } else { + self.incr_post_dislikes(data.asset).await + } + } { return Err(e); } } @@ -102,7 +114,7 @@ impl DataManager { Ok(()) } - pub async fn delete_reaction(&self, id: usize, user: User) -> Result<()> { + pub async fn delete_reaction(&self, id: usize, user: &User) -> Result<()> { let reaction = self.get_reaction_by_id(id).await?; if user.id != reaction.owner { @@ -130,13 +142,25 @@ impl DataManager { // decr corresponding match reaction.asset_type { - AssetType::Journal => { - if let Err(e) = self.decr_community_likes(reaction.asset).await { + AssetType::Community => { + if let Err(e) = { + if reaction.is_like { + self.decr_community_likes(reaction.asset).await + } else { + self.decr_community_dislikes(reaction.asset).await + } + } { return Err(e); } } - AssetType::JournalEntry => { - if let Err(e) = self.decr_post_likes(reaction.asset).await { + AssetType::Post => { + if let Err(e) = { + if reaction.is_like { + self.decr_post_likes(reaction.asset).await + } else { + self.decr_post_dislikes(reaction.asset).await + } + } { return Err(e); } } diff --git a/crates/core/src/model/reactions.rs b/crates/core/src/model/reactions.rs index 6e5c591..494d5d3 100644 --- a/crates/core/src/model/reactions.rs +++ b/crates/core/src/model/reactions.rs @@ -4,8 +4,8 @@ use tetratto_shared::{snow::AlmostSnowflake, unix_epoch_timestamp}; /// All of the items which support reactions. #[derive(Serialize, Deserialize)] pub enum AssetType { - Journal, - JournalEntry, + Community, + Post, } #[derive(Serialize, Deserialize)] diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index a1fbac8..bdab8a3 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -7,7 +7,9 @@ repository.workspace = true license.workspace = true [dependencies] +ammonia = "4.0.0" chrono = "0.4.40" +comrak = "0.36.0" hex_fmt = "0.3.0" num-bigint = "0.4.6" rand = "0.9.0" diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index 5a7f9c3..a3652dd 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -1,4 +1,5 @@ pub mod hash; +pub mod markdown; pub mod snow; pub mod time; diff --git a/crates/shared/src/markdown.rs b/crates/shared/src/markdown.rs new file mode 100644 index 0000000..5fc5994 --- /dev/null +++ b/crates/shared/src/markdown.rs @@ -0,0 +1,44 @@ +use ammonia::Builder; +use comrak::{Options, markdown_to_html}; +use std::collections::HashSet; + +/// Render markdown input into HTML +pub fn render_markdown(input: &str) -> String { + let mut options = Options::default(); + + options.extension.table = true; + options.extension.superscript = true; + options.extension.strikethrough = true; + options.extension.autolink = true; + options.extension.header_ids = Option::Some(String::new()); + options.extension.tagfilter = true; + options.render.unsafe_ = true; + // options.render.escape = true; + options.parse.smart = false; + + let html = markdown_to_html(input, &options); + + let mut allowed_attributes = HashSet::new(); + allowed_attributes.insert("id"); + allowed_attributes.insert("class"); + allowed_attributes.insert("ref"); + allowed_attributes.insert("aria-label"); + allowed_attributes.insert("lang"); + allowed_attributes.insert("title"); + allowed_attributes.insert("align"); + + allowed_attributes.insert("data-color"); + allowed_attributes.insert("data-font-family"); + + Builder::default() + .generic_attributes(allowed_attributes) + .clean(&html) + .to_string() + .replace( + "src=\"", + "loading=\"lazy\" src=\"/api/v1/util/ext/image?img=", + ) + .replace("-->", "") + .replace("->", "") + .replace("<-", "") +}