diff --git a/CHANGELOG.md b/CHANGELOG.md index fd56700e1f..9032b7dc3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - ### Client +- Feat: チュートリアルと初期設定ダイアログを統合 + - 管理者は新規登録したユーザーにチュートリアルを強制することができるように - Enhance: PC画面でチャンネルが複数列で表示されるように (Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13) - Enhance: 照会に失敗した場合、その理由を表示するように diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.ts index d2efbf709c..c5930d10f5 100644 --- a/cypress/e2e/basic.cy.ts +++ b/cypress/e2e/basic.cy.ts @@ -52,7 +52,7 @@ describe('After setup instance', () => { cy.visitHome(); }); - it('signup', () => { + it('signup / onboarding', () => { cy.visitHome(); cy.intercept('POST', '/api/signup').as('signup'); @@ -74,6 +74,60 @@ describe('After setup instance', () => { cy.get('[data-cy-signup-submit]').click(); cy.wait('@signup'); + + // /onboarding にリダイレクトされる + cy.wait(5000); + cy.url().should('equal', Cypress.config().baseUrl + '/onboarding'); + + // 「始める」 + // 最初にアニメーションがあるので待つ + cy.get('[data-cy-user-setup-start]', { timeout: 15000 }).click(); + + cy.wait(1000); // ← トランジション待ち(以下全てのページ遷移で待たせる) + + // 【設定】プロフィール + cy.get('[data-cy-user-setup-user-name] input').type('ありす'); + cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ'); + + cy.get('[data-cy-user-setup-next]').click(); + cy.wait(1000); + + // 【チュートリアル】ノートって何? + cy.get('[data-cy-user-setup-next]').click(); + cy.wait(1000); + + // 【チュートリアル】リアクションって何? + // インタラクティブ要素があるが、テスト時は無視できるようになっている + cy.get('[data-cy-user-setup-next]').click(); + cy.wait(1000); + + // 【チュートリアル】タイムラインのしくみ + cy.get('[data-cy-user-setup-next]').click(); + cy.wait(1000); + + // 【設定】フォロー + cy.get('[data-cy-user-setup-next]').click(); + cy.wait(1000); + + // 【チュートリアル】ノートの投稿設定 + cy.get('[data-cy-user-setup-next]').click(); + cy.wait(1000); + + // 【チュートリアル】添付ファイルをセンシティブにするには? + // インタラクティブ要素があるが、テスト時は無視できるようになっている + cy.get('[data-cy-user-setup-next]').click(); + cy.wait(1000); + + // 【設定】プライバシー設定 + cy.get('[data-cy-user-setup-next]').click(); + cy.wait(1000); + + // 完了(「ホーム画面に進む」ボタン) + cy.get('[data-cy-user-setup-complete] a').click(); + + // ホームにリダイレクトされる + cy.wait(5000); + cy.url().should('equal', Cypress.config().baseUrl + '/'); }); it('signup with duplicated username', () => { @@ -133,9 +187,9 @@ describe('After user signup', () => { cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); cy.wait('@signin'); - }); + }); - it('suspend', function() { + it('suspend', function () { cy.request('POST', '/api/admin/suspend-user', { i: this.admin.token, userId: this.alice.id, @@ -153,56 +207,6 @@ describe('After user signup', () => { }); }); -describe('After user signed in', () => { - beforeEach(() => { - cy.resetState(); - - // インスタンス初期セットアップ - cy.registerUser('admin', 'pass', true); - - // ユーザー作成 - cy.registerUser('alice', 'alice1234'); - - cy.login('alice', 'alice1234'); - }); - - afterEach(() => { - // テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。 - // waitを入れることでそれを防止できる - cy.wait(1000); - }); - - it('successfully loads', () => { - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).should('be.visible'); - }); - - it('account setup wizard', () => { - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).click(); - - cy.get('[data-cy-user-setup-user-name] input').type('ありす'); - cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ'); - // TODO: アイコン設定テスト - - cy.get('[data-cy-user-setup-continue]').click(); - - // プライバシー設定 - - cy.get('[data-cy-user-setup-continue]').click(); - - // フォローはスキップ - - cy.get('[data-cy-user-setup-continue]').click(); - - // プッシュ通知設定はスキップ - - cy.get('[data-cy-user-setup-continue]').click(); - - cy.get('[data-cy-user-setup-continue]').click(); - }); -}); - describe('After user setup', () => { beforeEach(() => { cy.resetState(); @@ -214,11 +218,6 @@ describe('After user setup', () => { cy.registerUser('alice', 'alice1234'); cy.login('alice', 'alice1234'); - - // アカウント初期設定ウィザード - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click(); - cy.get('[data-cy-modal-dialog-ok]').click(); }); afterEach(() => { diff --git a/cypress/e2e/router.cy.ts b/cypress/e2e/router.cy.ts index 8d8fb3af31..7cc5d55adb 100644 --- a/cypress/e2e/router.cy.ts +++ b/cypress/e2e/router.cy.ts @@ -16,12 +16,6 @@ describe('Router transition', () => { cy.registerUser('alice', 'alice1234'); cy.login('alice', 'alice1234'); - - // アカウント初期設定ウィザード - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click(); - cy.wait(500); - cy.get('[data-cy-modal-dialog-ok]').click(); }); it('redirect to user profile', () => { diff --git a/locales/index.d.ts b/locales/index.d.ts index 63878d3d47..2cdcf30f60 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5222,6 +5222,14 @@ export interface Locale extends ILocale { * 注意事項を理解した上でオンにします。 */ "acknowledgeNotesAndEnable": string; + /** + * チュートリアルをスキップできないようにする + */ + "prohibitSkippingInitialTutorial": string; + /** + * 新規登録したユーザーに表示されるチュートリアルをスキップできないようにします。チュートリアルを完了しなかったりチュートリアルページを回避したりした場合でも、強制的にリダイレクトされます。 + */ + "prohibitSkippingInitialTutorialDescription": string; "_accountSettings": { /** * コンテンツの表示にログインを必須にする @@ -5431,68 +5439,6 @@ export interface Locale extends ILocale { */ "silenceDescription": string; }; - "_initialAccountSetting": { - /** - * アカウントの作成が完了しました! - */ - "accountCreated": string; - /** - * さっそくアカウントの初期設定を行いましょう。 - */ - "letsStartAccountSetup": string; - /** - * まずはあなたのプロフィールを設定しましょう。 - */ - "letsFillYourProfile": string; - /** - * プロフィール設定 - */ - "profileSetting": string; - /** - * プライバシー設定 - */ - "privacySetting": string; - /** - * これらの設定は後から変更できます。 - */ - "theseSettingsCanEditLater": string; - /** - * この他にも様々な設定を「設定」ページから行えます。ぜひ後で確認してみてください。 - */ - "youCanEditMoreSettingsInSettingsPageLater": string; - /** - * タイムラインを構築するため、気になるユーザーをフォローしてみましょう。 - */ - "followUsers": string; - /** - * プッシュ通知を有効にすると{name}の通知をお使いのデバイスで受け取ることができます。 - */ - "pushNotificationDescription": ParameterizedString<"name">; - /** - * 初期設定が完了しました! - */ - "initialAccountSettingCompleted": string; - /** - * {name}をお楽しみください! - */ - "haveFun": ParameterizedString<"name">; - /** - * このまま{name}(Misskey)の使い方についてのチュートリアルに進むこともできますが、ここで中断してすぐに使い始めることもできます。 - */ - "youCanContinueTutorial": ParameterizedString<"name">; - /** - * チュートリアルを開始 - */ - "startTutorial": string; - /** - * 初期設定をスキップしますか? - */ - "skipAreYouSure": string; - /** - * 初期設定をあとでやり直しますか? - */ - "laterAreYouSure": string; - }; "_initialTutorial": { /** * チュートリアルを見る @@ -5520,6 +5466,21 @@ export interface Locale extends ILocale { */ "description": string; }; + "_profileSettings": { + /** + * プロフィール設定 + */ + "title": string; + /** + * まずは基本的なプロフィールを設定して、ユーザーにあなたのことを知ってもらえるようにしましょう。 + */ + "description": string; + /** + * ここで設定した項目は後でいつでも変更できます。 + * チュートリアル終了後には、更に多彩なプロフィール設定をご利用いただけます! + */ + "youCanChangeThemLater": string; + }; "_note": { /** * ノートって何? @@ -5606,6 +5567,16 @@ export interface Locale extends ILocale { */ "description3": ParameterizedString<"link">; }; + "_followUsers": { + /** + * 誰もフォローしていない状態だと、ホームタイムラインには何も表示されません。 + */ + "description1": string; + /** + * タイムラインを構築するため、気になるユーザーをフォローしてみましょう。 + */ + "description2": string; + }; "_postNote": { /** * ノートの投稿設定 @@ -5706,6 +5677,30 @@ export interface Locale extends ILocale { */ "doItToContinue": string; }; + "_pushNotification": { + /** + * プッシュ通知を有効にすると{name}の通知をお使いのデバイスで受け取ることができます。 + */ + "description": ParameterizedString<"name">; + }; + "_privacySettings": { + /** + * プライバシー設定 + */ + "title": string; + /** + * 多くのユーザーが利用しているプライバシー関連の設定項目をリストアップしました。必要に応じて変更してください。 + */ + "description1": string; + /** + * これらの設定は後から変更できます。 + */ + "theseSettingsCanEditLater": string; + /** + * この他にも様々な設定を「設定」ページから行えます。ぜひ後で確認してみてください。 + */ + "youCanEditMoreSettingsInSettingsPageLater": string; + }; "_done": { /** * チュートリアルは終了です🎉 @@ -5715,6 +5710,72 @@ export interface Locale extends ILocale { * ここで紹介した機能はほんの一部にすぎません。Misskeyの使い方をより詳しく知るには、{link}をご覧ください。 */ "description": ParameterizedString<"link">; + /** + * {name}をお楽しみください! + */ + "haveFun": ParameterizedString<"name">; + /** + * このチュートリアルは、「もっと!」→「情報」→「チュートリアルを見る」からいつでも見返すことができます。 + */ + "youCanReferTutorialBy": string; + }; + "_onboardingLanding": { + /** + * アカウントの作成が完了しました! + */ + "accountCreated": string; + /** + * ようこそ、{name}へ! + */ + "welcomeToX": ParameterizedString<"name">; + /** + * プロフィールを設定したり、{name}の基本的な使い方を学んだりして、すぐに使い始められるようにしましょう。 + */ + "description": ParameterizedString<"name">; + /** + * このチュートリアルの所要時間は{min}分程度です。 + * チュートリアルを完了すると実績が解除されます。 + */ + "takesAbout": ParameterizedString<"min">; + /** + * このサーバーの管理者は新規ユーザーにチュートリアルを完了することを義務付けています。 + * チュートリアルを完了するまでMisskeyを使い始めることはできません。 + */ + "adminForcesToTakeTutorial": string; + }; + "_onboardingDone": { + /** + * お疲れ様でした!次のステップに進んで、{name}をもっと楽しめるようにしましょう。 + */ + "description": ParameterizedString<"name">; + /** + * 元のページに戻る + */ + "backToOriginalPath": string; + /** + * あなたがアクセスしようとしていたページに戻ります。 + */ + "backToOriginalPathDescription": string; + /** + * プロフィール設定 + */ + "profile": string; + /** + * プロフィールをかんぺきにして、自分をアピールしましょう。 + */ + "profileDescription": string; + /** + * 人気のノートやユーザーを見つけて交流をはじめましょう。 + */ + "exploreDescription": string; + /** + * ホーム画面に進む + */ + "goToTimeline": string; + /** + * 設定等を行わず、通常のホーム画面(タイムライン)に進みます。 + */ + "goToTimelineDescription": string; }; }; "_timelineDescription": { diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index d78bd4ee65..4c4c2e8339 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1301,6 +1301,8 @@ lockdown: "ロックダウン" pleaseSelectAccount: "アカウントを選択してください" availableRoles: "利用可能なロール" acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。" +prohibitSkippingInitialTutorial: "チュートリアルをスキップできないようにする" +prohibitSkippingInitialTutorialDescription: "新規登録したユーザーに表示されるチュートリアルをスキップできないようにします。チュートリアルを完了しなかったりチュートリアルページを回避したりした場合でも、強制的にリダイレクトされます。" _accountSettings: requireSigninToViewContents: "コンテンツの表示にログインを必須にする" @@ -1363,23 +1365,6 @@ _announcement: silence: "非通知" silenceDescription: "オンにすると、このお知らせは通知されず、既読にする必要もなくなります。" -_initialAccountSetting: - accountCreated: "アカウントの作成が完了しました!" - letsStartAccountSetup: "さっそくアカウントの初期設定を行いましょう。" - letsFillYourProfile: "まずはあなたのプロフィールを設定しましょう。" - profileSetting: "プロフィール設定" - privacySetting: "プライバシー設定" - theseSettingsCanEditLater: "これらの設定は後から変更できます。" - youCanEditMoreSettingsInSettingsPageLater: "この他にも様々な設定を「設定」ページから行えます。ぜひ後で確認してみてください。" - followUsers: "タイムラインを構築するため、気になるユーザーをフォローしてみましょう。" - pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をお使いのデバイスで受け取ることができます。" - initialAccountSettingCompleted: "初期設定が完了しました!" - haveFun: "{name}をお楽しみください!" - youCanContinueTutorial: "このまま{name}(Misskey)の使い方についてのチュートリアルに進むこともできますが、ここで中断してすぐに使い始めることもできます。" - startTutorial: "チュートリアルを開始" - skipAreYouSure: "初期設定をスキップしますか?" - laterAreYouSure: "初期設定をあとでやり直しますか?" - _initialTutorial: launchTutorial: "チュートリアルを見る" title: "チュートリアル" @@ -1388,6 +1373,10 @@ _initialTutorial: _landing: title: "チュートリアルへようこそ" description: "ここでは、Misskeyの基本的な使い方や機能を確認できます。" + _profileSettings: + title: "プロフィール設定" + description: "まずは基本的なプロフィールを設定して、ユーザーにあなたのことを知ってもらえるようにしましょう。" + youCanChangeThemLater: "ここで設定した項目は後でいつでも変更できます。\nチュートリアル終了後には、更に多彩なプロフィール設定をご利用いただけます!" _note: title: "ノートって何?" description: "Misskeyでの投稿は「ノート」と呼びます。ノートはタイムラインに時系列で並んでいて、リアルタイムで更新されていきます。" @@ -1411,6 +1400,9 @@ _initialTutorial: global: "接続している他のすべてのサーバーからの投稿を見られます。" description2: "それぞれのタイムラインは、画面上部でいつでも切り替えられます。" description3: "その他にも、リストタイムラインやチャンネルタイムラインなどがあります。詳しくは{link}をご覧ください。" + _followUsers: + description1: "誰もフォローしていない状態だと、ホームタイムラインには何も表示されません。" + description2: "タイムラインを構築するため、気になるユーザーをフォローしてみましょう。" _postNote: title: "ノートの投稿設定" description1: "Misskeyにノートを投稿する際には、様々なオプションの設定が可能です。投稿フォームはこのようになっています。" @@ -1439,9 +1431,33 @@ _initialTutorial: method: "添付ファイルをセンシティブにする際は、そのファイルをクリックしてメニューを開き、「センシティブとして設定」をクリックします。" sensitiveSucceeded: "ファイルを添付する際は、サーバーのガイドラインに従ってセンシティブを適切に設定してください。" doItToContinue: "画像をセンシティブに設定すると先に進めるようになります。" + _pushNotification: + description: "プッシュ通知を有効にすると{name}の通知をお使いのデバイスで受け取ることができます。" + _privacySettings: + title: "プライバシー設定" + description1: "多くのユーザーが利用しているプライバシー関連の設定項目をリストアップしました。必要に応じて変更してください。" + theseSettingsCanEditLater: "これらの設定は後から変更できます。" + youCanEditMoreSettingsInSettingsPageLater: "この他にも様々な設定を「設定」ページから行えます。ぜひ後で確認してみてください。" _done: title: "チュートリアルは終了です🎉" description: "ここで紹介した機能はほんの一部にすぎません。Misskeyの使い方をより詳しく知るには、{link}をご覧ください。" + haveFun: "{name}をお楽しみください!" + youCanReferTutorialBy: "このチュートリアルは、「もっと!」→「情報」→「チュートリアルを見る」からいつでも見返すことができます。" + _onboardingLanding: + accountCreated: "アカウントの作成が完了しました!" + welcomeToX: "ようこそ、{name}へ!" + description: "プロフィールを設定したり、{name}の基本的な使い方を学んだりして、すぐに使い始められるようにしましょう。" + takesAbout: "このチュートリアルの所要時間は{min}分程度です。\nチュートリアルを完了すると実績が解除されます。" + adminForcesToTakeTutorial: "このサーバーの管理者は新規ユーザーにチュートリアルを完了することを義務付けています。\nチュートリアルを完了するまでMisskeyを使い始めることはできません。" + _onboardingDone: + description: "お疲れ様でした!次のステップに進んで、{name}をもっと楽しめるようにしましょう。" + backToOriginalPath: "元のページに戻る" + backToOriginalPathDescription: "あなたがアクセスしようとしていたページに戻ります。" + profile: "プロフィール設定" + profileDescription: "プロフィールをかんぺきにして、自分をアピールしましょう。" + exploreDescription: "人気のノートやユーザーを見つけて交流をはじめましょう。" + goToTimeline: "ホーム画面に進む" + goToTimelineDescription: "設定等を行わず、通常のホーム画面(タイムライン)に進みます。" _timelineDescription: home: "ホームタイムラインでは、あなたがフォローしているアカウントの投稿を見られます。" 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 409dca3426..09ff4d4a90 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -87,6 +87,7 @@ export class MetaEntityService { inquiryUrl: instance.inquiryUrl, 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 ad5e31ad6f..f44e811e70 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -189,6 +189,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 e3fd63464a..8051fc8ef4 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 64e3cc33bd..5d38b0206a 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, @@ -561,6 +565,7 @@ export default class extends Endpoint { // eslint- inquiryUrl: instance.inquiryUrl, 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 38ef0d1de8..49b26f92f4 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -70,6 +70,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 }, @@ -315,6 +316,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/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index f2bdc631d2..f5af8df9a6 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -412,8 +412,6 @@ function toStories(component: string): Promise { glob('src/components/MkFlashPreview.vue'), glob('src/components/MkGalleryPostPreview.vue'), glob('src/components/MkSignupServerRules.vue'), - glob('src/components/MkUserSetupDialog.vue'), - glob('src/components/MkUserSetupDialog.*.vue'), glob('src/components/MkInstanceCardMini.vue'), glob('src/components/MkInviteCode.vue'), glob('src/pages/admin/overview.ap-requests.vue'), diff --git a/packages/frontend/assets/tutorial/reaction.png b/packages/frontend/assets/tutorial/reaction.png new file mode 100644 index 0000000000..c09fda3444 Binary files /dev/null and b/packages/frontend/assets/tutorial/reaction.png differ diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts index c90cc6bdd0..75d3e87018 100644 --- a/packages/frontend/src/_boot_.ts +++ b/packages/frontend/src/_boot_.ts @@ -12,7 +12,7 @@ import '@/style.scss'; import { mainBoot } from '@/boot/main-boot.js'; import { subBoot } from '@/boot/sub-boot.js'; -const subBootPaths = ['/share', '/auth', '/miauth', '/oauth', '/signup-complete']; +const subBootPaths = ['/share', '/auth', '/miauth', '/oauth', '/signup-complete', '/onboarding']; if (subBootPaths.some(i => location.pathname === i || location.pathname.startsWith(i + '/'))) { subBoot(); diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index bfe5c4f5f7..5a15c5e6c4 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -12,7 +12,7 @@ import components from '@/components/index.js'; import { applyTheme } from '@/scripts/theme.js'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js'; import { updateI18n, i18n } from '@/i18n.js'; -import { $i, refreshAccount, login } from '@/account.js'; +import { $i, iAmModerator, refreshAccount, login } from '@/account.js'; import { defaultStore, ColdDeviceStorage } from '@/store.js'; import { fetchInstance, instance } from '@/instance.js'; import { deviceKind, updateDeviceKind } from '@/scripts/device-kind.js'; @@ -21,6 +21,7 @@ import { getUrlWithoutLoginId } from '@/scripts/login-id.js'; import { getAccountFromId } from '@/scripts/get-account-from-id.js'; import { deckStore } from '@/ui/deck/deck-store.js'; import { miLocalStorage } from '@/local-storage.js'; +import { claimedAchievements } from '@/scripts/achievements.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; import { setupRouter } from '@/router/main.js'; import { createMainRouter } from '@/router/definition.js'; @@ -119,6 +120,26 @@ export async function common(createVue: () => App) { await defaultStore.ready; await deckStore.ready; + // 2024年10月1日JST以降に作成されたアカウントで、チュートリアルを完了していない通常ユーザーの場合、チュートリアルにリダイレクト + if ( + !instance.canSkipInitialTutorial && + $i && + !iAmModerator && + new Date($i.createdAt).getTime() >= 1727708400000 && + !claimedAchievements.includes('tutorialCompleted') && + !location.pathname.startsWith('/onboarding') && + !location.pathname.startsWith('/signup-complete') + ) { + await refreshAccount(); + + if ($i && !$i.achievements.map((v) => v.name).includes('tutorialCompleted')) { + const param = new URLSearchParams(); + param.set('redirected_from', location.pathname + location.search + location.hash); + location.replace('/onboarding?' + param.toString()); + return; + } + } + const fetchInstanceMetaPromise = fetchInstance(); fetchInstanceMetaPromise.then(() => { diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 2bf9029479..f997fc6c9e 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -111,14 +111,6 @@ export async function mainBoot() { } if ($i) { - defaultStore.loaded.then(() => { - if (defaultStore.state.accountSetupWizard !== -1) { - const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, { - closed: () => dispose(), - }); - } - }); - for (const announcement of ($i.unreadAnnouncements ?? []).filter(x => x.display === 'dialog')) { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), { announcement, diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 0b5794d1e3..e2166856e7 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -539,6 +539,8 @@ function pushVisibleUser(user: Misskey.entities.UserDetailed) { } function addVisibleUser() { + if (props.mock) return; + os.selectUser().then(user => { pushVisibleUser(user); @@ -893,6 +895,8 @@ function cancel() { } function insertMention() { + if (props.mock) return; + os.selectUser({ localOnly: localOnly.value, includeSelf: true }).then(user => { insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' '); }); diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index e1f4e26d62..3e54c57c19 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -292,7 +292,7 @@ async function onSubmit(): Promise { emit('signup', resJson); if (props.autoSet) { - await login(resJson.token); + await login(resJson.token, '/onboarding'); } } } else { diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkTutorial.FollowUsers.UserCard.vue similarity index 100% rename from packages/frontend/src/components/MkUserSetupDialog.User.vue rename to packages/frontend/src/components/MkTutorial.FollowUsers.UserCard.vue diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkTutorial.FollowUsers.vue similarity index 74% rename from packages/frontend/src/components/MkUserSetupDialog.Follow.vue rename to packages/frontend/src/components/MkTutorial.FollowUsers.vue index 5153c06139..de034b7c56 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue +++ b/packages/frontend/src/components/MkTutorial.FollowUsers.vue @@ -5,14 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only