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",
"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"

View file

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

View file

@ -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"

View file

@ -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 {

View file

@ -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

View file

@ -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")

View file

@ -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`;
}

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -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

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -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\" }}")))))))

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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")

View file

@ -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 %}")

View file

@ -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

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 "{% 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 %}")
(div
("class" "flex gap_2")
(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 %}")))
(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

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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 %}"))))

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -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 %}")

View file

@ -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

View file

@ -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 %}")

View file

@ -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

View file

@ -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 %}")

View file

@ -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

View file

@ -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

View file

@ -1223,6 +1223,8 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
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}</textarea>` : ""}
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}</textarea>` : ""}
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}</textarea>` : ""}
].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}</textarea>` : ""}
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

View file

@ -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) => {

View file

@ -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))

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(
jar: CookieJar,
Extension(data): Extension<State>,
@ -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,

View file

@ -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(

View file

@ -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<UserStack>,
) {
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

View file

@ -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

View file

@ -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<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_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);

View file

@ -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
)

View file

@ -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
)

View file

@ -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;

View file

@ -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<Vec<Post>> {
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<Vec<Post>> {
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<User>,
before_time: usize,
) -> Result<Vec<Post>> {
@ -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<User>,
before_time: usize,
) -> Result<Vec<Post>> {
// 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<Vec<Post>> {
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<Vec<Post>> {
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,9 +1848,16 @@ impl DataManager {
));
}
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);
}
}
}
// ...

View file

@ -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);

View file

@ -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,
}
}

View file

@ -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,
}
}
}