enhance: スキップできるようにするかどうかを鯖管で決定できるように

This commit is contained in:
kakkokari-gtyih 2024-02-26 17:20:23 +09:00
parent 3604c470aa
commit 3dded16104
13 changed files with 71 additions and 9 deletions

6
locales/index.d.ts vendored
View File

@ -4887,11 +4887,11 @@ export interface Locale extends ILocale {
/** /**
* *
*/ */
"prohibitSkippingTutorial": string; "prohibitSkippingInitialTutorial": string;
/** /**
* *
*/ */
"prohibitSkippingTutorialDescription": string; "prohibitSkippingInitialTutorialDescription": string;
"_bubbleGame": { "_bubbleGame": {
/** /**
* *

View File

@ -1217,8 +1217,8 @@ hemisphere: "お住まいの地域"
withSensitive: "センシティブなファイルを含むノートを表示" withSensitive: "センシティブなファイルを含むノートを表示"
userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿" userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿"
enableHorizontalSwipe: "スワイプしてタブを切り替える" enableHorizontalSwipe: "スワイプしてタブを切り替える"
prohibitSkippingTutorial: "チュートリアルをスキップできないようにする" prohibitSkippingInitialTutorial: "チュートリアルをスキップできないようにする"
prohibitSkippingTutorialDescription: "新規登録したユーザーに表示されるチュートリアルをスキップできないようにします。チュートリアルを完了せずチュートリアルページを回避した場合でも、強制的にリダイレクトされます。" prohibitSkippingInitialTutorialDescription: "新規登録したユーザーに表示されるチュートリアルをスキップできないようにします。チュートリアルを完了しなかったりチュートリアルページを回避したりした場合でも、強制的にリダイレクトされます。"
_bubbleGame: _bubbleGame:
howToPlay: "遊び方" howToPlay: "遊び方"

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class CanSkipInitialTutorial1708933788259 {
name = 'CanSkipInitialTutorial1708933788259'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "canSkipInitialTutorial" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "canSkipInitialTutorial"`);
}
}

View File

@ -69,6 +69,7 @@ export class MetaEntityService {
privacyPolicyUrl: instance.privacyPolicyUrl, privacyPolicyUrl: instance.privacyPolicyUrl,
disableRegistration: instance.disableRegistration, disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup, emailRequiredForSignup: instance.emailRequiredForSignup,
canSkipInitialTutorial: instance.canSkipInitialTutorial,
enableHcaptcha: instance.enableHcaptcha, enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey, hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableMcaptcha: instance.enableMcaptcha, enableMcaptcha: instance.enableMcaptcha,

View File

@ -179,6 +179,11 @@ export class MiMeta {
}) })
public emailRequiredForSignup: boolean; public emailRequiredForSignup: boolean;
@Column('boolean', {
default: true,
})
public canSkipInitialTutorial: boolean;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })

View File

@ -79,6 +79,10 @@ export const packedMetaLiteSchema = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
canSkipInitialTutorial: {
type: 'boolean',
optional: false, nullable: false,
},
enableHcaptcha: { enableHcaptcha: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,

View File

@ -33,6 +33,10 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
canSkipInitialTutorial: {
type: 'boolean',
optional: false, nullable: false,
},
enableHcaptcha: { enableHcaptcha: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
@ -489,6 +493,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
privacyPolicyUrl: instance.privacyPolicyUrl, privacyPolicyUrl: instance.privacyPolicyUrl,
disableRegistration: instance.disableRegistration, disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup, emailRequiredForSignup: instance.emailRequiredForSignup,
canSkipInitialTutorial: instance.canSkipInitialTutorial,
enableHcaptcha: instance.enableHcaptcha, enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey, hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableMcaptcha: instance.enableMcaptcha, enableMcaptcha: instance.enableMcaptcha,

View File

@ -65,6 +65,7 @@ export const paramDef = {
cacheRemoteFiles: { type: 'boolean' }, cacheRemoteFiles: { type: 'boolean' },
cacheRemoteSensitiveFiles: { type: 'boolean' }, cacheRemoteSensitiveFiles: { type: 'boolean' },
emailRequiredForSignup: { type: 'boolean' }, emailRequiredForSignup: { type: 'boolean' },
canSkipInitialTutorial: { type: 'boolean' },
enableHcaptcha: { type: 'boolean' }, enableHcaptcha: { type: 'boolean' },
hcaptchaSiteKey: { type: 'string', nullable: true }, hcaptchaSiteKey: { type: 'string', nullable: true },
hcaptchaSecretKey: { type: 'string', nullable: true }, hcaptchaSecretKey: { type: 'string', nullable: true },
@ -269,6 +270,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.emailRequiredForSignup = ps.emailRequiredForSignup; set.emailRequiredForSignup = ps.emailRequiredForSignup;
} }
if (ps.canSkipInitialTutorial !== undefined) {
set.canSkipInitialTutorial = ps.canSkipInitialTutorial;
}
if (ps.enableHcaptcha !== undefined) { if (ps.enableHcaptcha !== undefined) {
set.enableHcaptcha = ps.enableHcaptcha; set.enableHcaptcha = ps.enableHcaptcha;
} }

View File

@ -120,7 +120,7 @@ export async function common(createVue: () => App<Element>) {
await deckStore.ready; await deckStore.ready;
// 2024年3月1日JST以降に作成されたアカウントで、チュートリアル完了していない場合、チュートリアルにリダイレクト // 2024年3月1日JST以降に作成されたアカウントで、チュートリアル完了していない場合、チュートリアルにリダイレクト
if ($i && new Date($i.createdAt).getTime() >= 1709218800000 && !claimedAchievements.includes('tutorialCompleted') && !location.pathname.startsWith('/onboarding') && !location.pathname.startsWith('/signup-complete')) { if (!instance.canSkipInitialTutorial && $i && new Date($i.createdAt).getTime() >= 1709218800000 && !claimedAchievements.includes('tutorialCompleted') && !location.pathname.startsWith('/onboarding') && !location.pathname.startsWith('/signup-complete')) {
const param = new URLSearchParams(); const param = new URLSearchParams();
param.set('redirected_from', location.pathname + location.search + location.hash); param.set('redirected_from', location.pathname + location.search + location.hash);
location.replace('/onboarding?' + param.toString()); location.replace('/onboarding?' + param.toString());

View File

@ -275,7 +275,7 @@ async function onSubmit(): Promise<void> {
emit('signup', res); emit('signup', res);
if (props.autoSet) { if (props.autoSet) {
return login(res.i); return login(res.i, '/onboarding');
} }
} }
} catch { } catch {

View File

@ -18,6 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template> <template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
</MkSwitch> </MkSwitch>
<MkSwitch v-model="prohibitSkippingInitialTutorial">
<template #label>{{ i18n.ts.prohibitSkippingInitialTutorial }}</template>
<template #caption>{{ i18n.ts.prohibitSkippingInitialTutorialDescription }}</template>
</MkSwitch>
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink> <FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
<MkInput v-model="tosUrl" type="url"> <MkInput v-model="tosUrl" type="url">
@ -80,6 +85,7 @@ import FormLink from '@/components/form/link.vue';
const enableRegistration = ref<boolean>(false); const enableRegistration = ref<boolean>(false);
const emailRequiredForSignup = ref<boolean>(false); const emailRequiredForSignup = ref<boolean>(false);
const prohibitSkippingInitialTutorial = ref<boolean>(false);
const sensitiveWords = ref<string>(''); const sensitiveWords = ref<string>('');
const prohibitedWords = ref<string>(''); const prohibitedWords = ref<string>('');
const hiddenTags = ref<string>(''); const hiddenTags = ref<string>('');
@ -91,6 +97,7 @@ async function init() {
const meta = await misskeyApi('admin/meta'); const meta = await misskeyApi('admin/meta');
enableRegistration.value = !meta.disableRegistration; enableRegistration.value = !meta.disableRegistration;
emailRequiredForSignup.value = meta.emailRequiredForSignup; emailRequiredForSignup.value = meta.emailRequiredForSignup;
prohibitSkippingInitialTutorial.value = !meta.canSkipInitialTutorial;
sensitiveWords.value = meta.sensitiveWords.join('\n'); sensitiveWords.value = meta.sensitiveWords.join('\n');
prohibitedWords.value = meta.prohibitedWords.join('\n'); prohibitedWords.value = meta.prohibitedWords.join('\n');
hiddenTags.value = meta.hiddenTags.join('\n'); hiddenTags.value = meta.hiddenTags.join('\n');
@ -103,6 +110,7 @@ function save() {
os.apiWithDialog('admin/update-meta', { os.apiWithDialog('admin/update-meta', {
disableRegistration: !enableRegistration.value, disableRegistration: !enableRegistration.value,
emailRequiredForSignup: emailRequiredForSignup.value, emailRequiredForSignup: emailRequiredForSignup.value,
canSkipInitialTutorial: !prohibitSkippingInitialTutorial.value,
tosUrl: tosUrl.value, tosUrl: tosUrl.value,
privacyPolicyUrl: privacyPolicyUrl.value, privacyPolicyUrl: privacyPolicyUrl.value,
sensitiveWords: sensitiveWords.value.split('\n'), sensitiveWords: sensitiveWords.value.split('\n'),

View File

@ -35,8 +35,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>{{ i18n.tsx._initialTutorial._onboardingLanding.welcomeToX({ name: instance.name ?? host }) }}</div> <div>{{ i18n.tsx._initialTutorial._onboardingLanding.welcomeToX({ name: instance.name ?? host }) }}</div>
</div> </div>
<div>{{ i18n.tsx._initialTutorial._onboardingLanding.description({ name: instance.name ?? host }) }}</div> <div>{{ i18n.tsx._initialTutorial._onboardingLanding.description({ name: instance.name ?? host }) }}</div>
<MkButton large primary rounded gradate style="margin: 16px auto;" @click="next">{{ i18n.ts.start }} <i class="ti ti-arrow-right"></i></MkButton> <MkButton large primary rounded gradate style="margin: 16px auto 0;" @click="next">{{ i18n.ts.start }} <i class="ti ti-arrow-right"></i></MkButton>
<MkInfo style="width: fit-content; margin: 0 auto; text-align: start; white-space: pre-wrap;">{{ i18n.tsx._initialTutorial._onboardingLanding.takesAbout({ min: 5 }) }}</MkInfo> <MkButton v-if="instance.canSkipInitialTutorial" transparent rounded style="margin: 0 auto;" @click="cancel">{{ i18n.ts.cancel }}</MkButton>
<MkInfo style="width: fit-content; margin: 0 auto; text-align: start; white-space: pre-wrap;">{{ i18n.tsx._initialTutorial._onboardingLanding.takesAbout({ min: 3 }) }}</MkInfo>
</div> </div>
</MkSpacer> </MkSpacer>
</div> </div>
@ -105,6 +106,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { host } from '@/config.js'; import { host } from '@/config.js';
import { confirm as osConfirm } from '@/os.js';
import MkAnimBg from '@/components/MkAnimBg.vue'; import MkAnimBg from '@/components/MkAnimBg.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
@ -132,6 +134,19 @@ const animationPhase = ref(0);
const query = new URLSearchParams(location.search); const query = new URLSearchParams(location.search);
const originalPath = query.get('redirected_from'); const originalPath = query.get('redirected_from');
async function cancel() {
const confirm = await osConfirm({
type: 'question',
text: i18n.ts._initialTutorial.skipAreYouSure,
okText: i18n.ts.yes,
cancelText: i18n.ts.no,
});
if (confirm.canceled) return;
location.href = '/';
}
// Y // Y
function getIconY(instanceIconEl: HTMLImageElement, welcomePageRootEl: HTMLDivElement) { function getIconY(instanceIconEl: HTMLImageElement, welcomePageRootEl: HTMLDivElement) {
const instanceIconElRect = instanceIconEl.getBoundingClientRect(); const instanceIconElRect = instanceIconEl.getBoundingClientRect();

View File

@ -4747,6 +4747,7 @@ export type components = {
defaultLightTheme: string | null; defaultLightTheme: string | null;
disableRegistration: boolean; disableRegistration: boolean;
emailRequiredForSignup: boolean; emailRequiredForSignup: boolean;
canSkipInitialTutorial: boolean;
enableHcaptcha: boolean; enableHcaptcha: boolean;
hcaptchaSiteKey: string | null; hcaptchaSiteKey: string | null;
enableMcaptcha: boolean; enableMcaptcha: boolean;
@ -4843,6 +4844,7 @@ export type operations = {
cacheRemoteFiles: boolean; cacheRemoteFiles: boolean;
cacheRemoteSensitiveFiles: boolean; cacheRemoteSensitiveFiles: boolean;
emailRequiredForSignup: boolean; emailRequiredForSignup: boolean;
canSkipInitialTutorial: boolean;
enableHcaptcha: boolean; enableHcaptcha: boolean;
hcaptchaSiteKey: string | null; hcaptchaSiteKey: string | null;
enableMcaptcha: boolean; enableMcaptcha: boolean;
@ -8811,6 +8813,7 @@ export type operations = {
cacheRemoteFiles?: boolean; cacheRemoteFiles?: boolean;
cacheRemoteSensitiveFiles?: boolean; cacheRemoteSensitiveFiles?: boolean;
emailRequiredForSignup?: boolean; emailRequiredForSignup?: boolean;
canSkipInitialTutorial?: boolean;
enableHcaptcha?: boolean; enableHcaptcha?: boolean;
hcaptchaSiteKey?: string | null; hcaptchaSiteKey?: string | null;
hcaptchaSecretKey?: string | null; hcaptchaSecretKey?: string | null;