From 5fafc8d7b93b3f9be3aee447b72ed052173c6d0e Mon Sep 17 00:00:00 2001 From: trisua Date: Sun, 31 Aug 2025 13:02:15 -0400 Subject: [PATCH] add: close friends stack --- Cargo.lock | 56 ++++----- crates/app/Cargo.toml | 2 +- crates/app/src/langs/en-US.toml | 1 + crates/app/src/public/css/root.css | 12 +- crates/app/src/public/html/chats/app.lisp | 2 +- .../app/src/public/html/communities/base.lisp | 2 +- .../public/html/communities/create_post.lisp | 8 +- .../app/src/public/html/communities/list.lisp | 4 +- .../src/public/html/communities/question.lisp | 2 +- .../src/public/html/communities/search.lisp | 2 +- .../src/public/html/communities/settings.lisp | 20 ++-- crates/app/src/public/html/components.lisp | 24 ++-- crates/app/src/public/html/developer/app.lisp | 10 +- .../app/src/public/html/developer/home.lisp | 4 +- .../app/src/public/html/developer/link.lisp | 2 +- crates/app/src/public/html/economy/edit.lisp | 2 +- .../app/src/public/html/economy/edit_ad.lisp | 2 +- .../app/src/public/html/economy/product.lisp | 2 +- .../app/src/public/html/economy/products.lisp | 2 +- .../app/src/public/html/economy/wallet.lisp | 2 +- crates/app/src/public/html/forge/base.lisp | 2 +- crates/app/src/public/html/forge/home.lisp | 4 +- crates/app/src/public/html/journals/app.lisp | 4 +- .../app/src/public/html/littleweb/domain.lisp | 2 +- .../src/public/html/littleweb/domains.lisp | 4 +- .../src/public/html/littleweb/service.lisp | 2 +- .../src/public/html/littleweb/services.lisp | 4 +- crates/app/src/public/html/macros.lisp | 6 + crates/app/src/public/html/mail/compose.lisp | 2 +- crates/app/src/public/html/mail/letter.lisp | 2 +- crates/app/src/public/html/mail/received.lisp | 2 +- crates/app/src/public/html/mail/sent.lisp | 2 +- .../src/public/html/misc/achievements.lisp | 2 +- crates/app/src/public/html/misc/error.lisp | 2 +- crates/app/src/public/html/misc/markdown.lisp | 2 +- .../src/public/html/misc/notifications.lisp | 2 +- crates/app/src/public/html/misc/requests.lisp | 6 +- crates/app/src/public/html/mod/audit_log.lisp | 2 +- .../app/src/public/html/mod/file_report.lisp | 2 +- crates/app/src/public/html/mod/ip_bans.lisp | 2 +- crates/app/src/public/html/mod/profile.lisp | 6 +- crates/app/src/public/html/mod/reports.lisp | 2 +- crates/app/src/public/html/mod/stats.lisp | 2 +- crates/app/src/public/html/mod/warnings.lisp | 2 +- crates/app/src/public/html/post/likes.lisp | 2 +- crates/app/src/public/html/post/post.lisp | 4 +- crates/app/src/public/html/post/quotes.lisp | 2 +- crates/app/src/public/html/post/reposts.lisp | 2 +- .../app/src/public/html/profile/banned.lisp | 2 +- crates/app/src/public/html/profile/base.lisp | 34 ++++-- .../app/src/public/html/profile/blocked.lisp | 2 +- .../app/src/public/html/profile/private.lisp | 2 +- .../app/src/public/html/profile/settings.lisp | 58 ++++++++-- .../app/src/public/html/profile/warning.lisp | 4 +- .../app/src/public/html/stacks/add_user.lisp | 2 +- crates/app/src/public/html/stacks/feed.lisp | 6 +- crates/app/src/public/html/stacks/list.lisp | 4 +- crates/app/src/public/html/stacks/manage.lisp | 11 +- crates/app/src/public/html/timelines/all.lisp | 2 +- .../html/timelines/all_forum_posts.lisp | 2 +- .../public/html/timelines/all_questions.lisp | 2 +- .../src/public/html/timelines/following.lisp | 4 +- .../html/timelines/following_questions.lisp | 2 +- .../app/src/public/html/timelines/home.lisp | 3 +- .../public/html/timelines/home_questions.lisp | 2 +- .../src/public/html/timelines/popular.lisp | 4 +- .../html/timelines/popular_questions.lisp | 2 +- .../app/src/public/html/timelines/search.lisp | 2 +- crates/app/src/public/js/atto.js | 26 ++++- .../src/routes/api/v1/communities/posts.rs | 2 +- crates/app/src/routes/api/v1/mod.rs | 4 + crates/app/src/routes/api/v1/stacks.rs | 49 ++++++++ crates/app/src/routes/pages/misc.rs | 12 +- crates/app/src/routes/pages/profile.rs | 59 ++++++++++ crates/core/Cargo.toml | 2 +- crates/core/src/database/auth.rs | 7 +- .../database/drivers/sql/create_stacks.sql | 3 +- .../src/database/drivers/sql/create_users.sql | 3 +- .../drivers/sql/version_migrations.sql | 8 ++ crates/core/src/database/posts.rs | 107 +++++++++++------- crates/core/src/database/stacks.rs | 10 +- crates/core/src/model/auth.rs | 7 ++ crates/core/src/model/stacks.rs | 4 + 83 files changed, 479 insertions(+), 213 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8812118..e7589f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -407,7 +407,7 @@ dependencies = [ "pathbufd", "serde", "serde_json", - "tetratto-core 15.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tetratto-core 15.0.2", "tetratto-shared 12.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.9.5", ] @@ -3350,7 +3350,7 @@ dependencies = [ [[package]] name = "tetratto" -version = "15.0.0" +version = "16.0.0" dependencies = [ "ammonia", "async-stripe", @@ -3370,7 +3370,7 @@ dependencies = [ "serde", "serde_json", "tera", - "tetratto-core 15.0.2", + "tetratto-core 16.0.0", "tetratto-l10n 12.0.0", "tetratto-shared 12.0.6", "tokio", @@ -3379,31 +3379,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "tetratto-core" -version = "15.0.2" -dependencies = [ - "async-recursion", - "base16ct", - "base64 0.22.1", - "bitflags 2.9.2", - "buckets-core", - "emojis", - "md-5", - "oiseau", - "paste", - "pathbufd", - "regex", - "reqwest", - "serde", - "serde_json", - "tetratto-l10n 12.0.0", - "tetratto-shared 12.0.6", - "tokio", - "toml 0.9.5", - "totp-rs", -] - [[package]] name = "tetratto-core" version = "15.0.2" @@ -3430,6 +3405,31 @@ dependencies = [ "totp-rs", ] +[[package]] +name = "tetratto-core" +version = "16.0.0" +dependencies = [ + "async-recursion", + "base16ct", + "base64 0.22.1", + "bitflags 2.9.2", + "buckets-core", + "emojis", + "md-5", + "oiseau", + "paste", + "pathbufd", + "regex", + "reqwest", + "serde", + "serde_json", + "tetratto-l10n 12.0.0", + "tetratto-shared 12.0.6", + "tokio", + "toml 0.9.5", + "totp-rs", +] + [[package]] name = "tetratto-l10n" version = "12.0.0" diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index e808140..3287f96 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto" -version = "15.0.0" +version = "16.0.0" edition = "2024" authors.workspace = true repository.workspace = true diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index 955960c..77e351e 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -178,6 +178,7 @@ version = "1.0.0" "settings:tab.theme" = "Theme" "settings:tab.sessions" = "Sessions" "settings:tab.grants" = "Grants" +"settings:tab.close_friends" = "Close friends" "settings:tab.images" = "Images" "settings:tab.presets" = "Presets" "settings:label.change_password" = "Change password" diff --git a/crates/app/src/public/css/root.css b/crates/app/src/public/css/root.css index d6e7a8c..c38372d 100644 --- a/crates/app/src/public/css/root.css +++ b/crates/app/src/public/css/root.css @@ -193,11 +193,13 @@ p { margin-bottom: var(--pad-4); } -p, -span:not(nav *):not(.dropdown *):not(a *):not(button *), -input, -textarea { - font-weight: 300; +body:not(.use_system_font) { + & p:not(b *), + & span:not(.notification, .name, b *, button *, a *, .dropdown *, nav *), + & input, + & textarea { + font-variation-settings: "wght" 325; + } } .no_p_margin p:last-child { diff --git a/crates/app/src/public/html/chats/app.lisp b/crates/app/src/public/html/chats/app.lisp index e64dc0b..11ab239 100644 --- a/crates/app/src/public/html/chats/app.lisp +++ b/crates/app/src/public/html/chats/app.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Chats - {{ config.name }}")) + (text "Chats — {{ config.name }}")) (link ("rel" "stylesheet") ("data-turbo-temporary" "true") ("href" "/css/chats.css?v=tetratto-{{ random_cache_breaker }}")) (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"chats\", hide_user_menu=true) }}") (nav diff --git a/crates/app/src/public/html/communities/base.lisp b/crates/app/src/public/html/communities/base.lisp index e5d3bf3..20ca032 100644 --- a/crates/app/src/public/html/communities/base.lisp +++ b/crates/app/src/public/html/communities/base.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "{{ community.context.display_name }} - {{ config.name }}")) + (text "{{ community.context.display_name }} — {{ config.name }}")) (meta ("name" "og:title") diff --git a/crates/app/src/public/html/communities/create_post.lisp b/crates/app/src/public/html/communities/create_post.lisp index 01e67be..7e3be37 100644 --- a/crates/app/src/public/html/communities/create_post.lisp +++ b/crates/app/src/public/html/communities/create_post.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Create post - {{ config.name }}")) + (text "Create post — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main @@ -98,13 +98,13 @@ ("selected" "{% if selected_community == community.id -%}true{% else %}false{%- endif %}") (text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}")) (text "{% endfor %}") - (text "{% for stack in stacks %}") + (text "{% for stack in stacks %} {% if not stack.is_locked or user.id == stack.owner -%}") (option ("value" "{{ stack.id }}") ("selected" "{% if selected_stack == stack.id -%}true{% else %}false{%- endif %}") ("is_stack" "true") (text "{{ stack.name }} (circle)")) - (text "{% endfor %}"))) + (text "{%- endif %} {% endfor %}"))) (form ("class" "card flex flex_col gap_2") ("id" "create_form") @@ -445,7 +445,7 @@ element.setAttribute(\"alt\", `${id}'s avatar`); if (id === town_square || is_stack) { - element.src = `/api/v1/auth/user/${user_id}/avatar?selector_type=id`; + element.src = `${_app_base.service_hosts.buckets}/avatars/${user_id}`; } else { element.src = `/api/v1/communities/${id}/avatar`; } diff --git a/crates/app/src/public/html/communities/list.lisp b/crates/app/src/public/html/communities/list.lisp index e65cd8a..bd6828c 100644 --- a/crates/app/src/public/html/communities/list.lisp +++ b/crates/app/src/public/html/communities/list.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "My communities - {{ config.name }}")) + (text "My communities — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"communities\") }}") (main @@ -19,7 +19,7 @@ ("class" "flex flex_col gap_1") (label ("for" "title") - (text "{{ text \"communities:label.name\" }}")) + (str (text "communities:label.name"))) (input ("type" "text") ("name" "title") diff --git a/crates/app/src/public/html/communities/question.lisp b/crates/app/src/public/html/communities/question.lisp index 707b5fd..ef95b9e 100644 --- a/crates/app/src/public/html/communities/question.lisp +++ b/crates/app/src/public/html/communities/question.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Question - {{ config.name }}")) + (text "Question — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/communities/search.lisp b/crates/app/src/public/html/communities/search.lisp index 778325d..9d10f97 100644 --- a/crates/app/src/public/html/communities/search.lisp +++ b/crates/app/src/public/html/communities/search.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Search communities - {{ config.name }}")) + (text "Search communities — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"communities\") }}") (main diff --git a/crates/app/src/public/html/communities/settings.lisp b/crates/app/src/public/html/communities/settings.lisp index aec080a..f95a900 100644 --- a/crates/app/src/public/html/communities/settings.lisp +++ b/crates/app/src/public/html/communities/settings.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Community settings - {{ config.name }}")) + (text "Community settings — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main ("class" "flex flex_col gap_2") @@ -145,7 +145,7 @@ ("required" "") ("minlength" "2"))) (button - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}")))))) (div @@ -168,7 +168,7 @@ ("class" "flex gap_2 flex_wrap") (button ("onclick" "save_context()") - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}"))) (a @@ -200,7 +200,7 @@ ("class" "w_content")) (button ("class" "small square big_icon") - (text "{{ icon \"check\" }}")))) + (icon (text "check"))))) (div ("class" "card_nest") ("ui_ident" "change_banner") @@ -223,7 +223,7 @@ ("class" "w_content")) (button ("class" "small square big_icon") - (text "{{ icon \"check\" }}"))) + (icon (text "check")))) (span ("class" "fade") (text "Use an image of 1100x350px for the best results."))))) @@ -271,7 +271,7 @@ (button ("class" "small lowered") ("onclick" "update_user_role(document.getElementById('uid').value, document.getElementById('role').value)") - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}")))) (div @@ -294,7 +294,7 @@ ("class" "flex flex_col gap_1") (label ("for" "title") - (text "{{ text \"communities:label.name\" }}")) + (str (text "communities:label.name"))) (input ("type" "text") ("name" "title") @@ -453,7 +453,7 @@ ("class" "flex flex_col gap_1") (label ("for" "name") - (text "{{ text \"communities:label.name\" }}")) + (str (text "communities:label.name"))) (input ("type" "text") ("name" "name") @@ -599,7 +599,7 @@ ("class" "flex flex_col gap_1") (label ("for" "title") - (text "{{ text \"communities:label.name\" }}")) + (str (text "communities:label.name"))) (input ("type" "text") ("name" "title") @@ -677,7 +677,7 @@ ("class" "flex flex_col gap_1") (label ("for" "title") - (text "{{ text \"communities:label.name\" }}")) + (str (text "communities:label.name"))) (input ("type" "text") ("name" "title") diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp index 3d26290..9dea998 100644 --- a/crates/app/src/public/html/components.lisp +++ b/crates/app/src/public/html/components.lisp @@ -137,7 +137,7 @@ ("style" "display: contents") (text "{{ self::post(post=post, owner=owner, secondary=secondary, community=community, show_community=show_community, can_manage_post=can_manage_post, repost=repost, expect_repost=true) }}")) (text "{%- endmacro %}") -(text "{% macro post_info(post, community) -%}") +(text "{% macro post_info(post, community, owner) -%}") ; info about the post: edited, date, etc. (text "{% if post.context.edited != 0 -%}") (div @@ -167,7 +167,7 @@ (text "{%- endif %} {% if post.stack -%}") (a ("title" "Posted to a stack you're in") - ("class" "flex items_center flush") + ("class" "flex items_center flush {% if post.stack == owner.close_friends_stack -%} green {%- endif %}") ("style" "color: var(--color-primary)") ("href" "/stacks/{{ post.stack }}") (text "{{ icon \"layers\" }}")) @@ -386,7 +386,7 @@ (span ; ("class" "name") (text "{{ self::full_username(user=owner) }}")) - (text "{{ self::post_info(post=post, community=community) }}") + (text "{{ self::post_info(post=post, community=community, owner=owner) }}") (text "{% if post.context.is_pinned or post.context.is_profile_pinned -%} {{ icon \"pin\" }} {%- endif %}")) (text "{% if not dont_show_title and post.title and community and community.context.enable_titles -%}") ; post has a title AND whatever is rendering this component wants to see it @@ -516,7 +516,7 @@ (button ("class" "green raised") ("onclick" "trigger('me::update_notification_read_status', ['{{ notification.id }}', true])") - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"notifs:action.mark_as_read\" }}"))) (text "{%- endif %}") @@ -552,7 +552,7 @@ (text "{{ self::community_avatar(id=post.community, community=community, size=\"18px\") }}"))) (div ("class" "flex gap_2") - (text "{{ self::post_info(post=post, community=community) }}"))) + (text "{{ self::post_info(post=post, community=community, owner=owner) }}"))) (div ("class" "card_nest_horizontal") ; author info @@ -630,7 +630,7 @@ ("class" "flex justify_between gap_2 w_full") (text "{% if page > 0 -%}") (a - ("class" "button lowered") + ("class" "button lowered pagination_previous") ("href" "?page={{ page - 1 }}{{ key }}{{ value }}") (text "{{ icon \"arrow-left\" }}") (span @@ -639,7 +639,7 @@ (div) (text "{%- endif %} {% if items != 0 -%}") (a - ("class" "button lowered") + ("class" "button lowered pagination_next") ("href" "?page={{ page + 1 }}{{ key }}{{ value }}") (span (text "{{ text \"general:link.next\" }}")) @@ -2168,7 +2168,15 @@ ("class" "card secondary flex flex_col gap_2") (div ("class" "flex items_center gap_2") - (text "{{ icon \"list\" }}") + (text "{% if stack.is_locked -%}") + (span + ("class" "red") + ("title" "Locked stack") + (icon (text "lock"))) + (text "{% else %}") + (icon (text "list")) + (text "{%- endif %}") + (b (text "{{ stack.name }}"))) (span diff --git a/crates/app/src/public/html/developer/app.lisp b/crates/app/src/public/html/developer/app.lisp index 35a3709..4d4d9c8 100644 --- a/crates/app/src/public/html/developer/app.lisp +++ b/crates/app/src/public/html/developer/app.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "{{ app.title }} - {{ config.name }}")) + (text "{{ app.title }} — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main @@ -93,7 +93,7 @@ ("required" "") ("minlength" "2"))) (button - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}"))))) (div @@ -118,7 +118,7 @@ ("required" "") ("minlength" "2"))) (button - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}"))))) (div @@ -143,7 +143,7 @@ ("required" "") ("minlength" "2"))) (button - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}"))))) (div @@ -183,7 +183,7 @@ (icon (text "external-link")) (text "Docs")))) (button - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}"))))) (div diff --git a/crates/app/src/public/html/developer/home.lisp b/crates/app/src/public/html/developer/home.lisp index 21dc65e..f7cf983 100644 --- a/crates/app/src/public/html/developer/home.lisp +++ b/crates/app/src/public/html/developer/home.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Developer panel - {{ config.name }}")) + (text "Developer panel — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") (main @@ -21,7 +21,7 @@ ("class" "flex flex_col gap_1") (label ("for" "title") - (text "{{ text \"communities:label.name\" }}")) + (str (text "communities:label.name"))) (input ("type" "text") ("name" "title") diff --git a/crates/app/src/public/html/developer/link.lisp b/crates/app/src/public/html/developer/link.lisp index 90731c1..37c4042 100644 --- a/crates/app/src/public/html/developer/link.lisp +++ b/crates/app/src/public/html/developer/link.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "{{ app.title }} - {{ config.name }}")) + (text "{{ app.title }} — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/economy/edit.lisp b/crates/app/src/public/html/economy/edit.lisp index 1f878f5..4c437cd 100644 --- a/crates/app/src/public/html/economy/edit.lisp +++ b/crates/app/src/public/html/economy/edit.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Manage product - {{ config.name }}")) + (text "Manage product — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") (main ("class" "flex flex_col gap_2") diff --git a/crates/app/src/public/html/economy/edit_ad.lisp b/crates/app/src/public/html/economy/edit_ad.lisp index 088a91a..96f4a40 100644 --- a/crates/app/src/public/html/economy/edit_ad.lisp +++ b/crates/app/src/public/html/economy/edit_ad.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Manage advertisement - {{ config.name }}")) + (text "Manage advertisement — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") (main ("class" "flex flex_col gap_2") diff --git a/crates/app/src/public/html/economy/product.lisp b/crates/app/src/public/html/economy/product.lisp index 638878e..dd6589a 100644 --- a/crates/app/src/public/html/economy/product.lisp +++ b/crates/app/src/public/html/economy/product.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "{{ product.title }} - {{ config.name }}")) + (text "{{ product.title }} — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") (main ("class" "flex flex_col gap_2") diff --git a/crates/app/src/public/html/economy/products.lisp b/crates/app/src/public/html/economy/products.lisp index fcd69f0..2396fd8 100644 --- a/crates/app/src/public/html/economy/products.lisp +++ b/crates/app/src/public/html/economy/products.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "My products - {{ config.name }}")) + (text "My products — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"products\") }}") (main diff --git a/crates/app/src/public/html/economy/wallet.lisp b/crates/app/src/public/html/economy/wallet.lisp index 0981f09..869d8d1 100644 --- a/crates/app/src/public/html/economy/wallet.lisp +++ b/crates/app/src/public/html/economy/wallet.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Wallet - {{ config.name }}")) + (text "Wallet — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"wallet\") }}") (main ("class" "flex flex_col gap_2") diff --git a/crates/app/src/public/html/forge/base.lisp b/crates/app/src/public/html/forge/base.lisp index 044bf60..f56aa07 100644 --- a/crates/app/src/public/html/forge/base.lisp +++ b/crates/app/src/public/html/forge/base.lisp @@ -2,7 +2,7 @@ ; changes to be more github-like instead of retrospring-like (text "{% extends \"root.html\" %} {% block head %}") (title - (text "{{ community.context.display_name }} - {{ config.name }}")) + (text "{{ community.context.display_name }} — {{ config.name }}")) (meta ("name" "og:title") diff --git a/crates/app/src/public/html/forge/home.lisp b/crates/app/src/public/html/forge/home.lisp index 56f1d01..61cfdfd 100644 --- a/crates/app/src/public/html/forge/home.lisp +++ b/crates/app/src/public/html/forge/home.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Forge - {{ config.name }}")) + (text "Forge — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") (main @@ -20,7 +20,7 @@ ("class" "flex flex_col gap_1") (label ("for" "title") - (text "{{ text \"communities:label.name\" }}")) + (str (text "communities:label.name"))) (input ("type" "text") ("name" "title") diff --git a/crates/app/src/public/html/journals/app.lisp b/crates/app/src/public/html/journals/app.lisp index 0c9e3df..337d39c 100644 --- a/crates/app/src/public/html/journals/app.lisp +++ b/crates/app/src/public/html/journals/app.lisp @@ -5,7 +5,7 @@ (text "{% else %}") (title (text "{{ journal.title }}")) (text "{%- endif %} {% else %}") -(title (text "Journals - {{ config.name }}")) +(title (text "Journals — {{ config.name }}")) (text "{%- endif %}") (text "{% if note and journal and owner -%}") @@ -253,7 +253,7 @@ ("required" "") ("minlength" "2"))) (button - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}"))))))) diff --git a/crates/app/src/public/html/littleweb/domain.lisp b/crates/app/src/public/html/littleweb/domain.lisp index 629ad15..b90bc7c 100644 --- a/crates/app/src/public/html/littleweb/domain.lisp +++ b/crates/app/src/public/html/littleweb/domain.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "My services - {{ config.name }}")) + (text "My services — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/littleweb/domains.lisp b/crates/app/src/public/html/littleweb/domains.lisp index c0a3779..d48e413 100644 --- a/crates/app/src/public/html/littleweb/domains.lisp +++ b/crates/app/src/public/html/littleweb/domains.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "My domains - {{ config.name }}")) + (text "My domains — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main @@ -35,7 +35,7 @@ ("class" "flex flex_col gap_1") (label ("for" "name") - (text "{{ text \"communities:label.name\" }}")) + (str (text "communities:label.name"))) (input ("type" "text") ("name" "name") diff --git a/crates/app/src/public/html/littleweb/service.lisp b/crates/app/src/public/html/littleweb/service.lisp index 13f6a70..1899b28 100644 --- a/crates/app/src/public/html/littleweb/service.lisp +++ b/crates/app/src/public/html/littleweb/service.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "My services - {{ config.name }}")) + (text "My services — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/littleweb/services.lisp b/crates/app/src/public/html/littleweb/services.lisp index 1325780..33332b1 100644 --- a/crates/app/src/public/html/littleweb/services.lisp +++ b/crates/app/src/public/html/littleweb/services.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "My services - {{ config.name }}")) + (text "My services — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main @@ -35,7 +35,7 @@ ("class" "flex flex_col gap_1") (label ("for" "name") - (text "{{ text \"communities:label.name\" }}")) + (str (text "communities:label.name"))) (input ("type" "text") ("name" "name") diff --git a/crates/app/src/public/html/macros.lisp b/crates/app/src/public/html/macros.lisp index 7ebee6a..f71deeb 100644 --- a/crates/app/src/public/html/macros.lisp +++ b/crates/app/src/public/html/macros.lisp @@ -387,4 +387,10 @@ (text "{{ icon \"cable\" }}") (span (text "{{ text \"settings:tab.grants\" }}"))) +(a + ("data-tab-button" "friends") + ("href" "#/friends") + (text "{{ icon \"book-user\" }}") + (span + (text "{{ text \"settings:tab.close_friends\" }}"))) (text "{%- endmacro %}") diff --git a/crates/app/src/public/html/mail/compose.lisp b/crates/app/src/public/html/mail/compose.lisp index 088449a..682ba43 100644 --- a/crates/app/src/public/html/mail/compose.lisp +++ b/crates/app/src/public/html/mail/compose.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Compose letter - {{ config.name }}")) + (text "Compose letter — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/mail/letter.lisp b/crates/app/src/public/html/mail/letter.lisp index 81a6bb9..8158a0e 100644 --- a/crates/app/src/public/html/mail/letter.lisp +++ b/crates/app/src/public/html/mail/letter.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Letter - {{ config.name }}")) + (text "Letter — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main ("class" "flex flex_col gap_2") diff --git a/crates/app/src/public/html/mail/received.lisp b/crates/app/src/public/html/mail/received.lisp index b6b71c3..fe9c043 100644 --- a/crates/app/src/public/html/mail/received.lisp +++ b/crates/app/src/public/html/mail/received.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Received mail - {{ config.name }}")) + (text "Received mail — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main ("class" "flex flex_col gap_2") diff --git a/crates/app/src/public/html/mail/sent.lisp b/crates/app/src/public/html/mail/sent.lisp index 095b61e..0dae422 100644 --- a/crates/app/src/public/html/mail/sent.lisp +++ b/crates/app/src/public/html/mail/sent.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Sent mail - {{ config.name }}")) + (text "Sent mail — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main ("class" "flex flex_col gap_2") diff --git a/crates/app/src/public/html/misc/achievements.lisp b/crates/app/src/public/html/misc/achievements.lisp index 9407faa..8fc3e5a 100644 --- a/crates/app/src/public/html/misc/achievements.lisp +++ b/crates/app/src/public/html/misc/achievements.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Achievements - {{ config.name }}")) + (text "Achievements — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"achievements\") }}") (main diff --git a/crates/app/src/public/html/misc/error.lisp b/crates/app/src/public/html/misc/error.lisp index 6a1e902..06d43b7 100644 --- a/crates/app/src/public/html/misc/error.lisp +++ b/crates/app/src/public/html/misc/error.lisp @@ -1,5 +1,5 @@ (text "{% extends \"root.html\" %} {% block head %}") -(title (text "{{ error_text }} - {{ config.name }}")) +(title (text "{{ error_text }} — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/misc/markdown.lisp b/crates/app/src/public/html/misc/markdown.lisp index 511b761..8ed76ab 100644 --- a/crates/app/src/public/html/misc/markdown.lisp +++ b/crates/app/src/public/html/misc/markdown.lisp @@ -1,5 +1,5 @@ (text "{% extends \"root.html\" %} {% block head %}") -(title (text "{{ file_name }} - {{ config.name }}")) +(title (text "{{ file_name }} — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/misc/notifications.lisp b/crates/app/src/public/html/misc/notifications.lisp index 31980b9..abc6084 100644 --- a/crates/app/src/public/html/misc/notifications.lisp +++ b/crates/app/src/public/html/misc/notifications.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Notifications - {{ config.name }}")) + (text "Notifications — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"notifications\") }}") (main diff --git a/crates/app/src/public/html/misc/requests.lisp b/crates/app/src/public/html/misc/requests.lisp index 90e988d..0a1cf84 100644 --- a/crates/app/src/public/html/misc/requests.lisp +++ b/crates/app/src/public/html/misc/requests.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Requests - {{ config.name }}")) + (text "Requests — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"requests\") }}") (main @@ -83,7 +83,7 @@ (button ("class" "lowered green") ("onclick" "accept_follow_request(event, '{{ request.id }}')") - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.accept\" }}"))) (button @@ -114,7 +114,7 @@ (button ("class" "lowered green") ("onclick" "accept_transfer_request(event, '{{ request.id }}', '{{ request.linked_asset }}', {{ request.data.Int32 }})") - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.accept\" }}"))) (button diff --git a/crates/app/src/public/html/mod/audit_log.lisp b/crates/app/src/public/html/mod/audit_log.lisp index d73b0d5..d41b58f 100644 --- a/crates/app/src/public/html/mod/audit_log.lisp +++ b/crates/app/src/public/html/mod/audit_log.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Audit log - {{ config.name }}")) + (text "Audit log — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/mod/file_report.lisp b/crates/app/src/public/html/mod/file_report.lisp index 154583f..e2b58cf 100644 --- a/crates/app/src/public/html/mod/file_report.lisp +++ b/crates/app/src/public/html/mod/file_report.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "File report - {{ config.name }}")) + (text "File report — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/mod/ip_bans.lisp b/crates/app/src/public/html/mod/ip_bans.lisp index d37011c..caf8a30 100644 --- a/crates/app/src/public/html/mod/ip_bans.lisp +++ b/crates/app/src/public/html/mod/ip_bans.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "IP Bans - {{ config.name }}")) + (text "IP Bans — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/mod/profile.lisp b/crates/app/src/public/html/mod/profile.lisp index 7e12f8c..843f40c 100644 --- a/crates/app/src/public/html/mod/profile.lisp +++ b/crates/app/src/public/html/mod/profile.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Manage profile - {{ config.name }}")) + (text "Manage profile — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main @@ -356,7 +356,7 @@ (button ("class" "small lowered") ("onclick" "update_user_role(Number.parseInt(document.getElementById('role').value))") - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}")))) (div @@ -374,7 +374,7 @@ (button ("class" "small lowered") ("onclick" "update_user_secondary_role(Number.parseInt(document.getElementById('secondary_role').value))") - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}")))) (div diff --git a/crates/app/src/public/html/mod/reports.lisp b/crates/app/src/public/html/mod/reports.lisp index 6f3ba92..a9fa5e7 100644 --- a/crates/app/src/public/html/mod/reports.lisp +++ b/crates/app/src/public/html/mod/reports.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Reports - {{ config.name }}")) + (text "Reports — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/mod/stats.lisp b/crates/app/src/public/html/mod/stats.lisp index 94a49f7..9d02f2e 100644 --- a/crates/app/src/public/html/mod/stats.lisp +++ b/crates/app/src/public/html/mod/stats.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Server stats - {{ config.name }}")) + (text "Server stats — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/mod/warnings.lisp b/crates/app/src/public/html/mod/warnings.lisp index f834aab..ab6f16e 100644 --- a/crates/app/src/public/html/mod/warnings.lisp +++ b/crates/app/src/public/html/mod/warnings.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "User warnings - {{ config.name }}")) + (text "User warnings — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/post/likes.lisp b/crates/app/src/public/html/post/likes.lisp index c7f0af8..ec04552 100644 --- a/crates/app/src/public/html/post/likes.lisp +++ b/crates/app/src/public/html/post/likes.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Post likes - {{ config.name }}")) + (text "Post likes — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/post/post.lisp b/crates/app/src/public/html/post/post.lisp index 8fc25cd..ad6fcb0 100644 --- a/crates/app/src/public/html/post/post.lisp +++ b/crates/app/src/public/html/post/post.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Post - {{ config.name }}")) + (text "Post — {{ config.name }}")) (meta ("name" "og:title") @@ -146,7 +146,7 @@ ("id" "post_context"))) (button ("onclick" "save_context()") - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}"))) (script diff --git a/crates/app/src/public/html/post/quotes.lisp b/crates/app/src/public/html/post/quotes.lisp index c178bcb..40ab27c 100644 --- a/crates/app/src/public/html/post/quotes.lisp +++ b/crates/app/src/public/html/post/quotes.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Post quotes - {{ config.name }}")) + (text "Post quotes — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/post/reposts.lisp b/crates/app/src/public/html/post/reposts.lisp index 4e5a8db..3689e84 100644 --- a/crates/app/src/public/html/post/reposts.lisp +++ b/crates/app/src/public/html/post/reposts.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Post reposts - {{ config.name }}")) + (text "Post reposts — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/profile/banned.lisp b/crates/app/src/public/html/profile/banned.lisp index ab8f40a..adce158 100644 --- a/crates/app/src/public/html/profile/banned.lisp +++ b/crates/app/src/public/html/profile/banned.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "{{ profile.username }} (banned) - {{ config.name }}")) + (text "{{ profile.username }} (banned) — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/profile/base.lisp b/crates/app/src/public/html/profile/base.lisp index 7684f87..72d9ebf 100644 --- a/crates/app/src/public/html/profile/base.lisp +++ b/crates/app/src/public/html/profile/base.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "{{ profile.username }} - {{ config.name }}")) + (text "{{ profile.username }} — {{ config.name }}")) (meta ("name" "og:title") @@ -57,7 +57,15 @@ (div ("class" "card flex gap_2") ("id" "user_avatar_and_name") - (text "{{ components::avatar(id=profile.id,size=\"72px\") }}") + (text "{% if user and close_friends_stack and (user.id in close_friends_stack.users) -%}") + (a + ("style" "border: solid 2px var(--color-yellow); border-radius: calc(var(--radius) / 1.2); height: max-content") + ("href" "/stacks/{{ close_friends_stack.id }}") + (text "{{ components::avatar(id=profile.id, size=\"72px\") }}")) + (text "{% else %}") + (text "{{ components::avatar(id=profile.id, size=\"72px\") }}") + (text "{%- endif %}") + (div ("class" "flex flex_col") (h3 @@ -131,13 +139,21 @@ (span (text "{{ text \"auth:label.following\" }}")))) (text "{%- endif %}") - (text "{% if is_following_you -%}") - (b - ("class" "notification chip w_content flex items_center gap_2") - (text "{{ icon \"heart\" }}") - (span - (text "Follows you"))) - (text "{%- endif %}"))) + (div + ("class" "flex gap_2") + (text "{% if is_following_you -%}") + (b + ("class" "notification chip w_content flex items_center gap_2") + (icon (text "heart")) + (span (text "Follows you"))) + (text "{%- endif %}") + + (text "{% if user and close_friends_stack and (user.id in close_friends_stack.users) -%}") + (b + ("class" "notification chip w_content flex items_center gap_2") + (icon (text "heart")) + (span (text "Friends"))) + (text "{%- endif %}")))) (div ("class" "card_nest flex flex_col") (div diff --git a/crates/app/src/public/html/profile/blocked.lisp b/crates/app/src/public/html/profile/blocked.lisp index 4d1922b..1c78ff6 100644 --- a/crates/app/src/public/html/profile/blocked.lisp +++ b/crates/app/src/public/html/profile/blocked.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "{{ profile.username }} (blocked) - {{ config.name }}")) + (text "{{ profile.username }} (blocked) — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/profile/private.lisp b/crates/app/src/public/html/profile/private.lisp index 4b1fdea..b452f29 100644 --- a/crates/app/src/public/html/profile/private.lisp +++ b/crates/app/src/public/html/profile/private.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "{{ profile.username }} (private profile) - {{ config.name }}")) + (text "{{ profile.username }} (private profile) — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp index b916678..0a69f2b 100644 --- a/crates/app/src/public/html/profile/settings.lisp +++ b/crates/app/src/public/html/profile/settings.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Settings - {{ config.name }}")) + (text "Settings — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (article ("class" "flex flex_row gap_2 content_container") @@ -280,7 +280,7 @@ ("required" "") ("minlength" "2"))) (button - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}")))))) (div @@ -335,7 +335,7 @@ (button ("onclick" "save_settings()") ("id" "save_button") - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}")))) (div @@ -442,7 +442,7 @@ ("minlength" "6") ("autocomplete" "off"))) (button - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}"))))))))) (div @@ -959,7 +959,7 @@ ("class" "w_content")) (button ("class" "small square big_icon") - (text "{{ icon \"check\" }}"))) + (icon (text "check")))) (span ("class" "fade") (text "Images must be less than 8 MB large. Animated GIFs are @@ -987,7 +987,7 @@ ("class" "w_content")) (button ("class" "small square big_icon") - (text "{{ icon \"check\" }}"))) + (icon (text "check")))) (span ("class" "fade") (text "Use an image of 1100x350px for the best results.")))) @@ -1032,7 +1032,7 @@ (button ("onclick" "save_settings()") ("id" "save_button") - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}")))) (div @@ -1191,7 +1191,7 @@ (button ("onclick" "save_settings()") ("id" "save_button") - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}")))) (div @@ -1304,6 +1304,48 @@ (span (text "{{ config.name }} ") (str (text "developer:label.for_developers"))))) + (div + ("class" "card w_full lowered hidden flex flex_col gap_2") + ("data-tab" "friends") + (h3 (text "Close friends stack")) + (p (text "Your close friends stack is a special stack where you can create posts for only your friends to see!")) + (text "{% if user.close_friends_stack -%}") + (div + ("class" "flex gap_2") + (a + ("href" "/stacks/{{ user.close_friends_stack }}/manage#/users") + ("class" "button") + (icon (text "settings")) + (text "Manage users")) + (a + ("href" "/communities/intents/post?stack={{ user.close_friends_stack }}") + ("class" "button raised") + (icon (text "plus")) + (text "Post"))) + (text "{% else %}") + (button + ("onclick" "create_close_friends_stack()") + (icon (text "plus")) + (text "Create stack")) + + (script + (text "globalThis.create_close_friends_stack = () => { + fetch(\"/api/v1/stacks/close_friends\", { + method: \"POST\", + }) + .then((res) => res.json()) + .then((res) => { + trigger(\"atto::toast\", [ + res.ok ? \"success\" : \"error\", + res.message, + ]); + + if (res.ok) { + window.location.href = `/stacks/${res.payload}/manage#/users`; + } + }); + }")) + (text "{%- endif %}")) (script ("type" "application/json") ("id" "settings_json") diff --git a/crates/app/src/public/html/profile/warning.lisp b/crates/app/src/public/html/profile/warning.lisp index fe36df0..47bfdc1 100644 --- a/crates/app/src/public/html/profile/warning.lisp +++ b/crates/app/src/public/html/profile/warning.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "{{ profile.username }} (warning) - {{ config.name }}")) + (text "{{ profile.username }} (warning) — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main @@ -26,7 +26,7 @@ ("class" "card w_full secondary flex gap_2") (button ("onclick" "trigger('warnings::accept', ['{{ profile.id }}', '{{ warning_hash }}'])") - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"dialog:action.continue\" }}"))) (a diff --git a/crates/app/src/public/html/stacks/add_user.lisp b/crates/app/src/public/html/stacks/add_user.lisp index 5804496..80ab718 100644 --- a/crates/app/src/public/html/stacks/add_user.lisp +++ b/crates/app/src/public/html/stacks/add_user.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Add user to stack - {{ config.name }}")) + (text "Add user to stack — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") (main diff --git a/crates/app/src/public/html/stacks/feed.lisp b/crates/app/src/public/html/stacks/feed.lisp index ca19b74..0139eb6 100644 --- a/crates/app/src/public/html/stacks/feed.lisp +++ b/crates/app/src/public/html/stacks/feed.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "{{ stack.name }} - {{ config.name }}")) + (text "{{ stack.name }} — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main @@ -19,7 +19,7 @@ (text "{{ stack.name }}"))) (div ("class" "flex gap_2") - (text "{% if stack.mode == 'Circle' -%}") + (text "{% if stack.mode == 'Circle' and (not stack.is_locked or user.id == stack.owner) -%}") ; post button for circle stacks (a ("href" "/communities/intents/post?stack={{ stack.id }}") @@ -94,7 +94,7 @@ (text "{% set paged = user and user.settings.paged_timelines %}") (script (text "setTimeout(() => { - trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?stack_id={{ stack.id }}&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]); + trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?stack_id={{ stack.id }}&before=$1$&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]); });")) (text "{%- endif %}")))) diff --git a/crates/app/src/public/html/stacks/list.lisp b/crates/app/src/public/html/stacks/list.lisp index 27714ce..c79c021 100644 --- a/crates/app/src/public/html/stacks/list.lisp +++ b/crates/app/src/public/html/stacks/list.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "My stacks - {{ config.name }}")) + (text "My stacks — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main @@ -19,7 +19,7 @@ ("class" "flex flex_col gap_1") (label ("for" "name") - (text "{{ text \"communities:label.name\" }}")) + (str (text "communities:label.name"))) (input ("type" "text") ("name" "name") diff --git a/crates/app/src/public/html/stacks/manage.lisp b/crates/app/src/public/html/stacks/manage.lisp index fe61525..95a4942 100644 --- a/crates/app/src/public/html/stacks/manage.lisp +++ b/crates/app/src/public/html/stacks/manage.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Stack settings - {{ config.name }}")) + (text "Stack settings — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main @@ -56,6 +56,7 @@ ("class" "card") (select ("onchange" "save_mode(event)") + ("disabled" "{{ stack.is_locked }}") (option ("value" "Include") ("selected" "{% if stack.mode == 'Include' -%}true{% else %}false{%- endif %}") @@ -105,7 +106,7 @@ ("class" "flex flex_col gap_1") (label ("for" "name") - (text "{{ text \"communities:label.name\" }}")) + (str (text "communities:label.name"))) (input ("type" "text") ("name" "name") @@ -114,9 +115,10 @@ ("required" "") ("minlength" "2"))) (button - (text "{{ icon \"check\" }}") + (icon (text "check")) (span (text "{{ text \"general:action.save\" }}")))))) + (text "{% if not stack.is_locked -%}") (div ("class" "card_nest") ("ui_ident" "danger_zone") @@ -132,7 +134,8 @@ ("onclick" "delete_stack()") (text "{{ icon \"trash\" }}") (span - (text "{{ text \"general:action.delete\" }}")))))) + (text "{{ text \"general:action.delete\" }}"))))) + (text "{%- endif %}")) (div ("class" "card w_full flex flex_col gap_2 hidden") ("data-tab" "users") diff --git a/crates/app/src/public/html/timelines/all.lisp b/crates/app/src/public/html/timelines/all.lisp index abf3ede..9cfdadf 100644 --- a/crates/app/src/public/html/timelines/all.lisp +++ b/crates/app/src/public/html/timelines/all.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Latest posts - {{ config.name }}")) + (text "Latest posts — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main ("class" "flex flex_col gap_2") diff --git a/crates/app/src/public/html/timelines/all_forum_posts.lisp b/crates/app/src/public/html/timelines/all_forum_posts.lisp index 7a171fb..15df6c4 100644 --- a/crates/app/src/public/html/timelines/all_forum_posts.lisp +++ b/crates/app/src/public/html/timelines/all_forum_posts.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Latest forum posts - {{ config.name }}")) + (text "Latest forum posts — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main ("class" "flex flex_col gap_2") diff --git a/crates/app/src/public/html/timelines/all_questions.lisp b/crates/app/src/public/html/timelines/all_questions.lisp index 00626de..cb31b0a 100644 --- a/crates/app/src/public/html/timelines/all_questions.lisp +++ b/crates/app/src/public/html/timelines/all_questions.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Latest questions - {{ config.name }}")) + (text "Latest questions — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main ("class" "flex flex_col gap_2") diff --git a/crates/app/src/public/html/timelines/following.lisp b/crates/app/src/public/html/timelines/following.lisp index 1f288b4..fbaed10 100644 --- a/crates/app/src/public/html/timelines/following.lisp +++ b/crates/app/src/public/html/timelines/following.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Following - {{ config.name }}")) + (text "Following — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main @@ -14,7 +14,7 @@ (text "{% set paged = user and user.settings.paged_timelines %}") (script (text "setTimeout(() => { - trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?tl=FollowingPosts&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]); + trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?tl=FollowingPosts&before=$1$&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]); });")) (text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/following_questions.lisp b/crates/app/src/public/html/timelines/following_questions.lisp index d8c4657..22b78a8 100644 --- a/crates/app/src/public/html/timelines/following_questions.lisp +++ b/crates/app/src/public/html/timelines/following_questions.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Following (questions) - {{ config.name }}")) + (text "Following (questions) — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/timelines/home.lisp b/crates/app/src/public/html/timelines/home.lisp index 47cd8d5..c6f5b06 100644 --- a/crates/app/src/public/html/timelines/home.lisp +++ b/crates/app/src/public/html/timelines/home.lisp @@ -46,7 +46,6 @@ (text "{% set paged = user and user.settings.paged_timelines %}") (script (text "setTimeout(() => { - trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?tl=MyCommunities&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]); + trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?tl=MyCommunities&before=$1$&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]); });")) - (text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/home_questions.lisp b/crates/app/src/public/html/timelines/home_questions.lisp index dab4512..4c9cce1 100644 --- a/crates/app/src/public/html/timelines/home_questions.lisp +++ b/crates/app/src/public/html/timelines/home_questions.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "From my communities (questions) - {{ config.name }}")) + (text "From my communities (questions) — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/timelines/popular.lisp b/crates/app/src/public/html/timelines/popular.lisp index 125ddb9..f0acb1c 100644 --- a/crates/app/src/public/html/timelines/popular.lisp +++ b/crates/app/src/public/html/timelines/popular.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Popular - {{ config.name }}")) + (text "Popular — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"popular\") }}") (main @@ -14,7 +14,7 @@ (text "{% set paged = user and user.settings.paged_timelines %}") (script (text "setTimeout(() => { - trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?tl=PopularPosts&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]); + trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?tl=PopularPosts&before=$1$&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]); });")) (text "{% endblock %}") diff --git a/crates/app/src/public/html/timelines/popular_questions.lisp b/crates/app/src/public/html/timelines/popular_questions.lisp index b8772ec..7ba257d 100644 --- a/crates/app/src/public/html/timelines/popular_questions.lisp +++ b/crates/app/src/public/html/timelines/popular_questions.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Popular (questions) - {{ config.name }}")) + (text "Popular (questions) — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/html/timelines/search.lisp b/crates/app/src/public/html/timelines/search.lisp index 4adcd61..fb0bef1 100644 --- a/crates/app/src/public/html/timelines/search.lisp +++ b/crates/app/src/public/html/timelines/search.lisp @@ -1,6 +1,6 @@ (text "{% extends \"root.html\" %} {% block head %}") (title - (text "Search - {{ config.name }}")) + (text "Search — {{ config.name }}")) (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main diff --git a/crates/app/src/public/js/atto.js b/crates/app/src/public/js/atto.js index 97850cf..847baad 100644 --- a/crates/app/src/public/js/atto.js +++ b/crates/app/src/public/js/atto.js @@ -1223,6 +1223,8 @@ ${option.input_element_type === "textarea" ? `${option.value}` : ""} return; } + const search = new URLSearchParams(window.location.search); + self.IO_DATA_TMPL = tmpl; self.IO_DATA_PAGE = page; self.IO_DATA_SEEN_IDS = []; @@ -1230,7 +1232,7 @@ ${option.input_element_type === "textarea" ? `${option.value}` : ""} self.IO_HAS_LOADED_AT_LEAST_ONCE = false; self.IO_DATA_DISCONNECTED = false; self.IO_DATA_DISABLE_RELOAD = false; - self.IO_DATA_LOAD_BEFORE = 0; + self.IO_DATA_LOAD_BEFORE = search.get("before") || "0"; if (!paginated_mode) { self.IO_DATA_OBSERVER.observe(self.IO_DATA_MARKER); @@ -1239,6 +1241,21 @@ ${option.input_element_type === "textarea" ? `${option.value}` : ""} self.IO_DATA_TMPL = self.IO_DATA_TMPL.replace("&page=", ""); self.IO_DATA_TMPL += `&paginated=true&page=`; self.io_load_data(); + + // update pagination buttons + setTimeout(() => { + for (const button of document.querySelectorAll( + ".pagination_previous", + )) { + button.href += `&before=${self.IO_DATA_LOAD_BEFORE_PREVIOUS}`; + } + + for (const button of document.querySelectorAll( + ".pagination_next", + )) { + button.href += `&before=${self.IO_DATA_LOAD_BEFORE}`; + } + }, 1000); } setTimeout(() => { @@ -1324,6 +1341,7 @@ ${option.input_element_type === "textarea" ? `${option.value}` : ""} ].after(self.IO_DATA_MARKER); // push ids + let first = ""; for (const opt of Array.from( document.querySelectorAll( `[ui_ident=list_posts_${self.IO_DATA_PAGE}] option`, @@ -1335,8 +1353,14 @@ ${option.input_element_type === "textarea" ? `${option.value}` : ""} self.IO_DATA_SEEN_IDS.push(v); } + if (!first) { + first = opt.getAttribute("data-created"); + } + self.IO_DATA_LOAD_BEFORE = opt.getAttribute("data-created"); } + + self.IO_DATA_LOAD_BEFORE_PREVIOUS = first; }, 150); // run hooks diff --git a/crates/app/src/routes/api/v1/communities/posts.rs b/crates/app/src/routes/api/v1/communities/posts.rs index 7b88c71..e22a7ff 100644 --- a/crates/app/src/routes/api/v1/communities/posts.rs +++ b/crates/app/src/routes/api/v1/communities/posts.rs @@ -850,7 +850,7 @@ pub async fn all_request( }; match data - .get_latest_posts(12, props.page, &Some(user.clone()), props.before) + .get_latest_posts(12, &Some(user.clone()), props.before) .await { Ok(posts) => { diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 2724e39..03593c7 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -674,6 +674,10 @@ pub fn routes() -> Router { .route("/stacks/{id}/block", post(stacks::block_request)) .route("/stacks/{id}/block", delete(stacks::unblock_request)) .route("/stacks/{id}", delete(stacks::delete_request)) + .route( + "/stacks/close_friends", + post(stacks::create_close_friends_request), + ) // journals .route("/journals", get(journals::list_request)) .route("/journals", post(journals::create_request)) diff --git a/crates/app/src/routes/api/v1/stacks.rs b/crates/app/src/routes/api/v1/stacks.rs index 98b8190..c01840b 100644 --- a/crates/app/src/routes/api/v1/stacks.rs +++ b/crates/app/src/routes/api/v1/stacks.rs @@ -91,6 +91,44 @@ pub async fn create_request( } } +pub async fn create_close_friends_request( + jar: CookieJar, + Extension(data): Extension, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateStacks) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + if user.close_friends_stack != 0 { + return Json(Error::NotAllowed.into()); + } + + let mut stack = UserStack::new("Close friends".to_string(), user.id, Vec::new()); + stack.mode = StackMode::Circle; + stack.is_locked = true; + + match data.create_stack(stack).await { + Ok(s) => { + if let Err(e) = data + .update_user_close_friends_stack(user.id, s.id as i64) + .await + { + return Json(e.into()); + } + + // ... + Json(ApiReturn { + ok: true, + message: "Stack created".to_string(), + payload: s.id.to_string(), + }) + } + Err(e) => Json(e.into()), + } +} + pub async fn clone_request( jar: CookieJar, Extension(data): Extension, @@ -168,6 +206,17 @@ pub async fn update_mode_request( None => return Json(Error::NotAllowed.into()), }; + // make sure stack is not locked + let stack = match data.get_stack_by_id(id).await { + Ok(x) => x, + Err(e) => return Json(e.into()), + }; + + if stack.is_locked { + return Json(Error::NotAllowed.into()); + } + + // ... match data.update_stack_mode(id, &user, req.mode).await { Ok(_) => Json(ApiReturn { ok: true, diff --git a/crates/app/src/routes/pages/misc.rs b/crates/app/src/routes/pages/misc.rs index d888dfe..4207c08 100644 --- a/crates/app/src/routes/pages/misc.rs +++ b/crates/app/src/routes/pages/misc.rs @@ -317,7 +317,7 @@ pub async fn all_forum_posts_request( let ignore_users = crate::ignore_users_gen!(user!, data); let list = match data .0 - .get_latest_forum_posts(48, req.page, &Some(user.clone()), req.before) + .get_latest_forum_posts(48, req.page, &Some(user.clone())) .await { Ok(l) => match data @@ -776,17 +776,15 @@ async fn swiss_army_timeline( // everything else match req.tl { DefaultTimelineChoice::AllPosts => { - data.0 - .get_latest_posts(12, req.page, &user, req.before) - .await + data.0.get_latest_posts(12, &user, req.before).await } DefaultTimelineChoice::PopularPosts => { - data.0.get_popular_posts(12, req.page, 604_800_000).await + data.0.get_popular_posts(12, req.before, 604_800_000).await } DefaultTimelineChoice::FollowingPosts => { if let Some(ref ua) = user { data.0 - .get_posts_from_user_following(ua.id, 12, req.page) + .get_posts_from_user_following(ua.id, 12, req.before) .await } else { return Err(Html( @@ -797,7 +795,7 @@ async fn swiss_army_timeline( DefaultTimelineChoice::MyCommunities => { if let Some(ref ua) = user { data.0 - .get_posts_from_user_communities(ua.id, 12, req.page, ua) + .get_posts_from_user_communities(ua.id, 12, req.before, ua) .await } else { return Err(Html( diff --git a/crates/app/src/routes/pages/profile.rs b/crates/app/src/routes/pages/profile.rs index b2de30b..db405a8 100644 --- a/crates/app/src/routes/pages/profile.rs +++ b/crates/app/src/routes/pages/profile.rs @@ -15,6 +15,7 @@ use tetratto_core::model::{ auth::{DefaultProfileTabChoice, User}, communities::Community, permissions::FinePermission, + stacks::UserStack, Error, }; use tetratto_shared::hash::hash; @@ -244,6 +245,7 @@ pub fn profile_context( is_following: bool, is_following_you: bool, is_blocking: bool, + close_friends_stack: Option, ) { context.insert("profile", &profile); context.insert("communities", &communities); @@ -253,6 +255,7 @@ pub fn profile_context( context.insert("is_blocking", &is_blocking); context.insert("warning_hash", &hash(profile.settings.warning.clone())); context.insert("applied_configurations", &applied_configurations); + context.insert("close_friends_stack", &close_friends_stack); context.insert( "is_supporter", @@ -394,6 +397,14 @@ pub async fn posts_request( is_following, is_following_you, is_blocking, + if other_user.close_friends_stack != 0 { + match data.0.get_stack_by_id(other_user.close_friends_stack).await { + Ok(x) => Some(x), + Err(_) => None, + } + } else { + None + }, ); // return @@ -514,6 +525,14 @@ pub async fn replies_request( is_following, is_following_you, is_blocking, + if other_user.close_friends_stack != 0 { + match data.0.get_stack_by_id(other_user.close_friends_stack).await { + Ok(x) => Some(x), + Err(_) => None, + } + } else { + None + }, ); // return @@ -630,6 +649,14 @@ pub async fn media_request( is_following, is_following_you, is_blocking, + if other_user.close_friends_stack != 0 { + match data.0.get_stack_by_id(other_user.close_friends_stack).await { + Ok(x) => Some(x), + Err(_) => None, + } + } else { + None + }, ); // return @@ -723,6 +750,14 @@ pub async fn shop_request( is_following, is_following_you, is_blocking, + if other_user.close_friends_stack != 0 { + match data.0.get_stack_by_id(other_user.close_friends_stack).await { + Ok(x) => Some(x), + Err(_) => None, + } + } else { + None + }, ); // return @@ -821,6 +856,14 @@ pub async fn outbox_request( is_following, is_following_you, is_blocking, + if other_user.close_friends_stack != 0 { + match data.0.get_stack_by_id(other_user.close_friends_stack).await { + Ok(x) => Some(x), + Err(_) => None, + } + } else { + None + }, ); // return @@ -934,6 +977,14 @@ pub async fn following_request( is_following, is_following_you, is_blocking, + if other_user.close_friends_stack != 0 { + match data.0.get_stack_by_id(other_user.close_friends_stack).await { + Ok(x) => Some(x), + Err(_) => None, + } + } else { + None + }, ); // return @@ -1047,6 +1098,14 @@ pub async fn followers_request( is_following, is_following_you, is_blocking, + if other_user.close_friends_stack != 0 { + match data.0.get_stack_by_id(other_user.close_friends_stack).await { + Ok(x) => Some(x), + Err(_) => None, + } + } else { + None + }, ); // return diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index c5096c4..5a4dcdf 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tetratto-core" description = "The core behind Tetratto" -version = "15.0.2" +version = "16.0.0" edition = "2024" readme = "../../README.md" authors.workspace = true diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index 88097be..4b277be 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -130,6 +130,7 @@ impl DataManager { checkouts: serde_json::from_str(&get!(x->32(String)).to_string()).unwrap(), applied_configurations: serde_json::from_str(&get!(x->33(String)).to_string()).unwrap(), last_policy_consent: get!(x->34(i64)) as usize, + close_friends_stack: get!(x->35(i64)) as usize, } } @@ -286,7 +287,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35)", + "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36)", params![ &(data.id as i64), &(data.created as i64), @@ -322,7 +323,8 @@ impl DataManager { &(data.coins as i32), &serde_json::to_string(&data.checkouts).unwrap(), &serde_json::to_string(&data.applied_configurations).unwrap(), - &(data.last_policy_consent as i64) + &(data.last_policy_consent as i64), + &(data.close_friends_stack as i64) ] ); @@ -1138,6 +1140,7 @@ impl DataManager { auto_method!(update_user_checkouts(Vec)@get_user_by_id -> "UPDATE users SET checkouts = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); auto_method!(update_user_applied_configurations(Vec<(AppliedConfigType, usize)>)@get_user_by_id -> "UPDATE users SET applied_configurations = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); auto_method!(update_user_last_policy_consent(i64)@get_user_by_id -> "UPDATE users SET last_policy_consent = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); + auto_method!(update_user_close_friends_stack(i64)@get_user_by_id -> "UPDATE users SET close_friends_stack = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); auto_method!(get_user_by_stripe_id(&str)@get_user_from_row -> "SELECT * FROM users WHERE stripe_id = $1" --name="user" --returns=User); auto_method!(update_user_stripe_id(&str)@get_user_by_id -> "UPDATE users SET stripe_id = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); diff --git a/crates/core/src/database/drivers/sql/create_stacks.sql b/crates/core/src/database/drivers/sql/create_stacks.sql index e9d0def..39b5f6d 100644 --- a/crates/core/src/database/drivers/sql/create_stacks.sql +++ b/crates/core/src/database/drivers/sql/create_stacks.sql @@ -6,5 +6,6 @@ CREATE TABLE IF NOT EXISTS stacks ( users TEXT NOT NULL, privacy TEXT NOT NULL, mode TEXT NOT NULL, - sort TEXT NOT NULL + sort TEXT NOT NULL, + is_locked INT NOT NULL ) diff --git a/crates/core/src/database/drivers/sql/create_users.sql b/crates/core/src/database/drivers/sql/create_users.sql index 1726d10..11b97f8 100644 --- a/crates/core/src/database/drivers/sql/create_users.sql +++ b/crates/core/src/database/drivers/sql/create_users.sql @@ -33,5 +33,6 @@ CREATE TABLE IF NOT EXISTS users ( coins INT NOT NULL, checkouts TEXT NOT NULL, applied_configurations TEXT NOT NULL, - last_policy_consent BIGINT NOT NULL + last_policy_consent BIGINT NOT NULL, + close_friends_stack BIGINT NOT NULL ) diff --git a/crates/core/src/database/drivers/sql/version_migrations.sql b/crates/core/src/database/drivers/sql/version_migrations.sql index 38320ee..e9e731f 100644 --- a/crates/core/src/database/drivers/sql/version_migrations.sql +++ b/crates/core/src/database/drivers/sql/version_migrations.sql @@ -77,3 +77,11 @@ ADD COLUMN IF NOT EXISTS likes INT DEFAULT 0; -- letters dislikes ALTER TABLE letters ADD COLUMN IF NOT EXISTS dislikes INT DEFAULT 0; + +-- users close_friends_stack +ALTER TABLE users +ADD COLUMN IF NOT EXISTS close_friends_stack BIGINT DEFAULT 0; + +-- stacks is_locked +ALTER TABLE stacks +ADD COLUMN IF NOT EXISTS is_locked INT DEFAULT 0; diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index 288ac13..f3667de 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -1209,12 +1209,12 @@ impl DataManager { /// # Arguments /// * `id` - the ID of the stack the requested posts belong to /// * `batch` - the limit of posts in each page - /// * `page` - the page number + /// * `before` - the timestamp to pull posts before pub async fn get_posts_by_stack( &self, id: usize, batch: usize, - page: usize, + before: usize, ) -> Result> { let conn = match self.0.connect().await { Ok(c) => c, @@ -1223,8 +1223,17 @@ impl DataManager { let res = query_rows!( &conn, - "SELECT * FROM posts WHERE stack = $1 AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $2 OFFSET $3", - &[&(id as i64), &(batch as i64), &((page * batch) as i64)], + &format!( + "SELECT * FROM posts WHERE stack = $1 AND replying_to = 0 AND is_deleted = 0{} ORDER BY created DESC LIMIT $2", + { + if before > 0 { + format!(" AND created < {before}") + } else { + String::new() + } + } + ), + &[&(id as i64), &(batch as i64)], |x| { Self::get_post_from_row(x) } ); @@ -1452,12 +1461,12 @@ impl DataManager { /// /// # Arguments /// * `batch` - the limit of posts in each page - /// * `page` - the page number + /// * `before` - the timestamp to pull posts before /// * `cutoff` - the maximum number of milliseconds ago the post could have been created pub async fn get_popular_posts( &self, batch: usize, - page: usize, + before: usize, cutoff: usize, ) -> Result> { let conn = match self.0.connect().await { @@ -1467,18 +1476,24 @@ impl DataManager { let res = query_rows!( &conn, - "SELECT * FROM posts WHERE replying_to = 0 AND NOT context LIKE '%\"is_nsfw\":true%' AND ($1 - created) < $2 ORDER BY likes - dislikes DESC, created ASC LIMIT $3 OFFSET $4", + &format!( + "SELECT * FROM posts WHERE replying_to = 0 AND NOT context LIKE '%\"is_nsfw\":true%' AND ($1 - created) < $2{} ORDER BY (likes - dislikes) DESC, created ASC LIMIT $3", + if before > 0 { + format!(" AND created < {before}") + } else { + String::new() + } + ), &[ &(unix_epoch_timestamp() as i64), &(cutoff as i64), &(batch as i64), - &((page * batch) as i64) ], |x| { Self::get_post_from_row(x) } ); - if res.is_err() { - return Err(Error::GeneralNotFound("post".to_string())); + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); } Ok(res.unwrap()) @@ -1492,7 +1507,6 @@ impl DataManager { pub async fn get_latest_posts( &self, batch: usize, - page: usize, as_user: &Option, before_time: usize, ) -> Result> { @@ -1518,7 +1532,7 @@ impl DataManager { let res = query_rows!( &conn, &format!( - "SELECT * FROM posts WHERE replying_to = 0{}{}{} AND NOT context LIKE '%\"full_unlist\":true%' AND topic = 0 ORDER BY created DESC LIMIT $1 OFFSET $2", + "SELECT * FROM posts WHERE replying_to = 0{}{}{} AND NOT context LIKE '%\"full_unlist\":true%' AND topic = 0 ORDER BY created DESC LIMIT $1", if before_time > 0 { format!(" AND created < {before_time}") } else { @@ -1535,12 +1549,12 @@ impl DataManager { "" } ), - &[&(batch as i64), &((page * batch) as i64)], + &[&(batch as i64)], |x| { Self::get_post_from_row(x) } ); - if res.is_err() { - return Err(Error::GeneralNotFound("post".to_string())); + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); } Ok(res.unwrap()) @@ -1556,7 +1570,6 @@ impl DataManager { batch: usize, page: usize, as_user: &Option, - before_time: usize, ) -> Result> { // check if we should hide nsfw posts let mut hide_nsfw: bool = true; @@ -1574,12 +1587,7 @@ impl DataManager { let res = query_rows!( &conn, &format!( - "SELECT * FROM posts WHERE replying_to = 0{}{} AND NOT context LIKE '%\"full_unlist\":true%' AND NOT topic = 0 ORDER BY created DESC LIMIT $1 OFFSET $2", - if before_time > 0 { - format!(" AND created < {before_time}") - } else { - String::new() - }, + "SELECT * FROM posts WHERE replying_to = 0{} AND NOT context LIKE '%\"full_unlist\":true%' AND NOT topic = 0 ORDER BY created DESC LIMIT $1 OFFSET $2", if hide_nsfw { " AND NOT context LIKE '%\"is_nsfw\":true%'" } else { @@ -1590,8 +1598,8 @@ impl DataManager { |x| { Self::get_post_from_row(x) } ); - if res.is_err() { - return Err(Error::GeneralNotFound("post".to_string())); + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); } Ok(res.unwrap()) @@ -1602,12 +1610,12 @@ impl DataManager { /// # Arguments /// * `id` - the ID of the user /// * `batch` - the limit of posts in each page - /// * `page` - the page number + /// * `before` - the timestamp to pull posts before pub async fn get_posts_from_user_communities( &self, id: usize, batch: usize, - page: usize, + before: usize, user: &User, ) -> Result> { let memberships = self.get_memberships_by_owner(id).await?; @@ -1635,20 +1643,25 @@ impl DataManager { let res = query_rows!( &conn, &format!( - "SELECT * FROM posts WHERE (community = {} {query_string}){} AND NOT context LIKE '%\"full_unlist\":true%' AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $1 OFFSET $2", + "SELECT * FROM posts WHERE (community = {} {query_string}){}{} AND NOT context LIKE '%\"full_unlist\":true%' AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $1", first.community, if hide_nsfw { " AND NOT context LIKE '%\"is_nsfw\":true%'" } else { "" }, + if before > 0 { + format!(" AND created < {before}") + } else { + String::new() + } ), - &[&(batch as i64), &((page * batch) as i64)], + &[&(batch as i64)], |x| { Self::get_post_from_row(x) } ); - if res.is_err() { - return Err(Error::GeneralNotFound("post".to_string())); + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); } Ok(res.unwrap()) @@ -1659,12 +1672,12 @@ impl DataManager { /// # Arguments /// * `id` - the ID of the user /// * `batch` - the limit of posts in each page - /// * `page` - the page number + /// * `before` - the timestamp to pull posts before pub async fn get_posts_from_user_following( &self, id: usize, batch: usize, - page: usize, + before: usize, ) -> Result> { let following = self.get_userfollows_by_initiator_all(id).await?; let mut following = following.iter(); @@ -1688,15 +1701,20 @@ impl DataManager { let res = query_rows!( &conn, &format!( - "SELECT * FROM posts WHERE (owner = {id} OR owner = {} {query_string}) AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $1 OFFSET $2", - first.receiver + "SELECT * FROM posts WHERE (owner = {id} OR owner = {} {query_string}) AND replying_to = 0 AND is_deleted = 0{} ORDER BY created DESC LIMIT $1", + first.receiver, + if before > 0 { + format!(" AND created < {before}") + } else { + String::new() + } ), - &[&(batch as i64), &((page * batch) as i64)], + &[&(batch as i64)], |x| { Self::get_post_from_row(x) } ); - if res.is_err() { - return Err(Error::GeneralNotFound("post".to_string())); + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); } Ok(res.unwrap()) @@ -1750,8 +1768,8 @@ impl DataManager { |x| { Self::get_post_from_row(x) } ); - if res.is_err() { - return Err(Error::GeneralNotFound("post".to_string())); + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); } Ok(res.unwrap()) @@ -1830,8 +1848,15 @@ impl DataManager { )); } - if stack.owner != data.owner && !stack.users.contains(&data.owner) { - return Err(Error::NotAllowed); + if !stack.is_locked || data.replying_to.is_some() { + if stack.owner != data.owner && !stack.users.contains(&data.owner) { + return Err(Error::NotAllowed); + } + } else { + // only the owner can post in locked stacks UNLESS we're creating a reply + if stack.owner != data.owner { + return Err(Error::NotAllowed); + } } } diff --git a/crates/core/src/database/stacks.rs b/crates/core/src/database/stacks.rs index 5d102c2..d3f8d63 100644 --- a/crates/core/src/database/stacks.rs +++ b/crates/core/src/database/stacks.rs @@ -23,6 +23,7 @@ impl DataManager { privacy: serde_json::from_str(&get!(x->5(String))).unwrap(), mode: serde_json::from_str(&get!(x->6(String))).unwrap(), sort: serde_json::from_str(&get!(x->7(String))).unwrap(), + is_locked: get!(x->8(i32)) == 1, } } @@ -56,7 +57,7 @@ impl DataManager { match stack.sort { StackSort::Created => { self.fill_posts_with_community( - self.get_latest_posts(batch, page, &user, 0).await?, + self.get_latest_posts(batch, &user, 0).await?, as_user_id, &ignore_users, user, @@ -184,7 +185,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO stacks VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + "INSERT INTO stacks VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", params![ &(data.id as i64), &(data.created as i64), @@ -194,6 +195,7 @@ impl DataManager { &serde_json::to_string(&data.privacy).unwrap(), &serde_json::to_string(&data.mode).unwrap(), &serde_json::to_string(&data.sort).unwrap(), + &if data.is_locked { 1 } else { 0 }, ] ); @@ -207,6 +209,10 @@ impl DataManager { pub async fn delete_stack(&self, id: usize, user: &User) -> Result<()> { let stack = self.get_stack_by_id(id).await?; + if stack.is_locked { + return Err(Error::NotAllowed); + } + // check user permission if user.id != stack.owner && !user.permissions.check(FinePermission::MANAGE_STACKS) { return Err(Error::NotAllowed); diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index ef6a6e4..806552b 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -107,6 +107,12 @@ pub struct User { /// The time in which the user last consented to the site's policies. #[serde(default)] pub last_policy_consent: usize, + /// The ID of the user's close friends stack. + /// + /// The user's close friends stack is a circle stack which only allows the owner + /// (the user) to post to it. + #[serde(default)] + pub close_friends_stack: usize, } pub type UserConnections = @@ -430,6 +436,7 @@ impl User { checkouts: Vec::new(), applied_configurations: Vec::new(), last_policy_consent: created, + close_friends_stack: 0, } } diff --git a/crates/core/src/model/stacks.rs b/crates/core/src/model/stacks.rs index 437f2cc..b5efdbe 100644 --- a/crates/core/src/model/stacks.rs +++ b/crates/core/src/model/stacks.rs @@ -60,6 +60,9 @@ pub struct UserStack { pub privacy: StackPrivacy, pub mode: StackMode, pub sort: StackSort, + /// Locked stacks cannot be deleted or have their mode changed. Stacks cannot + /// be locked after creation, and must be locked by the server. + pub is_locked: bool, } impl UserStack { @@ -74,6 +77,7 @@ impl UserStack { privacy: StackPrivacy::default(), mode: StackMode::default(), sort: StackSort::default(), + is_locked: false, } } }