diff --git a/locales/index.d.ts b/locales/index.d.ts index 44dcfc63fc..b673329a8a 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -4887,11 +4887,11 @@ export interface Locale extends ILocale { /** * チュートリアルをスキップできないようにする */ - "prohibitSkippingTutorial": string; + "prohibitSkippingInitialTutorial": string; /** - * 新規登録したユーザーに表示されるチュートリアルをスキップできないようにします。チュートリアルを完了せずチュートリアルページを回避した場合でも、強制的にリダイレクトされます。 + * 新規登録したユーザーに表示されるチュートリアルをスキップできないようにします。チュートリアルを完了しなかったりチュートリアルページを回避したりした場合でも、強制的にリダイレクトされます。 */ - "prohibitSkippingTutorialDescription": string; + "prohibitSkippingInitialTutorialDescription": string; "_bubbleGame": { /** * 遊び方 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 401285ca3c..da84153bce 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1217,8 +1217,8 @@ hemisphere: "お住まいの地域" withSensitive: "センシティブなファイルを含むノートを表示" userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿" enableHorizontalSwipe: "スワイプしてタブを切り替える" -prohibitSkippingTutorial: "チュートリアルをスキップできないようにする" -prohibitSkippingTutorialDescription: "新規登録したユーザーに表示されるチュートリアルをスキップできないようにします。チュートリアルを完了せずチュートリアルページを回避した場合でも、強制的にリダイレクトされます。" +prohibitSkippingInitialTutorial: "チュートリアルをスキップできないようにする" +prohibitSkippingInitialTutorialDescription: "新規登録したユーザーに表示されるチュートリアルをスキップできないようにします。チュートリアルを完了しなかったりチュートリアルページを回避したりした場合でも、強制的にリダイレクトされます。" _bubbleGame: howToPlay: "遊び方" diff --git a/packages/backend/migration/1708933788259-canSkipInitialTutorial.js b/packages/backend/migration/1708933788259-canSkipInitialTutorial.js new file mode 100644 index 0000000000..eaf7a5f844 --- /dev/null +++ b/packages/backend/migration/1708933788259-canSkipInitialTutorial.js @@ -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"`); + } +} diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index b50d76288f..46a3c4f706 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -69,6 +69,7 @@ export class MetaEntityService { privacyPolicyUrl: instance.privacyPolicyUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, + canSkipInitialTutorial: instance.canSkipInitialTutorial, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, enableMcaptcha: instance.enableMcaptcha, diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 66f19ce197..fa3bbb2416 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -179,6 +179,11 @@ export class MiMeta { }) public emailRequiredForSignup: boolean; + @Column('boolean', { + default: true, + }) + public canSkipInitialTutorial: boolean; + @Column('boolean', { default: false, }) diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index 17789f3b46..628bfebfeb 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -79,6 +79,10 @@ export const packedMetaLiteSchema = { type: 'boolean', optional: false, nullable: false, }, + canSkipInitialTutorial: { + type: 'boolean', + optional: false, nullable: false, + }, enableHcaptcha: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 88c5907bcc..112e2b9a1a 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -33,6 +33,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + canSkipInitialTutorial: { + type: 'boolean', + optional: false, nullable: false, + }, enableHcaptcha: { type: 'boolean', optional: false, nullable: false, @@ -489,6 +493,7 @@ export default class extends Endpoint { // eslint- privacyPolicyUrl: instance.privacyPolicyUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, + canSkipInitialTutorial: instance.canSkipInitialTutorial, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, enableMcaptcha: instance.enableMcaptcha, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index bffceef815..c5159f27ad 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -65,6 +65,7 @@ export const paramDef = { cacheRemoteFiles: { type: 'boolean' }, cacheRemoteSensitiveFiles: { type: 'boolean' }, emailRequiredForSignup: { type: 'boolean' }, + canSkipInitialTutorial: { type: 'boolean' }, enableHcaptcha: { type: 'boolean' }, hcaptchaSiteKey: { type: 'string', nullable: true }, hcaptchaSecretKey: { type: 'string', nullable: true }, @@ -269,6 +270,10 @@ export default class extends Endpoint { // eslint- set.emailRequiredForSignup = ps.emailRequiredForSignup; } + if (ps.canSkipInitialTutorial !== undefined) { + set.canSkipInitialTutorial = ps.canSkipInitialTutorial; + } + if (ps.enableHcaptcha !== undefined) { set.enableHcaptcha = ps.enableHcaptcha; } diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index ae4278ea28..fefbca40e5 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -120,7 +120,7 @@ export async function common(createVue: () => App) { await deckStore.ready; // 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(); param.set('redirected_from', location.pathname + location.search + location.hash); location.replace('/onboarding?' + param.toString()); diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 5f08e416c1..a0118a055f 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -275,7 +275,7 @@ async function onSubmit(): Promise { emit('signup', res); if (props.autoSet) { - return login(res.i); + return login(res.i, '/onboarding'); } } } catch { diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index 9efb34ac9a..86f8e18fa3 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -18,6 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + {{ i18n.ts.serverRules }} @@ -80,6 +85,7 @@ import FormLink from '@/components/form/link.vue'; const enableRegistration = ref(false); const emailRequiredForSignup = ref(false); +const prohibitSkippingInitialTutorial = ref(false); const sensitiveWords = ref(''); const prohibitedWords = ref(''); const hiddenTags = ref(''); @@ -91,6 +97,7 @@ async function init() { const meta = await misskeyApi('admin/meta'); enableRegistration.value = !meta.disableRegistration; emailRequiredForSignup.value = meta.emailRequiredForSignup; + prohibitSkippingInitialTutorial.value = !meta.canSkipInitialTutorial; sensitiveWords.value = meta.sensitiveWords.join('\n'); prohibitedWords.value = meta.prohibitedWords.join('\n'); hiddenTags.value = meta.hiddenTags.join('\n'); @@ -103,6 +110,7 @@ function save() { os.apiWithDialog('admin/update-meta', { disableRegistration: !enableRegistration.value, emailRequiredForSignup: emailRequiredForSignup.value, + canSkipInitialTutorial: !prohibitSkippingInitialTutorial.value, tosUrl: tosUrl.value, privacyPolicyUrl: privacyPolicyUrl.value, sensitiveWords: sensitiveWords.value.split('\n'), diff --git a/packages/frontend/src/pages/onboarding.vue b/packages/frontend/src/pages/onboarding.vue index f2628c973e..5bff51e9e8 100644 --- a/packages/frontend/src/pages/onboarding.vue +++ b/packages/frontend/src/pages/onboarding.vue @@ -35,8 +35,9 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.tsx._initialTutorial._onboardingLanding.welcomeToX({ name: instance.name ?? host }) }}
{{ i18n.tsx._initialTutorial._onboardingLanding.description({ name: instance.name ?? host }) }}
- {{ i18n.ts.start }} - {{ i18n.tsx._initialTutorial._onboardingLanding.takesAbout({ min: 5 }) }} + {{ i18n.ts.start }} + {{ i18n.ts.cancel }} + {{ i18n.tsx._initialTutorial._onboardingLanding.takesAbout({ min: 3 }) }} @@ -105,6 +106,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import { host } from '@/config.js'; +import { confirm as osConfirm } from '@/os.js'; import MkAnimBg from '@/components/MkAnimBg.vue'; import MkButton from '@/components/MkButton.vue'; @@ -132,6 +134,19 @@ const animationPhase = ref(0); const query = new URLSearchParams(location.search); 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座標を取得 function getIconY(instanceIconEl: HTMLImageElement, welcomePageRootEl: HTMLDivElement) { const instanceIconElRect = instanceIconEl.getBoundingClientRect(); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 18bc45b983..14861d17cf 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4747,6 +4747,7 @@ export type components = { defaultLightTheme: string | null; disableRegistration: boolean; emailRequiredForSignup: boolean; + canSkipInitialTutorial: boolean; enableHcaptcha: boolean; hcaptchaSiteKey: string | null; enableMcaptcha: boolean; @@ -4843,6 +4844,7 @@ export type operations = { cacheRemoteFiles: boolean; cacheRemoteSensitiveFiles: boolean; emailRequiredForSignup: boolean; + canSkipInitialTutorial: boolean; enableHcaptcha: boolean; hcaptchaSiteKey: string | null; enableMcaptcha: boolean; @@ -8811,6 +8813,7 @@ export type operations = { cacheRemoteFiles?: boolean; cacheRemoteSensitiveFiles?: boolean; emailRequiredForSignup?: boolean; + canSkipInitialTutorial?: boolean; enableHcaptcha?: boolean; hcaptchaSiteKey?: string | null; hcaptchaSecretKey?: string | null;