diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-04-19 21:24:31 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-04-19 21:24:31 +0900 |
| commit | e1f9ab77f86f5a12091c864cdb502970715cd46e (patch) | |
| tree | 46990eae87d352e6674e43a64c3bdcd74c13119e /packages/frontend/src | |
| parent | Update test-frontend.yml (diff) | |
| download | sharkey-e1f9ab77f86f5a12091c864cdb502970715cd46e.tar.gz sharkey-e1f9ab77f86f5a12091c864cdb502970715cd46e.tar.bz2 sharkey-e1f9ab77f86f5a12091c864cdb502970715cd46e.zip | |
feat: Server rules (#10660)
* enhance(frontend): サーバールールのデザイン調整
* enhance(frontend): i18n
* enhance(frontend): 利用規約URLの設定を「モデレーション」ページへ移動
* enhance(frontend): サーバールールのデザイン調整
* Update CHANGELOG.md
* 不要な差分を削除
* fix(frontend): lint
* ui tweak
* test: add stories
* tweak
* test: bind args
* test: add interaction tests
* fix bug
* Update packages/frontend/src/pages/admin/server-rules.vue
Co-authored-by: Ebise Lutica <7106976+EbiseLutica@users.noreply.github.com>
* Update misskey-js.api.md
* chore: windowを明示
* :art:
* refactor
* :art:
* :art:
* fix e2e test
* :art:
* :art:
* fix icon
* fix e2e
---------
Co-authored-by: Ebise Lutica <7106976+EbiseLutica@users.noreply.github.com>
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
Diffstat (limited to 'packages/frontend/src')
| -rw-r--r-- | packages/frontend/src/components/MkFolder.vue | 8 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkModal.vue | 6 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkModalWindow.vue | 5 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkSignup.vue | 263 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkSignupDialog.form.vue | 272 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts | 94 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkSignupDialog.rules.vue | 114 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkSignupDialog.vue | 45 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkSwitch.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/pages/about.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/pages/admin/moderation.vue | 9 | ||||
| -rw-r--r-- | packages/frontend/src/pages/admin/server-rules.vue | 128 | ||||
| -rw-r--r-- | packages/frontend/src/pages/admin/settings.vue | 8 | ||||
| -rw-r--r-- | packages/frontend/src/router.ts | 4 | ||||
| -rw-r--r-- | packages/frontend/src/store.ts | 4 |
15 files changed, 670 insertions, 294 deletions
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index 58cc0de5c8..fd070a5f13 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -1,8 +1,8 @@ <template> -<div ref="rootEl" :class="$style.root"> +<div ref="rootEl" :class="$style.root" role="group" :aria-expanded="opened"> <MkStickyContainer> <template #header> - <div :class="[$style.header, { [$style.opened]: opened }]" class="_button" @click="toggle"> + <div :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle"> <div :class="$style.headerIcon"><slot name="icon"></slot></div> <div :class="$style.headerText"> <div :class="$style.headerTextMain"> @@ -20,7 +20,7 @@ </div> </template> - <div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null, overflow: maxHeight ? `auto` : null }"> + <div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null, overflow: maxHeight ? `auto` : null }" :aria-hidden="!opened"> <Transition :enter-active-class="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''" :leave-active-class="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''" @@ -196,7 +196,7 @@ onMounted(() => { .headerRight { margin-left: auto; - opacity: 0.7; + color: var(--fgTransparentWeak); white-space: nowrap; } diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index 852c72f6ff..99df9e8150 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -404,16 +404,10 @@ defineExpose({ right: 0; margin: auto; padding: 32px; - // TODO: mask-imageはiOSだとやたら重い。なんとかしたい - -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%); - mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%); - overflow: auto; display: flex; @media (max-width: 500px) { padding: 16px; - -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%); - mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%); } } } diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue index dd115246ff..2d2f8411f1 100644 --- a/packages/frontend/src/components/MkModalWindow.vue +++ b/packages/frontend/src/components/MkModalWindow.vue @@ -1,6 +1,6 @@ <template> <MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')"> - <div ref="rootEl" class="ebkgoccj" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown"> + <div ref="rootEl" class="ebkgoccj" :style="{ width: `${width}px`, height: height ? `${height}px` : null }" @keydown="onKeydown"> <div ref="headerEl" class="header"> <button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button> <span class="title"> @@ -25,13 +25,11 @@ const props = withDefaults(defineProps<{ okButtonDisabled: boolean; width: number; height: number | null; - scroll: boolean; }>(), { withOkButton: false, okButtonDisabled: false, width: 400, height: null, - scroll: true, }); const emit = defineEmits<{ @@ -86,6 +84,7 @@ defineExpose({ <style lang="scss" scoped> .ebkgoccj { margin: auto; + max-height: 100%; overflow: hidden; display: flex; flex-direction: column; diff --git a/packages/frontend/src/components/MkSignup.vue b/packages/frontend/src/components/MkSignup.vue deleted file mode 100644 index 30279148f8..0000000000 --- a/packages/frontend/src/components/MkSignup.vue +++ /dev/null @@ -1,263 +0,0 @@ -<template> -<form class="qlvuhzng _gaps_m" autocomplete="new-password" @submit.prevent="onSubmit"> - <MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required> - <template #label>{{ i18n.ts.invitationCode }}</template> - <template #prefix><i class="ti ti-key"></i></template> - </MkInput> - <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:model-value="onChangeUsername"> - <template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-question-circle"></i></div></template> - <template #prefix>@</template> - <template #suffix>@{{ host }}</template> - <template #caption> - <div><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div> - <span v-if="usernameState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span> - <span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> - <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> - <span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> - <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span> - <span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span> - <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span> - </template> - </MkInput> - <MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:model-value="onChangeEmail"> - <template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ti ti-question-circle"></i></div></template> - <template #prefix><i class="ti ti-mail"></i></template> - <template #caption> - <span v-if="emailState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span> - <span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> - <span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span> - <span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span> - <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span> - <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span> - <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span> - <span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> - <span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> - </template> - </MkInput> - <MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:model-value="onChangePassword"> - <template #label>{{ i18n.ts.password }}</template> - <template #prefix><i class="ti ti-lock"></i></template> - <template #caption> - <span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span> - <span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span> - <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span> - </template> - </MkInput> - <MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:model-value="onChangePasswordRetype"> - <template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template> - <template #prefix><i class="ti ti-lock"></i></template> - <template #caption> - <span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span> - <span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span> - </template> - </MkInput> - <MkSwitch v-model="ToSAgreement" class="tou"> - <template #label>{{ i18n.ts.agreeBelow }}</template> - </MkSwitch> - <ul style="margin: 0; padding-left: 2em;"> - <li v-if="instance.tosUrl"><a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.tos }}</a></li> - <li><a href="https://misskey-hub.net/docs/notes.html" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }}</a></li> - </ul> - <MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/> - <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/> - <MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" class="captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/> - <MkButton type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ i18n.ts.start }}</MkButton> -</form> -</template> - -<script lang="ts" setup> -import { } from 'vue'; -import getPasswordStrength from 'syuilo-password-strength'; -import { toUnicode } from 'punycode/'; -import MkButton from './MkButton.vue'; -import MkInput from './MkInput.vue'; -import MkSwitch from './MkSwitch.vue'; -import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue'; -import * as config from '@/config'; -import * as os from '@/os'; -import { login } from '@/account'; -import { instance } from '@/instance'; -import { i18n } from '@/i18n'; - -const props = withDefaults(defineProps<{ - autoSet?: boolean; -}>(), { - autoSet: false, -}); - -const emit = defineEmits<{ - (ev: 'signup', user: Record<string, any>): void; - (ev: 'signupEmailPending'): void; -}>(); - -const host = toUnicode(config.host); - -let hcaptcha = $ref<Captcha | undefined>(); -let recaptcha = $ref<Captcha | undefined>(); -let turnstile = $ref<Captcha | undefined>(); - -let username: string = $ref(''); -let password: string = $ref(''); -let retypedPassword: string = $ref(''); -let invitationCode: string = $ref(''); -let email = $ref(''); -let usernameState: null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range' = $ref(null); -let emailState: null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error' = $ref(null); -let passwordStrength: '' | 'low' | 'medium' | 'high' = $ref(''); -let passwordRetypeState: null | 'match' | 'not-match' = $ref(null); -let submitting: boolean = $ref(false); -let ToSAgreement: boolean = $ref(false); -let hCaptchaResponse = $ref(null); -let reCaptchaResponse = $ref(null); -let turnstileResponse = $ref(null); -let usernameAbortController: null | AbortController = $ref(null); -let emailAbortController: null | AbortController = $ref(null); - -const shouldDisableSubmitting = $computed((): boolean => { - return submitting || - instance.tosUrl && !ToSAgreement || - instance.enableHcaptcha && !hCaptchaResponse || - instance.enableRecaptcha && !reCaptchaResponse || - instance.enableTurnstile && !turnstileResponse || - instance.emailRequiredForSignup && emailState !== 'ok' || - usernameState !== 'ok' || - passwordRetypeState !== 'match'; -}); - -function onChangeUsername(): void { - if (username === '') { - usernameState = null; - return; - } - - { - const err = - !username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' : - username.length < 1 ? 'min-range' : - username.length > 20 ? 'max-range' : - null; - - if (err) { - usernameState = err; - return; - } - } - - if (usernameAbortController != null) { - usernameAbortController.abort(); - } - usernameState = 'wait'; - usernameAbortController = new AbortController(); - - os.api('username/available', { - username, - }, undefined, usernameAbortController.signal).then(result => { - usernameState = result.available ? 'ok' : 'unavailable'; - }).catch((err) => { - if (err.name !== 'AbortError') { - usernameState = 'error'; - } - }); -} - -function onChangeEmail(): void { - if (email === '') { - emailState = null; - return; - } - - if (emailAbortController != null) { - emailAbortController.abort(); - } - emailState = 'wait'; - emailAbortController = new AbortController(); - - os.api('email-address/available', { - emailAddress: email, - }, undefined, emailAbortController.signal).then(result => { - emailState = result.available ? 'ok' : - result.reason === 'used' ? 'unavailable:used' : - result.reason === 'format' ? 'unavailable:format' : - result.reason === 'disposable' ? 'unavailable:disposable' : - result.reason === 'mx' ? 'unavailable:mx' : - result.reason === 'smtp' ? 'unavailable:smtp' : - 'unavailable'; - }).catch((err) => { - if (err.name !== 'AbortError') { - emailState = 'error'; - } - }); -} - -function onChangePassword(): void { - if (password === '') { - passwordStrength = ''; - return; - } - - const strength = getPasswordStrength(password); - passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; -} - -function onChangePasswordRetype(): void { - if (retypedPassword === '') { - passwordRetypeState = null; - return; - } - - passwordRetypeState = password === retypedPassword ? 'match' : 'not-match'; -} - -async function onSubmit(): Promise<void> { - if (submitting) return; - submitting = true; - - try { - await os.api('signup', { - username, - password, - emailAddress: email, - invitationCode, - 'hcaptcha-response': hCaptchaResponse, - 'g-recaptcha-response': reCaptchaResponse, - 'turnstile-response': turnstileResponse, - }); - if (instance.emailRequiredForSignup) { - os.alert({ - type: 'success', - title: i18n.ts._signup.almostThere, - text: i18n.t('_signup.emailSent', { email }), - }); - emit('signupEmailPending'); - } else { - const res = await os.api('signin', { - username, - password, - }); - emit('signup', res); - - if (props.autoSet) { - return login(res.i); - } - } - } catch { - submitting = false; - hcaptcha?.reset?.(); - recaptcha?.reset?.(); - turnstile?.reset?.(); - - os.alert({ - type: 'error', - text: i18n.ts.somethingHappened, - }); - } -} -</script> - -<style lang="scss" scoped> -.qlvuhzng { - .captcha { - margin: 16px 0; - } -} -</style> diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue new file mode 100644 index 0000000000..0e8bdb321e --- /dev/null +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -0,0 +1,272 @@ +<template> +<div> + <div :class="$style.banner"> + <i class="ti ti-user-edit"></i> + </div> + <MkSpacer :margin-min="20" :margin-max="32"> + <form class="_gaps_m" autocomplete="new-password" @submit.prevent="onSubmit"> + <MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required> + <template #label>{{ i18n.ts.invitationCode }}</template> + <template #prefix><i class="ti ti-key"></i></template> + </MkInput> + <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:model-value="onChangeUsername"> + <template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template> + <template #prefix>@</template> + <template #suffix>@{{ host }}</template> + <template #caption> + <div><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div> + <span v-if="usernameState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span> + <span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> + <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> + <span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> + <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span> + <span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span> + <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span> + </template> + </MkInput> + <MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:model-value="onChangeEmail"> + <template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template> + <template #prefix><i class="ti ti-mail"></i></template> + <template #caption> + <span v-if="emailState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span> + <span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> + <span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span> + <span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span> + <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span> + <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span> + <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span> + <span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> + <span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> + </template> + </MkInput> + <MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:model-value="onChangePassword"> + <template #label>{{ i18n.ts.password }}</template> + <template #prefix><i class="ti ti-lock"></i></template> + <template #caption> + <span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span> + <span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span> + <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span> + </template> + </MkInput> + <MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:model-value="onChangePasswordRetype"> + <template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template> + <template #prefix><i class="ti ti-lock"></i></template> + <template #caption> + <span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span> + <span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span> + </template> + </MkInput> + <MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/> + <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/> + <MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/> + <MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;"> + <template v-if="submitting"> + <MkLoading :em="true" :colored="false"/> + </template> + <template v-else>{{ i18n.ts.start }}</template> + </MkButton> + </form> + </MkSpacer> +</div> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import getPasswordStrength from 'syuilo-password-strength'; +import { toUnicode } from 'punycode/'; +import MkButton from './MkButton.vue'; +import MkInput from './MkInput.vue'; +import MkSwitch from './MkSwitch.vue'; +import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue'; +import * as config from '@/config'; +import * as os from '@/os'; +import { login } from '@/account'; +import { instance } from '@/instance'; +import { i18n } from '@/i18n'; + +const props = withDefaults(defineProps<{ + autoSet?: boolean; +}>(), { + autoSet: false, +}); + +const emit = defineEmits<{ + (ev: 'signup', user: Record<string, any>): void; + (ev: 'signupEmailPending'): void; +}>(); + +const host = toUnicode(config.host); + +let hcaptcha = $ref<Captcha | undefined>(); +let recaptcha = $ref<Captcha | undefined>(); +let turnstile = $ref<Captcha | undefined>(); + +let username: string = $ref(''); +let password: string = $ref(''); +let retypedPassword: string = $ref(''); +let invitationCode: string = $ref(''); +let email = $ref(''); +let usernameState: null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range' = $ref(null); +let emailState: null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error' = $ref(null); +let passwordStrength: '' | 'low' | 'medium' | 'high' = $ref(''); +let passwordRetypeState: null | 'match' | 'not-match' = $ref(null); +let submitting: boolean = $ref(false); +let hCaptchaResponse = $ref(null); +let reCaptchaResponse = $ref(null); +let turnstileResponse = $ref(null); +let usernameAbortController: null | AbortController = $ref(null); +let emailAbortController: null | AbortController = $ref(null); + +const shouldDisableSubmitting = $computed((): boolean => { + return submitting || + instance.enableHcaptcha && !hCaptchaResponse || + instance.enableRecaptcha && !reCaptchaResponse || + instance.enableTurnstile && !turnstileResponse || + instance.emailRequiredForSignup && emailState !== 'ok' || + usernameState !== 'ok' || + passwordRetypeState !== 'match'; +}); + +function onChangeUsername(): void { + if (username === '') { + usernameState = null; + return; + } + + { + const err = + !username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' : + username.length < 1 ? 'min-range' : + username.length > 20 ? 'max-range' : + null; + + if (err) { + usernameState = err; + return; + } + } + + if (usernameAbortController != null) { + usernameAbortController.abort(); + } + usernameState = 'wait'; + usernameAbortController = new AbortController(); + + os.api('username/available', { + username, + }, undefined, usernameAbortController.signal).then(result => { + usernameState = result.available ? 'ok' : 'unavailable'; + }).catch((err) => { + if (err.name !== 'AbortError') { + usernameState = 'error'; + } + }); +} + +function onChangeEmail(): void { + if (email === '') { + emailState = null; + return; + } + + if (emailAbortController != null) { + emailAbortController.abort(); + } + emailState = 'wait'; + emailAbortController = new AbortController(); + + os.api('email-address/available', { + emailAddress: email, + }, undefined, emailAbortController.signal).then(result => { + emailState = result.available ? 'ok' : + result.reason === 'used' ? 'unavailable:used' : + result.reason === 'format' ? 'unavailable:format' : + result.reason === 'disposable' ? 'unavailable:disposable' : + result.reason === 'mx' ? 'unavailable:mx' : + result.reason === 'smtp' ? 'unavailable:smtp' : + 'unavailable'; + }).catch((err) => { + if (err.name !== 'AbortError') { + emailState = 'error'; + } + }); +} + +function onChangePassword(): void { + if (password === '') { + passwordStrength = ''; + return; + } + + const strength = getPasswordStrength(password); + passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low'; +} + +function onChangePasswordRetype(): void { + if (retypedPassword === '') { + passwordRetypeState = null; + return; + } + + passwordRetypeState = password === retypedPassword ? 'match' : 'not-match'; +} + +async function onSubmit(): Promise<void> { + if (submitting) return; + submitting = true; + + try { + await os.api('signup', { + username, + password, + emailAddress: email, + invitationCode, + 'hcaptcha-response': hCaptchaResponse, + 'g-recaptcha-response': reCaptchaResponse, + 'turnstile-response': turnstileResponse, + }); + if (instance.emailRequiredForSignup) { + os.alert({ + type: 'success', + title: i18n.ts._signup.almostThere, + text: i18n.t('_signup.emailSent', { email }), + }); + emit('signupEmailPending'); + } else { + const res = await os.api('signin', { + username, + password, + }); + emit('signup', res); + + if (props.autoSet) { + return login(res.i); + } + } + } catch { + submitting = false; + hcaptcha?.reset?.(); + recaptcha?.reset?.(); + turnstile?.reset?.(); + + os.alert({ + type: 'error', + text: i18n.ts.somethingHappened, + }); + } +} +</script> + +<style lang="scss" module> +.banner { + padding: 16px; + text-align: center; + font-size: 26px; + background-color: var(--accentedBg); + color: var(--accent); +} + +.captcha { + margin: 16px 0; +} +</style> diff --git a/packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts b/packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts new file mode 100644 index 0000000000..1308dfff9a --- /dev/null +++ b/packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts @@ -0,0 +1,94 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { expect } from '@storybook/jest'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; +import { StoryObj } from '@storybook/vue3'; +import { onBeforeUnmount } from 'vue'; +import MkSignupServerRules from './MkSignupDialog,rules.vue'; +import { i18n } from '@/i18n'; +import { instance } from '@/instance'; +export const Empty = { + render(args) { + return { + components: { + MkSignupServerRules, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkSignupServerRules v-bind="props" />', + }; + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const groups = await canvas.findAllByRole('group'); + const buttons = await canvas.findAllByRole('button'); + for (const group of groups) { + if (group.ariaExpanded === 'true') { + continue; + } + const button = await within(group).findByRole('button'); + userEvent.click(button); + await waitFor(() => expect(group).toHaveAttribute('aria-expanded', 'true')); + } + const labels = await canvas.findAllByText(i18n.ts.agree); + for (const label of labels) { + expect(buttons.at(-1)).toBeDisabled(); + await waitFor(() => userEvent.click(label)); + } + expect(buttons.at(-1)).toBeEnabled(); + }, + args: { + serverRules: [], + tosUrl: null, + }, + decorators: [ + (_, context) => ({ + setup() { + instance.serverRules = context.args.serverRules; + instance.tosUrl = context.args.tosUrl; + onBeforeUnmount(() => { + // FIXME: 呼び出されない + instance.serverRules = []; + instance.tosUrl = null; + }); + }, + template: '<story/>', + }), + ], + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkSignupServerRules>; +export const ServerRulesOnly = { + ...Empty, + args: { + ...Empty.args, + serverRules: [ + 'ルール', + ], + }, +} satisfies StoryObj<typeof MkSignupServerRules>; +export const TOSOnly = { + ...Empty, + args: { + ...Empty.args, + tosUrl: 'https://example.com/tos', + }, +} satisfies StoryObj<typeof MkSignupServerRules>; +export const ServerRulesAndTOS = { + ...Empty, + args: { + ...Empty.args, + serverRules: ServerRulesOnly.args.serverRules, + tosUrl: TOSOnly.args.tosUrl, + }, +} satisfies StoryObj<typeof MkSignupServerRules>; diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue new file mode 100644 index 0000000000..be1c7f8c35 --- /dev/null +++ b/packages/frontend/src/components/MkSignupDialog.rules.vue @@ -0,0 +1,114 @@ +<template> +<div> + <div :class="$style.banner"> + <i class="ti ti-checklist"></i> + </div> + <MkSpacer :margin-min="20" :margin-max="28"> + <div class="_gaps_m"> + <div style="text-align: center;">{{ i18n.ts.pleaseConfirmBelowBeforeSignup }}</div> + + <MkFolder v-if="availableServerRules" :default-open="true"> + <template #label>{{ i18n.ts.serverRules }}</template> + <template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--success)"></i></template> + + <ol class="_gaps_s" :class="$style.rules"> + <li v-for="item in instance.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li> + </ol> + + <MkSwitch v-model="agreeServerRules" style="margin-top: 16px;">{{ i18n.ts.agree }}</MkSwitch> + </MkFolder> + + <MkFolder v-if="availableTos"> + <template #label>{{ i18n.ts.termsOfService }}</template> + <template #suffix><i v-if="agreeTos" class="ti ti-check" style="color: var(--success)"></i></template> + + <a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a> + + <MkSwitch v-model="agreeTos" style="margin-top: 16px;">{{ i18n.ts.agree }}</MkSwitch> + </MkFolder> + + <MkFolder data-cy-signup-rules-notes> + <template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template> + <template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--success)"></i></template> + + <a href="https://misskey-hub.net/docs/notes.html" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a> + + <MkSwitch v-model="agreeNote" style="margin-top: 16px;" data-cy-signup-rules-notes-agree>{{ i18n.ts.agree }}</MkSwitch> + </MkFolder> + + <MkButton primary rounded gradate style="margin: 0 auto;" :disabled="!agreed" data-cy-signup-rules-continue @click="emit('accept')">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> + </MkSpacer> +</div> +</template> + +<script lang="ts" setup> +import { computed, ref } from 'vue'; +import { instance } from '@/instance'; +import { i18n } from '@/i18n'; +import MkButton from '@/components/MkButton.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; + +const availableServerRules = instance.serverRules.length > 0; +const availableTos = instance.tosUrl != null; + +const agreeServerRules = ref(false); +const agreeTos = ref(false); +const agreeNote = ref(false); + +const agreed = computed(() => { + return (!availableServerRules || agreeServerRules.value) && (!availableTos || agreeTos.value) && agreeNote.value; +}); + +const emit = defineEmits<{ + (ev: 'accept'): void; +}>(); +</script> + +<style lang="scss" module> +.banner { + padding: 16px; + text-align: center; + font-size: 26px; + background-color: var(--accentedBg); + color: var(--accent); +} + +.rules { + counter-reset: item; + list-style: none; + padding: 0; + margin: 0; +} + +.rule { + display: flex; + align-items: center; + gap: 8px; + word-break: break-word; + + &::before { + flex-shrink: 0; + display: flex; + position: sticky; + top: calc(var(--stickyTop, 0px) + 8px); + counter-increment: item; + content: counter(item); + width: 32px; + height: 32px; + line-height: 32px; + background-color: var(--accentedBg); + color: var(--accent); + font-size: 13px; + font-weight: bold; + align-items: center; + justify-content: center; + border-radius: 999px; + } +} + +.ruleText { + padding-top: 6px; +} +</style> diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue index 790c1e94df..b4fc564d36 100644 --- a/packages/frontend/src/components/MkSignupDialog.vue +++ b/packages/frontend/src/components/MkSignupDialog.vue @@ -1,24 +1,40 @@ <template> <MkModalWindow ref="dialog" - :width="366" - :height="500" + :width="500" + :height="600" @close="dialog.close()" @closed="$emit('closed')" > <template #header>{{ i18n.ts.signup }}</template> - <MkSpacer :margin-min="20" :margin-max="28"> - <XSignup :auto-set="autoSet" @signup="onSignup" @signup-email-pending="onSignupEmailPending"/> - </MkSpacer> + <div style="overflow-x: clip;"> + <Transition + mode="out-in" + :enter-active-class="$style.transition_x_enterActive" + :leave-active-class="$style.transition_x_leaveActive" + :enter-from-class="$style.transition_x_enterFrom" + :leave-to-class="$style.transition_x_leaveTo" + > + <template v-if="!isAcceptedServerRule"> + <XServerRules @accept="isAcceptedServerRule = true"/> + </template> + <template v-else> + <XSignup :auto-set="autoSet" @signup="onSignup" @signup-email-pending="onSignupEmailPending"/> + </template> + </Transition> + </div> </MkModalWindow> </template> <script lang="ts" setup> import { } from 'vue'; -import XSignup from '@/components/MkSignup.vue'; +import { $ref } from 'vue/macros'; +import XSignup from '@/components/MkSignupDialog.form.vue'; +import XServerRules from '@/components/MkSignupDialog.rules.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n'; +import { instance } from '@/instance'; const props = withDefaults(defineProps<{ autoSet?: boolean; @@ -33,6 +49,8 @@ const emit = defineEmits<{ const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>(); +const isAcceptedServerRule = $ref(false); + function onSignup(res) { emit('done', res); dialog.close(); @@ -42,3 +60,18 @@ function onSignupEmailPending() { dialog.close(); } </script> + +<style lang="scss" module> +.transition_x_enterActive, +.transition_x_leaveActive { + transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1); +} +.transition_x_enterFrom { + opacity: 0; + transform: translateX(50px); +} +.transition_x_leaveTo { + opacity: 0; + transform: translateX(-50px); +} +</style> diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue index 8bb8637dda..d9f6716f92 100644 --- a/packages/frontend/src/components/MkSwitch.vue +++ b/packages/frontend/src/components/MkSwitch.vue @@ -9,7 +9,7 @@ :disabled="disabled" @keydown.enter="toggle" > - <span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" @click.prevent="toggle"> + <span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" data-cy-switch-toggle @click.prevent="toggle"> <div class="knob"></div> </span> <span class="label"> diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index d54d93eaee..1ca5ba6ca7 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -41,7 +41,7 @@ <template #value>{{ instance.maintainerEmail }}</template> </MkKeyValue> </FormSplit> - <FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.tos }}</FormLink> + <FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.termsOfService }}</FormLink> </div> </FormSection> diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index ebe1a8ade0..e7e3cb5368 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -3,10 +3,15 @@ <MkStickyContainer> <template #header><XHeader :tabs="headerTabs"/></template> <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink> <FormSuspense :p="init"> <div class="_gaps_m"> <FormSection first> <div class="_gaps_m"> + <MkInput v-model="tosUrl"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts.tosUrl }}</template> + </MkInput> <MkTextarea v-model="sensitiveWords"> <template #label>{{ i18n.ts.sensitiveWords }}</template> <template #caption>{{ i18n.ts.sensitiveWordsDescription }}</template> @@ -41,16 +46,20 @@ import { fetchInstance } from '@/instance'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import MkButton from '@/components/MkButton.vue'; +import FormLink from "@/components/form/link.vue"; let sensitiveWords: string = $ref(''); +let tosUrl: string | null = $ref(null); async function init() { const meta = await os.api('admin/meta'); sensitiveWords = meta.sensitiveWords.join('\n'); + tosUrl = meta.tosUrl; } function save() { os.apiWithDialog('admin/update-meta', { + tosUrl, sensitiveWords: sensitiveWords.split('\n'), }).then(() => { fetchInstance(); diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue new file mode 100644 index 0000000000..85781c0bd0 --- /dev/null +++ b/packages/frontend/src/pages/admin/server-rules.vue @@ -0,0 +1,128 @@ +<template> +<div> + <MkStickyContainer> + <template #header><XHeader :tabs="headerTabs"/></template> + <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <div class="_gaps_m"> + <div>{{ i18n.ts._serverRules.description }}</div> + <Sortable + v-model="serverRules" + class="_gaps_m" + :item-key="(_, i) => i" + :animation="150" + :handle="'.' + $style.itemHandle" + @start="e => e.item.classList.add('active')" + @end="e => e.item.classList.remove('active')" + > + <template #item="{element,index}"> + <div :class="$style.item"> + <div :class="$style.itemHeader"> + <div :class="$style.itemNumber" v-text="String(index + 1)"/> + <span :class="$style.itemHandle"><i class="ti ti-menu"/></span> + <button class="_button" :class="$style.itemRemove" @click="remove(index)"><i class="ti ti-x"></i></button> + </div> + <MkInput v-model="serverRules[index]"/> + </div> + </template> + </Sortable> + <div :class="$style.commands"> + <MkButton rounded @click="serverRules.push('')"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> + <MkButton primary rounded :class="$style.buttonSave" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + </div> + </div> + </MkSpacer> + </MkStickyContainer> +</div> +</template> + +<script lang="ts" setup> +import { defineAsyncComponent } from 'vue'; +import XHeader from './_header_.vue'; +import * as os from '@/os'; +import { fetchInstance, instance } from '@/instance'; +import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; + +const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); + +let serverRules: string[] = $ref(instance.serverRules); + +const save = async () => { + await os.apiWithDialog('admin/update-meta', { + serverRules, + }); + fetchInstance(); +}; + +const remove = (index: number): void => { + serverRules.splice(index, 1); +}; + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.serverRules, + icon: 'ti ti-checkbox', +}); +</script> + +<style lang="scss" module> +.item { + display: block; + color: var(--navFg); +} + +.itemHeader { + display: flex; + margin-bottom: 8px; + align-items: center; +} + +.itemHandle { + display: flex; + width: 40px; + height: 40px; + align-items: center; + justify-content: center; + cursor: move; +} + +.itemNumber { + display: flex; + background-color: var(--accentedBg); + color: var(--accent); + font-size: 14px; + font-weight: bold; + width: 28px; + height: 28px; + align-items: center; + justify-content: center; + border-radius: 999px; + margin-right: 8px; +} + +.itemEdit { + width: 100%; + max-width: 100%; + min-width: 100%; +} + +.itemRemove { + width: 40px; + height: 40px; + color: var(--error); + margin-left: auto; + border-radius: 6px; + + &:hover { + background: var(--X5); + } +} + +.commands { + display: flex; + gap: 16px; +} +</style> diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 65e64930d5..e9de6f7b0e 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -13,11 +13,6 @@ <template #label>{{ i18n.ts.instanceDescription }}</template> </MkTextarea> - <MkInput v-model="tosUrl"> - <template #prefix><i class="ti ti-link"></i></template> - <template #label>{{ i18n.ts.tosUrl }}</template> - </MkInput> - <FormSplit :min-width="300"> <MkInput v-model="maintainerName"> <template #label>{{ i18n.ts.maintainerName }}</template> @@ -169,7 +164,6 @@ import MkButton from '@/components/MkButton.vue'; let name: string | null = $ref(null); let description: string | null = $ref(null); -let tosUrl: string | null = $ref(null); let maintainerName: string | null = $ref(null); let maintainerEmail: string | null = $ref(null); let iconUrl: string | null = $ref(null); @@ -194,7 +188,6 @@ async function init() { const meta = await os.api('admin/meta'); name = meta.name; description = meta.description; - tosUrl = meta.tosUrl; iconUrl = meta.iconUrl; bannerUrl = meta.bannerUrl; backgroundImageUrl = meta.backgroundImageUrl; @@ -220,7 +213,6 @@ function save() { os.apiWithDialog('admin/update-meta', { name, description, - tosUrl, iconUrl, bannerUrl, backgroundImageUrl, diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index 0769ec2614..fa19682f32 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -428,6 +428,10 @@ export const routes = [{ name: 'other-settings', component: page(() => import('./pages/admin/other-settings.vue')), }, { + path: '/server-rules', + name: 'server-rules', + component: page(() => import('./pages/admin/server-rules.vue')), + }, { path: '/', component: page(() => import('./pages/_empty_.vue')), }], diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 710b08d9e0..af1b97d87f 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -164,7 +164,7 @@ export const defaultStore = markRaw(new Storage('base', { }, animation: { where: 'device', - default: !matchMedia('(prefers-reduced-motion)').matches, + default: !window.matchMedia('(prefers-reduced-motion)').matches, }, animatedMfm: { where: 'device', @@ -188,7 +188,7 @@ export const defaultStore = markRaw(new Storage('base', { }, disableShowingAnimatedImages: { where: 'device', - default: matchMedia('(prefers-reduced-motion)').matches, + default: window.matchMedia('(prefers-reduced-motion)').matches, }, emojiStyle: { where: 'device', |