add: user links and location

This commit is contained in:
trisua 2025-08-31 23:41:12 -04:00
parent 5fafc8d7b9
commit 140a11ff72
18 changed files with 442 additions and 222 deletions

91
Cargo.lock generated
View file

@ -1603,6 +1603,7 @@ checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
"serde",
] ]
[[package]] [[package]]
@ -1667,6 +1668,15 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.15"
@ -2379,6 +2389,27 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.95" version = "1.0.95"
@ -2587,7 +2618,7 @@ dependencies = [
"built", "built",
"cfg-if", "cfg-if",
"interpolate_name", "interpolate_name",
"itertools", "itertools 0.12.1",
"libc", "libc",
"libfuzzer-sys", "libfuzzer-sys",
"log", "log",
@ -3028,6 +3059,51 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_valid"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b615bed66931a7a9809b273937adc8a402d038b1e509d027fcaf62f084d33d1"
dependencies = [
"indexmap",
"itertools 0.13.0",
"num-traits",
"once_cell",
"paste",
"regex",
"serde",
"serde_json",
"serde_valid_derive",
"serde_valid_literal",
"thiserror 1.0.69",
"unicode-segmentation",
]
[[package]]
name = "serde_valid_derive"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fa1a5a21ea5aab06d2e6a6b59837d450fb2be9695be97735a711edfbe79ea07"
dependencies = [
"itertools 0.13.0",
"paste",
"proc-macro-error2",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.104",
]
[[package]]
name = "serde_valid_literal"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd07331596ea967dccf9a35bde71ecd757490e09827b938a5c6226c648e3a25e"
dependencies = [
"paste",
"regex",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.6" version = "0.10.6"
@ -3214,6 +3290,12 @@ dependencies = [
"unicode-properties", "unicode-properties",
] ]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@ -3423,6 +3505,7 @@ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"serde_valid",
"tetratto-l10n 12.0.0", "tetratto-l10n 12.0.0",
"tetratto-shared 12.0.6", "tetratto-shared 12.0.6",
"tokio", "tokio",
@ -4027,6 +4110,12 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.2.1" version = "0.2.1"

View file

@ -175,6 +175,7 @@ version = "1.0.0"
"settings:tab.general" = "General" "settings:tab.general" = "General"
"settings:tab.account" = "Account" "settings:tab.account" = "Account"
"settings:tab.profile" = "Profile" "settings:tab.profile" = "Profile"
"settings:tab.experience" = "Experience"
"settings:tab.theme" = "Theme" "settings:tab.theme" = "Theme"
"settings:tab.sessions" = "Sessions" "settings:tab.sessions" = "Sessions"
"settings:tab.grants" = "Grants" "settings:tab.grants" = "Grants"

View file

@ -199,6 +199,34 @@ body:not(.use_system_font) {
& input, & input,
& textarea { & textarea {
font-variation-settings: "wght" 325; font-variation-settings: "wght" 325;
& h1 {
font-variation-settings: "wght" 600;
}
& h2 {
font-variation-settings: "wght" 550;
}
& h3 {
font-variation-settings: "wght" 500;
}
& h4 {
font-variation-settings: "wght" 450;
}
& h5 {
font-variation-settings: "wght" 400;
}
& h6 {
font-variation-settings: "wght" 350;
}
& b {
font-variation-settings: "wght" 500;
}
} }
} }

View file

@ -1256,6 +1256,11 @@ details summary::-webkit-details-marker {
display: none; display: none;
} }
details summary.button {
height: max-content;
justify-content: start;
}
details[open] > summary { details[open] > summary {
position: relative; position: relative;
color: var(--color-text-lowered) !important; color: var(--color-text-lowered) !important;
@ -1288,7 +1293,7 @@ details.accordion {
details.accordion summary { details.accordion summary {
background: var(--color-lowered); background: var(--color-lowered);
border-radius: var(--radius); border-radius: var(--radius);
padding: var(--pad-3) var(--pad-4); padding: var(--pad-3) var(--pad-4) !important;
margin: 0; margin: 0;
width: 100%; width: 100%;
user-select: none; user-select: none;

View file

@ -147,7 +147,7 @@
(button (button
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))))) (str (text "general:action.save")))))))
(div (div
("class" "card_nest") ("class" "card_nest")
("ui_ident" "danger_zone") ("ui_ident" "danger_zone")
@ -170,7 +170,7 @@
("onclick" "save_context()") ("onclick" "save_context()")
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}"))) (str (text "general:action.save"))))
(a (a
("href" "/community/{{ community.title }}") ("href" "/community/{{ community.title }}")
("class" "button secondary") ("class" "button secondary")
@ -273,7 +273,7 @@
("onclick" "update_user_role(document.getElementById('uid').value, document.getElementById('role').value)") ("onclick" "update_user_role(document.getElementById('uid').value, document.getElementById('role').value)")
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))) (str (text "general:action.save")))))
(div (div
("class" "card flex flex_col gap_2") ("class" "card flex flex_col gap_2")
("id" "permission_builder")))) ("id" "permission_builder"))))

