add: blocked users page
This commit is contained in:
parent
6893aeedb5
commit
3706764437
18 changed files with 427 additions and 178 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
@ -122,12 +122,6 @@ version = "1.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
|
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arc-swap"
|
|
||||||
version = "1.7.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arg_enum_proc_macro"
|
name = "arg_enum_proc_macro"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
|
@ -2852,11 +2846,10 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redis"
|
name = "redis"
|
||||||
version = "0.30.0"
|
version = "0.31.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "438a4e5f8e9aa246d6f3666d6978441bf1b37d5f417b50c4dd220be09f5fcc17"
|
checksum = "0bc1ea653e0b2e097db3ebb5b7f678be339620b8041f66b30a308c1d45d36a7f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"combine",
|
"combine",
|
||||||
|
|
|
@ -15,7 +15,7 @@ serde = { version = "1.0.219", features = ["derive"] }
|
||||||
tera = "1.20.0"
|
tera = "1.20.0"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||||
tower-http = { version = "0.6.3", features = ["trace", "fs", "catch-panic"] }
|
tower-http = { version = "0.6.2", features = ["trace", "fs", "catch-panic"] }
|
||||||
axum = { version = "0.8.4", features = ["macros", "ws"] }
|
axum = { version = "0.8.4", features = ["macros", "ws"] }
|
||||||
tokio = { version = "1.45.0", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.45.0", features = ["macros", "rt-multi-thread"] }
|
||||||
axum-extra = { version = "0.10.1", features = ["cookie", "multipart"] }
|
axum-extra = { version = "0.10.1", features = ["cookie", "multipart"] }
|
||||||
|
@ -35,7 +35,7 @@ cf-turnstile = "0.2.0"
|
||||||
contrasted = "0.1.2"
|
contrasted = "0.1.2"
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
|
|
||||||
redis = { version = "0.30.0", features = [
|
redis = { version = "0.31.0", features = [
|
||||||
"aio",
|
"aio",
|
||||||
"tokio-comp",
|
"tokio-comp",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
|
|
@ -33,6 +33,9 @@ version = "1.0.0"
|
||||||
"general:label.account_banned_body" = "Your account has been banned for violating our policies."
|
"general:label.account_banned_body" = "Your account has been banned for violating our policies."
|
||||||
"general:label.better_with_account" = "It's better with an account! Login or sign up to explore more."
|
"general:label.better_with_account" = "It's better with an account! Login or sign up to explore more."
|
||||||
|
|
||||||
|
"general:label.supporter_motivation" = "Become a supporter!"
|
||||||
|
"general:action.become_supporter" = "Become supporter"
|
||||||
|
|
||||||
"dialog:action.okay" = "Ok"
|
"dialog:action.okay" = "Ok"
|
||||||
"dialog:action.continue" = "Continue"
|
"dialog:action.continue" = "Continue"
|
||||||
"dialog:action.cancel" = "Cancel"
|
"dialog:action.cancel" = "Cancel"
|
||||||
|
@ -136,6 +139,11 @@ version = "1.0.0"
|
||||||
"settings:label.theme_lit" = "Theme lit"
|
"settings:label.theme_lit" = "Theme lit"
|
||||||
"settings:label.import" = "Import"
|
"settings:label.import" = "Import"
|
||||||
"settings:label.export" = "Export"
|
"settings:label.export" = "Export"
|
||||||
|
"settings:label.manage_blocks" = "Manage blocks"
|
||||||
|
"settings:label.users" = "Users"
|
||||||
|
"settings:tab.security" = "Security"
|
||||||
|
"settings:tab.blocks" = "Blocks"
|
||||||
|
"settings:tab.billing" = "Billing"
|
||||||
|
|
||||||
"mod_panel:label.open_reported_content" = "Open reported content"
|
"mod_panel:label.open_reported_content" = "Open reported content"
|
||||||
"mod_panel:label.manage_profile" = "Manage profile"
|
"mod_panel:label.manage_profile" = "Manage profile"
|
||||||
|
|
|
@ -461,6 +461,39 @@ table ol {
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* supporter card */
|
||||||
|
@property --border-angle {
|
||||||
|
syntax: "<angle>";
|
||||||
|
initial-value: 0deg;
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
.supporter_ad {
|
||||||
|
--border-angle: 0deg;
|
||||||
|
padding: 2px;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(var(--color-raised), var(--color-raised)),
|
||||||
|
repeating-conic-gradient(
|
||||||
|
from var(--border-angle),
|
||||||
|
var(--color-primary) 0%,
|
||||||
|
var(--color-primary-lowered) 50%
|
||||||
|
);
|
||||||
|
background-origin: border-box;
|
||||||
|
background-clip: content-box, border-box;
|
||||||
|
animation: 10s rotate_angle linear infinite;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate_angle {
|
||||||
|
from {
|
||||||
|
--border-angle: 0deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
--border-angle: 360deg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* buttons */
|
/* buttons */
|
||||||
button,
|
button,
|
||||||
.button {
|
.button {
|
||||||
|
|
|
@ -30,7 +30,9 @@
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
{% if list|length >= 4 %} {{ components::supporter_ad(body="Become a
|
||||||
|
supporter to create up to 10 communities!") }} {% endif %} {% endif %}
|
||||||
|
|
||||||
<div class="card-nest w-full">
|
<div class="card-nest w-full">
|
||||||
<div class="card small flex items-center justify-between gap-2">
|
<div class="card small flex items-center justify-between gap-2">
|
||||||
|
|
|
@ -1242,4 +1242,24 @@ show_kick=false, secondary=false) -%}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
{% endif %} {%- endmacro %} {% macro supporter_ad(body="") -%} {% if
|
||||||
|
config.stripe and not is_supporter %}
|
||||||
|
<div
|
||||||
|
class="card w-full supporter_ad"
|
||||||
|
ui_ident="supporter_ad"
|
||||||
|
onclick="window.location.href = '/settings#/account/billing'"
|
||||||
|
>
|
||||||
|
<div class="card w-full flex flex-wrap items-center gap-2 justify-between">
|
||||||
|
{% if body %}
|
||||||
|
<b>{{ body }}</b>
|
||||||
|
{% else %}
|
||||||
|
<b>{{ text "general:label.supporter_motivation" }}</b>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<a href="/settings#/account/billing" class="button small">
|
||||||
|
{{ icon "heart" }}
|
||||||
|
<span>{{ text "general:action.become_supporter" }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %} {%- endmacro %}
|
{% endif %} {%- endmacro %}
|
||||||
|
|
|
@ -38,36 +38,24 @@
|
||||||
|
|
||||||
<div class="w-full flex flex-col gap-2" data-tab="account">
|
<div class="w-full flex flex-col gap-2" data-tab="account">
|
||||||
<div class="card tertiary flex flex-col gap-2" id="account_settings">
|
<div class="card tertiary flex flex-col gap-2" id="account_settings">
|
||||||
{% if config.stripe %}
|
<div class="pillmenu" ui_ident="account_settings_tabs">
|
||||||
<div class="card-nest" ui_ident="supporter_card">
|
<a data-tab-button="account/security" href="#/account/security">
|
||||||
<div class="card small flex items-center gap-2">
|
{{ icon "user-lock" }}
|
||||||
{{ icon "star" }}
|
<span>{{ text "settings:tab.security" }}</span>
|
||||||
<b>Supporter status</b>
|
</a>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
<a data-tab-button="account/blocks" href="#/account/blocks">
|
||||||
{% if is_supporter %}
|
{{ icon "shield" }}
|
||||||
<p>You <b>are</b> a supporter! Thank you for all that you do. You can manage your billing information below. <b>Please use your email address you supplied when paying to login to the billing portal.</b></p>
|
<span>{{ text "settings:tab.blocks" }}</span>
|
||||||
<a href="{{ config.stripe.billing_portal_url }}" class="button quaternary" target="_blank">Manage billing</a>
|
</a>
|
||||||
{% else %}
|
|
||||||
<p>You're <b>not</b> currently a supporter! No pressure, but it helps us do some pretty cool things! As a supporter, you'll get:</p>
|
|
||||||
|
|
||||||
<ul style="margin-bottom: 1rem">
|
{% if config.stripe %}
|
||||||
<li>Vanity badge on profile</li>
|
<a data-tab-button="account/billing" href="#/account/billing">
|
||||||
<li>Ability to upload gif avatars/banners</li>
|
{{ icon "credit-card" }}
|
||||||
<li>Be an admin/owner of up to 10 communities</li>
|
<span>{{ text "settings:tab.billing" }}</span>
|
||||||
<li>Use custom CSS on your profile</li>
|
</a>
|
||||||
<li>Ability to use community emojis outside of their community <b>(soon)</b></li>
|
{% endif %}
|
||||||
<li>Ability to upload and use gif emojis <b>(soon)</b></li>
|
|
||||||
<li><b>Create infinite stack timelines</b></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<a href="{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}" class="button" target="_blank">Become a supporter</a>
|
|
||||||
<span class="fade">Please use your <b>real email</b> when completing payment. It is required to manage your billing settings.</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="card-nest" ui_ident="home_timeline">
|
<div class="card-nest" ui_ident="home_timeline">
|
||||||
<div class="card small">
|
<div class="card small">
|
||||||
|
@ -153,62 +141,22 @@
|
||||||
|
|
||||||
<div class="card flex flex-col gap-2">
|
<div class="card flex flex-col gap-2">
|
||||||
<button id="notifications_button"></button>
|
<button id="notifications_button"></button>
|
||||||
<span class="fade">Notifications require you to keep {{ config.name }} open in your browser for real-time updates. This setting does not sync across browsers.</span>
|
<span class="fade"
|
||||||
|
>Notifications require you to keep {{ config.name }}
|
||||||
|
open in your browser for real-time updates. This setting
|
||||||
|
does not sync across browsers.</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
trigger("me::notifications_button", [document.getElementById("notifications_button")]);
|
trigger("me::notifications_button", [
|
||||||
}, 150)
|
document.getElementById("notifications_button"),
|
||||||
|
]);
|
||||||
|
}, 150);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card-nest" ui_ident="change_password">
|
|
||||||
<div class="card small">
|
|
||||||
<b>{{ text "settings:label.change_password" }}</b>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form
|
|
||||||
class="card flex flex-col gap-2"
|
|
||||||
onsubmit="change_password(event)"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label for="current_password"
|
|
||||||
>{{ text "settings:label.current_password" }}</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="current_password"
|
|
||||||
id="current_password"
|
|
||||||
placeholder="current_password"
|
|
||||||
required
|
|
||||||
minlength="6"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label for="new_password"
|
|
||||||
>{{ text "settings:label.new_password" }}</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="new_password"
|
|
||||||
id="new_password"
|
|
||||||
placeholder="new_password"
|
|
||||||
required
|
|
||||||
minlength="6"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="primary">
|
|
||||||
{{ icon "check" }}
|
|
||||||
<span>{{ text "general:action.save" }}</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-nest" ui_ident="change_username">
|
<div class="card-nest" ui_ident="change_username">
|
||||||
<div class="card small">
|
<div class="card small">
|
||||||
<b>{{ text "settings:label.change_username" }}</b>
|
<b>{{ text "settings:label.change_username" }}</b>
|
||||||
|
@ -238,63 +186,9 @@
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-nest" ui_ident="two_factor_authentication">
|
|
||||||
<div class="card small">
|
|
||||||
<b>{{ text "settings:label.two_factor_authentication" }}</b>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card flex flex-col gap-2">
|
|
||||||
{% if profile.totp|length == 0 %}
|
|
||||||
<div id="totp_stuff" style="display: none">
|
|
||||||
<span
|
|
||||||
>Scan this QR code in a TOTP authenticator app (like
|
|
||||||
Google Authenticator):
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<img id="totp_qr" style="max-width: 250px" />
|
|
||||||
|
|
||||||
<span>TOTP secret (do NOT share):</span>
|
|
||||||
<pre id="totp_secret"></pre>
|
|
||||||
|
|
||||||
<span
|
|
||||||
>Recovery codes (STORE SAFELY, these can only be
|
|
||||||
viewed once):</span
|
|
||||||
>
|
|
||||||
|
|
||||||
<pre id="totp_recovery_codes"></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="quaternary green"
|
|
||||||
onclick="enable_totp(event)"
|
|
||||||
>
|
|
||||||
Enable TOTP 2FA
|
|
||||||
</button>
|
|
||||||
{% else %}
|
|
||||||
<pre id="totp_recovery_codes" style="display: none"></pre>
|
|
||||||
|
|
||||||
<div class="flex gap-2 flex-wrap">
|
|
||||||
<button
|
|
||||||
class="quaternary red"
|
|
||||||
onclick="refresh_totp_codes(event)"
|
|
||||||
>
|
|
||||||
Refresh recovery codes
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="quaternary red"
|
|
||||||
onclick="disable_totp(event)"
|
|
||||||
>
|
|
||||||
Disable TOTP 2FA
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-nest" ui_ident="change_password">
|
<div class="card-nest" ui_ident="delete_account">
|
||||||
<div class="card small flex items-center gap-2 red">
|
<div class="card small flex items-center gap-2 red">
|
||||||
{{ icon "skull" }}
|
{{ icon "skull" }}
|
||||||
<b>{{ text "settings:label.delete_account" }}</b>
|
<b>{{ text "settings:label.delete_account" }}</b>
|
||||||
|
@ -332,8 +226,262 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full flex flex-col gap-2" data-tab="account/security">
|
||||||
|
<div class="card tertiary flex flex-col gap-2">
|
||||||
|
<a href="#/account" class="button secondary">
|
||||||
|
{{ icon "arrow-left" }}
|
||||||
|
<span>{{ text "general:action.back" }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="card-nest">
|
||||||
|
<div class="card flex items-center gap-2 small">
|
||||||
|
{{ icon "user-lock" }}
|
||||||
|
<span>{{ text "settings:tab.security" }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card flex flex-col gap-2 secondary">
|
||||||
|
<div class="card-nest" ui_ident="two_factor_authentication">
|
||||||
|
<div class="card small">
|
||||||
|
<b
|
||||||
|
>{{ text
|
||||||
|
"settings:label.two_factor_authentication" }}</b
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card flex flex-col gap-2">
|
||||||
|
{% if profile.totp|length == 0 %}
|
||||||
|
<div id="totp_stuff" style="display: none">
|
||||||
|
<span
|
||||||
|
>Scan this QR code in a TOTP authenticator
|
||||||
|
app (like Google Authenticator):
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<img id="totp_qr" style="max-width: 250px" />
|
||||||
|
|
||||||
|
<span>TOTP secret (do NOT share):</span>
|
||||||
|
<pre id="totp_secret"></pre>
|
||||||
|
|
||||||
|
<span
|
||||||
|
>Recovery codes (STORE SAFELY, these can
|
||||||
|
only be viewed once):</span
|
||||||
|
>
|
||||||
|
|
||||||
|
<pre id="totp_recovery_codes"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="quaternary green"
|
||||||
|
onclick="enable_totp(event)"
|
||||||
|
>
|
||||||
|
Enable TOTP 2FA
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<pre
|
||||||
|
id="totp_recovery_codes"
|
||||||
|
style="display: none"
|
||||||
|
></pre>
|
||||||
|
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
<button
|
||||||
|
class="quaternary red"
|
||||||
|
onclick="refresh_totp_codes(event)"
|
||||||
|
>
|
||||||
|
Refresh recovery codes
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="quaternary red"
|
||||||
|
onclick="disable_totp(event)"
|
||||||
|
>
|
||||||
|
Disable TOTP 2FA
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-nest" ui_ident="change_password">
|
||||||
|
<div class="card small">
|
||||||
|
<b>{{ text "settings:label.change_password" }}</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="card flex flex-col gap-2"
|
||||||
|
onsubmit="change_password(event)"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="current_password"
|
||||||
|
>{{ text "settings:label.current_password"
|
||||||
|
}}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="current_password"
|
||||||
|
id="current_password"
|
||||||
|
placeholder="current_password"
|
||||||
|
required
|
||||||
|
minlength="6"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="new_password"
|
||||||
|
>{{ text "settings:label.new_password"
|
||||||
|
}}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="new_password"
|
||||||
|
id="new_password"
|
||||||
|
placeholder="new_password"
|
||||||
|
required
|
||||||
|
minlength="6"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="primary">
|
||||||
|
{{ icon "check" }}
|
||||||
|
<span>{{ text "general:action.save" }}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full flex flex-col gap-2" data-tab="account/blocks">
|
||||||
|
<div class="card tertiary flex flex-col gap-2">
|
||||||
|
<a href="#/account" class="button secondary">
|
||||||
|
{{ icon "arrow-left" }}
|
||||||
|
<span>{{ text "general:action.back" }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="card-nest">
|
||||||
|
<div class="card flex items-center gap-2 small">
|
||||||
|
{{ icon "users-round" }}
|
||||||
|
<span>{{ text "settings:label.users" }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card flex flex-col gap-2">
|
||||||
|
{% for user in blocks %}
|
||||||
|
<div
|
||||||
|
class="card secondary flex flex-wrap gap-2 items-center justify-between"
|
||||||
|
>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
{{ components::avatar(username=user.username) }} {{
|
||||||
|
components::full_username(user=user) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/@{{ user.username }}"
|
||||||
|
class="button quaternary small"
|
||||||
|
>
|
||||||
|
{{ icon "external-link" }}
|
||||||
|
<span
|
||||||
|
>{{ text "requests:action.view_profile" }}</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full flex flex-col gap-2" data-tab="account/billing">
|
||||||
|
<div class="card tertiary flex flex-col gap-2">
|
||||||
|
<a href="#/account" class="button secondary">
|
||||||
|
{{ icon "arrow-left" }}
|
||||||
|
<span>{{ text "general:action.back" }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="card-nest">
|
||||||
|
<div class="card flex items-center gap-2 small">
|
||||||
|
{{ icon "credit-card" }}
|
||||||
|
<span>{{ text "settings:tab.billing" }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card flex flex-col gap-2 secondary">
|
||||||
|
{% if config.stripe %}
|
||||||
|
<div class="card-nest" ui_ident="supporter_card">
|
||||||
|
<div class="card small flex items-center gap-2">
|
||||||
|
{{ icon "star" }}
|
||||||
|
<b>Supporter status</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card flex flex-col gap-2">
|
||||||
|
{% if is_supporter %}
|
||||||
|
<p>
|
||||||
|
You <b>are</b> a supporter! Thank you for all
|
||||||
|
that you do. You can manage your billing
|
||||||
|
information below.
|
||||||
|
<b
|
||||||
|
>Please use your email address you supplied
|
||||||
|
when paying to login to the billing
|
||||||
|
portal.</b
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="{{ config.stripe.billing_portal_url }}"
|
||||||
|
class="button quaternary"
|
||||||
|
target="_blank"
|
||||||
|
>Manage billing</a
|
||||||
|
>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
You're <b>not</b> currently a supporter! No
|
||||||
|
pressure, but it helps us do some pretty cool
|
||||||
|
things! As a supporter, you'll get:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul style="margin-bottom: 1rem">
|
||||||
|
<li>Vanity badge on profile</li>
|
||||||
|
<li>No more supporter ads (duh)</li>
|
||||||
|
<li>Ability to upload gif avatars/banners</li>
|
||||||
|
<li>
|
||||||
|
Be an admin/owner of up to 10 communities
|
||||||
|
</li>
|
||||||
|
<li>Use custom CSS on your profile</li>
|
||||||
|
<li>
|
||||||
|
Ability to use community emojis outside of
|
||||||
|
their community <b>(soon)</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Ability to upload and use gif emojis
|
||||||
|
<b>(soon)</b>
|
||||||
|
</li>
|
||||||
|
<li>Create infinite stack timelines</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}"
|
||||||
|
class="button"
|
||||||
|
target="_blank"
|
||||||
|
>Become a supporter</a
|
||||||
|
>
|
||||||
|
|
||||||
|
<span class="fade"
|
||||||
|
>Please use your <b>real email</b> when
|
||||||
|
completing payment. It is required to manage
|
||||||
|
your billing settings.</span
|
||||||
|
>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="w-full hidden flex flex-col gap-2" data-tab="profile">
|
<div class="w-full hidden flex flex-col gap-2" data-tab="profile">
|
||||||
<div class="card tertiary flex flex-col gap-2" id="profile_settings">
|
<div class="card tertiary flex flex-col gap-2" id="profile_settings">
|
||||||
|
{{ components::supporter_ad(body="Become a supporter to upload GIF
|
||||||
|
images!") }}
|
||||||
|
|
||||||
<div class="card-nest" ui_ident="change_avatar">
|
<div class="card-nest" ui_ident="change_avatar">
|
||||||
<div class="card small">
|
<div class="card small">
|
||||||
<b>{{ text "settings:label.change_avatar" }}</b>
|
<b>{{ text "settings:label.change_avatar" }}</b>
|
||||||
|
@ -358,7 +506,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="fade"
|
<span class="fade"
|
||||||
>Images must be less than 8 MB large. Animated GIFs are only supported for supporter users. GIFs can be at most 2 MB large.</span
|
>Images must be less than 8 MB large. Animated GIFs are
|
||||||
|
only supported for supporter users. GIFs can be at most
|
||||||
|
2 MB large.</span
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -477,6 +627,9 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{ components::supporter_ad(body="Become a supporter to add custom
|
||||||
|
CSS!") }}
|
||||||
|
|
||||||
<div class="card-nest" ui_ident="theme_preference">
|
<div class="card-nest" ui_ident="theme_preference">
|
||||||
<div class="card small">
|
<div class="card small">
|
||||||
<b>Theme preference</b>
|
<b>Theme preference</b>
|
||||||
|
@ -607,9 +760,9 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<label for="{{ key }}-shown" class="flex items-center gap-2">
|
<label for="{{ key }}-shown" class="flex items-center gap-2">
|
||||||
|
<!-- prettier-ignore -->
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
<!-- prettier-ignore -->
|
|
||||||
{% if value[0].show_on_profile %}checked{% endif %}
|
{% if value[0].show_on_profile %}checked{% endif %}
|
||||||
id="{{ key }}-shown"
|
id="{{ key }}-shown"
|
||||||
onchange="trigger('connections::push_con_shown', ['{{ key }}', event.target.checked])"
|
onchange="trigger('connections::push_con_shown', ['{{ key }}', event.target.checked])"
|
||||||
|
@ -933,18 +1086,20 @@
|
||||||
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_card",
|
"supporter_ad",
|
||||||
|
"account_settings_tabs",
|
||||||
"home_timeline",
|
"home_timeline",
|
||||||
"notifications",
|
"notifications",
|
||||||
"change_password",
|
|
||||||
"change_username",
|
"change_username",
|
||||||
"two_factor_authentication",
|
"delete_account",
|
||||||
]);
|
]);
|
||||||
ui.refresh_container(profile_settings, [
|
ui.refresh_container(profile_settings, [
|
||||||
|
"supporter_ad",
|
||||||
"change_avatar",
|
"change_avatar",
|
||||||
"change_banner",
|
"change_banner",
|
||||||
]);
|
]);
|
||||||
ui.refresh_container(theme_settings, [
|
ui.refresh_container(theme_settings, [
|
||||||
|
"supporter_ad",
|
||||||
"awful_contrast",
|
"awful_contrast",
|
||||||
"import_export",
|
"import_export",
|
||||||
"theme_preference",
|
"theme_preference",
|
||||||
|
@ -964,11 +1119,7 @@
|
||||||
settings.biography,
|
settings.biography,
|
||||||
"textarea",
|
"textarea",
|
||||||
],
|
],
|
||||||
[
|
[["status", "Status"], settings.status, "textarea"],
|
||||||
["status", "Status"],
|
|
||||||
settings.status,
|
|
||||||
"textarea",
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
["warning", "Profile warning"],
|
["warning", "Profile warning"],
|
||||||
settings.warning,
|
settings.warning,
|
||||||
|
|
|
@ -23,7 +23,7 @@ pub async fn stripe_webhook(
|
||||||
|
|
||||||
let req = match stripe::Webhook::construct_event(
|
let req = match stripe::Webhook::construct_event(
|
||||||
&body,
|
&body,
|
||||||
&sig.to_str().unwrap(),
|
sig.to_str().unwrap(),
|
||||||
&data.0.stripe.as_ref().unwrap().webhook_signing_secret,
|
&data.0.stripe.as_ref().unwrap().webhook_signing_secret,
|
||||||
) {
|
) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
|
|
|
@ -216,15 +216,13 @@ pub async fn handle_socket(socket: WebSocket, db: DataManager, community_id: Str
|
||||||
if !cs {
|
if !cs {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else if !headers.is_channel {
|
||||||
if !headers.is_channel {
|
// since we didn't select by just a channel, there HAS to be
|
||||||
// since we didn't select by just a channel, there HAS to be
|
// an entry for the channel for us to check this message
|
||||||
// an entry for the channel for us to check this message
|
continue;
|
||||||
continue;
|
// we don't need to check messages when we're subscribed to
|
||||||
// we don't need to check messages when we're subscribed to
|
// a channel, since that is checked on headers submission when
|
||||||
// a channel, since that is checked on headers submission when
|
// we subscribe to a channel
|
||||||
// we subscribe to a channel
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if sink.send(WsMessage::Text(smsg.into())).await.is_err() {
|
if sink.send(WsMessage::Text(smsg.into())).await.is_err() {
|
||||||
|
|
|
@ -67,7 +67,7 @@ pub async fn app_request(
|
||||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref channel) = channels.get(0) {
|
if let Some(channel) = channels.first() {
|
||||||
return Ok(Html(format!(
|
return Ok(Html(format!(
|
||||||
"<!doctype html><html><head><meta http-equiv=\"refresh\" content=\"0; url=/chats/{}/{}?nav={}\" /></head></html>",
|
"<!doctype html><html><head><meta http-equiv=\"refresh\" content=\"0; url=/chats/{}/{}?nav={}\" /></head></html>",
|
||||||
selected_community, channel.id, props.nav
|
selected_community, channel.id, props.nav
|
||||||
|
|
|
@ -56,6 +56,15 @@ pub async fn settings_request(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let blocks = match data
|
||||||
|
.0
|
||||||
|
.fill_userblocks_receivers(data.0.get_userblocks_by_initiator(profile.id).await)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &None).await)),
|
||||||
|
};
|
||||||
|
|
||||||
let tokens = profile.tokens.clone();
|
let tokens = profile.tokens.clone();
|
||||||
|
|
||||||
let lang = get_lang!(jar, data.0);
|
let lang = get_lang!(jar, data.0);
|
||||||
|
@ -63,6 +72,7 @@ pub async fn settings_request(
|
||||||
|
|
||||||
context.insert("profile", &profile);
|
context.insert("profile", &profile);
|
||||||
context.insert("stacks", &stacks);
|
context.insert("stacks", &stacks);
|
||||||
|
context.insert("blocks", &blocks);
|
||||||
context.insert("user_settings_serde", &clean_settings(&profile.settings));
|
context.insert("user_settings_serde", &clean_settings(&profile.settings));
|
||||||
context.insert(
|
context.insert(
|
||||||
"user_tokens_serde",
|
"user_tokens_serde",
|
||||||
|
|
|
@ -23,7 +23,7 @@ async-recursion = "1.1.1"
|
||||||
md-5 = "0.10.6"
|
md-5 = "0.10.6"
|
||||||
base16ct = { version = "0.2.0", features = ["alloc"] }
|
base16ct = { version = "0.2.0", features = ["alloc"] }
|
||||||
|
|
||||||
redis = { version = "0.30.0", features = [
|
redis = { version = "0.31.0", features = [
|
||||||
"aio",
|
"aio",
|
||||||
"tokio-comp",
|
"tokio-comp",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
|
|
@ -43,7 +43,7 @@ impl DataManager {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
|
|
||||||
for member in members {
|
for member in members {
|
||||||
if ignore_users.contains(&member) {
|
if ignore_users.contains(member) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ impl DataManager {
|
||||||
let mut users: HashMap<usize, User> = HashMap::new();
|
let mut users: HashMap<usize, User> = HashMap::new();
|
||||||
for (i, message) in messages.iter().enumerate() {
|
for (i, message) in messages.iter().enumerate() {
|
||||||
let next_owner: usize = match messages.get(i + 1) {
|
let next_owner: usize = match messages.get(i + 1) {
|
||||||
Some(ref m) => m.owner,
|
Some(m) => m.owner,
|
||||||
None => 0,
|
None => 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -171,10 +171,8 @@ impl DataManager {
|
||||||
let stack = self.get_stack_by_id(id).await?;
|
let stack = self.get_stack_by_id(id).await?;
|
||||||
|
|
||||||
// check user permission
|
// check user permission
|
||||||
if user.id != stack.owner {
|
if user.id != stack.owner && !user.permissions.check(FinePermission::MANAGE_STACKS) {
|
||||||
if !user.permissions.check(FinePermission::MANAGE_STACKS) {
|
return Err(Error::NotAllowed);
|
||||||
return Err(Error::NotAllowed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
|
|
|
@ -25,6 +25,17 @@ impl DataManager {
|
||||||
|
|
||||||
auto_method!(get_userblock_by_id()@get_userblock_from_row -> "SELECT * FROM userblocks WHERE id = $1" --name="user block" --returns=UserBlock --cache-key-tmpl="atto.userblock:{}");
|
auto_method!(get_userblock_by_id()@get_userblock_from_row -> "SELECT * FROM userblocks WHERE id = $1" --name="user block" --returns=UserBlock --cache-key-tmpl="atto.userblock:{}");
|
||||||
|
|
||||||
|
/// Fill a vector of user blocks with their receivers.
|
||||||
|
pub async fn fill_userblocks_receivers(&self, list: Vec<UserBlock>) -> Result<Vec<User>> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
for block in list {
|
||||||
|
out.push(self.get_user_by_id(block.receiver).await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a user block by `initiator` and `receiver` (in that order).
|
/// Get a user block by `initiator` and `receiver` (in that order).
|
||||||
pub async fn get_userblock_by_initiator_receiver(
|
pub async fn get_userblock_by_initiator_receiver(
|
||||||
&self,
|
&self,
|
||||||
|
@ -104,6 +115,28 @@ impl DataManager {
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all user blocks created by the given `initiator`.
|
||||||
|
pub async fn get_userblocks_by_initiator(&self, initiator: usize) -> Vec<UserBlock> {
|
||||||
|
let conn = match self.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => return Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = query_rows!(
|
||||||
|
&conn,
|
||||||
|
"SELECT * FROM userblocks WHERE initiator = $1",
|
||||||
|
&[&(initiator as i64)],
|
||||||
|
|x| { Self::get_userblock_from_row(x) }
|
||||||
|
);
|
||||||
|
|
||||||
|
if res.is_err() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// return
|
||||||
|
res.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new user block in the database.
|
/// Create a new user block in the database.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|
|
@ -111,22 +111,21 @@ impl CustomEmoji {
|
||||||
|
|
||||||
let mut chars = input.chars();
|
let mut chars = input.chars();
|
||||||
while let Some(char) = chars.next() {
|
while let Some(char) = chars.next() {
|
||||||
if char == '\\' && escape == false {
|
if char == '\\' && !escape {
|
||||||
escape = true;
|
escape = true;
|
||||||
continue;
|
continue;
|
||||||
} else if char == ':' && escape == false {
|
} else if char == ':' && !escape {
|
||||||
let mut community_id: String = String::new();
|
let mut community_id: String = String::new();
|
||||||
let mut accepting_community_id_chars: bool = true;
|
let mut accepting_community_id_chars: bool = true;
|
||||||
let mut emoji_name: String = String::new();
|
let mut emoji_name: String = String::new();
|
||||||
|
|
||||||
let mut char_count: u32 = 0; // if we're past the first 4 characters and we haven't hit a digit, stop accepting community id
|
for (char_count, char) in (0_u32..).zip(chars.by_ref()) {
|
||||||
while let Some(char) = chars.next() {
|
|
||||||
if (char == ':') | (char == ' ') {
|
if (char == ':') | (char == ' ') {
|
||||||
in_emoji = false;
|
in_emoji = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if char.is_digit(10) && accepting_community_id_chars {
|
if char.is_ascii_digit() && accepting_community_id_chars {
|
||||||
community_id.push(char);
|
community_id.push(char);
|
||||||
} else if char == '.' {
|
} else if char == '.' {
|
||||||
// the period closes the community id
|
// the period closes the community id
|
||||||
|
@ -138,8 +137,6 @@ impl CustomEmoji {
|
||||||
if char_count >= 4 && community_id.is_empty() {
|
if char_count >= 4 && community_id.is_empty() {
|
||||||
accepting_community_id_chars = false;
|
accepting_community_id_chars = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
char_count += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
out.push((
|
out.push((
|
||||||
|
|
|
@ -7,6 +7,12 @@ pub const EPOCH_2024: u64 = 1704067200000;
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct Snowflake(String);
|
pub struct Snowflake(String);
|
||||||
|
|
||||||
|
impl Default for Snowflake {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Snowflake {
|
impl Snowflake {
|
||||||
pub fn builder() -> Builder {
|
pub fn builder() -> Builder {
|
||||||
Builder::new().epoch(UNIX_EPOCH + Duration::from_millis(EPOCH_2024))
|
Builder::new().epoch(UNIX_EPOCH + Duration::from_millis(EPOCH_2024))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue