Merge remote-tracking branch 'origin/develop' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2025-02-10 23:22:19 +02:00
commit 33a239b89b
19 changed files with 447 additions and 398 deletions

View file

@ -0,0 +1 @@
Fix few markup panel inconsistencies; better ToS and registration

View file

View file

@ -67,19 +67,19 @@
"babel-plugin-lodash": "3.3.4", "babel-plugin-lodash": "3.3.4",
"chai": "4.5.0", "chai": "4.5.0",
"chalk": "5.4.1", "chalk": "5.4.1",
"chromedriver": "132.0.2", "chromedriver": "133.0.0",
"connect-history-api-fallback": "2.0.0", "connect-history-api-fallback": "2.0.0",
"copy-webpack-plugin": "12.0.2", "copy-webpack-plugin": "12.0.2",
"cross-spawn": "7.0.6", "cross-spawn": "7.0.6",
"css-loader": "7.1.2", "css-loader": "7.1.2",
"css-minimizer-webpack-plugin": "7.0.0", "css-minimizer-webpack-plugin": "7.0.0",
"custom-event-polyfill": "1.0.7", "custom-event-polyfill": "1.0.7",
"eslint": "9.19.0", "eslint": "9.20.0",
"eslint-config-standard": "17.1.0", "eslint-config-standard": "17.1.0",
"eslint-formatter-friendly": "7.0.0", "eslint-formatter-friendly": "7.0.0",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-n": "15.7.0", "eslint-plugin-n": "15.7.0",
"eslint-plugin-promise": "6.6.0", "eslint-plugin-promise": "7.2.1",
"eslint-plugin-vue": "9.32.0", "eslint-plugin-vue": "9.32.0",
"eslint-webpack-plugin": "4.2.0", "eslint-webpack-plugin": "4.2.0",
"eventsource-polyfill": "0.9.6", "eventsource-polyfill": "0.9.6",
@ -87,7 +87,7 @@
"function-bind": "1.1.2", "function-bind": "1.1.2",
"html-webpack-plugin": "5.6.3", "html-webpack-plugin": "5.6.3",
"http-proxy-middleware": "2.0.7", "http-proxy-middleware": "2.0.7",
"iso-639-1": "2.1.15", "iso-639-1": "3.1.5",
"json-loader": "0.5.7", "json-loader": "0.5.7",
"karma": "6.4.4", "karma": "6.4.4",
"karma-coverage": "2.2.1", "karma-coverage": "2.2.1",
@ -108,10 +108,10 @@
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"postcss-loader": "7.3.4", "postcss-loader": "7.3.4",
"postcss-scss": "^4.0.6", "postcss-scss": "^4.0.6",
"sass": "1.83.4", "sass": "1.84.0",
"sass-loader": "13.3.3", "sass-loader": "13.3.3",
"selenium-server": "3.141.59", "selenium-server": "3.141.59",
"semver": "7.7.0", "semver": "7.7.1",
"serviceworker-webpack5-plugin": "2.0.0", "serviceworker-webpack5-plugin": "2.0.0",
"shelljs": "0.8.5", "shelljs": "0.8.5",
"sinon": "15.2.0", "sinon": "15.2.0",

View file

@ -129,6 +129,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('theme') copyInstanceOption('theme')
copyInstanceOption('style') copyInstanceOption('style')
copyInstanceOption('palette') copyInstanceOption('palette')
copyInstanceOption('embeddedToS')
copyInstanceOption('nsfwCensorImage') copyInstanceOption('nsfwCensorImage')
copyInstanceOption('background') copyInstanceOption('background')
copyInstanceOption('hidePostStats') copyInstanceOption('hidePostStats')

View file