View file

@ -95,7 +95,7 @@
(button (button
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}"))))) (str (text "general:action.save"))))))
(div (div
("class" "card_nest") ("class" "card_nest")
(div (div
@ -120,7 +120,7 @@
(button (button
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}"))))) (str (text "general:action.save"))))))
(div (div
("class" "card_nest") ("class" "card_nest")
(div (div
@ -145,7 +145,7 @@
(button (button
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}"))))) (str (text "general:action.save"))))))
(div (div
("class" "card_nest") ("class" "card_nest")
(div (div
@ -185,7 +185,7 @@
(button (button
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}"))))) (str (text "general:action.save"))))))
(div (div
("class" "card_nest") ("class" "card_nest")
(div (div

View file

@ -255,7 +255,7 @@
(button (button
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}"))))))) (str (text "general:action.save"))))))))
; users should also be able to manage the journal's sub directories here ; users should also be able to manage the journal's sub directories here
(details (details

View file

@ -354,7 +354,7 @@
(str (text "forge:tab.tickets")))) (str (text "forge:tab.tickets"))))
(text "{%- endmacro %}") (text "{%- endmacro %}")
(text "{% macro profile_settings_nav_options() -%}") (text "{% macro user_settings_nav_options() -%}")
(a (a
("data-tab-button" "account") ("data-tab-button" "account")
("class" "active") ("class" "active")
@ -368,6 +368,12 @@
(text "{{ icon \"user-round\" }}") (text "{{ icon \"user-round\" }}")
(span (span
(text "{{ text \"settings:tab.profile\" }}"))) (text "{{ text \"settings:tab.profile\" }}")))
(a
("data-tab-button" "experience")
("href" "#/experience")
(text "{{ icon \"settings-2\" }}")
(span
(text "{{ text \"settings:tab.experience\" }}")))
(a (a
("data-tab-button" "theme") ("data-tab-button" "theme")
("href" "#/theme") ("href" "#/theme")
@ -393,4 +399,10 @@
(text "{{ icon \"book-user\" }}") (text "{{ icon \"book-user\" }}")
(span (span
(text "{{ text \"settings:tab.close_friends\" }}"))) (text "{{ text \"settings:tab.close_friends\" }}")))
(a
("data-tab-button" "presets")
("href" "#/presets")
(icon (text "cooking-pot"))
(span
(str (text "settings:tab.presets"))))
(text "{%- endmacro %}") (text "{%- endmacro %}")

View file

@ -358,7 +358,7 @@
("onclick" "update_user_role(Number.parseInt(document.getElementById('role').value))") ("onclick" "update_user_role(Number.parseInt(document.getElementById('role').value))")
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))) (str (text "general:action.save")))))
(div (div
("class" "card lowered flex flex_col gap_2") ("class" "card lowered flex flex_col gap_2")
("id" "permission_builder"))) ("id" "permission_builder")))
@ -376,7 +376,7 @@
("onclick" "update_user_secondary_role(Number.parseInt(document.getElementById('secondary_role').value))") ("onclick" "update_user_secondary_role(Number.parseInt(document.getElementById('secondary_role').value))")
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))) (str (text "general:action.save")))))
(div (div
("class" "card lowered flex flex_col gap_2") ("class" "card lowered flex flex_col gap_2")
("id" "secondary_permission_builder"))) ("id" "secondary_permission_builder")))

View file

@ -148,7 +148,7 @@
("onclick" "save_context()") ("onclick" "save_context()")
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}"))) (str (text "general:action.save"))))
(script (script
(text "setTimeout(async () => { (text "setTimeout(async () => {
const ui = await ns(\"ui\"); const ui = await ns(\"ui\");
@ -286,7 +286,7 @@
("class" "flex gap_2") ("class" "flex gap_2")
(text "{{ components::emoji_picker(element_id=\"new_content\", render_dialog=false) }}") (text "{{ components::emoji_picker(element_id=\"new_content\", render_dialog=false) }}")
(button (button
(text "{{ text \"general:action.save\" }}"))))) (str (text "general:action.save"))))))
(script (script
(text "async function edit_post_from_form(e) { (text "async function edit_post_from_form(e) {
e.preventDefault(); e.preventDefault();

View file

@ -115,7 +115,7 @@
("class" "fade") ("class" "fade")
(text "{{ profile.username }}")))) (text "{{ profile.username }}"))))
(div (div
("class" "card flex flex_col items_center gap_2") ("class" "card flex flex_col items_center small gap_2")
("id" "social") ("id" "social")
(text "{% if profile.settings.status -%}") (text "{% if profile.settings.status -%}")
(p (p
@ -159,7 +159,20 @@
(div (div
("id" "bio") ("id" "bio")
("class" "card small no_p_margin") ("class" "card small no_p_margin")
(text "{{ profile.settings.biography|markdown|safe }}")) (text "{{ profile.settings.biography|markdown|safe }}")
(text "{% if profile.settings.location|length > 0 -%}")
(span ("class" "flex items_center gap_2") (icon (text "map-pin")) (text "{{ profile.settings.location }}"))
(text "{%- endif %}")
(text "{% for link in profile.settings.links -%}")
(span
("class" "flex items_center gap_2")
(icon (text "link"))
(a
("href" "{{ link[1] }}")
(text "{{ link[0] }}")))
(text "{%- endfor %}"))
(div (div
("class" "card flex flex_col gap_2") ("class" "card flex flex_col gap_2")
(text "{% if user -%}") (text "{% if user -%}")

View file

@ -7,7 +7,7 @@
; nav desktop ; nav desktop
(menu (menu
("class" "desktop col") ("class" "desktop col")
(text "{{ macros::profile_settings_nav_options() }}")) (text "{{ macros::user_settings_nav_options() }}"))
; content ; content
(main (main
@ -35,7 +35,7 @@
(span ("class" "current_tab_text") (text "account"))) (span ("class" "current_tab_text") (text "account")))
(div (div
("class" "inner left") ("class" "inner left")
(text "{{ macros::profile_settings_nav_options() }}")))) (text "{{ macros::user_settings_nav_options() }}"))))
; ... ; ...
(div (div
@ -43,81 +43,70 @@
("data-tab" "presets") ("data-tab" "presets")
(div (div
("class" "card lowered flex flex_col gap_2") ("class" "card lowered flex flex_col gap_2")
(a (p (text "Not sure where to start? Try some settings presets!"))
("href" "#/account") (details
("class" "button secondary") ("class" "w_full accordion")
(icon (text "arrow-left")) (summary
(span ("class" "button raised")
(str (text "general:action.back")))) (icon (text "rss"))
(div (text "Microblogging"))
("class" "card_nest")
(div (div
("class" "card flex items_center gap_2 small") ("class" "inner flex flex_col gap_2")
(icon (text "cooking-pot")) (p ("class" "fade") (text "Focus on yourself and your communities."))
(span (ul ("id" "preset_microblogging_ul"))
(str (text "settings:tab.presets")))) (button
("onclick" "apply_preset(PRESET_MICROBLOGGING)")
(icon (text "settings"))
(str (text "general:action.apply")))))
(details
("class" "w_full accordion")
(summary
("class" "button raised")
(icon (text "message-circle-heart"))
(text "Q&A"))
(div (div
("class" "card flex flex_col gap_2 secondary") ("class" "inner flex flex_col gap_2")
(p (text "Not sure where to start? Try some settings presets!")) (p ("class" "fade") (text "Just like Neospring!"))
(details (ul ("id" "preset_questions_ul"))
("class" "w_full accordion") (button
(summary ("onclick" "apply_preset(PRESET_QUESTIONS)")
(icon (text "rss")) (icon (text "settings"))
(text "Microblogging")) (str (text "general:action.apply")))))
(div (details
("class" "inner flex flex_col gap_2") ("class" "w_full accordion")
(p ("class" "fade") (text "Focus on yourself and your communities.")) (summary
(ul ("id" "preset_microblogging_ul")) ("class" "button raised")
(button (icon (text "key"))
("onclick" "apply_preset(PRESET_MICROBLOGGING)") (text "Private"))
(icon (text "settings"))
(str (text "general:action.apply")))))
(details (div
("class" "w_full accordion") ("class" "inner flex flex_col gap_2")
(summary (p ("class" "fade") (text "This preset allows you to keep your profile and posts hidden to people you aren't following."))
(icon (text "message-circle-heart")) (ul ("id" "preset_private_ul"))
(text "Q&A")) (button
("onclick" "apply_preset(PRESET_PRIVATE)")
(icon (text "settings"))
(str (text "general:action.apply")))))
(div (details
("class" "inner flex flex_col gap_2") ("class" "w_full accordion")
(p ("class" "fade") (text "Just like Neospring!")) (summary
(ul ("id" "preset_questions_ul")) ("class" "button raised")
(button (icon (text "eye-closed"))
("onclick" "apply_preset(PRESET_QUESTIONS)") (text "NSFW"))
(icon (text "settings"))
(str (text "general:action.apply")))))
(details (div
("class" "w_full accordion") ("class" "inner flex flex_col gap_2")
(summary (p ("class" "fade") (text "NSFW content is allowed if it is hidden from main timelines. This preset will help you do that quickly."))
(icon (text "key")) (ul ("id" "preset_nsfw_ul"))
(text "Private")) (button
("onclick" "apply_preset(PRESET_NSFW)")
(div (icon (text "settings"))
("class" "inner flex flex_col gap_2") (str (text "general:action.apply")))))))
(p ("class" "fade") (text "This preset allows you to keep your profile and posts hidden to people you aren't following."))
(ul ("id" "preset_private_ul"))
(button
("onclick" "apply_preset(PRESET_PRIVATE)")
(icon (text "settings"))
(str (text "general:action.apply")))))
(details
("class" "w_full accordion")
(summary
(icon (text "eye-closed"))
(text "NSFW"))
(div
("class" "inner flex flex_col gap_2")
(p ("class" "fade") (text "NSFW content is allowed if it is hidden from main timelines. This preset will help you do that quickly."))
(ul ("id" "preset_nsfw_ul"))
(button
("onclick" "apply_preset(PRESET_NSFW)")
(icon (text "settings"))
(str (text "general:action.apply")))))))))
(div (div
("class" "w_full flex flex_col gap_2") ("class" "w_full flex flex_col gap_2")
@ -180,61 +169,6 @@
(span (span
(text "{{ text \"settings:tab.billing\" }}")))) (text "{{ text \"settings:tab.billing\" }}"))))
(text "{%- endif %}") (text "{%- endif %}")
(div
("class" "card_nest")
("ui_ident" "home_timeline")
(div
("class" "card small")
(b
(text "Home timeline")))
(div
("class" "card")
(select
("onchange" "window.SETTING_SET_FUNCTIONS[0]('default_timeline', event.target.selectedOptions[0].value.startsWith('{') ? JSON.parse(event.target.selectedOptions[0].value) : event.target.selectedOptions[0].value)")
(option
("value" "MyCommunities")
("selected" "{% if home == '/' -%}true{% else %}false{%- endif %}")
(text "My communities"))
(option
("value" "MyCommunitiesQuestions")
("selected" "{% if home == '/questions' -%}true{% else %}false{%- endif %}")
(text "My communities (questions)"))
(option
("value" "PopularPosts")
("selected" "{% if home == '/popular' -%}true{% else %}false{%- endif %}")
(text "Popular"))
(option
("value" "PopularQuestions")
("selected" "{% if home == '/popular/questions' -%}true{% else %}false{%- endif %}")
(text "Popular (questions)"))
(option
("value" "FollowingPosts")
("selected" "{% if home == '/following' -%}true{% else %}false{%- endif %}")
(text "Following"))
(option
("value" "FollowingQuestions")
("selected" "{% if home == '/following/questions' -%}true{% else %}false{%- endif %}")
(text "Following (questions)"))
(option
("value" "AllPosts")
("selected" "{% if home == '/all' -%}true{% else %}false{%- endif %}")
(text "All"))
(option
("value" "AllQuestions")
("selected" "{% if home == '/all/questions' -%}true{% else %}false{%- endif %}")
(text "All (questions)"))
(text "{% for stack in stacks %}")
(text "<option
value='{\"Stack\":\"{{ stack.id }}\"}'
selected=\"{% if home is ending_with(stack.id|as_str) -%}true{% else %}false{%- endif %}\"
>
{{ stack.name }} (stack)
</option>")
(text "{% endfor %}"))
(span
("class" "fade")
(text "This represents the timeline the home button takes you to."))))
(div (div
("class" "card_nest desktop") ("class" "card_nest desktop")
("ui_ident" "notifications") ("ui_ident" "notifications")
@ -282,7 +216,7 @@
(button (button
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))))) (str (text "general:action.save")))))))
(div (div
("class" "card_nest") ("class" "card_nest")
("ui_ident" "delete_account") ("ui_ident" "delete_account")
@ -337,7 +271,7 @@
("id" "save_button") ("id" "save_button")
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))) (str (text "general:action.save")))))
(div (div
("class" "w_full flex flex_col gap_2 hidden") ("class" "w_full flex flex_col gap_2 hidden")
("data-tab" "account/security") ("data-tab" "account/security")
@ -444,7 +378,7 @@
(button (button
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}"))))))))) (str (text "general:action.save"))))))))))
(div (div
("class" "w_full flex flex_col gap_2 hidden") ("class" "w_full flex flex_col gap_2 hidden")
("data-tab" "account/following") ("data-tab" "account/following")
@ -991,13 +925,13 @@
(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."))))
(div (div
("class" "card_nest") ("class" "card_nest")
("ui_ident" "default_profile_page") ("ui_ident" "default_profile_page")
(div (div
("class" "card small") ("class" "card small")
(b (b (text "Default profile tab")))
(text "Default profile tab")))
(div (div
("class" "card") ("class" "card")
(select (select
@ -1013,10 +947,36 @@
(span (span
("class" "fade") ("class" "fade")
(text "This represents the timeline that is shown on your profile by default.")))) (text "This represents the timeline that is shown on your profile by default."))))
(div
("class" "card_nest")
("ui_ident" "user_links")
(div
("class" "card small")
(b (text "My links")))
(div
("class" "card flex flex_col gap_2")
(button
("onclick" "add_link()")
(icon (text "plus"))
(text "Add link"))
(ul ("id" "user_links")))))
(button
("onclick" "save_settings()")
("id" "save_button")
(icon (text "check"))
(span
(str (text "general:action.save")))))
(div
("class" "w_full hidden flex flex_col gap_2")
("data-tab" "experience")
(div
("class" "card lowered flex flex_col gap_2")
("id" "experience_settings")
(div (div
("class" "flex flex_col gap_2") ("class" "flex flex_col gap_2")
("ui_ident" "show_presets") ("ui_ident" "show_presets")
(hr ("class" "margin"))
(div (div
("class" "card_nest") ("class" "card_nest")
(div (div
@ -1028,13 +988,67 @@
(p (p
(text "Quickly set up your account with ") (text "Quickly set up your account with ")
(a ("href" "/settings#/presets") (text "settings presets")) (a ("href" "/settings#/presets") (text "settings presets"))
(text "!")))))) (text "!")))))
(div
("class" "card_nest")
("ui_ident" "home_timeline")
(div
("class" "card small")
(b
(text "Home timeline")))
(div
("class" "card")
(select
("onchange" "window.SETTING_SET_FUNCTIONS[0]('default_timeline', event.target.selectedOptions[0].value.startsWith('{') ? JSON.parse(event.target.selectedOptions[0].value) : event.target.selectedOptions[0].value)")
(option
("value" "MyCommunities")
("selected" "{% if home == '/' -%}true{% else %}false{%- endif %}")
(text "My communities"))
(option
("value" "MyCommunitiesQuestions")
("selected" "{% if home == '/questions' -%}true{% else %}false{%- endif %}")
(text "My communities (questions)"))
(option
("value" "PopularPosts")
("selected" "{% if home == '/popular' -%}true{% else %}false{%- endif %}")
(text "Popular"))
(option
("value" "PopularQuestions")
("selected" "{% if home == '/popular/questions' -%}true{% else %}false{%- endif %}")
(text "Popular (questions)"))
(option
("value" "FollowingPosts")
("selected" "{% if home == '/following' -%}true{% else %}false{%- endif %}")
(text "Following"))
(option
("value" "FollowingQuestions")
("selected" "{% if home == '/following/questions' -%}true{% else %}false{%- endif %}")
(text "Following (questions)"))
(option
("value" "AllPosts")
("selected" "{% if home == '/all' -%}true{% else %}false{%- endif %}")
(text "All"))
(option
("value" "AllQuestions")
("selected" "{% if home == '/all/questions' -%}true{% else %}false{%- endif %}")
(text "All (questions)"))
(text "{% for stack in stacks %}")
(text "<option
value='{\"Stack\":\"{{ stack.id }}\"}'
selected=\"{% if home is ending_with(stack.id|as_str) -%}true{% else %}false{%- endif %}\"
>
{{ stack.name }} (stack)
</option>")
(text "{% endfor %}"))
(span
("class" "fade")
(text "This represents the timeline the home button takes you to.")))))
(button (button
("onclick" "save_settings()") ("onclick" "save_settings()")
("id" "save_button") ("id" "save_button")
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))) (str (text "general:action.save")))))
(div (div
("class" "card w_full lowered hidden flex flex_col gap_2") ("class" "card w_full lowered hidden flex flex_col gap_2")
("data-tab" "sessions") ("data-tab" "sessions")
@ -1193,7 +1207,7 @@
("id" "save_button") ("id" "save_button")
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))) (str (text "general:action.save")))))
(div (div
("class" "card w_full lowered hidden flex flex_col gap_2") ("class" "card w_full lowered hidden flex flex_col gap_2")
("data-tab" "grants") ("data-tab" "grants")
@ -1595,7 +1609,7 @@
`data:image/png;base64,${qr}`; `data:image/png;base64,${qr}`;
document.getElementById( document.getElementById(
\"totp_recovery_codes\", \"totp_recovery_codes\",
).innerText = recovery_codes.join(\"\n\"); ).innerText = recovery_codes.join(\"\\n\");
document.getElementById(\"totp_stuff\").style.display = document.getElementById(\"totp_stuff\").style.display =
\"contents\"; \"contents\";
@ -1787,12 +1801,13 @@
document.getElementById(\"account_settings\"); document.getElementById(\"account_settings\");
const profile_settings = const profile_settings =
document.getElementById(\"profile_settings\"); document.getElementById(\"profile_settings\");
const experience_settings =
document.getElementById(\"experience_settings\");
const theme_settings = document.getElementById(\"theme_settings\"); const theme_settings = document.getElementById(\"theme_settings\");
ui.refresh_container(account_settings, [ ui.refresh_container(account_settings, [
\"supporter_ad\", \"supporter_ad\",
\"account_settings_tabs\", \"account_settings_tabs\",
\"home_timeline\",
\"notifications\", \"notifications\",
\"change_username\", \"change_username\",
\"delete_account\", \"delete_account\",
@ -1802,6 +1817,11 @@
\"change_avatar\", \"change_avatar\",
\"change_banner\", \"change_banner\",
\"default_profile_page\", \"default_profile_page\",
\"user_links\",
]);
ui.refresh_container(experience_settings, [
\"supporter_ad\",
\"home_timeline\",
\"show_presets\", \"show_presets\",
]); ]);
ui.refresh_container(theme_settings, [ ui.refresh_container(theme_settings, [
@ -1814,7 +1834,7 @@
]); ]);
ui.generate_settings_ui( ui.generate_settings_ui(
account_settings, profile_settings,
[ [
[ [
[\"display_name\", \"Display name\"], [\"display_name\", \"Display name\"],
@ -1835,50 +1855,23 @@
'<span class=\"fade\">This biography is only shown to users you are not following while your account is private.</span>', '<span class=\"fade\">This biography is only shown to users you are not following while your account is private.</span>',
}, },
], ],
[
[\"location\", \"Location\"],
\"{{ profile.settings.location }}\",
\"input\",
],
[[\"status\", \"Status\"], settings.status, \"textarea\"], [[\"status\", \"Status\"], settings.status, \"textarea\"],
[ [
[\"warning\", \"Profile warning\"], [\"warning\", \"Profile warning\"],
settings.warning, settings.warning,
\"textarea\", \"textarea\",
], ],
[[\"muted\", \"Muted phrases\"], settings.muted.join(\"\\n\"), \"textarea\", {
embed_html:
'<span class=\"fade\">Muted phrases should all be on new lines.</span>',
}],
[[], \"Accessibility\", \"title\"],
[
[\"large_text\", \"Increase UI text size\"],
\"{{ profile.settings.large_text }}\",
\"checkbox\",
],
[
[\"use_system_font\", \"Always use system font instead\"],
\"{{ profile.settings.use_system_font }}\",
\"checkbox\",
],
[
[\"paged_timelines\", \"Make timelines paged instead of infinitely scrolled\"],
\"{{ profile.settings.paged_timelines }}\",
\"checkbox\",
],
[
[\"auto_clear_notifs\", \"Automatically clear all notifications when you open the notifications page\"],
\"{{ profile.settings.auto_clear_notifs }}\",
\"checkbox\",
],
], ],
settings, settings,
{
muted: (new_muted) => {
settings.muted = new_muted
.split(\"\\n\")
.map((t) => t.trim());
},
},
); );
ui.generate_settings_ui( ui.generate_settings_ui(
profile_settings, experience_settings,
[ [
[[], \"Privacy\", \"title\"], [[], \"Privacy\", \"title\"],
[ [
@ -1988,6 +1981,10 @@
\"{{ profile.settings.hide_username_badges }}\", \"{{ profile.settings.hide_username_badges }}\",
\"checkbox\", \"checkbox\",
], ],
[[\"muted\", \"Muted phrases\"], settings.muted.join(\"\\n\"), \"textarea\", {
embed_html:
'<span class=\"fade\">Muted phrases should all be on new lines.</span>',
}],
[[], \"Questions\", \"title\"], [[], \"Questions\", \"title\"],
[ [
[ [
@ -2074,8 +2071,36 @@
\"{{ profile.settings.disable_achievements }}\", \"{{ profile.settings.disable_achievements }}\",
\"checkbox\", \"checkbox\",
], ],
[[], \"Accessibility\", \"title\"],
[
[\"large_text\", \"Increase UI text size\"],
\"{{ profile.settings.large_text }}\",
\"checkbox\",
],
[
[\"use_system_font\", \"Always use system font instead\"],
\"{{ profile.settings.use_system_font }}\",
\"checkbox\",
],
[
[\"paged_timelines\", \"Make timelines paged instead of infinitely scrolled\"],
\"{{ profile.settings.paged_timelines }}\",
\"checkbox\",
],
[
[\"auto_clear_notifs\", \"Automatically clear all notifications when you open the notifications page\"],
\"{{ profile.settings.auto_clear_notifs }}\",
\"checkbox\",
],
], ],
settings, settings,
{
muted: (new_muted) => {
settings.muted = new_muted
.split(\"\\n\")
.map((t) => t.trim());
},
},
); );
const can_use_custom_css = const can_use_custom_css =
@ -2351,5 +2376,40 @@
anchor.click(); anchor.click();
anchor.remove(); anchor.remove();
}; };
// links
function render_links() {
document.getElementById(\"user_links\").innerHTML = \"\";
let i = 0;
for (const link of settings.links) {
document.getElementById(\"user_links\").innerHTML += `<li id=\"link_${i}\"><span>${link[0]}</span> (<a href=\"${link[1]}\">${link[1]}</a>) (<a class=\"red\" href=\"javascript:remove_link(${i})\">delete</a>)</li>`;
i += 1;
}
}
globalThis.add_link = async () => {
const label = await trigger(\"atto::prompt\", [\"Link label:\"]);
if (!label) {
return;
}
const url = await trigger(\"atto::prompt\", [\"Link URL:\"]);
if (!url) {
return;
}
settings.links.push([label, url]);
render_links();
}
globalThis.remove_link = (idx) => {
settings.links.splice(idx, 1);
document.getElementById(`link_${idx}`).remove();
}
render_links();
});")))) });"))))
(text "{% endblock %}") (text "{% endblock %}")

View file

@ -9,7 +9,7 @@
("class" "card_nest") ("class" "card_nest")
(div (div
("class" "card small flex items_center gap_2") ("class" "card small flex items_center gap_2")
(text "{{ components::avatar(id=add_user.username, size=\"24px\") }}") (text "{{ components::avatar(id=add_user.id, size=\"24px\") }}")
(text "{{ components::full_username(user=add_user) }}")) (text "{{ components::full_username(user=add_user) }}"))
(div (div
("class" "card flex flex_col gap_2") ("class" "card flex flex_col gap_2")

View file

@ -117,7 +117,7 @@
(button (button
(icon (text "check")) (icon (text "check"))
(span (span
(text "{{ text \"general:action.save\" }}")))))) (str (text "general:action.save")))))))
(text "{% if not stack.is_locked -%}") (text "{% if not stack.is_locked -%}")
(div (div
("class" "card_nest") ("class" "card_nest")

View file

@ -153,28 +153,8 @@ pub async fn update_user_settings_request(
} }
// check lengths // check lengths
if req.display_name.len() > 32 { if let Err(e) = req.verify_values() {
return Json(Error::DataTooLong("display name".to_string()).into()); return Json(e.into());
}
if req.warning.len() > 2048 {
return Json(Error::DataTooLong("warning".to_string()).into());
}
if req.status.len() > 256 {
return Json(Error::DataTooLong("status".to_string()).into());
}
if req.biography.len() > 4096 {
return Json(Error::DataTooLong("warning".to_string()).into());
}
if req.mail_signature.len() > 2048 {
return Json(Error::DataTooLong("mail signature".to_string()).into());
}
if req.forum_signature.len() > 2048 {
return Json(Error::DataTooLong("forum signature".to_string()).into());
} }
// check percentage themes // check percentage themes

View file

@ -50,3 +50,4 @@ oiseau = { version = "0.1.2", default-features = false, features = [
paste = { version = "1.0.15", optional = true } paste = { version = "1.0.15", optional = true }
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
buckets-core = "1.0.4" buckets-core = "1.0.4"
serde_valid = "1.0.5"

View file

@ -709,15 +709,17 @@ impl DataManager {
self.cache_clear_user(&other_user).await; self.cache_clear_user(&other_user).await;
// create audit log entry // create audit log entry (if we aren't the user that is being updated)
self.create_audit_log_entry(AuditLogEntry::new( if user.id != other_user.id {
user.id, self.create_audit_log_entry(AuditLogEntry::new(
format!( user.id,
"invoked `update_user_is_deactivated` with x value `{}` and y value `{}`", format!(
other_user.id, x "invoked `update_user_is_deactivated` with x value `{}` and y value `{}`",
), other_user.id, x
)) ),
.await?; ))
.await?;
}
// ... // ...
Ok(()) Ok(())

View file

@ -1,4 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::model::{Error, Result};
use super::{ use super::{
oauth::AuthGrant, oauth::AuthGrant,
permissions::{FinePermission, SecondaryPermission}, permissions::{FinePermission, SecondaryPermission},
@ -10,6 +12,7 @@ use tetratto_shared::{
snow::Snowflake, snow::Snowflake,
unix_epoch_timestamp, unix_epoch_timestamp,
}; };
use serde_valid::Validate;
/// `(ip, token, creation timestamp)` /// `(ip, token, creation timestamp)`
pub type Token = (String, String, usize); pub type Token = (String, String, usize);
@ -187,13 +190,16 @@ impl Default for DefaultProfileTabChoice {
} }
} }
#[derive(Clone, Debug, Serialize, Deserialize, Default)] #[derive(Clone, Debug, Serialize, Deserialize, Default, Validate)]
pub struct UserSettings { pub struct UserSettings {
#[serde(default)] #[serde(default)]
#[validate(max_length = 32)]
pub display_name: String, pub display_name: String,
#[serde(default)] #[serde(default)]
#[validate(max_length = 4096)]
pub biography: String, pub biography: String,
#[serde(default)] #[serde(default)]
#[validate(max_length = 2048)]
pub warning: String, pub warning: String,
#[serde(default)] #[serde(default)]
pub private_profile: bool, pub private_profile: bool,
@ -303,6 +309,7 @@ pub struct UserSettings {
pub private_mails: bool, pub private_mails: bool,
/// The user's status. Shows over connection info. /// The user's status. Shows over connection info.
#[serde(default)] #[serde(default)]
#[validate(max_length = 256)]
pub status: String, pub status: String,
/// The mime type of the user's banner. /// The mime type of the user's banner.
#[serde(default = "mime_avif")] #[serde(default = "mime_avif")]
@ -365,9 +372,11 @@ pub struct UserSettings {
pub hide_social_follows: bool, pub hide_social_follows: bool,
/// The signature automatically attached to new mail letters. /// The signature automatically attached to new mail letters.
#[serde(default)] #[serde(default)]
#[validate(max_length = 2048)]
pub mail_signature: String, pub mail_signature: String,
/// The signature automatically attached to new forum posts. /// The signature automatically attached to new forum posts.
#[serde(default)] #[serde(default)]
#[validate(max_length = 2048)]
pub forum_signature: String, pub forum_signature: String,
/// If coin transfer requests are disabled. /// If coin transfer requests are disabled.
#[serde(default)] #[serde(default)]
@ -381,6 +390,26 @@ pub struct UserSettings {
/// If the user's system font is always used over Lexend. /// If the user's system font is always used over Lexend.
#[serde(default)] #[serde(default)]
pub use_system_font: bool, pub use_system_font: bool,
/// The user's location. This isn't actually verified or anything, so it can really
/// be whatever the user wants.
#[serde(default)]
#[validate(max_length = 128)]
pub location: String,
/// External links for the user's other profiles on other websites.
#[serde(default)]
#[validate(max_items = 5)]
#[validate(unique_items)]
pub links: Vec<(String, String)>,
}
impl UserSettings {
pub fn verify_values(&self) -> Result<()> {
if let Err(e) = self.validate() {
return Err(Error::MiscError(e.to_string()));
}
Ok(())
}
} }
fn mime_avif() -> String { fn mime_avif() -> String {