2025-06-01 12:25:33 -04:00
( text "{% extends \"root.html\" %} {% block head %}" )
( title
( text "Settings - {{ config.name }}" ) )
( text "{% endblock %} {% block body %} {{ macros::nav() }}" )
( main
( "class" "flex flex-col gap-2" )
( text "{% if profile.id != user.id -%}" )
( div
( "class" "card w-full red flex gap-2 items-center" )
( text "{{ icon \"skull\" }}" )
( b
( text "Editing other user's settings! Please be careful." ) ) )
( text "{%- endif %}" )
2025-06-13 10:32:09 -04:00
; nav
2025-06-01 12:25:33 -04:00
( div
2025-06-13 10:32:09 -04:00
( "class" "mobile_nav mobile" )
; primary nav
( div
( "class" "dropdown" )
( "style" "width: max-content" )
( button
2025-06-22 18:53:02 -04:00
( "class" "raised small" )
2025-06-13 10:32:09 -04:00
( "onclick" "trigger('atto::hooks::dropdown', [event])" )
( "exclude" "dropdown" )
( icon ( text "sliders-horizontal" ) )
( span ( "class" "current_tab_text" ) ( text "account" ) ) )
( div
( "class" "inner left" )
( text "{{ macros::profile_settings_nav_options() }}" ) ) ) )
; nav desktop
( div
( "class" "desktop pillmenu" )
( text "{{ macros::profile_settings_nav_options() }}" ) )
; ...
2025-06-01 12:25:33 -04:00
( div
( "class" "w-full flex flex-col gap-2" )
( "data-tab" "account" )
( div
2025-06-12 13:53:23 -04:00
( "class" "card lowered flex flex-col gap-2" )
2025-06-01 12:25:33 -04:00
( "id" "account_settings" )
( div
( "class" "pillmenu" )
( "ui_ident" "account_settings_tabs" )
( a
( "data-tab-button" "account/security" )
( "href" "#/account/security" )
( text "{{ icon \"user-lock\" }}" )
( span
( text "{{ text \"settings:tab.security\" }}" ) ) )
( a
( "data-tab-button" "account/following" )
( "href" "#/account/following" )
( text "{{ icon \"rss\" }}" )
( span
( text "{{ text \"auth:label.following\" }}" ) ) )
( a
( "data-tab-button" "account/blocks" )
( "href" "#/account/blocks" )
( text "{{ icon \"shield\" }}" )
( span
2025-06-22 13:03:02 -04:00
( text "{{ text \"settings:tab.blocks\" }}" ) ) ) )
( text "{% if config.stripe -%}" )
; stripe menu
( div
( "class" "pillmenu" )
( "ui_ident" "account_settings_tabs" )
2025-06-01 12:25:33 -04:00
( a
( "data-tab-button" "account/uploads" )
( "href" "?page=0#/account/uploads" )
( text "{{ icon \"image-up\" }}" )
( span
( text "{{ text \"settings:tab.uploads\" }}" ) ) )
2025-06-22 13:03:02 -04:00
( text "{% if config.security.enable_invite_codes -%}" )
( a
( "data-tab-button" "account/invites" )
2025-06-23 13:48:16 -04:00
( "href" "?page=0#/account/invites" )
2025-06-22 13:03:02 -04:00
( text "{{ icon \"ticket\" }}" )
( span
( text "{{ text \"settings:tab.invites\" }}" ) ) )
( text "{%- endif %}" )
2025-06-01 12:25:33 -04:00
( a
( "data-tab-button" "account/billing" )
( "href" "#/account/billing" )
( text "{{ icon \"credit-card\" }}" )
( span
2025-06-22 13:03:02 -04:00
( text "{{ text \"settings:tab.billing\" }}" ) ) ) )
( text "{%- endif %}" )
2025-06-01 12:25:33 -04:00
( div
( "class" "card-nest" )
( "ui_ident" "home_timeline" )
( div
( "class" "card small" )
( b
( text "Home timeline" ) ) )
( div
( "class" "card" )
( select
2025-06-12 13:53:23 -04:00
( "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)" )
2025-06-01 12:25:33 -04:00
( 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 %}" )
( option
( "value" "{\\\"Stack\\\":\\\"{{ stack.id }}\\\"}" )
( "selected" "{% if home is ending_with(stack.id|as_str) -%}true{% else %}false{%- endif %}" )
( text "{{ stack.name }} (stack)" ) )
( text "{% endfor %}" ) )
( span
( "class" "fade" )
( text " This represents the timeline the home button takes you
to. " ) ) ) )
( div
( "class" "card-nest desktop" )
( "ui_ident" "notifications" )
( div
( "class" "card small" )
( b
( text "Notifications" ) ) )
( div
( "class" "card flex flex-col gap-2" )
( button
( "id" "notifications_button" ) )
( span
( "class" "fade" )
( text " Notifications require you to keep {{ config.name }}
open in your browser for real-time updates. This setting
does not sync across browsers. " ) ) ) )
( script
( text " setTimeout ( ( ) => {
trigger ( \"me::notifications_button\", [
document.getElementById ( \"notifications_button\" ) ,
] ) ;
}, 150 ) ;"))
( div
( "class" "card-nest" )
( "ui_ident" "change_username" )
( div
( "class" "card small" )
( b
( text "{{ text \"settings:label.change_username\" }}" ) ) )
( form
( "class" "card flex flex-col gap-2" )
( "onsubmit" "change_username(event)" )
( div
( "class" "flex flex-col gap-1" )
( label
( "for" "new_username" )
( text "{{ text \"settings:label.new_username\" }}" ) )
( input
( "type" "text" )
( "name" "new_username" )
( "id" "new_username" )
( "placeholder" "new_username" )
( "required" "" )
( "minlength" "2" ) ) )
( button
( "class" "primary" )
( text "{{ icon \"check\" }}" )
( span
( text "{{ text \"general:action.save\" }}" ) ) ) ) ) )
( div
( "class" "card-nest" )
( "ui_ident" "delete_account" )
( div
( "class" "card small flex items-center gap-2 red" )
( text "{{ icon \"skull\" }}" )
( b
( text "{{ text \"settings:label.delete_account\" }}" ) ) )
( form
( "class" "card flex flex-col gap-2" )
( "onsubmit" "delete_account(event)" )
( div
( "class" "flex flex-col gap-1" )
( label
( "for" "current_password" )
( text "{{ text \"settings:label.current_password\" }}" ) )
( input
( "type" "password" )
( "name" "current_password" )
( "id" "current_password" )
( "placeholder" "current_password" )
( "required" "" )
( "minlength" "6" )
( "autocomplete" "off" ) ) )
( button
( "class" "primary" )
( text "{{ icon \"trash\" }}" )
( span
( text "{{ text \"general:action.delete\" }}" ) ) ) ) )
( button
( "onclick" "save_settings()" )
( "id" "save_button" )
( text "{{ icon \"check\" }}" )
( span
( text "{{ text \"general:action.save\" }}" ) ) ) )
( div
( "class" "w-full flex flex-col gap-2 hidden" )
( "data-tab" "account/security" )
( div
2025-06-12 13:53:23 -04:00
( "class" "card lowered flex flex-col gap-2" )
2025-06-01 12:25:33 -04:00
( a
( "href" "#/account" )
( "class" "button secondary" )
( text "{{ icon \"arrow-left\" }}" )
( span
( text "{{ text \"general:action.back\" }}" ) ) )
( div
( "class" "card-nest" )
( div
( "class" "card flex items-center gap-2 small" )
( text "{{ icon \"user-lock\" }}" )
( span
( text "{{ text \"settings:tab.security\" }}" ) ) )
( div
( "class" "card flex flex-col gap-2 secondary" )
( div
( "class" "card-nest" )
( "ui_ident" "two_factor_authentication" )
( div
( "class" "card small" )
( b
( text "{{ text \"settings:label.two_factor_authentication\" }}" ) ) )
( div
( "class" "card flex flex-col gap-2" )
( text "{% if profile.totp|length == 0 -%}" )
( div
( "id" "totp_stuff" )
( "style" "display: none" )
( span
( text " Scan this QR code in a TOTP authenticator
app ( like Google Authenticator ) : " ) )
( img
( "id" "totp_qr" )
( "style" "max-width: 250px" ) )
( span
( text "TOTP secret (do NOT share):" ) )
( pre
( "id" "totp_secret" ) )
( span
( text " Recovery codes ( STORE SAFELY, these can
only be viewed once ) : " ) )
( pre
( "id" "totp_recovery_codes" ) ) )
( button
2025-06-12 13:53:23 -04:00
( "class" "lowered green" )
2025-06-01 12:25:33 -04:00
( "onclick" "enable_totp(event)" )
( text "Enable TOTP 2FA" ) )
( text "{% else %}" )
( pre
( "id" "totp_recovery_codes" )
( "style" "display: none" ) )
( div
( "class" "flex gap-2 flex-wrap" )
( button
2025-06-12 13:53:23 -04:00
( "class" "lowered red" )
2025-06-01 12:25:33 -04:00
( "onclick" "refresh_totp_codes(event)" )
( text "Refresh recovery codes" ) )
( button
2025-06-12 13:53:23 -04:00
( "class" "lowered red" )
2025-06-01 12:25:33 -04:00
( "onclick" "disable_totp(event)" )
( text "Disable TOTP 2FA" ) ) )
( text "{%- endif %}" ) ) )
( div
( "class" "card-nest" )
( "ui_ident" "change_password" )
( div
( "class" "card small" )
( b
( text "{{ text \"settings:label.change_password\" }}" ) ) )
( form
( "class" "card flex flex-col gap-2" )
( "onsubmit" "change_password(event)" )
( div
( "class" "flex flex-col gap-1" )
( label
( "for" "current_password" )
( text "{{ text \"settings:label.current_password\" }}" ) )
( input
( "type" "password" )
( "name" "current_password" )
( "id" "current_password" )
( "placeholder" "current_password" )
( "required" "" )
( "minlength" "6" )
( "autocomplete" "off" ) ) )
( div
( "class" "flex flex-col gap-1" )
( label
( "for" "new_password" )
( text "{{ text \"settings:label.new_password\" }}" ) )
( input
( "type" "password" )
( "name" "new_password" )
( "id" "new_password" )
( "placeholder" "new_password" )
( "required" "" )
( "minlength" "6" )
( "autocomplete" "off" ) ) )
( button
( "class" "primary" )
( text "{{ icon \"check\" }}" )
( span
( text "{{ text \"general:action.save\" }}" ) ) ) ) ) ) ) ) )
( div
( "class" "w-full flex flex-col gap-2 hidden" )
( "data-tab" "account/following" )
( div
2025-06-12 13:53:23 -04:00
( "class" "card lowered flex flex-col gap-2" )
2025-06-01 12:25:33 -04:00
( a
( "href" "#/account" )
( "class" "button secondary" )
( text "{{ icon \"arrow-left\" }}" )
( span
( text "{{ text \"general:action.back\" }}" ) ) )
( div
( "class" "card-nest" )
( div
( "class" "card flex items-center gap-2 small" )
( text "{{ icon \"rss\" }}" )
( span
( text "{{ text \"auth:label.following\" }}" ) ) )
( div
( "class" "card flex flex-col gap-2" )
( text "{% for userfollow in following %} {% set user = userfollow[1] %}" )
( div
( "class" "card secondary flex flex-wrap gap-2 items-center justify-between" )
( div
( "class" "flex gap-2" )
( text "{{ components::avatar(username=user.username) }} {{ components::full_username(user=user) }}" ) )
( div
( "class" "flex gap-2" )
( button
2025-06-12 13:53:23 -04:00
( "class" "lowered red small" )
2025-06-01 12:25:33 -04:00
( "onclick" "toggle_follow_user('{{ user.id }}')" )
( text "{{ icon \"user-minus\" }}" )
( span
( text "{{ text \"auth:action.unfollow\" }}" ) ) )
( a
( "href" "/@{{ user.username }}" )
2025-06-12 13:53:23 -04:00
( "class" "button lowered small" )
2025-06-01 12:25:33 -04:00
( text "{{ icon \"external-link\" }}" )
( span
( text "{{ text \"requests:action.view_profile\" }}" ) ) ) ) )
( text "{% endfor %}" ) ) ) )
( script
( text " globalThis.toggle_follow_user = async ( uid ) => {
await trigger ( \"atto::debounce\", [\"users::follow\"] ) ;
fetch ( ` /api/v1/auth/user/${uid}/follow ` , {
method: \"POST\",
} )
. then ( ( res ) => res.json ( ) )
. then ( ( res ) => {
trigger ( \"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
] ) ;
} ) ;
} ;")))
( div
( "class" "w-full flex flex-col gap-2 hidden" )
( "data-tab" "account/blocks" )
( div
2025-06-12 13:53:23 -04:00
( "class" "card lowered flex flex-col gap-2" )
2025-06-01 12:25:33 -04:00
( a
( "href" "#/account" )
( "class" "button secondary" )
( text "{{ icon \"arrow-left\" }}" )
( span
( text "{{ text \"general:action.back\" }}" ) ) )
2025-06-15 11:52:44 -04:00
; stack blocks
( div
( "class" "card-nest" )
( div
( "class" "card flex items-center gap-2 small" )
( text "{{ icon \"layers\" }}" )
( span
( text "{{ text \"stacks:link.stacks\" }}" ) ) )
( div
( "class" "card flex flex-col gap-2" )
( text "{% for stack in stackblocks %}" )
( text "{{ components::stack_listing(stack=stack) }}" )
( text "{% endfor %}" ) ) )
; user blocks
2025-06-01 12:25:33 -04:00
( div
( "class" "card-nest" )
( div
( "class" "card flex items-center gap-2 small" )
( text "{{ icon \"users-round\" }}" )
( span
( text "{{ text \"settings:label.users\" }}" ) ) )
( div
( "class" "card flex flex-col gap-2" )
( text "{% for user in blocks %}" )
( div
( "class" "card secondary flex flex-wrap gap-2 items-center justify-between" )
( div
( "class" "flex gap-2" )
( text "{{ components::avatar(username=user.username) }} {{ components::full_username(user=user) }}" ) )
2025-06-22 21:07:35 -04:00
( div
( "class" "flex gap-2" )
( a
( "href" "/stacks/add_user/{{ user.id }}" )
( "target" "_blank" )
( "class" "button lowered small" )
( icon ( text "plus" ) )
( span ( str ( text "settings:label.add_to_stack" ) ) ) )
( a
( "href" "/@{{ user.username }}" )
( "class" "button lowered small" )
( icon ( text "external-link" ) )
( span ( str ( text "requests:action.view_profile" ) ) ) ) ) )
2025-06-28 13:15:37 -04:00
( text "{% endfor %}" ) ) )
; ip blocks
( div
( "class" "card-nest" )
( div
( "class" "card flex items-center gap-2 small" )
( text "{{ icon \"wifi\" }}" )
( span
( text "{{ text \"settings:label.ips\" }}" ) ) )
( div
( "class" "card flex flex-col gap-2" )
( text "{% for ip in ipblocks %}" )
( div
( "class" "card secondary flex flex-wrap gap-2 items-center justify-between" )
( span
( text "Block from: " ) ( span ( "class" "date" ) ( text "{{ ip.created }}" ) ) )
( div
( "class" "flex gap-2" )
( button
( "onclick" "trigger('me::remove_ip_block', ['{{ ip.id }}'])" )
( "class" "lowered small red" )
( icon ( text "x" ) )
( span ( str ( text "auth:action.unblock" ) ) ) ) ) )
2025-06-01 12:25:33 -04:00
( text "{% endfor %}" ) ) ) ) )
( div
( "class" "w-full flex flex-col gap-2 hidden" )
( "data-tab" "account/uploads" )
( div
2025-06-12 13:53:23 -04:00
( "class" "card lowered flex flex-col gap-2" )
2025-06-01 12:25:33 -04:00
( a
( "href" "#/account" )
( "class" "button secondary" )
( text "{{ icon \"arrow-left\" }}" )
( span
( text "{{ text \"general:action.back\" }}" ) ) )
( div
( "class" "card-nest" )
( div
( "class" "card flex items-center gap-2 small" )
( text "{{ icon \"image-up\" }}" )
( span
( text "{{ text \"settings:tab.uploads\" }}" ) ) )
( div
( "class" "card flex flex-col gap-2 secondary" )
( text "{{ components::supporter_ad(body=\"Become a supporter to upload images directly to posts!\") }} {% for upload in uploads %}" )
( div
( "class" "card flex flex-wrap gap-2 items-center justify-between" )
( div
( "class" "flex gap-2 items-center" )
( "onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload.id }}'])" )
( "style" "cursor: pointer" )
( text "{{ icon \"file-image\" }}" )
( b
( span
( "class" "date" )
( text "{{ upload.created }}" ) )
( text "({{ upload.what }})" ) ) )
( div
( "class" "flex gap-2" )
( button
2025-06-12 13:53:23 -04:00
( "class" "lowered small" )
2025-06-01 12:25:33 -04:00
( "onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload.id }}'])" )
( text "{{ icon \"view\" }}" )
( span
( text "{{ text \"general:action.view\" }}" ) ) )
( button
2025-06-12 13:53:23 -04:00
( "class" "lowered small red" )
2025-06-01 12:25:33 -04:00
( "onclick" "remove_upload('{{ upload.id }}')" )
( text "{{ icon \"x\" }}" )
( span
( text "{{ text \"stacks:label.remove\" }}" ) ) ) ) )
( text "{% endfor %} {{ components::pagination(page=page, items=uploads|length, key=\"#/account/uploads\") }}" )
( script
( text " globalThis.remove_upload = async ( id ) => {
if (
! ( await trigger ( \"atto::confirm\", [
\"Are you sure you would like to do this? This action is permanent.\",
] ) )
) {
return ;
}
fetch ( ` /api/v1/uploads/${id} ` , {
method: \"DELETE\",
} )
. then ( ( res ) => res.json ( ) )
. then ( ( res ) => {
trigger ( \"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
] ) ;
} ) ;
} ;"))))))
2025-06-22 13:03:02 -04:00
( text "{% if config.security.enable_invite_codes -%}" )
( div
( "class" "w-full flex flex-col gap-2 hidden" )
( "data-tab" "account/invites" )
( div
( "class" "card lowered flex flex-col gap-2" )
( a
( "href" "#/account" )
( "class" "button secondary" )
( text "{{ icon \"arrow-left\" }}" )
( span
( text "{{ text \"general:action.back\" }}" ) ) )
( div
( "class" "card-nest" )
( div
( "class" "card flex items-center gap-2 small" )
( text "{{ icon \"ticket\" }}" )
( span
( text "{{ text \"settings:tab.invites\" }}" ) ) )
( div
( "class" "card flex flex-col gap-2 secondary" )
2025-06-23 13:48:16 -04:00
( pre ( "id" "invite_codes_output" ) ( "class" "hidden" ) ( code ) )
2025-06-23 14:07:15 -04:00
( pre ( "id" "invite_codes_error_output" ) ( "class" "hidden" ) ( code ( "class" "red" ) ) )
2025-06-23 13:48:16 -04:00
2025-06-22 13:03:02 -04:00
( button
2025-06-23 13:48:16 -04:00
( "onclick" "generate_invite_codes()" )
2025-06-22 13:03:02 -04:00
( icon ( text "plus" ) )
2025-06-23 13:48:16 -04:00
( str ( text "settings:label.generate_invites" ) ) )
2025-06-22 13:03:02 -04:00
2025-06-22 15:06:21 -04:00
( text "{{ components::supporter_ad(body=\"Become a supporter to generate up to 48 invite codes! You can currently have 2 maximum.\") }} {% for code in invites %}" )
2025-06-22 13:03:02 -04:00
( div
( "class" "card flex flex-col gap-2" )
( text "{% if code[1].is_used -%}" )
; used
( b ( "class" "{% if code[1].is_used -%} fade {%- endif %}" ) ( s ( text "{{ code[1].code }}" ) ) )
( text "{{ components::full_username(user=code[0]) }}" )
( text "{% else %}" )
; unused
( b ( text "{{ code[1].code }}" ) )
( text "{%- endif %}" ) )
( text "{% endfor %}" )
2025-06-23 13:48:16 -04:00
( text "{{ components::pagination(page=page, items=invites|length, key=\"#/account/invites\") }}" )
2025-06-22 13:03:02 -04:00
( script
2025-06-23 13:48:16 -04:00
( text " globalThis.generate_invite_codes = async ( ) => {
await trigger ( \"atto::debounce\", [\"invites::create\"] ) ;
2025-06-22 13:03:02 -04:00
if (
! ( await trigger ( \"atto::confirm\", [
\"Are you sure you would like to do this? This action is permanent.\",
] ) )
) {
return ;
}
2025-06-23 13:48:16 -04:00
const count = Number.parseInt ( await trigger ( \"atto::prompt\", [\"Count ( 1-48 ) :\"] ) ) ;
if ( !count ) {
return ;
}
document.getElementById ( \"invite_codes_output\" ) . classList.remove ( \"hidden\" ) ;
2025-06-23 14:07:15 -04:00
document.getElementById ( \"invite_codes_error_output\" ) . classList.remove ( \"hidden\" ) ;
2025-06-23 14:17:01 -04:00
document.getElementById ( \"invite_codes_output\" ) . children[0].innerText = \"Working... expect to wait 50ms per invite code\" ;
2025-06-23 13:48:16 -04:00
fetch ( ` /api/v1/invites/${count} ` , {
2025-06-22 13:03:02 -04:00
method: \"POST\",
} )
. then ( ( res ) => res.json ( ) )
. then ( ( res ) => {
trigger ( \"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
] ) ;
if ( res.ok ) {
2025-06-23 14:07:15 -04:00
document.getElementById ( \"invite_codes_output\" ) . children[0].innerText = res.payload[0] ;
document.getElementById ( \"invite_codes_error_output\" ) . children[0].innerText = res.payload[1] ;
2025-06-22 13:03:02 -04:00
}
} ) ;
} ;"))))))
( text "{%- endif %}" )
2025-06-01 12:25:33 -04:00
( div
( "class" "w-full flex flex-col gap-2 hidden" )
( "data-tab" "account/billing" )
( div
2025-06-12 13:53:23 -04:00
( "class" "card lowered flex flex-col gap-2" )
2025-06-01 12:25:33 -04:00
( a
( "href" "#/account" )
( "class" "button secondary" )
( text "{{ icon \"arrow-left\" }}" )
( span
( text "{{ text \"general:action.back\" }}" ) ) )
( div
( "class" "card-nest" )
( div
( "class" "card flex items-center gap-2 small" )
( text "{{ icon \"credit-card\" }}" )
( span
( text "{{ text \"settings:tab.billing\" }}" ) ) )
( div
( "class" "card flex flex-col gap-2 secondary" )
( text "{% if config.stripe -%}" )
( div
( "class" "card-nest" )
( "ui_ident" "supporter_card" )
( div
( "class" "card small flex items-center gap-2" )
( text "{{ icon \"star\" }}" )
( b
( text "Supporter status" ) ) )
( div
( "class" "card flex flex-col gap-2" )
( text "{% if is_supporter -%}" )
( p
2025-06-03 23:35:34 -04:00
( text "You " )
2025-06-01 12:25:33 -04:00
( b
2025-06-03 23:35:34 -04:00
( text "are " ) )
2025-06-01 12:25:33 -04:00
( text " a supporter! Thank you for all
that you do. You can manage your billing
information below. " )
( b
( text " Please use your email address you supplied
when paying to login to the billing
portal. " ) ) )
( a
( "href" "{{ config.stripe.billing_portal_url }}" )
2025-06-12 13:53:23 -04:00
( "class" "button lowered" )
2025-06-01 12:25:33 -04:00
( "target" "_blank" )
( text "Manage billing" ) )
( text "{% else %}" )
( p
2025-06-03 23:35:34 -04:00
( text "You're " )
2025-06-01 12:25:33 -04:00
( b
2025-06-03 23:35:34 -04:00
( text "not " ) )
2025-06-01 12:25:33 -04:00
( text " currently a supporter! No
pressure, but it helps us do some pretty cool
things! As a supporter, you 'll get: " ) )
( ul
2025-06-09 16:45:36 -04:00
( "style" "margin-bottom: var(--pad-4)" )
2025-06-01 12:25:33 -04:00
( li
( text "Vanity badge on profile" ) )
( li
( text "No more supporter ads (duh)" ) )
( li
( text "Ability to upload gif avatars/banners" ) )
( li
( text "Be an admin/owner of up to 10 communities" ) )
( li
( text "Use custom CSS on your profile" ) )
( li
2025-06-19 19:13:07 -04:00
( text " Use community emojis outside of
2025-06-01 12:25:33 -04:00
their community " ) )
( li
2025-06-19 19:13:07 -04:00
( text "Upload and use gif emojis" ) )
2025-06-01 12:25:33 -04:00
( li
( text "Create infinite stack timelines" ) )
( li
2025-06-19 19:13:07 -04:00
( text "Upload images to posts" ) )
2025-06-01 12:25:33 -04:00
( li
( text "Save infinite post drafts" ) )
( li
2025-06-09 16:45:36 -04:00
( text "Ability to search through all posts" ) )
( li
2025-06-14 14:45:52 -04:00
( text "Ability to create forges" ) )
( li
2025-06-19 19:13:07 -04:00
( text "Create more than 1 app" ) )
2025-06-15 11:58:07 -04:00
( li
2025-06-15 16:09:02 -04:00
( text "Create up to 10 stack blocks" ) )
( li
2025-06-16 19:50:10 -04:00
( text "Add unlimited users to stacks" ) )
( li
2025-06-18 19:21:01 -04:00
( text "Increased proxied image size" ) )
( li
2025-06-19 19:13:07 -04:00
( text "Create infinite journals" ) )
( li
2025-06-22 13:50:12 -04:00
( text "Create infinite notes in each journal" ) )
2025-06-26 02:56:22 -04:00
( li
( text "Publish up to 50 notes" ) )
2025-06-22 13:50:12 -04:00
( text "{% if config.security.enable_invite_codes -%}" )
( li
2025-06-22 15:06:21 -04:00
( text "Create up to 48 invite codes" ) )
2025-06-22 13:50:12 -04:00
( text "{%- endif %}" ) )
2025-06-01 12:25:33 -04:00
( a
( "href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}" )
( "class" "button" )
( "target" "_blank" )
( text "Become a supporter" ) )
( span
( "class" "fade" )
( text "Please use your" )
( b
( text "real email" ) )
( text " when
completing payment. It is required to manage
your billing settings. " ) )
( text "{%- endif %}" ) ) )
( text "{%- endif %}" ) ) ) ) )
( div
( "class" "w-full hidden flex flex-col gap-2" )
( "data-tab" "profile" )
( div
2025-06-12 13:53:23 -04:00
( "class" "card lowered flex flex-col gap-2" )
2025-06-01 12:25:33 -04:00
( "id" "profile_settings" )
( text "{{ components::supporter_ad(body=\"Become a supporter to upload GIF images!\") }}" )
( div
( "class" "card-nest" )
( "ui_ident" "change_avatar" )
( div
( "class" "card small" )
( b
( text "{{ text \"settings:label.change_avatar\" }}" ) ) )
( form
( "class" "card flex gap-2 flex-row flex-wrap items-center" )
( "method" "post" )
( "enctype" "multipart/form-data" )
( "onsubmit" "upload_avatar(event)" )
( div
( "class" "flex gap-2 flex-row flex-wrap items-center" )
( input
( "id" "avatar_file" )
( "name" "file" )
( "type" "file" )
( "accept" "image/png,image/jpeg,image/avif,image/webp,image/gif" )
( "class" "w-content" ) )
( button
( "class" "primary" )
( text "{{ icon \"check\" }}" ) ) )
( span
( "class" "fade" )
( text " Images must be less than 8 MB large. Animated GIFs are
only supported for supporter users. GIFs can be at most
2 MB large. " ) ) ) )
( div
( "class" "card-nest" )
( "ui_ident" "change_banner" )
( div
( "class" "card small" )
( b
( text "{{ text \"settings:label.change_banner\" }}" ) ) )
( form
( "class" "card flex flex-col gap-2" )
( "method" "post" )
( "enctype" "multipart/form-data" )
( "onsubmit" "upload_banner(event)" )
( div
( "class" "flex gap-2 flex-row flex-wrap items-center" )
( input
( "id" "banner_file" )
( "name" "file" )
( "type" "file" )
2025-06-02 21:18:19 -04:00
( "accept" "image/png,image/jpeg,image/avif,image/webp,image/gif" )
2025-06-01 12:25:33 -04:00
( "class" "w-content" ) )
( button
( "class" "primary" )
( text "{{ icon \"check\" }}" ) ) )
( span
( "class" "fade" )
( text "Use an image of 1100x350px for the best results." ) ) ) ) )
( button
( "onclick" "save_settings()" )
( "id" "save_button" )
( text "{{ icon \"check\" }}" )
( span
( text "{{ text \"general:action.save\" }}" ) ) ) )
( div
2025-06-12 13:53:23 -04:00
( "class" "card w-full lowered hidden flex flex-col gap-2" )
2025-06-01 12:25:33 -04:00
( "data-tab" "sessions" )
( text "{% for token in profile.tokens %}" )
( div
( "class" "card w-full flex justify-between flex-collapse gap-2" )
( div
( "class" "flex flex-col gap-1" )
( b
( "style" "
width: 200px ;
overflow: hidden ;
text-overflow: ellipsis ;
" )
( text "{{ token[1] }}" ) )
( text "{% if is_helper -%}" )
( span
( "class" "flex gap-2 items-center" )
( span
( "class" "fade" )
( a
( "href" "/api/v1/auth/user/find_by_ip/{{ token[0] }}" )
( code
( text "{{ token[0] }}" ) ) ) ) )
( text "{% else %}" )
( span
( "class" "fade" )
( code
( text "{{ token[0] }}" ) ) )
( text "{%- endif %}" )
( span
( "class" "fade date" )
( text "{{ token[2] }}" ) ) )
( button
2025-06-12 13:53:23 -04:00
( "class" "lowered red" )
2025-06-01 12:25:33 -04:00
( "onclick" "remove_token('{{ token[1] }}')" )
( text "{{ text \"general:action.delete\" }}" ) ) )
( text "{% endfor %}" ) )
( div
( "class" "w-full hidden flex flex-col gap-2" )
( "data-tab" "theme" )
( div
2025-06-12 13:53:23 -04:00
( "class" "card lowered flex flex-col gap-2" )
2025-06-01 12:25:33 -04:00
( "id" "theme_settings" )
( text "{% if failing_color_keys|length > 0 -%}" )
( div
( "class" "card flex flex-col gap-2" )
( "style" "background: white; color: black" )
( "ui_ident" "awful_contrast" )
( div
( "class" "flex gap-2 items-center" )
( span
( "class" "desktop" )
( "style" "display: contents" )
( text "{{ icon \"contrast\" }}" ) )
( b
( text "Some of your custom colors fail contrast checks:" ) ) )
( ul
( text "{% for key in failing_color_keys %}" )
( li
2025-06-12 13:53:23 -04:00
( text "{{ key[0] }} " )
2025-06-01 12:25:33 -04:00
( b
( text "{{ key[1] }} < 4.5" ) ) )
( text "{% endfor %}" ) ) )
( text "{%- endif %}" )
( div
( "class" "card w-full flex flex-wrap gap-2" )
( "ui_ident" "import_export" )
( button
( "class" "primary" )
( "onclick" "import_theme_settings()" )
( text "{{ icon \"upload\" }}" )
( span
( text "{{ text \"settings:label.import\" }}" ) ) )
( button
( "class" "secondary" )
( "onclick" "export_theme_settings()" )
( text "{{ icon \"download\" }}" )
( span
( text "{{ text \"settings:label.export\" }}" ) ) ) )
( text "{{ components::supporter_ad(body=\"Become a supporter to add custom CSS!\") }}" )
( div
( "class" "card-nest" )
( "ui_ident" "theme_preference" )
( div
( "class" "card small" )
( b
( text "Theme preference" ) ) )
( div
( "class" "card" )
( select
2025-06-12 13:53:23 -04:00
( "onchange" "window.SETTING_SET_FUNCTIONS[0]('theme_preference', event.target.selectedOptions[0].value)" )
2025-06-01 12:25:33 -04:00
( option
( "value" "Auto" )
( "selected" "{% if user.settings.theme_preference == 'Auto' -%}true{% else %}false{%- endif %}" )
( text "Auto" ) )
( option
( "value" "Light" )
( "selected" "{% if user.settings.theme_preference == 'Light' -%}true{% else %}false{%- endif %}" )
( text "Light" ) )
( option
( "value" "Dark" )
( "selected" "{% if user.settings.theme_preference == 'Dark' -%}true{% else %}false{%- endif %}" )
( text "Dark" ) ) )
( span
( "class" "fade" )
( text "This represents your local site theme." ) ) ) )
( div
( "class" "card-nest" )
( "ui_ident" "profile_theme" )
( div
( "class" "card small" )
( b
( text "Profile theme base" ) ) )
( div
( "class" "card" )
( select
2025-06-12 13:53:23 -04:00
( "onchange" "window.SETTING_SET_FUNCTIONS[0]('profile_theme', event.target.selectedOptions[0].value)" )
2025-06-01 12:25:33 -04:00
( option
( "value" "Auto" )
( "selected" "{% if user.settings.profile_theme == 'Auto' -%}true{% else %}false{%- endif %}" )
( text "Auto" ) )
( option
( "value" "Light" )
( "selected" "{% if user.settings.profile_theme == 'Light' -%}true{% else %}false{%- endif %}" )
( text "Light" ) )
( option
( "value" "Dark" )
( "selected" "{% if user.settings.profile_theme == 'Dark' -%}true{% else %}false{%- endif %}" )
( text "Dark" ) ) )
( span
( "class" "fade" )
( text " This represents the site theme shown to users viewing
your profile. " ) ) ) ) )
( button
( "onclick" "save_settings()" )
( "id" "save_button" )
( text "{{ icon \"check\" }}" )
( span
( text "{{ text \"general:action.save\" }}" ) ) ) )
( div
2025-06-12 13:53:23 -04:00
( "class" "card w-full lowered hidden flex flex-col gap-2" )
2025-06-01 12:25:33 -04:00
( "data-tab" "connections" )
( div
( "class" "card w-full flex flex-wrap gap-2" )
( text "{% if config.connections.spotify_client_id and not profile.connections.Spotify %}" )
( button
2025-06-12 13:53:23 -04:00
( "class" "lowered" )
2025-06-01 12:25:33 -04:00
( "onclick" "trigger('spotify::create_connection', ['{{ config.connections.spotify_client_id }}'])" )
( text "{{ icon \"spotify\" }}" )
( span
( text "Spotify" ) ) )
( text "{%- endif %} {% if config.connections.last_fm_key and not profile.connections.LastFm %}" )
( button
2025-06-12 13:53:23 -04:00
( "class" "lowered" )
2025-06-01 12:25:33 -04:00
( "onclick" "trigger('last_fm::create_connection', ['{{ config.connections.last_fm_key }}'])" )
( text "{{ icon \"last_fm\" }}" )
( span
( text "Last.fm" ) ) )
( text "{%- endif %}" ) )
( text "{% for key, value in profile.connections %}" )
( div
( "class" "card-nest" )
( div
( "class" "card small flex items-center gap-2" )
( text "{{ components::connection_icon(key=key) }}" )
( b
( "class" "flex items-center gap-2" )
( text "{% if value[0].data.name -%}" )
( span
( text "{{ value[0].data.name }}" ) )
( span
( "style" "display: contents;" )
( "title" "Verified connection" )
( text "{{ icon \"badge-check\" }}" ) )
( text "{% else %}" )
( span
( text "{{ key }}" ) )
( span
( "style" "display: contents;" )
( text "{{ icon \"badge-alert\" }}" ) )
( text "{%- endif %}" ) ) )
( div
( "class" "card flex flex-col gap-2" )
( button
2025-06-12 13:53:23 -04:00
( "class" "lowered red small" )
2025-06-01 12:25:33 -04:00
( "onclick" "trigger('connections::delete', ['{{ key }}'])" )
( text "{{ text \"general:action.delete\" }}" ) )
( label
( "for" "{{ key }}-shown" )
( "class" "flex items-center gap-2" )
( input
( "type" "checkbox" )
( "checked" "{% if value[0].show_on_profile -%}true{% else %}false{%- endif %}" )
( "id" "{{ key }}-shown" )
( "onchange" "trigger('connections::push_con_shown', ['{{ key }}', event.target.checked])" )
( "class" "w-content" ) )
( span
( text "Shown on profile" ) ) ) ) )
2025-06-14 20:26:54 -04:00
( text "{% endfor %}" )
( text "{% for grant in profile_grants %}" )
( div
( "class" "card-nest" )
( div
( "class" "card small flex items-center gap-4" )
( div
( "class" "flex items-center gap-2" )
( icon ( text "bot" ) )
( a
( "class" "flush" )
( "href" "{{ grant[0].homepage }}" )
( "target" "_blank" )
( b
( "class" "flex items-center gap-2" )
( text "{{ grant[0].title }}" ) ) ) )
( span
( "class" "fade flex items-center gap-2" )
( icon ( text "clock" ) )
( span ( "class" "date" ) ( text "{{ grant[1].last_updated }}" ) ) ) )
( div
( "class" "card flex flex-col gap-2" )
( details
( summary ( icon ( text "scan-eye" ) ) ( text "{{ grant[1].scopes|length }} scope{{ grant[1].scopes|length|pluralize }}" ) )
( div
( "class" "card lowered w-full" )
( ul
( text "{% for scope in grant[1].scopes -%}" )
( li
( a
( "href" "https://tetratto.com/reference/tetratto/model/oauth/enum.AppScope.html#variant.{{ scope }}" )
( "target" "_blank" )
( text "{{ scope }}" ) ) )
( text "{%- endfor %}" ) ) ) )
( button
( "class" "lowered red small" )
( "onclick" "remove_grant('{{ grant[0].id }}')" )
( text "{{ text \"general:action.delete\" }}" ) ) ) )
( text "{% endfor %}" )
( hr )
( a
( "class" "button" )
( "href" "/developer" )
( icon ( text "code" ) )
( span
( text "{{ config.name }} " )
( str ( text "developer:label.for_developers" ) ) ) ) )
2025-06-01 12:25:33 -04:00
( script
( "type" "application/json" )
( "id" "settings_json" )
2025-06-05 16:23:57 -04:00
( text "{{ profile.settings|json_encode()|remove_script_tags|safe }}" ) )
2025-06-01 12:25:33 -04:00
( script
2025-06-25 23:15:24 -04:00
( text " setTimeout ( async ( ) => {
const ui = await ns ( \"ui\" ) ;
2025-06-01 12:25:33 -04:00
const settings = JSON.parse (
document.getElementById ( \"settings_json\" ) . innerHTML,
) ;
let tokens = JSON.parse ( \"{{ user_tokens_serde | safe }}\" ) ;
globalThis.remove_token = async ( id ) => {
if (
! ( await trigger ( \"atto::confirm\", [
\"Are you sure you would like to do this?\",
] ) )
) {
return ;
}
// reconstruct tokens ( but without the token with the given id )
const new_tokens = [] ;
for ( const token of tokens ) {
if ( token[1] === id ) {
continue ;
}
new_tokens.push ( token ) ;
}
tokens = new_tokens ;
// send request to save
fetch ( \"/api/v1/auth/user/{{ profile.id }}/tokens\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify ( tokens ) ,
} )
. then ( ( res ) => res.json ( ) )
. then ( ( res ) => {
trigger ( \"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
2025-06-14 20:26:54 -04:00
] ) ;
} ) ;
} ;
globalThis.remove_grant = async ( id ) => {
if (
! ( await trigger ( \"atto::confirm\", [
\"Are you sure you would like to do this?\",
] ) )
) {
return ;
}
fetch ( ` /api/v1/auth/user/{{ profile.id }}/grants/${id} ` , {
method: \"DELETE\",
} )
. then ( ( res ) => res.json ( ) )
. then ( ( res ) => {
trigger ( \"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
2025-06-01 12:25:33 -04:00
] ) ;
} ) ;
} ;
globalThis.save_settings = ( ) => {
fetch ( \"/api/v1/auth/user/{{ profile.id }}/settings\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify ( settings ) ,
} )
. then ( ( res ) => res.json ( ) )
. then ( ( res ) => {
trigger ( \"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
] ) ;
} ) ;
} ;
globalThis.change_password = ( e ) => {
e.preventDefault ( ) ;
fetch ( \"/api/v1/auth/user/{{ profile.id }}/password\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify ( {
from: e.target.current_password.value,
to: e.target.new_password.value,
} ) ,
} )
. then ( ( res ) => res.json ( ) )
. then ( ( res ) => {
trigger ( \"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
] ) ;
} ) ;
} ;
globalThis.change_username = async ( e ) => {
e.preventDefault ( ) ;
if (
! ( await trigger ( \"atto::confirm\", [
\"Are you sure you would like to do this?\",
] ) )
) {
return ;
}
fetch ( \"/api/v1/auth/user/{{ profile.id }}/username\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify ( {
to: e.target.new_username.value,
} ) ,
} )
. then ( ( res ) => res.json ( ) )
. then ( ( res ) => {
trigger ( \"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
] ) ;
} ) ;
} ;
globalThis.delete_account = async ( e ) => {
e.preventDefault ( ) ;
if (
! ( await trigger ( \"atto::confirm\", [
\"Are you sure you would like to do this?\",
] ) )
) {
return ;
}
fetch ( \"/api/v1/auth/user/{{ profile.id }}\", {
method: \"DELETE\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify ( {
password: e.target.current_password.value,
} ) ,
} )
. then ( ( res ) => res.json ( ) )
. then ( ( res ) => {
trigger ( \"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
] ) ;
} ) ;
} ;
globalThis.upload_avatar = ( e ) => {
e.preventDefault ( ) ;
e.target.querySelector ( \"button\" ) . style.display = \"none\" ;
fetch ( \"/api/v1/auth/upload/avatar\", {
method: \"POST\",
body: e.target.file.files[0],
} )
. then ( ( res ) => res.json ( ) )
. then ( ( res ) => {
trigger ( \"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
] ) ;
e.target
. querySelector ( \"button\" )
. removeAttribute ( \"style\" ) ;
} ) ;
alert ( \"Avatar upload in progress. Please wait!\" ) ;
} ;
globalThis.upload_banner = ( e ) => {
e.preventDefault ( ) ;
e.target.querySelector ( \"button\" ) . style.display = \"none\" ;
fetch ( \"/api/v1/auth/upload/banner\", {
method: \"POST\",
body: e.target.file.files[0],
} )
. then ( ( res ) => res.json ( ) )
. then ( ( res ) => {
trigger ( \"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
] ) ;
e.target
. querySelector ( \"button\" )
. removeAttribute ( \"style\" ) ;
} ) ;
alert ( \"Banner upload in progress. Please wait!\" ) ;
} ;
globalThis.enable_totp = async ( event ) => {
if (
! ( await trigger ( \"atto::confirm\", [
\"Are you sure you want to do this? You must have access to your TOTP codes to disable TOTP.\",
] ) )
) {
return ;
}
fetch ( \"/api/v1/auth/user/{{ user.id }}/totp\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
} )
. then ( ( res ) => res.json ( ) )
. then ( ( res ) => {
trigger ( \"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
] ) ;
const [secret, qr, recovery_codes] = res.payload ;
document.getElementById ( \"totp_secret\" ) . innerText =
secret ;
document.getElementById ( \"totp_qr\" ) . src =
` data:image/png ;base64,${qr}`;
document.getElementById (
\"totp_recovery_codes\",
) . innerText = recovery_codes.join ( \"\n\" ) ;
document.getElementById ( \"totp_stuff\" ) . style.display =
\"contents\" ;
event.target.remove ( ) ;
} ) ;
} ;
globalThis.disable_totp = async ( event ) => {
if (
! ( await trigger ( \"atto::confirm\", [
\"Are you sure you want to do this?\",
] ) )
) {
return ;
}
const totp_code = await trigger ( \"atto::prompt\", [\"TOTP code:\"] ) ;
if ( !totp_code ) {
return ;
}
fetch ( \"/api/v1/auth/user/{{ profile.id }}/totp\", {
method: \"DELETE\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify ( { totp: totp_code } ) ,
} )
. then ( ( res ) => res.json ( ) )
. then ( ( res ) => {
trigger ( \"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
] ) ;
event.target.remove ( ) ;
} ) ;
} ;
globalThis.refresh_totp_codes = async ( event ) => {
if (
! ( await trigger ( \"atto::confirm\", [
\"Are you sure you want to do this? The old codes will no longer work.\",
] ) )
) {
return ;
}
const totp_code = await trigger ( \"atto::prompt\", [\"TOTP code:\"] ) ;
if ( !totp_code ) {
return ;
}
fetch ( \"/api/v1/auth/user/{{ profile.id }}/totp/codes\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify ( { totp: totp_code } ) ,
} )
. then ( ( res ) => res.json ( ) )
. then ( ( res ) => {
trigger ( \"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
] ) ;
document.getElementById (
\"totp_recovery_codes\",
) . innerText = res.payload.join ( \"\n\" ) ;
document.getElementById (
\"totp_recovery_codes\",
) . style.display = \"block\" ;
event.target.remove ( ) ;
} ) ;
} ;
const account_settings =
document.getElementById ( \"account_settings\" ) ;
const profile_settings =
document.getElementById ( \"profile_settings\" ) ;
const theme_settings = document.getElementById ( \"theme_settings\" ) ;
ui.refresh_container ( account_settings, [
\"supporter_ad\",
\"account_settings_tabs\",
\"home_timeline\",
\"notifications\",
\"change_username\",
\"delete_account\",
] ) ;
ui.refresh_container ( profile_settings, [
\"supporter_ad\",
\"change_avatar\",
\"change_banner\",
] ) ;
ui.refresh_container ( theme_settings, [
\"supporter_ad\",
\"awful_contrast\",
\"import_export\",
\"theme_preference\",
\"profile_theme\",
] ) ;
ui.generate_settings_ui (
account_settings,
[
[
[\"display_name\", \"Display name\"],
\"{{ profile.settings.display_name }}\",
\"input\",
],
[
[\"biography\", \"Biography\"],
settings.biography,
\"textarea\",
],
[[\"status\", \"Status\"], settings.status, \"textarea\"],
[
[\"warning\", \"Profile warning\"],
settings.warning,
\"textarea\",
],
2025-06-10 13:49:17 -04:00
[[\"muted\", \"Muted phrases\"], settings.muted.join ( \"\\n\" ) , \"textarea\", {
embed_html:
'<span class=\"fade\">Muted phrases should all be on new lines.</span> ',
}],
2025-06-27 13:10:04 -04:00
[[], \"Accessibility\", \"title\"],
[
[\"large_text\", \"Increase UI text size\"],
\"{{ profile.settings.large_text }}\",
\"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\",
],
2025-06-01 12:25:33 -04:00
],
settings,
2025-06-10 13:49:17 -04:00
{
muted: ( new_muted ) => {
settings.muted = new_muted
. split ( \"\\n\" )
. map ( ( t ) => t.trim ( ) ) ;
},
},
2025-06-01 12:25:33 -04:00
) ;
ui.generate_settings_ui (
profile_settings,
[
[[], \"Privacy\", \"title\"],
[
[
\"require_account\",
\"Require an account to view my profile\",
],
\"{{ profile.settings.require_account }}\",
\"checkbox\",
],
[
[
\"private_profile\",
\"Only allow users I 'm following to view my profile\",
],
\"{{ profile.settings.private_profile }}\",
\"checkbox\",
],
[
[
\"private_chats\",
\"Only allow users I 'm following to add me to chats\",
],
\"{{ profile.settings.private_chats }}\",
\"checkbox\",
],
[
[
\"private_communities\",
\"Keep my joined communities private\",
],
\"{{ profile.settings.private_communities }}\",
\"checkbox\",
],
[
[\"private_last_seen\", \"Keep my last seen time private\"],
\"{{ profile.settings.private_last_seen }}\",
\"checkbox\",
],
2025-06-02 16:11:27 -04:00
[
[\"hide_extra_post_tabs\", \"Hide extra post tabs ( replies, media ) \"],
\"{{ profile.settings.hide_extra_post_tabs }}\",
\"checkbox\",
],
[
[\"show_nsfw\", \"Show NSFW posts\"],
\"{{ profile.settings.show_nsfw }}\",
\"checkbox\",
],
2025-06-22 00:04:32 -04:00
[
[\"auto_unlist\", \"Automatically mark my posts as NSFW\"],
\"{{ profile.settings.auto_unlist }}\",
\"checkbox\",
],
2025-06-22 00:45:05 -04:00
[
[\"all_timeline_hide_answers\", 'Hide posts answering questions from the \"All\" timeline '],
\"{{ profile.settings.all_timeline_hide_answers }}\",
\"checkbox\",
],
2025-06-30 18:10:00 -04:00
[
[
\"hide_associated_blocked_users\",
2025-06-30 18:49:41 -04:00
\"Hide users that you 've blocked on your other accounts from timelines\",
2025-06-30 18:10:00 -04:00
],
\"{{ profile.settings.hide_associated_blocked_users }}\",
\"checkbox\",
],
2025-06-01 12:25:33 -04:00
[[], \"Questions\", \"title\"],
[
[
\"enable_questions\",
\"Allow users to ask you questions\",
],
\"{{ profile.settings.enable_questions }}\",
\"checkbox\",
],
[
[
\"allow_anonymous_questions\",
\"Allow anonymous questions\",
],
\"{{ profile.settings.allow_anonymous_questions }}\",
\"checkbox\",
2025-06-20 17:40:55 -04:00
],
[
[
\"enable_drawings\",
\"Allow users to create drawings and submit them with questions\",
],
\"{{ profile.settings.enable_drawings }}\",
\"checkbox\",
2025-06-01 12:25:33 -04:00
],
[
[\"motivational_header\", \"Motivational header\"],
settings.motivational_header,
\"input\",
],
[[], \"Anonymous\", \"title\"],
[
[\"anonymous_username\", \"Anonymous username\"],
settings.anonymous_username,
\"input\",
],
[
[\"anonymous_avatar_url\", \"Anonymous avatar URL\"],
settings.anonymous_avatar_url,
\"input\",
],
[[], \"Misc\", \"title\"],
[
[\"hide_dislikes\", \"Hide post dislikes\"],
\"{{ profile.settings.hide_dislikes }}\",
\"checkbox\",
],
[
[],
2025-06-18 21:32:05 -04:00
\"Hides dislikes on all posts. Users will also no longer be able to dislike your posts.\",
2025-06-01 12:25:33 -04:00
\"text\",
],
2025-06-10 13:49:17 -04:00
[[], \"Fun\", \"title\"],
[
[\"disable_gpa_fun\", \"Disable GPA\"],
\"{{ profile.settings.disable_gpa_fun }}\",
\"checkbox\",
],
2025-06-27 14:21:42 -04:00
[
[\"disable_achievements\", \"Disable achievements\"],
\"{{ profile.settings.disable_achievements }}\",
\"checkbox\",
],
2025-06-01 12:25:33 -04:00
],
settings,
) ;
const can_use_custom_css =
\"{{ user.permissions | has_supporter }}\" === \"true\" ;
const theme_settings_ui_json = [
[
[
\"disable_other_themes\",
\"Disable the profile theme of other users\",
],
\"{{ profile.settings.disable_other_themes }}\",
\"checkbox\",
],
[[], \"Theme builder\", \"title\"],
[
[],
\"Allow the site to build the theme for you given a base hue, saturation, and lightness. Scroll down to the next section to manually build the theme.\",
\"text\",
],
[
[\"theme_hue\", \"Theme hue ( integer 0-255 ) \"],
\"{{ profile.settings.theme_hue }}\",
\"input\",
],
[
[\"theme_sat\", \"Theme sat ( percentage 0%-100% ) \"],
\"{{ profile.settings.theme_sat }}\",
\"input\",
],
[
[\"theme_lit\", \"Theme lit ( percentage 0%-100% ) \"],
\"{{ profile.settings.theme_lit }}\",
\"input\",
],
[[], \"Manual theme builder\", \"title\"],
[[], \"Override individual colors.\", \"text\"],
// surface
[
[\"theme_color_surface\", \"Surface\"],
\"{{ profile.settings.theme_color_surface }}\",
\"color\",
{
description: \"Page background.\",
},
],
[
[\"theme_color_text\", \"Text\"],
\"{{ profile.settings.theme_color_text }}\",
\"color\",
{
description:
\"Text on elements with the surface background.\",
},
],
[
[\"theme_color_text_link\", \"Links\"],
\"{{ profile.settings.theme_color_text_link }}\",
\"color\",
{
description: \"Links on all elements.\",
},
],
// lowered
[[], \"\", \"divider\"],
[
[\"theme_color_lowered\", \"Lowered\"],
\"{{ profile.settings.theme_color_lowered }}\",
\"color\",
{
description:
\"Some cards, buttons, or anything else with a darker background color than the surface.\",
},
],
[
[\"theme_color_text_lowered\", \"Text\"],
\"{{ profile.settings.theme_color_text_lowered }}\",
\"color\",
{
description:
\"Text on elements with the lowered backgrounds.\",
},
],
[
[\"theme_color_super_lowered\", \"Super lowered\"],
\"{{ profile.settings.theme_color_super_lowered }}\",
\"color\",
{
description: \"Borders.\",
},
],
// raised
[[], \"\", \"divider\"],
[
[\"theme_color_raised\", \"Raised\"],
\"{{ profile.settings.theme_color_raised }}\",
\"color\",
{
description:
\"Some cards, buttons, or anything else with a lighter background color than the surface.\",
},
],
[
[\"theme_color_text_raised\", \"Text\"],
\"{{ profile.settings.theme_color_text_raised }}\",
\"color\",
{
description:
\"Text on elements with the raised backgrounds.\",
},
],
[
[\"theme_color_super_raised\", \"Super raised\"],
\"{{ profile.settings.theme_color_super_raised }}\",
\"color\",
{
description: \"Some borders.\",
},
],
// primary
[[], \"\", \"divider\"],
[
[\"theme_color_primary\", \"Primary\"],
\"{{ profile.settings.theme_color_primary }}\",
\"color\",
{
description:
\"Primary color ; navigation bar, some buttons, etc.\",
},
],
[
[\"theme_color_text_primary\", \"Text\"],
\"{{ profile.settings.theme_color_text_primary }}\",
\"color\",
{
description:
\"Text on elements with the primary backgrounds.\",
},
],
[
[\"theme_color_primary_lowered\", \"Lowered\"],
\"{{ profile.settings.theme_color_primary_lowered }}\",
\"color\",
{
description: \"Hover state for primary buttons.\",
},
],
// secondary
[[], \"\", \"divider\"],
[
[\"theme_color_secondary\", \"Secondary\"],
\"{{ profile.settings.theme_color_secondary }}\",
\"color\",
{
description: \"Secondary color.\",
},
],
[
[\"theme_color_text_secondary\", \"Text\"],
\"{{ profile.settings.theme_color_text_secondary }}\",
\"color\",
{
description:
\"Text on elements with the secondary backgrounds.\",
},
],
[
[\"theme_color_secondary_lowered\", \"Lowered\"],
\"{{ profile.settings.theme_color_secondary_lowered }}\",
\"color\",
{
description: \"Hover state for secondary buttons.\",
},
],
2025-06-28 13:15:37 -04:00
// online indicator
[[], \"\", \"divider\"],
[
[\"theme_color_online\", \"Online indicator ( online ) \"],
\"{{ profile.settings.theme_color_online }}\",
\"color\",
{
description:
\"The green dot next to the name of online users.\",
},
],
[
[\"theme_color_idle\", \"Online indicator ( idle ) \"],
\"{{ profile.settings.theme_color_idle }}\",
\"color\",
{
description:
\"The yellow dot next to the name of online users.\",
},
],
[
[\"theme_color_offline\", \"Online indicator ( offline ) \"],
\"{{ profile.settings.theme_color_offline }}\",
\"color\",
{
description:
\"The grey next to the name of online users.\",
},
],
2025-06-01 12:25:33 -04:00
] ;
if ( can_use_custom_css ) {
theme_settings_ui_json.push ( [[], \"Advanced\", \"title\"] ) ;
theme_settings_ui_json.push ( [
[\"theme_custom_css\", \"Custom CSS\"],
settings.theme_custom_css,
\"textarea\",
{
embed_html:
'<span class=\"fade\">Custom CSS input embedded into your theme.</span> ',
},
] ) ;
}
ui.generate_settings_ui (
theme_settings,
theme_settings_ui_json,
settings,
) ;
globalThis.import_theme_settings = ( ) => {
const input = document.createElement ( \"input\" ) ;
input.type = \"file\" ;
input.accept = \"application/json\" ;
document.body.appendChild ( input ) ;
input.addEventListener ( \"change\", async ( e ) => {
const json = JSON.parse ( await e.target.files[0].text ( ) ) ;
for ( const setting of Object.entries ( json ) ) {
settings[setting[0]] = setting[1] ;
}
input.remove ( ) ;
save_settings ( ) ;
setTimeout ( ( ) => {
window.location.reload ( ) ;
}, 150 ) ;
} ) ;
input.click ( ) ;
} ;
globalThis.export_theme_settings = ( ) => {
const theme_settings = {
profile_theme: settings.profile_theme,
} ;
for ( const setting of Object.entries ( settings ) ) {
if ( setting[0].startsWith ( \"theme_\" ) ) {
theme_settings[setting[0]] = setting[1] ;
}
}
const blob = new Blob (
[JSON.stringify ( theme_settings, null, 4 ) ],
{
type: \"appliction/json\",
},
) ;
const url = URL.createObjectURL ( blob ) ;
const anchor = document.createElement ( \"a\" ) ;
anchor.href = url ;
anchor.setAttribute ( \"download\", \"theme.json\" ) ;
document.body.appendChild ( anchor ) ;
anchor.click ( ) ;
anchor.remove ( ) ;
} ;
} ) ;")))
( text "{% endblock %}" )