add: close friends stack

This commit is contained in:
trisua 2025-08-31 13:02:15 -04:00
parent 407155e6c4
commit 5fafc8d7b9
83 changed files with 479 additions and 213 deletions

56
Cargo.lock generated
View file

@ -407,7 +407,7 @@ dependencies = [
"pathbufd", "pathbufd",
"serde", "serde",
"serde_json", "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)", "tetratto-shared 12.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"toml 0.9.5", "toml 0.9.5",
] ]
@ -3350,7 +3350,7 @@ dependencies = [
[[package]] [[package]]
name = "tetratto" name = "tetratto"
version = "15.0.0" version = "16.0.0"
dependencies = [ dependencies = [
"ammonia", "ammonia",
"async-stripe", "async-stripe",
@ -3370,7 +3370,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tera", "tera",
"tetratto-core 15.0.2", "tetratto-core 16.0.0",
"tetratto-l10n 12.0.0", "tetratto-l10n 12.0.0",
"tetratto-shared 12.0.6", "tetratto-shared 12.0.6",
"tokio", "tokio",
@ -3379,31 +3379,6 @@ dependencies = [
"tracing-subscriber", "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]] [[package]]
name = "tetratto-core" name = "tetratto-core"
version = "15.0.2" version = "15.0.2"
@ -3430,6 +3405,31 @@ dependencies = [
"totp-rs", "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]] [[package]]
name = "tetratto-l10n" name = "tetratto-l10n"
version = "12.0.0" version = "12.0.0"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "tetratto" name = "tetratto"
version = "15.0.0" version = "16.0.0"
edition = "2024" edition = "2024"
authors.workspace = true authors.workspace = true
repository.workspace = true repository.workspace = true

View file

@ -178,6 +178,7 @@ version = "1.0.0"
"settings:tab.theme" = "Theme" "settings:tab.theme" = "Theme"
"settings:tab.sessions" = "Sessions" "settings:tab.sessions" = "Sessions"
"settings:tab.grants" = "Grants" "settings:tab.grants" = "Grants"
"settings:tab.close_friends" = "Close friends"
"settings:tab.images" = "Images" "settings:tab.images" = "Images"
"settings:tab.presets" = "Presets" "settings:tab.presets" = "Presets"
"settings:label.change_password" = "Change password" "settings:label.change_password" = "Change password"

View file

@ -193,11 +193,13 @@ p {
margin-bottom: var(--pad-4); margin-bottom: var(--pad-4);
} }
p, body:not(.use_system_font) {
span:not(nav *):not(.dropdown *):not(a *):not(button *), & p:not(b *),
input, & span:not(.notification, .name, b *, button *, a *, .dropdown *, nav *),
textarea { & input,
font-weight: 300; & textarea {
font-variation-settings: "wght" 325;
}
} }
.no_p_margin p:last-child { .no_p_margin p:last-child {

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (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 }}")) (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) }}") (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"chats\", hide_user_menu=true) }}")
(nav (nav

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "{{ community.context.display_name }} - {{ config.name }}")) (text "{{ community.context.display_name }} {{ config.name }}"))
(meta (meta
("name" "og:title") ("name" "og:title")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Create post - {{ config.name }}")) (text "Create post {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
@ -98,13 +98,13 @@
("selected" "{% if selected_community == community.id -%}true{% else %}false{%- endif %}") ("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 "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}"))
(text "{% endfor %}") (text "{% endfor %}")
(text "{% for stack in stacks %}") (text "{% for stack in stacks %} {% if not stack.is_locked or user.id == stack.owner -%}")
(option (option
("value" "{{ stack.id }}") ("value" "{{ stack.id }}")
("selected" "{% if selected_stack == stack.id -%}true{% else %}false{%- endif %}") ("selected" "{% if selected_stack == stack.id -%}true{% else %}false{%- endif %}")
("is_stack" "true") ("is_stack" "true")
(text "{{ stack.name }} (circle)")) (text "{{ stack.name }} (circle)"))
(text "{% endfor %}"))) (text "{%- endif %} {% endfor %}")))
(form (form
("class" "card flex flex_col gap_2") ("class" "card flex flex_col gap_2")
("id" "create_form") ("id" "create_form")
@ -445,7 +445,7 @@
element.setAttribute(\"alt\", `${id}'s avatar`); element.setAttribute(\"alt\", `${id}'s avatar`);
if (id === town_square || is_stack) { 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 { } else {
element.src = `/api/v1/communities/${id}/avatar`; element.src = `/api/v1/communities/${id}/avatar`;
} }

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "My communities - {{ config.name }}")) (text "My communities {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"communities\") }}") (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"communities\") }}")
(main (main
@ -19,7 +19,7 @@
("class" "flex flex_col gap_1") ("class" "flex flex_col gap_1")
(label (label
("for" "title") ("for" "title")
(text "{{ text \"communities:label.name\" }}")) (str (text "communities:label.name")))
(input (input
("type" "text") ("type" "text")
("name" "title") ("name" "title")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Question - {{ config.name }}")) (text "Question {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Search communities - {{ config.name }}")) (text "Search communities {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"communities\") }}") (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"communities\") }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Community settings - {{ config.name }}")) (text "Community settings {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
("class" "flex flex_col gap_2") ("class" "flex flex_col gap_2")
@ -145,7 +145,7 @@
("required" "") ("required" "")
("minlength" "2"))) ("minlength" "2")))
(button (button
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))))) (text "{{ text \"general:action.save\" }}"))))))
(div (div
@ -168,7 +168,7 @@
("class" "flex gap_2 flex_wrap") ("class" "flex gap_2 flex_wrap")
(button (button
("onclick" "save_context()") ("onclick" "save_context()")
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}"))) (text "{{ text \"general:action.save\" }}")))
(a (a
@ -200,7 +200,7 @@
("class" "w_content")) ("class" "w_content"))
(button (button
("class" "small square big_icon") ("class" "small square big_icon")
(text "{{ icon \"check\" }}")))) (icon (text "check")))))
(div (div
("class" "card_nest") ("class" "card_nest")
("ui_ident" "change_banner") ("ui_ident" "change_banner")
@ -223,7 +223,7 @@
("class" "w_content")) ("class" "w_content"))
(button (button
("class" "small square big_icon") ("class" "small square big_icon")
(text "{{ icon \"check\" }}"))) (icon (text "check"))))
(span (span
("class" "fade") ("class" "fade")
(text "Use an image of 1100x350px for the best results."))))) (text "Use an image of 1100x350px for the best results.")))))
@ -271,7 +271,7 @@
(button (button
("class" "small lowered") ("class" "small lowered")
("onclick" "update_user_role(document.getElementById('uid').value, document.getElementById('role').value)") ("onclick" "update_user_role(document.getElementById('uid').value, document.getElementById('role').value)")
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))) (text "{{ text \"general:action.save\" }}"))))
(div (div
@ -294,7 +294,7 @@
("class" "flex flex_col gap_1") ("class" "flex flex_col gap_1")
(label (label
("for" "title") ("for" "title")
(text "{{ text \"communities:label.name\" }}")) (str (text "communities:label.name")))
(input (input
("type" "text") ("type" "text")
("name" "title") ("name" "title")
@ -453,7 +453,7 @@
("class" "flex flex_col gap_1") ("class" "flex flex_col gap_1")
(label (label
("for" "name") ("for" "name")
(text "{{ text \"communities:label.name\" }}")) (str (text "communities:label.name")))
(input (input
("type" "text") ("type" "text")
("name" "name") ("name" "name")
@ -599,7 +599,7 @@
("class" "flex flex_col gap_1") ("class" "flex flex_col gap_1")
(label (label
("for" "title") ("for" "title")
(text "{{ text \"communities:label.name\" }}")) (str (text "communities:label.name")))
(input (input
("type" "text") ("type" "text")
("name" "title") ("name" "title")
@ -677,7 +677,7 @@
("class" "flex flex_col gap_1") ("class" "flex flex_col gap_1")
(label (label
("for" "title") ("for" "title")
(text "{{ text \"communities:label.name\" }}")) (str (text "communities:label.name")))
(input (input
("type" "text") ("type" "text")
("name" "title") ("name" "title")

View file

@ -137,7 +137,7 @@
("style" "display: contents") ("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 "{{ 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 "{%- endmacro %}")
(text "{% macro post_info(post, community) -%}") (text "{% macro post_info(post, community, owner) -%}")
; info about the post: edited, date, etc. ; info about the post: edited, date, etc.
(text "{% if post.context.edited != 0 -%}") (text "{% if post.context.edited != 0 -%}")
(div (div
@ -167,7 +167,7 @@
(text "{%- endif %} {% if post.stack -%}") (text "{%- endif %} {% if post.stack -%}")
(a (a
("title" "Posted to a stack you're in") ("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)") ("style" "color: var(--color-primary)")
("href" "/stacks/{{ post.stack }}") ("href" "/stacks/{{ post.stack }}")
(text "{{ icon \"layers\" }}")) (text "{{ icon \"layers\" }}"))
@ -386,7 +386,7 @@
(span (span
; ("class" "name") ; ("class" "name")
(text "{{ self::full_username(user=owner) }}")) (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 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 -%}") (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 ; post has a title AND whatever is rendering this component wants to see it
@ -516,7 +516,7 @@
(button (button
("class" "green raised") ("class" "green raised")
("onclick" "trigger('me::update_notification_read_status', ['{{ notification.id }}', true])") ("onclick" "trigger('me::update_notification_read_status', ['{{ notification.id }}', true])")
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"notifs:action.mark_as_read\" }}"))) (text "{{ text \"notifs:action.mark_as_read\" }}")))
(text "{%- endif %}") (text "{%- endif %}")
@ -552,7 +552,7 @@
(text "{{ self::community_avatar(id=post.community, community=community, size=\"18px\") }}"))) (text "{{ self::community_avatar(id=post.community, community=community, size=\"18px\") }}")))
(div (div
("class" "flex gap_2") ("class" "flex gap_2")
(text "{{ self::post_info(post=post, community=community) }}"))) (text "{{ self::post_info(post=post, community=community, owner=owner) }}")))
(div (div
("class" "card_nest_horizontal") ("class" "card_nest_horizontal")
; author info ; author info
@ -630,7 +630,7 @@
("class" "flex justify_between gap_2 w_full") ("class" "flex justify_between gap_2 w_full")
(text "{% if page > 0 -%}") (text "{% if page > 0 -%}")
(a (a
("class" "button lowered") ("class" "button lowered pagination_previous")
("href" "?page={{ page - 1 }}{{ key }}{{ value }}") ("href" "?page={{ page - 1 }}{{ key }}{{ value }}")
(text "{{ icon \"arrow-left\" }}") (text "{{ icon \"arrow-left\" }}")
(span (span
@ -639,7 +639,7 @@
(div) (div)
(text "{%- endif %} {% if items != 0 -%}") (text "{%- endif %} {% if items != 0 -%}")
(a (a
("class" "button lowered") ("class" "button lowered pagination_next")
("href" "?page={{ page + 1 }}{{ key }}{{ value }}") ("href" "?page={{ page + 1 }}{{ key }}{{ value }}")
(span (span
(text "{{ text \"general:link.next\" }}")) (text "{{ text \"general:link.next\" }}"))
@ -2168,7 +2168,15 @@
("class" "card secondary flex flex_col gap_2") ("class" "card secondary flex flex_col gap_2")
(div (div
("class" "flex items_center gap_2") ("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 (b
(text "{{ stack.name }}"))) (text "{{ stack.name }}")))
(span (span

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "{{ app.title }} - {{ config.name }}")) (text "{{ app.title }} {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
@ -93,7 +93,7 @@
("required" "") ("required" "")
("minlength" "2"))) ("minlength" "2")))
(button (button
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}"))))) (text "{{ text \"general:action.save\" }}")))))
(div (div
@ -118,7 +118,7 @@
("required" "") ("required" "")
("minlength" "2"))) ("minlength" "2")))
(button (button
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}"))))) (text "{{ text \"general:action.save\" }}")))))
(div (div
@ -143,7 +143,7 @@
("required" "") ("required" "")
("minlength" "2"))) ("minlength" "2")))
(button (button
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}"))))) (text "{{ text \"general:action.save\" }}")))))
(div (div
@ -183,7 +183,7 @@
(icon (text "external-link")) (text "Docs")))) (icon (text "external-link")) (text "Docs"))))
(button (button
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}"))))) (text "{{ text \"general:action.save\" }}")))))
(div (div

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Developer panel - {{ config.name }}")) (text "Developer panel {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}")
(main (main
@ -21,7 +21,7 @@
("class" "flex flex_col gap_1") ("class" "flex flex_col gap_1")
(label (label
("for" "title") ("for" "title")
(text "{{ text \"communities:label.name\" }}")) (str (text "communities:label.name")))
(input (input
("type" "text") ("type" "text")
("name" "title") ("name" "title")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "{{ app.title }} - {{ config.name }}")) (text "{{ app.title }} {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Manage product - {{ config.name }}")) (text "Manage product {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}")
(main (main
("class" "flex flex_col gap_2") ("class" "flex flex_col gap_2")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Manage advertisement - {{ config.name }}")) (text "Manage advertisement {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}")
(main (main
("class" "flex flex_col gap_2") ("class" "flex flex_col gap_2")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "{{ product.title }} - {{ config.name }}")) (text "{{ product.title }} {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}")
(main (main
("class" "flex flex_col gap_2") ("class" "flex flex_col gap_2")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "My products - {{ config.name }}")) (text "My products {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"products\") }}") (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"products\") }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Wallet - {{ config.name }}")) (text "Wallet {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"wallet\") }}") (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"wallet\") }}")
(main (main
("class" "flex flex_col gap_2") ("class" "flex flex_col gap_2")

View file

@ -2,7 +2,7 @@
; changes to be more github-like instead of retrospring-like ; changes to be more github-like instead of retrospring-like
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "{{ community.context.display_name }} - {{ config.name }}")) (text "{{ community.context.display_name }} {{ config.name }}"))
(meta (meta
("name" "og:title") ("name" "og:title")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Forge - {{ config.name }}")) (text "Forge {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}")
(main (main
@ -20,7 +20,7 @@
("class" "flex flex_col gap_1") ("class" "flex flex_col gap_1")
(label (label
("for" "title") ("for" "title")
(text "{{ text \"communities:label.name\" }}")) (str (text "communities:label.name")))
(input (input
("type" "text") ("type" "text")
("name" "title") ("name" "title")

View file

@ -5,7 +5,7 @@
(text "{% else %}") (text "{% else %}")
(title (text "{{ journal.title }}")) (title (text "{{ journal.title }}"))
(text "{%- endif %} {% else %}") (text "{%- endif %} {% else %}")
(title (text "Journals - {{ config.name }}")) (title (text "Journals {{ config.name }}"))
(text "{%- endif %}") (text "{%- endif %}")
(text "{% if note and journal and owner -%}") (text "{% if note and journal and owner -%}")
@ -253,7 +253,7 @@
("required" "") ("required" "")
("minlength" "2"))) ("minlength" "2")))
(button (button
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}"))))))) (text "{{ text \"general:action.save\" }}")))))))

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "My services - {{ config.name }}")) (text "My services {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "My domains - {{ config.name }}")) (text "My domains {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
@ -35,7 +35,7 @@
("class" "flex flex_col gap_1") ("class" "flex flex_col gap_1")
(label (label
("for" "name") ("for" "name")
(text "{{ text \"communities:label.name\" }}")) (str (text "communities:label.name")))
(input (input
("type" "text") ("type" "text")
("name" "name") ("name" "name")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "My services - {{ config.name }}")) (text "My services {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "My services - {{ config.name }}")) (text "My services {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
@ -35,7 +35,7 @@
("class" "flex flex_col gap_1") ("class" "flex flex_col gap_1")
(label (label
("for" "name") ("for" "name")
(text "{{ text \"communities:label.name\" }}")) (str (text "communities:label.name")))
(input (input
("type" "text") ("type" "text")
("name" "name") ("name" "name")

View file

@ -387,4 +387,10 @@
(text "{{ icon \"cable\" }}") (text "{{ icon \"cable\" }}")
(span (span
(text "{{ text \"settings:tab.grants\" }}"))) (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 %}") (text "{%- endmacro %}")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Compose letter - {{ config.name }}")) (text "Compose letter {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Letter - {{ config.name }}")) (text "Letter {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
("class" "flex flex_col gap_2") ("class" "flex flex_col gap_2")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Received mail - {{ config.name }}")) (text "Received mail {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
("class" "flex flex_col gap_2") ("class" "flex flex_col gap_2")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Sent mail - {{ config.name }}")) (text "Sent mail {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
("class" "flex flex_col gap_2") ("class" "flex flex_col gap_2")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Achievements - {{ config.name }}")) (text "Achievements {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"achievements\") }}") (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"achievements\") }}")
(main (main

View file

@ -1,5 +1,5 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (text "{{ error_text }} - {{ config.name }}")) (title (text "{{ error_text }} {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,5 +1,5 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (text "{{ file_name }} - {{ config.name }}")) (title (text "{{ file_name }} {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Notifications - {{ config.name }}")) (text "Notifications {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"notifications\") }}") (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"notifications\") }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Requests - {{ config.name }}")) (text "Requests {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"requests\") }}") (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"requests\") }}")
(main (main
@ -83,7 +83,7 @@
(button (button
("class" "lowered green") ("class" "lowered green")
("onclick" "accept_follow_request(event, '{{ request.id }}')") ("onclick" "accept_follow_request(event, '{{ request.id }}')")
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.accept\" }}"))) (text "{{ text \"general:action.accept\" }}")))
(button (button
@ -114,7 +114,7 @@
(button (button
("class" "lowered green") ("class" "lowered green")
("onclick" "accept_transfer_request(event, '{{ request.id }}', '{{ request.linked_asset }}', {{ request.data.Int32 }})") ("onclick" "accept_transfer_request(event, '{{ request.id }}', '{{ request.linked_asset }}', {{ request.data.Int32 }})")
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.accept\" }}"))) (text "{{ text \"general:action.accept\" }}")))
(button (button

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Audit log - {{ config.name }}")) (text "Audit log {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "File report - {{ config.name }}")) (text "File report {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "IP Bans - {{ config.name }}")) (text "IP Bans {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Manage profile - {{ config.name }}")) (text "Manage profile {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
@ -356,7 +356,7 @@
(button (button
("class" "small lowered") ("class" "small lowered")
("onclick" "update_user_role(Number.parseInt(document.getElementById('role').value))") ("onclick" "update_user_role(Number.parseInt(document.getElementById('role').value))")
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))) (text "{{ text \"general:action.save\" }}"))))
(div (div
@ -374,7 +374,7 @@
(button (button
("class" "small lowered") ("class" "small lowered")
("onclick" "update_user_secondary_role(Number.parseInt(document.getElementById('secondary_role').value))") ("onclick" "update_user_secondary_role(Number.parseInt(document.getElementById('secondary_role').value))")
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))) (text "{{ text \"general:action.save\" }}"))))
(div (div

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Reports - {{ config.name }}")) (text "Reports {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Server stats - {{ config.name }}")) (text "Server stats {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "User warnings - {{ config.name }}")) (text "User warnings {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Post likes - {{ config.name }}")) (text "Post likes {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Post - {{ config.name }}")) (text "Post {{ config.name }}"))
(meta (meta
("name" "og:title") ("name" "og:title")
@ -146,7 +146,7 @@
("id" "post_context"))) ("id" "post_context")))
(button (button
("onclick" "save_context()") ("onclick" "save_context()")
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}"))) (text "{{ text \"general:action.save\" }}")))
(script (script

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Post quotes - {{ config.name }}")) (text "Post quotes {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Post reposts - {{ config.name }}")) (text "Post reposts {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "{{ profile.username }} (banned) - {{ config.name }}")) (text "{{ profile.username }} (banned) {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "{{ profile.username }} - {{ config.name }}")) (text "{{ profile.username }} {{ config.name }}"))
(meta (meta
("name" "og:title") ("name" "og:title")
@ -57,7 +57,15 @@
(div (div
("class" "card flex gap_2") ("class" "card flex gap_2")
("id" "user_avatar_and_name") ("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 (div
("class" "flex flex_col") ("class" "flex flex_col")
(h3 (h3
@ -131,13 +139,21 @@
(span (span
(text "{{ text \"auth:label.following\" }}")))) (text "{{ text \"auth:label.following\" }}"))))
(text "{%- endif %}") (text "{%- endif %}")
(div
("class" "flex gap_2")
(text "{% if is_following_you -%}") (text "{% if is_following_you -%}")
(b (b
("class" "notification chip w_content flex items_center gap_2") ("class" "notification chip w_content flex items_center gap_2")
(text "{{ icon \"heart\" }}") (icon (text "heart"))
(span (span (text "Follows you")))
(text "Follows you"))) (text "{%- endif %}")
(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 (div
("class" "card_nest flex flex_col") ("class" "card_nest flex flex_col")
(div (div

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "{{ profile.username }} (blocked) - {{ config.name }}")) (text "{{ profile.username }} (blocked) {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "{{ profile.username }} (private profile) - {{ config.name }}")) (text "{{ profile.username }} (private profile) {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Settings - {{ config.name }}")) (text "Settings {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(article (article
("class" "flex flex_row gap_2 content_container") ("class" "flex flex_row gap_2 content_container")
@ -280,7 +280,7 @@
("required" "") ("required" "")
("minlength" "2"))) ("minlength" "2")))
(button (button
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))))) (text "{{ text \"general:action.save\" }}"))))))
(div (div
@ -335,7 +335,7 @@
(button (button
("onclick" "save_settings()") ("onclick" "save_settings()")
("id" "save_button") ("id" "save_button")
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))) (text "{{ text \"general:action.save\" }}"))))
(div (div
@ -442,7 +442,7 @@
("minlength" "6") ("minlength" "6")
("autocomplete" "off"))) ("autocomplete" "off")))
(button (button
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}"))))))))) (text "{{ text \"general:action.save\" }}")))))))))
(div (div
@ -959,7 +959,7 @@
("class" "w_content")) ("class" "w_content"))
(button (button
("class" "small square big_icon") ("class" "small square big_icon")
(text "{{ icon \"check\" }}"))) (icon (text "check"))))
(span (span
("class" "fade") ("class" "fade")
(text "Images must be less than 8 MB large. Animated GIFs are (text "Images must be less than 8 MB large. Animated GIFs are
@ -987,7 +987,7 @@
("class" "w_content")) ("class" "w_content"))
(button (button
("class" "small square big_icon") ("class" "small square big_icon")
(text "{{ icon \"check\" }}"))) (icon (text "check"))))
(span (span
("class" "fade") ("class" "fade")
(text "Use an image of 1100x350px for the best results.")))) (text "Use an image of 1100x350px for the best results."))))
@ -1032,7 +1032,7 @@
(button (button
("onclick" "save_settings()") ("onclick" "save_settings()")
("id" "save_button") ("id" "save_button")
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))) (text "{{ text \"general:action.save\" }}"))))
(div (div
@ -1191,7 +1191,7 @@
(button (button
("onclick" "save_settings()") ("onclick" "save_settings()")
("id" "save_button") ("id" "save_button")
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))) (text "{{ text \"general:action.save\" }}"))))
(div (div
@ -1304,6 +1304,48 @@
(span (span
(text "{{ config.name }} ") (text "{{ config.name }} ")
(str (text "developer:label.for_developers"))))) (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 (script
("type" "application/json") ("type" "application/json")
("id" "settings_json") ("id" "settings_json")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "{{ profile.username }} (warning) - {{ config.name }}")) (text "{{ profile.username }} (warning) {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
@ -26,7 +26,7 @@
("class" "card w_full secondary flex gap_2") ("class" "card w_full secondary flex gap_2")
(button (button
("onclick" "trigger('warnings::accept', ['{{ profile.id }}', '{{ warning_hash }}'])") ("onclick" "trigger('warnings::accept', ['{{ profile.id }}', '{{ warning_hash }}'])")
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"dialog:action.continue\" }}"))) (text "{{ text \"dialog:action.continue\" }}")))
(a (a

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Add user to stack - {{ config.name }}")) (text "Add user to stack {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "{{ stack.name }} - {{ config.name }}")) (text "{{ stack.name }} {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
@ -19,7 +19,7 @@
(text "{{ stack.name }}"))) (text "{{ stack.name }}")))
(div (div
("class" "flex gap_2") ("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 ; post button for circle stacks
(a (a
("href" "/communities/intents/post?stack={{ stack.id }}") ("href" "/communities/intents/post?stack={{ stack.id }}")
@ -94,7 +94,7 @@
(text "{% set paged = user and user.settings.paged_timelines %}") (text "{% set paged = user and user.settings.paged_timelines %}")
(script (script
(text "setTimeout(() => { (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 %}")))) (text "{%- endif %}"))))

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "My stacks - {{ config.name }}")) (text "My stacks {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
@ -19,7 +19,7 @@
("class" "flex flex_col gap_1") ("class" "flex flex_col gap_1")
(label (label
("for" "name") ("for" "name")
(text "{{ text \"communities:label.name\" }}")) (str (text "communities:label.name")))
(input (input
("type" "text") ("type" "text")
("name" "name") ("name" "name")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Stack settings - {{ config.name }}")) (text "Stack settings {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
@ -56,6 +56,7 @@
("class" "card") ("class" "card")
(select (select
("onchange" "save_mode(event)") ("onchange" "save_mode(event)")
("disabled" "{{ stack.is_locked }}")
(option (option
("value" "Include") ("value" "Include")
("selected" "{% if stack.mode == 'Include' -%}true{% else %}false{%- endif %}") ("selected" "{% if stack.mode == 'Include' -%}true{% else %}false{%- endif %}")
@ -105,7 +106,7 @@
("class" "flex flex_col gap_1") ("class" "flex flex_col gap_1")
(label (label
("for" "name") ("for" "name")
(text "{{ text \"communities:label.name\" }}")) (str (text "communities:label.name")))
(input (input
("type" "text") ("type" "text")
("name" "name") ("name" "name")
@ -114,9 +115,10 @@
("required" "") ("required" "")
("minlength" "2"))) ("minlength" "2")))
(button (button
(text "{{ icon \"check\" }}") (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))))) (text "{{ text \"general:action.save\" }}"))))))
(text "{% if not stack.is_locked -%}")
(div (div
("class" "card_nest") ("class" "card_nest")
("ui_ident" "danger_zone") ("ui_ident" "danger_zone")
@ -132,7 +134,8 @@
("onclick" "delete_stack()") ("onclick" "delete_stack()")
(text "{{ icon \"trash\" }}") (text "{{ icon \"trash\" }}")
(span (span
(text "{{ text \"general:action.delete\" }}")))))) (text "{{ text \"general:action.delete\" }}")))))
(text "{%- endif %}"))
(div (div
("class" "card w_full flex flex_col gap_2 hidden") ("class" "card w_full flex flex_col gap_2 hidden")
("data-tab" "users") ("data-tab" "users")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Latest posts - {{ config.name }}")) (text "Latest posts {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
("class" "flex flex_col gap_2") ("class" "flex flex_col gap_2")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Latest forum posts - {{ config.name }}")) (text "Latest forum posts {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
("class" "flex flex_col gap_2") ("class" "flex flex_col gap_2")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Latest questions - {{ config.name }}")) (text "Latest questions {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
("class" "flex flex_col gap_2") ("class" "flex flex_col gap_2")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Following - {{ config.name }}")) (text "Following {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
@ -14,7 +14,7 @@
(text "{% set paged = user and user.settings.paged_timelines %}") (text "{% set paged = user and user.settings.paged_timelines %}")
(script (script
(text "setTimeout(() => { (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 %}") (text "{% endblock %}")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Following (questions) - {{ config.name }}")) (text "Following (questions) {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -46,7 +46,6 @@
(text "{% set paged = user and user.settings.paged_timelines %}") (text "{% set paged = user and user.settings.paged_timelines %}")
(script (script
(text "setTimeout(() => { (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 %}") (text "{% endblock %}")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "From my communities (questions) - {{ config.name }}")) (text "From my communities (questions) {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Popular - {{ config.name }}")) (text "Popular {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"popular\") }}") (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"popular\") }}")
(main (main
@ -14,7 +14,7 @@
(text "{% set paged = user and user.settings.paged_timelines %}") (text "{% set paged = user and user.settings.paged_timelines %}")
(script (script
(text "setTimeout(() => { (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 %}") (text "{% endblock %}")

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Popular (questions) - {{ config.name }}")) (text "Popular (questions) {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1,6 +1,6 @@
(text "{% extends \"root.html\" %} {% block head %}") (text "{% extends \"root.html\" %} {% block head %}")
(title (title
(text "Search - {{ config.name }}")) (text "Search {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main

View file

@ -1223,6 +1223,8 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
return; return;
} }
const search = new URLSearchParams(window.location.search);
self.IO_DATA_TMPL = tmpl; self.IO_DATA_TMPL = tmpl;
self.IO_DATA_PAGE = page; self.IO_DATA_PAGE = page;
self.IO_DATA_SEEN_IDS = []; self.IO_DATA_SEEN_IDS = [];
@ -1230,7 +1232,7 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
self.IO_HAS_LOADED_AT_LEAST_ONCE = false; self.IO_HAS_LOADED_AT_LEAST_ONCE = false;
self.IO_DATA_DISCONNECTED = false; self.IO_DATA_DISCONNECTED = false;
self.IO_DATA_DISABLE_RELOAD = 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) { if (!paginated_mode) {
self.IO_DATA_OBSERVER.observe(self.IO_DATA_MARKER); self.IO_DATA_OBSERVER.observe(self.IO_DATA_MARKER);
@ -1239,6 +1241,21 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
self.IO_DATA_TMPL = self.IO_DATA_TMPL.replace("&page=", ""); self.IO_DATA_TMPL = self.IO_DATA_TMPL.replace("&page=", "");
self.IO_DATA_TMPL += `&paginated=true&page=`; self.IO_DATA_TMPL += `&paginated=true&page=`;
self.io_load_data(); 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(() => { setTimeout(() => {
@ -1324,6 +1341,7 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
].after(self.IO_DATA_MARKER); ].after(self.IO_DATA_MARKER);
// push ids // push ids
let first = "";
for (const opt of Array.from( for (const opt of Array.from(
document.querySelectorAll( document.querySelectorAll(
`[ui_ident=list_posts_${self.IO_DATA_PAGE}] option`, `[ui_ident=list_posts_${self.IO_DATA_PAGE}] option`,
@ -1335,8 +1353,14 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
self.IO_DATA_SEEN_IDS.push(v); 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 = opt.getAttribute("data-created");
} }
self.IO_DATA_LOAD_BEFORE_PREVIOUS = first;
}, 150); }, 150);
// run hooks // run hooks

View file

@ -850,7 +850,7 @@ pub async fn all_request(
}; };
match data match data
.get_latest_posts(12, props.page, &Some(user.clone()), props.before) .get_latest_posts(12, &Some(user.clone()), props.before)
.await .await
{ {
Ok(posts) => { Ok(posts) => {

View file

@ -674,6 +674,10 @@ pub fn routes() -> Router {
.route("/stacks/{id}/block", post(stacks::block_request)) .route("/stacks/{id}/block", post(stacks::block_request))
.route("/stacks/{id}/block", delete(stacks::unblock_request)) .route("/stacks/{id}/block", delete(stacks::unblock_request))
.route("/stacks/{id}", delete(stacks::delete_request)) .route("/stacks/{id}", delete(stacks::delete_request))
.route(
"/stacks/close_friends",
post(stacks::create_close_friends_request),
)
// journals // journals
.route("/journals", get(journals::list_request)) .route("/journals", get(journals::list_request))
.route("/journals", post(journals::create_request)) .route("/journals", post(journals::create_request))

View file

@ -91,6 +91,44 @@ pub async fn create_request(
} }
} }
pub async fn create_close_friends_request(
jar: CookieJar,
Extension(data): Extension<State>,
) -> 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( pub async fn clone_request(
jar: CookieJar, jar: CookieJar,
Extension(data): Extension<State>, Extension(data): Extension<State>,
@ -168,6 +206,17 @@ pub async fn update_mode_request(
None => return Json(Error::NotAllowed.into()), 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 { match data.update_stack_mode(id, &user, req.mode).await {
Ok(_) => Json(ApiReturn { Ok(_) => Json(ApiReturn {
ok: true, ok: true,

View file

@ -317,7 +317,7 @@ pub async fn all_forum_posts_request(
let ignore_users = crate::ignore_users_gen!(user!, data); let ignore_users = crate::ignore_users_gen!(user!, data);
let list = match data let list = match data
.0 .0
.get_latest_forum_posts(48, req.page, &Some(user.clone()), req.before) .get_latest_forum_posts(48, req.page, &Some(user.clone()))
.await .await
{ {
Ok(l) => match data Ok(l) => match data
@ -776,17 +776,15 @@ async fn swiss_army_timeline(
// everything else // everything else
match req.tl { match req.tl {
DefaultTimelineChoice::AllPosts => { DefaultTimelineChoice::AllPosts => {
data.0 data.0.get_latest_posts(12, &user, req.before).await
.get_latest_posts(12, req.page, &user, req.before)
.await
} }
DefaultTimelineChoice::PopularPosts => { 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 => { DefaultTimelineChoice::FollowingPosts => {
if let Some(ref ua) = user { if let Some(ref ua) = user {
data.0 data.0
.get_posts_from_user_following(ua.id, 12, req.page) .get_posts_from_user_following(ua.id, 12, req.before)
.await .await
} else { } else {
return Err(Html( return Err(Html(
@ -797,7 +795,7 @@ async fn swiss_army_timeline(
DefaultTimelineChoice::MyCommunities => { DefaultTimelineChoice::MyCommunities => {
if let Some(ref ua) = user { if let Some(ref ua) = user {
data.0 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 .await
} else { } else {
return Err(Html( return Err(Html(

View file

@ -15,6 +15,7 @@ use tetratto_core::model::{
auth::{DefaultProfileTabChoice, User}, auth::{DefaultProfileTabChoice, User},
communities::Community, communities::Community,
permissions::FinePermission, permissions::FinePermission,
stacks::UserStack,
Error, Error,
}; };
use tetratto_shared::hash::hash; use tetratto_shared::hash::hash;
@ -244,6 +245,7 @@ pub fn profile_context(
is_following: bool, is_following: bool,
is_following_you: bool, is_following_you: bool,
is_blocking: bool, is_blocking: bool,
close_friends_stack: Option<UserStack>,
) { ) {
context.insert("profile", &profile); context.insert("profile", &profile);
context.insert("communities", &communities); context.insert("communities", &communities);
@ -253,6 +255,7 @@ pub fn profile_context(
context.insert("is_blocking", &is_blocking); context.insert("is_blocking", &is_blocking);
context.insert("warning_hash", &hash(profile.settings.warning.clone())); context.insert("warning_hash", &hash(profile.settings.warning.clone()));
context.insert("applied_configurations", &applied_configurations); context.insert("applied_configurations", &applied_configurations);
context.insert("close_friends_stack", &close_friends_stack);
context.insert( context.insert(
"is_supporter", "is_supporter",
@ -394,6 +397,14 @@ pub async fn posts_request(
is_following, is_following,
is_following_you, is_following_you,
is_blocking, 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 // return
@ -514,6 +525,14 @@ pub async fn replies_request(
is_following, is_following,
is_following_you, is_following_you,
is_blocking, 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 // return
@ -630,6 +649,14 @@ pub async fn media_request(
is_following, is_following,
is_following_you, is_following_you,
is_blocking, 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 // return
@ -723,6 +750,14 @@ pub async fn shop_request(
is_following, is_following,
is_following_you, is_following_you,
is_blocking, 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 // return
@ -821,6 +856,14 @@ pub async fn outbox_request(
is_following, is_following,
is_following_you, is_following_you,
is_blocking, 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 // return
@ -934,6 +977,14 @@ pub async fn following_request(
is_following, is_following,
is_following_you, is_following_you,
is_blocking, 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 // return
@ -1047,6 +1098,14 @@ pub async fn followers_request(
is_following, is_following,
is_following_you, is_following_you,
is_blocking, 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 // return

View file

@ -1,7 +1,7 @@
[package] [package]
name = "tetratto-core" name = "tetratto-core"
description = "The core behind Tetratto" description = "The core behind Tetratto"
version = "15.0.2" version = "16.0.0"
edition = "2024" edition = "2024"
readme = "../../README.md" readme = "../../README.md"
authors.workspace = true authors.workspace = true

View file

@ -130,6 +130,7 @@ impl DataManager {
checkouts: serde_json::from_str(&get!(x->32(String)).to_string()).unwrap(), 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(), applied_configurations: serde_json::from_str(&get!(x->33(String)).to_string()).unwrap(),
last_policy_consent: get!(x->34(i64)) as usize, 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!( let res = execute!(
&conn, &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![ params![
&(data.id as i64), &(data.id as i64),
&(data.created as i64), &(data.created as i64),
@ -322,7 +323,8 @@ impl DataManager {
&(data.coins as i32), &(data.coins as i32),
&serde_json::to_string(&data.checkouts).unwrap(), &serde_json::to_string(&data.checkouts).unwrap(),
&serde_json::to_string(&data.applied_configurations).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<String>)@get_user_by_id -> "UPDATE users SET checkouts = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); auto_method!(update_user_checkouts(Vec<String>)@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_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_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!(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); 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);

View file

@ -6,5 +6,6 @@ CREATE TABLE IF NOT EXISTS stacks (
users TEXT NOT NULL, users TEXT NOT NULL,
privacy TEXT NOT NULL, privacy TEXT NOT NULL,
mode TEXT NOT NULL, mode TEXT NOT NULL,
sort TEXT NOT NULL sort TEXT NOT NULL,
is_locked INT NOT NULL
) )

View file

@ -33,5 +33,6 @@ CREATE TABLE IF NOT EXISTS users (
coins INT NOT NULL, coins INT NOT NULL,
checkouts TEXT NOT NULL, checkouts TEXT NOT NULL,
applied_configurations 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
) )

View file

@ -77,3 +77,11 @@ ADD COLUMN IF NOT EXISTS likes INT DEFAULT 0;
-- letters dislikes -- letters dislikes
ALTER TABLE letters ALTER TABLE letters
ADD COLUMN IF NOT EXISTS dislikes INT DEFAULT 0; 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;

View file

@ -1209,12 +1209,12 @@ impl DataManager {
/// # Arguments /// # Arguments
/// * `id` - the ID of the stack the requested posts belong to /// * `id` - the ID of the stack the requested posts belong to
/// * `batch` - the limit of posts in each page /// * `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( pub async fn get_posts_by_stack(
&self, &self,
id: usize, id: usize,
batch: usize, batch: usize,
page: usize, before: usize,
) -> Result<Vec<Post>> { ) -> Result<Vec<Post>> {
let conn = match self.0.connect().await { let conn = match self.0.connect().await {
Ok(c) => c, Ok(c) => c,
@ -1223,8 +1223,17 @@ impl DataManager {
let res = query_rows!( let res = query_rows!(
&conn, &conn,
"SELECT * FROM posts WHERE stack = $1 AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $2 OFFSET $3", &format!(
&[&(id as i64), &(batch as i64), &((page * batch) as i64)], "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) } |x| { Self::get_post_from_row(x) }
); );
@ -1452,12 +1461,12 @@ impl DataManager {
/// ///
/// # Arguments /// # Arguments
/// * `batch` - the limit of posts in each page /// * `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 /// * `cutoff` - the maximum number of milliseconds ago the post could have been created
pub async fn get_popular_posts( pub async fn get_popular_posts(
&self, &self,
batch: usize, batch: usize,
page: usize, before: usize,
cutoff: usize, cutoff: usize,
) -> Result<Vec<Post>> { ) -> Result<Vec<Post>> {
let conn = match self.0.connect().await { let conn = match self.0.connect().await {
@ -1467,18 +1476,24 @@ impl DataManager {
let res = query_rows!( let res = query_rows!(
&conn, &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), &(unix_epoch_timestamp() as i64),
&(cutoff as i64), &(cutoff as i64),
&(batch as i64), &(batch as i64),
&((page * batch) as i64)
], ],
|x| { Self::get_post_from_row(x) } |x| { Self::get_post_from_row(x) }
); );
if res.is_err() { if let Err(e) = res {
return Err(Error::GeneralNotFound("post".to_string())); return Err(Error::DatabaseError(e.to_string()));
} }
Ok(res.unwrap()) Ok(res.unwrap())
@ -1492,7 +1507,6 @@ impl DataManager {
pub async fn get_latest_posts( pub async fn get_latest_posts(
&self, &self,
batch: usize, batch: usize,
page: usize,
as_user: &Option<User>, as_user: &Option<User>,
before_time: usize, before_time: usize,
) -> Result<Vec<Post>> { ) -> Result<Vec<Post>> {
@ -1518,7 +1532,7 @@ impl DataManager {
let res = query_rows!( let res = query_rows!(
&conn, &conn,
&format!( &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 { if before_time > 0 {
format!(" AND created < {before_time}") format!(" AND created < {before_time}")
} else { } else {
@ -1535,12 +1549,12 @@ impl DataManager {
"" ""
} }
), ),
&[&(batch as i64), &((page * batch) as i64)], &[&(batch as i64)],
|x| { Self::get_post_from_row(x) } |x| { Self::get_post_from_row(x) }
); );
if res.is_err() { if let Err(e) = res {
return Err(Error::GeneralNotFound("post".to_string())); return Err(Error::DatabaseError(e.to_string()));
} }
Ok(res.unwrap()) Ok(res.unwrap())
@ -1556,7 +1570,6 @@ impl DataManager {
batch: usize, batch: usize,
page: usize, page: usize,
as_user: &Option<User>, as_user: &Option<User>,
before_time: usize,
) -> Result<Vec<Post>> { ) -> Result<Vec<Post>> {
// check if we should hide nsfw posts // check if we should hide nsfw posts
let mut hide_nsfw: bool = true; let mut hide_nsfw: bool = true;
@ -1574,12 +1587,7 @@ impl DataManager {
let res = query_rows!( let res = query_rows!(
&conn, &conn,
&format!( &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", "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()
},
if hide_nsfw { if hide_nsfw {
" AND NOT context LIKE '%\"is_nsfw\":true%'" " AND NOT context LIKE '%\"is_nsfw\":true%'"
} else { } else {
@ -1590,8 +1598,8 @@ impl DataManager {
|x| { Self::get_post_from_row(x) } |x| { Self::get_post_from_row(x) }
); );
if res.is_err() { if let Err(e) = res {
return Err(Error::GeneralNotFound("post".to_string())); return Err(Error::DatabaseError(e.to_string()));
} }
Ok(res.unwrap()) Ok(res.unwrap())
@ -1602,12 +1610,12 @@ impl DataManager {
/// # Arguments /// # Arguments
/// * `id` - the ID of the user /// * `id` - the ID of the user
/// * `batch` - the limit of posts in each page /// * `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( pub async fn get_posts_from_user_communities(
&self, &self,
id: usize, id: usize,
batch: usize, batch: usize,
page: usize, before: usize,
user: &User, user: &User,
) -> Result<Vec<Post>> { ) -> Result<Vec<Post>> {
let memberships = self.get_memberships_by_owner(id).await?; let memberships = self.get_memberships_by_owner(id).await?;
@ -1635,20 +1643,25 @@ impl DataManager {
let res = query_rows!( let res = query_rows!(
&conn, &conn,
&format!( &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, first.community,
if hide_nsfw { if hide_nsfw {
" AND NOT context LIKE '%\"is_nsfw\":true%'" " AND NOT context LIKE '%\"is_nsfw\":true%'"
} else { } 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) } |x| { Self::get_post_from_row(x) }
); );
if res.is_err() { if let Err(e) = res {
return Err(Error::GeneralNotFound("post".to_string())); return Err(Error::DatabaseError(e.to_string()));
} }
Ok(res.unwrap()) Ok(res.unwrap())
@ -1659,12 +1672,12 @@ impl DataManager {
/// # Arguments /// # Arguments
/// * `id` - the ID of the user /// * `id` - the ID of the user
/// * `batch` - the limit of posts in each page /// * `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( pub async fn get_posts_from_user_following(
&self, &self,
id: usize, id: usize,
batch: usize, batch: usize,
page: usize, before: usize,
) -> Result<Vec<Post>> { ) -> Result<Vec<Post>> {
let following = self.get_userfollows_by_initiator_all(id).await?; let following = self.get_userfollows_by_initiator_all(id).await?;
let mut following = following.iter(); let mut following = following.iter();
@ -1688,15 +1701,20 @@ impl DataManager {
let res = query_rows!( let res = query_rows!(
&conn, &conn,
&format!( &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", "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 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) } |x| { Self::get_post_from_row(x) }
); );
if res.is_err() { if let Err(e) = res {
return Err(Error::GeneralNotFound("post".to_string())); return Err(Error::DatabaseError(e.to_string()));
} }
Ok(res.unwrap()) Ok(res.unwrap())
@ -1750,8 +1768,8 @@ impl DataManager {
|x| { Self::get_post_from_row(x) } |x| { Self::get_post_from_row(x) }
); );
if res.is_err() { if let Err(e) = res {
return Err(Error::GeneralNotFound("post".to_string())); return Err(Error::DatabaseError(e.to_string()));
} }
Ok(res.unwrap()) Ok(res.unwrap())
@ -1830,9 +1848,16 @@ impl DataManager {
)); ));
} }
if !stack.is_locked || data.replying_to.is_some() {
if stack.owner != data.owner && !stack.users.contains(&data.owner) { if stack.owner != data.owner && !stack.users.contains(&data.owner) {
return Err(Error::NotAllowed); 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);
}
}
} }
// ... // ...

View file

@ -23,6 +23,7 @@ impl DataManager {
privacy: serde_json::from_str(&get!(x->5(String))).unwrap(), privacy: serde_json::from_str(&get!(x->5(String))).unwrap(),
mode: serde_json::from_str(&get!(x->6(String))).unwrap(), mode: serde_json::from_str(&get!(x->6(String))).unwrap(),
sort: serde_json::from_str(&get!(x->7(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 { match stack.sort {
StackSort::Created => { StackSort::Created => {
self.fill_posts_with_community( 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, as_user_id,
&ignore_users, &ignore_users,
user, user,
@ -184,7 +185,7 @@ impl DataManager {
let res = execute!( let res = execute!(
&conn, &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![ params![
&(data.id as i64), &(data.id as i64),
&(data.created as i64), &(data.created as i64),
@ -194,6 +195,7 @@ impl DataManager {
&serde_json::to_string(&data.privacy).unwrap(), &serde_json::to_string(&data.privacy).unwrap(),
&serde_json::to_string(&data.mode).unwrap(), &serde_json::to_string(&data.mode).unwrap(),
&serde_json::to_string(&data.sort).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<()> { pub async fn delete_stack(&self, id: usize, user: &User) -> Result<()> {
let stack = self.get_stack_by_id(id).await?; let stack = self.get_stack_by_id(id).await?;
if stack.is_locked {
return Err(Error::NotAllowed);
}
// check user permission // check user permission
if user.id != stack.owner && !user.permissions.check(FinePermission::MANAGE_STACKS) { if user.id != stack.owner && !user.permissions.check(FinePermission::MANAGE_STACKS) {
return Err(Error::NotAllowed); return Err(Error::NotAllowed);

View file

@ -107,6 +107,12 @@ pub struct User {
/// The time in which the user last consented to the site's policies. /// The time in which the user last consented to the site's policies.
#[serde(default)] #[serde(default)]
pub last_policy_consent: usize, 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 = pub type UserConnections =
@ -430,6 +436,7 @@ impl User {
checkouts: Vec::new(), checkouts: Vec::new(),
applied_configurations: Vec::new(), applied_configurations: Vec::new(),
last_policy_consent: created, last_policy_consent: created,
close_friends_stack: 0,
} }
} }

View file

@ -60,6 +60,9 @@ pub struct UserStack {
pub privacy: StackPrivacy, pub privacy: StackPrivacy,
pub mode: StackMode, pub mode: StackMode,
pub sort: StackSort, 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 { impl UserStack {
@ -74,6 +77,7 @@ impl UserStack {
privacy: StackPrivacy::default(), privacy: StackPrivacy::default(),
mode: StackMode::default(), mode: StackMode::default(),
sort: StackSort::default(), sort: StackSort::default(),
is_locked: false,
} }
} }
} }