@ -34,7 +34,7 @@
</div> </div>
<div <div
v-else v-else
class="emtpy-chat-list-alert" class="empty-chat-list-alert"
> >
<span>{{ $t('chats.empty_chat_list_placeholder') }}</span> <span>{{ $t('chats.empty_chat_list_placeholder') }}</span>
</div> </div>
@ -50,7 +50,7 @@
margin-bottom: 0; margin-bottom: 0;
} }
.emtpy-chat-list-alert { .empty-chat-list-alert {
padding: 3em; padding: 3em;
font-size: 1.2em; font-size: 1.2em;
display: flex; display: flex;

View file

@ -1,15 +1,18 @@
<template> <template>
<div class="Drafts"> <div class="Drafts">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading -sticky">
<div class="title"> <div class="title">
{{ $t('drafts.drafts') }} {{ $t('drafts.drafts') }}
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p v-if="drafts.length === 0"> <div
v-if="drafts.length === 0"
class="empty-drafs-list-alert"
>
{{ $t('drafts.no_drafts') }} {{ $t('drafts.no_drafts') }}
</p> </div>
<List <List
v-else v-else
:items="drafts" :items="drafts"
@ -33,4 +36,13 @@
.draft { .draft {
margin: 1em 0; margin: 1em 0;
} }
.empty-drafs-list-alert {
padding: 3em;
font-size: 1.2em;
display: flex;
justify-content: center;
color: var(--textFaint);
}
</style> </style>

View file

@ -1,12 +1,12 @@
<template> <template>
<div class="features-panel"> <div class="features-panel">
<div class="panel panel-default base01-background"> <div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background base04"> <div class="panel-heading timeline-heading base02-background base04 -sticky">
<h1 class="title"> <h1 class="title">
{{ $t('features_panel.title') }} {{ $t('features_panel.title') }}
</h1> </h1>
</div> </div>
<div class="panel-body features-panel"> <div class="panel-body">
<ul> <ul>
<li v-if="shout"> <li v-if="shout">
{{ $t('features_panel.shout') }} {{ $t('features_panel.shout') }}

View file

@ -4,7 +4,7 @@
class="mrf-transparency-panel" class="mrf-transparency-panel"
> >
<div class="panel panel-default base01-background"> <div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background"> <div class="panel-heading timeline-heading base02-background -sticky">
<div class="title"> <div class="title">
{{ $t("about.mrf.federation") }} {{ $t("about.mrf.federation") }}
</div> </div>

View file

@ -4,6 +4,7 @@ import { mapActions, mapState } from 'vuex'
import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue' import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
import localeService from '../../services/locale/locale.service.js' import localeService from '../../services/locale/locale.service.js'
import { DAY } from 'src/services/date_utils/date_utils.js' import { DAY } from 'src/services/date_utils/date_utils.js'
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
const registration = { const registration = {
setup () { return { v$: useVuelidate() } }, setup () { return { v$: useVuelidate() } },
@ -21,7 +22,8 @@ const registration = {
captcha: {} captcha: {}
}), }),
components: { components: {
InterfaceLanguageSwitcher InterfaceLanguageSwitcher,
TermsOfServicePanel
}, },
validations () { validations () {
return { return {
@ -86,6 +88,7 @@ const registration = {
signUpNotice: (state) => state.users.signUpNotice, signUpNotice: (state) => state.users.signUpNotice,
hasSignUpNotice: (state) => !!state.users.signUpNotice.message, hasSignUpNotice: (state) => !!state.users.signUpNotice.message,
termsOfService: (state) => state.instance.tos, termsOfService: (state) => state.instance.tos,
embeddedToS: (state) => state.instance.embeddedToS,
accountActivationRequired: (state) => state.instance.accountActivationRequired, accountActivationRequired: (state) => state.instance.accountActivationRequired,
accountApprovalRequired: (state) => state.instance.accountApprovalRequired, accountApprovalRequired: (state) => state.instance.accountApprovalRequired,
birthdayRequired: (state) => state.instance.birthdayRequired, birthdayRequired: (state) => state.instance.birthdayRequired,

View file

@ -1,321 +1,325 @@
<template> <template>
<div class="settings panel panel-default"> <div class="column-inner">
<div class="panel-heading"> <TermsOfServicePanel v-if="!hasSignUpNotice && !embeddedToS" />
<h1 class="title"> <div class="settings panel panel-default">
{{ $t('registration.registration') }} <div class="panel-heading">
</h1> <h1 class="title">
</div> {{ $t('registration.registration') }}
<div </h1>
v-if="!hasSignUpNotice" </div>
class="panel-body" <div
> v-if="!hasSignUpNotice"
<form class="panel-body"
class="registration-form"
@submit.prevent="submit(user)"
> >
<div class="container"> <form
<div class="text-fields"> class="registration-form"
<div @submit.prevent="submit(user)"
class="form-group" >
:class="{ 'form-group--error': v$.user.username.$error }" <div class="container">
> <div class="text-fields">
<label <div
class="form--label" class="form-group"
for="sign-up-username" :class="{ 'form-group--error': v$.user.username.$error }"
>{{ $t('login.username') }}</label>
<input
id="sign-up-username"
v-model.trim="v$.user.username.$model"
:disabled="isPending"
class="input form-control"
:aria-required="true"
:placeholder="$t('registration.username_placeholder')"
> >
</div> <label
<div class="form--label"
v-if="v$.user.username.$dirty" for="sign-up-username"
class="form-error" >{{ $t('login.username') }}</label>
>
<ul>
<li v-if="!v$.user.username.required">
<span>{{ $t('registration.validations.username_required') }}</span>
</li>
</ul>
</div>
<div
class="form-group"
:class="{ 'form-group--error': v$.user.fullname.$error }"
>
<label
class="form--label"
for="sign-up-fullname"
>{{ $t('registration.fullname') }}</label>
<input
id="sign-up-fullname"
v-model.trim="v$.user.fullname.$model"
:disabled="isPending"
class="input form-control"
:aria-required="true"
:placeholder="$t('registration.fullname_placeholder')"
>
</div>
<div
v-if="v$.user.fullname.$dirty"
class="form-error"
>
<ul>
<li v-if="!v$.user.fullname.required">
<span>{{ $t('registration.validations.fullname_required') }}</span>
</li>
</ul>
</div>
<div
class="form-group"
:class="{ 'form-group--error': v$.user.email.$error }"
>
<label
class="form--label"
for="email"
>{{ accountActivationRequired ? $t('registration.email') : $t('registration.email_optional') }}</label>
<input
id="email"
v-model="v$.user.email.$model"
:disabled="isPending"
class="input form-control"
type="email"
:aria-required="accountActivationRequired"
>
</div>
<div
v-if="v$.user.email.$dirty"
class="form-error"
>
<ul>
<li v-if="!v$.user.email.required">
<span>{{ $t('registration.validations.email_required') }}</span>
</li>
</ul>
</div>
<div class="form-group">
<label
class="form--label"
for="bio"
>{{ $t('registration.bio_optional') }}</label>
<textarea
id="bio"
v-model="user.bio"
:disabled="isPending"
class="input form-control"
:placeholder="bioPlaceholder"
/>
</div>
<div
class="form-group"
:class="{ 'form-group--error': v$.user.password.$error }"
>
<label
class="form--label"
for="sign-up-password"
>{{ $t('login.password') }}</label>
<input
id="sign-up-password"
v-model="user.password"
:disabled="isPending"
class="input form-control"
type="password"
:aria-required="true"
>
</div>
<div
v-if="v$.user.password.$dirty"
class="form-error"
>
<ul>
<li v-if="!v$.user.password.required">
<span>{{ $t('registration.validations.password_required') }}</span>
</li>
</ul>
</div>
<div
class="form-group"
:class="{ 'form-group--error': v$.user.confirm.$error }"
>
<label
class="form--label"
for="sign-up-password-confirmation"
>{{ $t('registration.password_confirm') }}</label>
<input
id="sign-up-password-confirmation"
v-model="user.confirm"
:disabled="isPending"
class="input form-control"
type="password"
:aria-required="true"
>
</div>
<div
v-if="v$.user.confirm.$dirty"
class="form-error"
>
<ul>
<li v-if="v$.user.confirm.required.$invalid">
<span>{{ $t('registration.validations.password_confirmation_required') }}</span>
</li>
<li v-if="v$.user.confirm.sameAs.$invalid">
<span>{{ $t('registration.validations.password_confirmation_match') }}</span>
</li>
</ul>
</div>
<div
class="form-group"
:class="{ 'form-group--error': v$.user.birthday.$error }"
>
<label
class="form--label"
for="sign-up-birthday"
>
{{ birthdayRequired ? $t('registration.birthday') : $t('registration.birthday_optional') }}
</label>
<input
id="sign-up-birthday"
v-model="user.birthday"
:disabled="isPending"
class="input form-control"
type="date"
:max="birthdayRequired ? birthdayMinAttr : undefined"
:aria-required="birthdayRequired"
>
</div>
<div
v-if="v$.user.birthday.$dirty"
class="form-error"
>
<ul>
<li v-if="v$.user.birthday.required.$invalid">
<span>{{ $t('registration.validations.birthday_required') }}</span>
</li>
<li v-if="v$.user.birthday.maxValue.$invalid">
<span>{{ $t('registration.validations.birthday_min_age', { date: birthdayMinFormatted }) }}</span>
</li>
</ul>
</div>
<div
class="form-group"
:class="{ 'form-group--error': v$.user.language.$error }"
>
<interface-language-switcher
for="email-language"
:prompt-text="$t('registration.email_language')"
:language="v$.user.language.$model"
:set-language="val => v$.user.language.$model = val"
@click.stop.prevent
/>
</div>
<div
v-if="accountApprovalRequired"
class="form-group"
>
<label
class="form--label"
for="reason"
>{{ $t('registration.reason') }}</label>
<textarea
id="reason"
v-model="user.reason"
:disabled="isPending"
class="input form-control"
:placeholder="reasonPlaceholder"
/>
</div>
<div
v-if="captcha.type != 'none'"
id="captcha-group"
class="form-group"
>
<label
class="form--label"
for="captcha-label"
>{{ $t('registration.captcha') }}</label>
<template v-if="['kocaptcha', 'native'].includes(captcha.type)">
<img
:src="captcha.url"
@click="setCaptcha"
>
<sub>{{ $t('registration.new_captcha') }}</sub>
<input <input
id="captcha-answer" id="sign-up-username"
v-model="captcha.solution" v-model.trim="v$.user.username.$model"
:disabled="isPending" :disabled="isPending"
class="input form-control" class="input form-control"
type="text" :aria-required="true"
autocomplete="off" :placeholder="$t('registration.username_placeholder')"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
> >
</template> </div>
<div
v-if="v$.user.username.$dirty"
class="form-error"
>
<ul>
<li v-if="!v$.user.username.required">
<span>{{ $t('registration.validations.username_required') }}</span>
</li>
</ul>
</div>
<div
class="form-group"
:class="{ 'form-group--error': v$.user.fullname.$error }"
>
<label
class="form--label"
for="sign-up-fullname"
>{{ $t('registration.fullname') }}</label>
<input
id="sign-up-fullname"
v-model.trim="v$.user.fullname.$model"
:disabled="isPending"
class="input form-control"
:aria-required="true"
:placeholder="$t('registration.fullname_placeholder')"
>
</div>
<div
v-if="v$.user.fullname.$dirty"
class="form-error"
>
<ul>
<li v-if="!v$.user.fullname.required">
<span>{{ $t('registration.validations.fullname_required') }}</span>
</li>
</ul>
</div>
<div
class="form-group"
:class="{ 'form-group--error': v$.user.email.$error }"
>
<label
class="form--label"
for="email"
>{{ accountActivationRequired ? $t('registration.email') : $t('registration.email_optional') }}</label>
<input
id="email"
v-model="v$.user.email.$model"
:disabled="isPending"
class="input form-control"
type="email"
:aria-required="accountActivationRequired"
>
</div>
<div
v-if="v$.user.email.$dirty"
class="form-error"
>
<ul>
<li v-if="!v$.user.email.required">
<span>{{ $t('registration.validations.email_required') }}</span>
</li>
</ul>
</div>
<div class="form-group">
<label
class="form--label"
for="bio"
>{{ $t('registration.bio_optional') }}</label>
<textarea
id="bio"
v-model="user.bio"
:disabled="isPending"
class="input form-control"
:placeholder="bioPlaceholder"
/>
</div>
<div
class="form-group"
:class="{ 'form-group--error': v$.user.password.$error }"
>
<label
class="form--label"
for="sign-up-password"
>{{ $t('login.password') }}</label>
<input
id="sign-up-password"
v-model="user.password"
:disabled="isPending"
class="input form-control"
type="password"
:aria-required="true"
>
</div>
<div
v-if="v$.user.password.$dirty"
class="form-error"
>
<ul>
<li v-if="!v$.user.password.required">
<span>{{ $t('registration.validations.password_required') }}</span>
</li>
</ul>
</div>
<div
class="form-group"
:class="{ 'form-group--error': v$.user.confirm.$error }"
>
<label
class="form--label"
for="sign-up-password-confirmation"
>{{ $t('registration.password_confirm') }}</label>
<input
id="sign-up-password-confirmation"
v-model="user.confirm"
:disabled="isPending"
class="input form-control"
type="password"
:aria-required="true"
>
</div>
<div
v-if="v$.user.confirm.$dirty"
class="form-error"
>
<ul>
<li v-if="v$.user.confirm.required.$invalid">
<span>{{ $t('registration.validations.password_confirmation_required') }}</span>
</li>
<li v-if="v$.user.confirm.sameAs.$invalid">
<span>{{ $t('registration.validations.password_confirmation_match') }}</span>
</li>
</ul>
</div>
<div
class="form-group"
:class="{ 'form-group--error': v$.user.birthday.$error }"
>
<label
class="form--label"
for="sign-up-birthday"
>
{{ birthdayRequired ? $t('registration.birthday') : $t('registration.birthday_optional') }}
</label>
<input
id="sign-up-birthday"
v-model="user.birthday"
:disabled="isPending"
class="input form-control"
type="date"
:max="birthdayRequired ? birthdayMinAttr : undefined"
:aria-required="birthdayRequired"
>
</div>
<div
v-if="v$.user.birthday.$dirty"
class="form-error"
>
<ul>
<li v-if="v$.user.birthday.required.$invalid">
<span>{{ $t('registration.validations.birthday_required') }}</span>
</li>
<li v-if="v$.user.birthday.maxValue.$invalid">
<span>{{ $t('registration.validations.birthday_min_age', { date: birthdayMinFormatted }) }}</span>
</li>
</ul>
</div>
<div
class="form-group"
:class="{ 'form-group--error': v$.user.language.$error }"
>
<interface-language-switcher
for="email-language"
:prompt-text="$t('registration.email_language')"
:language="v$.user.language.$model"
:set-language="val => v$.user.language.$model = val"
@click.stop.prevent
/>
</div>
<div
v-if="accountApprovalRequired"
class="form-group"
>
<label
class="form--label"
for="reason"
>{{ $t('registration.reason') }}</label>
<textarea
id="reason"
v-model="user.reason"
:disabled="isPending"
class="input form-control"
:placeholder="reasonPlaceholder"
/>
</div>
<div
v-if="captcha.type != 'none'"
id="captcha-group"
class="form-group"
>
<label
class="form--label"
for="captcha-label"
>{{ $t('registration.captcha') }}</label>
<template v-if="['kocaptcha', 'native'].includes(captcha.type)">
<img
:src="captcha.url"
@click="setCaptcha"
>
<sub>{{ $t('registration.new_captcha') }}</sub>
<input
id="captcha-answer"
v-model="captcha.solution"
:disabled="isPending"
class="input form-control"
type="text"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
>
</template>
</div>
<div
v-if="token"
class="form-group"
>
<label for="token">{{ $t('registration.token') }}</label>
<input
id="token"
v-model="token"
disabled="true"
class="input form-control"
type="text"
>
</div>
<div class="form-group">
<button
:disabled="isPending"
type="submit"
class="btn button-default"
>
{{ $t('registration.register') }}
</button>
</div>
</div> </div>
<!-- eslint-disable vue/no-v-html -->
<div <div
v-if="token" v-if="embeddedToS"
class="form-group" class="terms-of-service"
> v-html="termsOfService"
<label for="token">{{ $t('registration.token') }}</label> />
<input
id="token"
v-model="token"
disabled="true"
class="input form-control"
type="text"
>
</div>
<div class="form-group">
<button
:disabled="isPending"
type="submit"
class="btn button-default"
>
{{ $t('registration.register') }}
</button>
</div>
</div>
<!-- eslint-disable vue/no-v-html -->
<div
class="terms-of-service"
v-html="termsOfService"
/>
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
</div>
<div
v-if="serverValidationErrors.length"
class="form-group"
>
<div class="alert error">
<span
v-for="error in serverValidationErrors"
:key="error"
>{{ error }}</span>
</div> </div>
</div> <div
</form> v-if="serverValidationErrors.length"
</div> class="form-group"
<div v-else> >
<p class="registration-notice"> <div class="alert error">
{{ signUpNotice.message }} <span
</p> v-for="error in serverValidationErrors"
:key="error"
>{{ error }}</span>
</div>
</div>
</form>
</div>
<div v-else>
<p class="registration-notice">
{{ signUpNotice.message }}
</p>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -325,7 +329,7 @@
.registration-form { .registration-form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 0.6em; margin: 1em;
.container { .container {
display: flex; display: flex;
@ -338,7 +342,7 @@
.terms-of-service { .terms-of-service {
flex: 0 1 50%; flex: 0 1 50%;
margin: 0.8em; margin: 0.6em 0 0 0.8em;
} }
.text-fields { .text-fields {
@ -417,6 +421,10 @@
@media all and (max-width: 800px) { @media all and (max-width: 800px) {
.registration-form .container { .registration-form .container {
flex-direction: column-reverse; flex-direction: column-reverse;
.terms-of-service {
margin: 0;
}
} }
} }
</style> </style>

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="staff-panel"> <div class="staff-panel">
<div class="panel panel-default base01-background"> <div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background"> <div class="panel-heading timeline-heading base02-background -sticky">
<div class="title"> <div class="title">
{{ $t("about.staff") }} {{ $t("about.staff") }}
</div> </div>
@ -28,8 +28,7 @@
<style lang="scss"> <style lang="scss">
.staff-group { .staff-group {
padding-left: 1em; padding: 1em;
padding-top: 1em;
.basic-user-card { .basic-user-card {
padding-left: 0; padding-left: 0;

View file

@ -2,6 +2,9 @@ const TermsOfServicePanel = {
computed: { computed: {
content () { content () {
return this.$store.state.instance.tos return this.$store.state.instance.tos
},
embedded () {
return this.$store.state.instance.embeddedToS
} }
} }
} }

View file

@ -1,13 +1,21 @@
<template> <template>
<div> <div class="terms-of-service-panel">
<div class="panel panel-default"> <div class="panel panel-default">
<div
v-if="!embedded"
class="panel-heading -sticky"
>
<div class="title">
{{ $t("about.terms") }}
</div>
</div>
<div class="panel-body"> <div class="panel-body">
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div <div
class="tos-content" class="tos-content"
v-html="content" v-html="content"
/> />
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
</div> </div>
</div> </div>
</div> </div>
@ -19,4 +27,5 @@
.tos-content { .tos-content {
margin: 1em; margin: 1em;
} }
</style> </style>

View file

@ -275,7 +275,10 @@
/> />
</div> </div>
</div> </div>
<div v-if="!hideBio"> <div
v-if="!hideBio"
class="user-bio"
>
<div <div
v-if="!mergedConfig.hideUserStats && switcher" v-if="!mergedConfig.hideUserStats && switcher"
class="user-counts" class="user-counts"

View file

@ -30,7 +30,8 @@
"media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:" "media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:"
} }
}, },
"staff": "Staff" "staff": "Staff",
"terms": "Terms of Service"
}, },
"announcements": { "announcements": {
"page_header": "Announcements", "page_header": "Announcements",

View file

@ -1,11 +0,0 @@
import { capitalize } from 'lodash'
export function humanizeErrors (errors) {
return Object.entries(errors).reduce((errs, [k, val]) => {
const message = val.reduce((acc, message) => {
const key = capitalize(k.replace(/_/g, ' '))
return acc + [key, message].join(' ') + '. '
}, '')
return [...errs, message]
}, [])
}

View file

@ -54,6 +54,7 @@ const defaultState = {
defaultAvatar: '/images/avi.png', defaultAvatar: '/images/avi.png',
defaultBanner: '/images/banner.png', defaultBanner: '/images/banner.png',
background: '/static/aurora_borealis.jpg', background: '/static/aurora_borealis.jpg',
embeddedToS: true,
collapseMessageWithSubject: false, collapseMessageWithSubject: false,
greentext: false, greentext: false,
useAtIcon: false, useAtIcon: false,

View file

@ -1,4 +1,14 @@
import { humanizeErrors } from '../../modules/errors' import { capitalize } from 'lodash'
function humanizeErrors (errors) {
return Object.entries(errors).reduce((errs, [k, val]) => {
const message = val.reduce((acc, message) => {
const key = capitalize(k.replace(/_/g, ' '))
return acc + [key, message].join(' ') + '. '
}, '')
return [...errs, message]
}, [])
}
export function StatusCodeError (statusCode, body, options, response) { export function StatusCodeError (statusCode, body, options, response) {
this.name = 'StatusCodeError' this.name = 'StatusCodeError'

121
yarn.lock
View file

@ -882,6 +882,13 @@
dependencies: dependencies:
"@types/json-schema" "^7.0.15" "@types/json-schema" "^7.0.15"
"@eslint/core@^0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.11.0.tgz#7a9226e850922e42cbd2ba71361eacbe74352a12"
integrity sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==
dependencies:
"@types/json-schema" "^7.0.15"
"@eslint/eslintrc@^3.2.0": "@eslint/eslintrc@^3.2.0":
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.2.0.tgz#57470ac4e2e283a6bf76044d63281196e370542c" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.2.0.tgz#57470ac4e2e283a6bf76044d63281196e370542c"
@ -897,10 +904,10 @@
minimatch "^3.1.2" minimatch "^3.1.2"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@eslint/js@9.19.0": "@eslint/js@9.20.0":
version "9.19.0" version "9.20.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.19.0.tgz#51dbb140ed6b49d05adc0b171c41e1a8713b7789" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.20.0.tgz#7421bcbe74889fcd65d1be59f00130c289856eb4"
integrity sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ== integrity sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==
"@eslint/object-schema@^2.1.6": "@eslint/object-schema@^2.1.6":
version "2.1.6" version "2.1.6"
@ -1430,9 +1437,9 @@
integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==
"@types/node@*", "@types/node@>=10.0.0": "@types/node@*", "@types/node@>=10.0.0":
version "22.13.1" version "22.13.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.1.tgz#a2a3fefbdeb7ba6b89f40371842162fac0934f33" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.0.tgz#d376dd9a0ee2f9382d86c2d5d7beb4d198b4ea8c"
integrity sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew== integrity sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==
dependencies: dependencies:
undici-types "~6.20.0" undici-types "~6.20.0"
@ -2358,9 +2365,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0" lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001688: caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001688:
version "1.0.30001697" version "1.0.30001696"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001697.tgz#040bbbb54463c4b4b3377c716b34a322d16e6fc7" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz#00c30a2fc11e3c98c25e5125418752af3ae2f49f"
integrity sha512-GwNPlWJin8E+d7Gxq96jxM6w0w+VFeyyXRsjU58emtkYqnbwHqXm5uT2uCmO0RQE9htWknOP4xtBlLmM/gWxvQ== integrity sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==
chai-nightwatch@0.5.3: chai-nightwatch@0.5.3:
version "0.5.3" version "0.5.3"
@ -2474,10 +2481,10 @@ chrome-trace-event@^1.0.2:
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b"
integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==
chromedriver@132.0.2: chromedriver@133.0.0:
version "132.0.2" version "133.0.0"
resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-132.0.2.tgz#9bba1d82b14346769a37d708846da7c2d3110fcd" resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-133.0.0.tgz#56dedd23974f986ef5fb1ec3402d2d5b845e35dc"
integrity sha512-aywIWYggkAwdFN5zkyYHXUyUBJt2hFMweS3XU0XINIdfOG386z+sqAlExB4P1A10cSz+SO5gxcCgxd8UJNBvgg== integrity sha512-7arRrtD9WGSlemMLE4IOoD42OSKKyOtQP/Z0x/WB5jYSaCzcI95j67EK0wQ2w1y5IjSJnYvnmXOJM6Nla4OG2w==
dependencies: dependencies:
"@testim/chrome-version" "^1.1.4" "@testim/chrome-version" "^1.1.4"
axios "^1.7.4" axios "^1.7.4"
@ -3311,9 +3318,9 @@ ejs@3.1.8:
jake "^10.8.5" jake "^10.8.5"
electron-to-chromium@^1.5.73: electron-to-chromium@^1.5.73:
version "1.5.92" version "1.5.90"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.92.tgz#81e8ebe06f8e2a49fdba84bd10e9ad5b63efffe0" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.90.tgz#4717e5a5413f95bbb12d0af14c35057e9c65e0b6"
integrity sha512-BeHgmNobs05N1HMmMZ7YIuHfYBGlq/UmvlsTgg+fsbFs9xVMj+xJHFg19GN04+9Q+r8Xnh9LXqaYIyEWElnNgQ== integrity sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==
emoji-regex@^8.0.0: emoji-regex@^8.0.0:
version "8.0.0" version "8.0.0"
@ -3368,9 +3375,9 @@ engine.io@~6.6.0:
ws "~8.17.1" ws "~8.17.1"
enhanced-resolve@^5.17.1: enhanced-resolve@^5.17.1:
version "5.18.1" version "5.18.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz#91eb1db193896b9801251eeff1c6980278b1e404"
integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg== integrity sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==
dependencies: dependencies:
graceful-fs "^4.2.4" graceful-fs "^4.2.4"
tapable "^2.2.0" tapable "^2.2.0"
@ -3629,10 +3636,12 @@ eslint-plugin-n@15.7.0:
resolve "^1.22.1" resolve "^1.22.1"
semver "^7.3.8" semver "^7.3.8"
eslint-plugin-promise@6.6.0: eslint-plugin-promise@7.2.1:
version "6.6.0" version "7.2.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz#acd3fd7d55cead7a10f92cf698f36c0aafcd717a" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-7.2.1.tgz#a0652195700aea40b926dc3c74b38e373377bfb0"
integrity sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ== integrity sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
eslint-plugin-vue@9.32.0: eslint-plugin-vue@9.32.0:
version "9.32.0" version "9.32.0"
@ -3717,17 +3726,17 @@ eslint-webpack-plugin@4.2.0:
normalize-path "^3.0.0" normalize-path "^3.0.0"
schema-utils "^4.2.0" schema-utils "^4.2.0"
eslint@9.19.0: eslint@9.20.0:
version "9.19.0" version "9.20.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.19.0.tgz#ffa1d265fc4205e0f8464330d35f09e1d548b1bf" resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.20.0.tgz#6244c46c1640cd5e577a31ebc460fca87838c0b7"
integrity sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA== integrity sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA==
dependencies: dependencies:
"@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.12.1" "@eslint-community/regexpp" "^4.12.1"
"@eslint/config-array" "^0.19.0" "@eslint/config-array" "^0.19.0"
"@eslint/core" "^0.10.0" "@eslint/core" "^0.11.0"
"@eslint/eslintrc" "^3.2.0" "@eslint/eslintrc" "^3.2.0"
"@eslint/js" "9.19.0" "@eslint/js" "9.20.0"
"@eslint/plugin-kit" "^0.2.5" "@eslint/plugin-kit" "^0.2.5"
"@humanfs/node" "^0.16.6" "@humanfs/node" "^0.16.6"
"@humanwhocodes/module-importer" "^1.0.1" "@humanwhocodes/module-importer" "^1.0.1"
@ -4768,11 +4777,11 @@ is-binary-path@~2.1.0:
binary-extensions "^2.0.0" binary-extensions "^2.0.0"
is-boolean-object@^1.2.1: is-boolean-object@^1.2.1:
version "1.2.2" version "1.2.1"
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.1.tgz#c20d0c654be05da4fbc23c562635c019e93daf89"
integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A== integrity sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==
dependencies: dependencies:
call-bound "^1.0.3" call-bound "^1.0.2"
has-tostringtag "^1.0.2" has-tostringtag "^1.0.2"
is-callable@^1.2.7: is-callable@^1.2.7:
@ -4960,11 +4969,11 @@ is-weakmap@^2.0.2:
integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==
is-weakref@^1.0.2, is-weakref@^1.1.0: is-weakref@^1.0.2, is-weakref@^1.1.0:
version "1.1.1" version "1.1.0"
resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.1.tgz#eea430182be8d64174bd96bffbc46f21bf3f9293" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.0.tgz#47e3472ae95a63fa9cf25660bcf0c181c39770ef"
integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== integrity sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==
dependencies: dependencies:
call-bound "^1.0.3" call-bound "^1.0.2"
is-weakset@^2.0.3: is-weakset@^2.0.3:
version "2.0.4" version "2.0.4"
@ -5015,10 +5024,10 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
iso-639-1@2.1.15: iso-639-1@3.1.5:
version "2.1.15" version "3.1.5"
resolved "https://registry.yarnpkg.com/iso-639-1/-/iso-639-1-2.1.15.tgz#20cf78a4f691aeb802c16f17a6bad7d99271e85d" resolved "https://registry.yarnpkg.com/iso-639-1/-/iso-639-1-3.1.5.tgz#e8205aceeeea0f64d6b12f5fac6a943b0d5b452c"
integrity sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg== integrity sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA==
isobject@^3.0.1: isobject@^3.0.1:
version "3.0.1" version "3.0.1"
@ -6292,9 +6301,9 @@ object-assign@^4:
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-inspect@^1.13.3: object-inspect@^1.13.3:
version "1.13.4" version "1.13.3"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a"
integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==
object-keys@^1.1.1: object-keys@^1.1.1:
version "1.1.1" version "1.1.1"
@ -7467,10 +7476,10 @@ sass-loader@13.3.3:
dependencies: dependencies:
neo-async "^2.6.2" neo-async "^2.6.2"
sass@1.83.4: sass@1.84.0:
version "1.83.4" version "1.84.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.83.4.tgz#5ccf60f43eb61eeec300b780b8dcb85f16eec6d1" resolved "https://registry.yarnpkg.com/sass/-/sass-1.84.0.tgz#da9154cbccb2d2eac7a9486091b6d9ba93ef5bad"
integrity sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA== integrity sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg==
dependencies: dependencies:
chokidar "^4.0.0" chokidar "^4.0.0"
immutable "^5.0.2" immutable "^5.0.2"
@ -7530,10 +7539,10 @@ semver@7.3.5:
dependencies: dependencies:
lru-cache "^6.0.0" lru-cache "^6.0.0"
semver@7.7.0: semver@7.7.1:
version "7.7.0" version "7.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.0.tgz#9c6fe61d0c6f9fa9e26575162ee5a9180361b09c" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
integrity sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ== integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
semver@^6.3.0, semver@^6.3.1: semver@^6.3.0, semver@^6.3.1:
version "6.3.1" version "6.3.1"
@ -7541,9 +7550,9 @@ semver@^6.3.0, semver@^6.3.1:
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.0.0, semver@^7.3.4, semver@^7.3.5, semver@^7.3.6, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3: semver@^7.0.0, semver@^7.3.4, semver@^7.3.5, semver@^7.3.6, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3:
version "7.7.1" version "7.7.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.0.tgz#9c6fe61d0c6f9fa9e26575162ee5a9180361b09c"
integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== integrity sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==
send@0.19.0: send@0.19.0:
version "0.19.0" version "0.19.0"