mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-23 00:29:22 +09:00
Merge 3c592d6db5
into f123be38b9
This commit is contained in:
commit
881b87c166
@ -4,6 +4,8 @@
|
||||
-
|
||||
|
||||
### Client
|
||||
- Feat: チュートリアルと初期設定ダイアログを統合
|
||||
- 管理者は新規登録したユーザーにチュートリアルを強制することができるように
|
||||
- Enhance: PC画面でチャンネルが複数列で表示されるように
|
||||
(Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13)
|
||||
- Enhance: 照会に失敗した場合、その理由を表示するように
|
||||
|
@ -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(() => {
|
||||
|
@ -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', () => {
|
||||
|
185
locales/index.d.ts
vendored
185
locales/index.d.ts
vendored
@ -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": {
|
||||
|
@ -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: "ホームタイムラインでは、あなたがフォローしているアカウントの投稿を見られます。"
|
||||
|
@ -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"`);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -189,6 +189,11 @@ export class MiMeta {
|
||||
})
|
||||
public emailRequiredForSignup: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
})
|
||||
public canSkipInitialTutorial: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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<typeof meta, typeof paramDef> { // eslint-
|
||||
inquiryUrl: instance.inquiryUrl,
|
||||
disableRegistration: instance.disableRegistration,
|
||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
canSkipInitialTutorial: instance.canSkipInitialTutorial,
|
||||
enableHcaptcha: instance.enableHcaptcha,
|
||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||
enableMcaptcha: instance.enableMcaptcha,
|
||||
|
@ -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<typeof meta, typeof paramDef> { // eslint-
|
||||
set.emailRequiredForSignup = ps.emailRequiredForSignup;
|
||||
}
|
||||
|
||||
if (ps.canSkipInitialTutorial !== undefined) {
|
||||
set.canSkipInitialTutorial = ps.canSkipInitialTutorial;
|
||||
}
|
||||
|
||||
if (ps.enableHcaptcha !== undefined) {
|
||||
set.enableHcaptcha = ps.enableHcaptcha;
|
||||
}
|
||||
|
@ -412,8 +412,6 @@ function toStories(component: string): Promise<string> {
|
||||
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'),
|
||||
|
BIN
packages/frontend/assets/tutorial/reaction.png
Normal file
BIN
packages/frontend/assets/tutorial/reaction.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
@ -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();
|
||||
|
@ -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<Element>) {
|
||||
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(() => {
|
||||
|
@ -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,
|
||||
|
@ -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) + ' ');
|
||||
});
|
||||
|
@ -292,7 +292,7 @@ async function onSubmit(): Promise<void> {
|
||||
emit('signup', resJson);
|
||||
|
||||
if (props.autoSet) {
|
||||
await login(resJson.token);
|
||||
await login(resJson.token, '/onboarding');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -5,14 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<div style="text-align: center;">{{ i18n.ts._initialAccountSetting.followUsers }}</div>
|
||||
<div style="word-break: auto-phrase; text-align: center;">{{ i18n.ts._initialTutorial._followUsers.description1 }}<br>{{ i18n.ts._initialTutorial._followUsers.description2 }}</div>
|
||||
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts.recommended }}</template>
|
||||
|
||||
<MkPagination :pagination="pinnedUsers">
|
||||
<template #default="{ items }">
|
||||
<div :class="$style.users">
|
||||
<div class="_gaps_s">
|
||||
<XUser v-for="item in (items as Misskey.entities.UserDetailed[])" :key="item.id" :user="item"/>
|
||||
</div>
|
||||
</template>
|
||||
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<MkPagination :pagination="popularUsers">
|
||||
<template #default="{ items }">
|
||||
<div :class="$style.users">
|
||||
<div class="_gaps_s">
|
||||
<XUser v-for="item in (items as Misskey.entities.UserDetailed[])" :key="item.id" :user="item"/>
|
||||
</div>
|
||||
</template>
|
||||
@ -37,8 +37,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import XUser from '@/components/MkUserSetupDialog.User.vue';
|
||||
import XUser from '@/components/MkTutorial.FollowUsers.UserCard.vue';
|
||||
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
|
||||
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||
|
||||
const pinnedUsers: Paging = {
|
||||
endpoint: 'pinned-users',
|
||||
@ -56,13 +57,8 @@ const popularUsers: Paging = {
|
||||
sort: '+follower',
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.users {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
||||
grid-gap: var(--MI-margin);
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
defineExpose<TutorialPageCommonExpose>({
|
||||
canContinue: true,
|
||||
});
|
||||
</script>
|
@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div v-if="phase === 'aboutNote'" class="_gaps">
|
||||
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._note.description }}</div>
|
||||
<MkNote :class="$style.exampleNoteRoot" style="pointer-events: none;" :note="exampleNote" :mock="true"/>
|
||||
<div style="word-break: auto-phrase; text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._note.description }}</div>
|
||||
<MkNote tabindex="-1" :class="$style.exampleNoteRoot" style="pointer-events: none;" :note="exampleNote" :mock="true"/>
|
||||
<div class="_gaps_s">
|
||||
<div><i class="ti ti-arrow-back-up"></i> <b>{{ i18n.ts.reply }}</b> … {{ i18n.ts._initialTutorial._note.reply }}</div>
|
||||
<div><i class="ti ti-repeat"></i> <b>{{ i18n.ts.renote }}</b> … {{ i18n.ts._initialTutorial._note.renote }}</div>
|
||||
@ -15,29 +15,30 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="phase === 'howToReact'" class="_gaps">
|
||||
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div>
|
||||
<div class="_gaps_s">
|
||||
<div style="word-break: auto-phrase; text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div>
|
||||
<img :class="$style.image" src="/client-assets/tutorial/reaction.png"/>
|
||||
</div>
|
||||
<div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div>
|
||||
<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/>
|
||||
<div v-if="onceReacted"><b style="color: var(--MI_THEME-accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div>
|
||||
<div v-else><b :class="$style.actionWaitText">{{ i18n.ts._initialTutorial._reaction.reactToContinue }}</b></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { ref, reactive } from 'vue';
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { $i } from '@/account.js';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
phase: 'aboutNote' | 'howToReact';
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'reacted'): void;
|
||||
}>();
|
||||
|
||||
const exampleNote = reactive<Misskey.entities.Note>({
|
||||
id: '0000000000',
|
||||
createdAt: '2019-04-14T17:30:49.181Z',
|
||||
@ -71,14 +72,27 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
||||
replyId: null,
|
||||
renoteId: null,
|
||||
});
|
||||
|
||||
const onceReacted = ref<boolean>(false);
|
||||
|
||||
const canContinue = computed(() => {
|
||||
if (props.phase === 'aboutNote') {
|
||||
return true;
|
||||
} else if (props.phase === 'howToReact') {
|
||||
return onceReacted.value;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
function addReaction(emoji) {
|
||||
onceReacted.value = true;
|
||||
emit('reacted');
|
||||
exampleNote.reactions[emoji] = 1;
|
||||
exampleNote.myReaction = emoji;
|
||||
doNotification(emoji);
|
||||
|
||||
// 通知音も鳴らしたいのでちょっと遅らせる
|
||||
setTimeout(() => {
|
||||
doNotification(emoji);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function doNotification(emoji: string): void {
|
||||
@ -101,6 +115,10 @@ function removeReaction(emoji) {
|
||||
delete exampleNote.reactions[emoji];
|
||||
exampleNote.myReaction = undefined;
|
||||
}
|
||||
|
||||
defineExpose<TutorialPageCommonExpose>({
|
||||
canContinue,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
@ -114,4 +132,14 @@ function removeReaction(emoji) {
|
||||
height: 1px;
|
||||
background: var(--MI_THEME-divider);
|
||||
}
|
||||
|
||||
.image {
|
||||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
border-radius: var(--MI-radius);
|
||||
}
|
||||
|
||||
.actionWaitText {
|
||||
color: var(--MI_THEME-error);
|
||||
}
|
||||
</style>
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._postNote.description1 }}</div>
|
||||
<div style="word-break: auto-phrase; text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._postNote.description1 }}</div>
|
||||
<MkPostForm :class="$style.exampleRoot" :mock="true" :autofocus="false"/>
|
||||
<MkFormSection>
|
||||
<template #label>{{ i18n.ts.visibility }}</template>
|
||||
@ -42,6 +42,7 @@ import MkNote from '@/components/MkNote.vue';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import MkFormSection from '@/components/form/section.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||
|
||||
const exampleCWNote = reactive<Misskey.entities.Note>({
|
||||
id: '0000000000',
|
||||
@ -76,6 +77,10 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
|
||||
replyId: null,
|
||||
renoteId: null,
|
||||
});
|
||||
|
||||
defineExpose<TutorialPageCommonExpose>({
|
||||
canContinue: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
@ -0,0 +1,60 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<div style="word-break: auto-phrase; text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._privacySettings.description1 }}</div>
|
||||
|
||||
<MkInfo>{{ i18n.ts._initialTutorial._privacySettings.theseSettingsCanEditLater }}</MkInfo>
|
||||
|
||||
<MkSwitch v-model="isLocked">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
|
||||
|
||||
<MkSwitch v-model="publicReactions">{{ i18n.ts.makeReactionsPublic }}<template #caption>{{ i18n.ts.makeReactionsPublicDescription }}</template></MkSwitch>
|
||||
|
||||
<MkSwitch v-model="hideOnlineStatus">{{ i18n.ts.hideOnlineStatus }}<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template></MkSwitch>
|
||||
|
||||
<MkSwitch v-model="noCrawle">{{ i18n.ts.noCrawle }}<template #caption>{{ i18n.ts.noCrawleDescription }}</template></MkSwitch>
|
||||
|
||||
<MkSwitch v-model="preventAiLearning">{{ i18n.ts.preventAiLearning }}<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template></MkSwitch>
|
||||
|
||||
<MkInfo>{{ i18n.ts._initialTutorial._privacySettings.youCanEditMoreSettingsInSettingsPageLater }}</MkInfo>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
const isLocked = ref($i.isLocked);
|
||||
const publicReactions = ref($i.publicReactions);
|
||||
const hideOnlineStatus = ref($i.hideOnlineStatus);
|
||||
const noCrawle = ref($i.noCrawle);
|
||||
const preventAiLearning = ref($i.preventAiLearning);
|
||||
|
||||
watch([isLocked, publicReactions, hideOnlineStatus, noCrawle, preventAiLearning], () => {
|
||||
misskeyApi('i/update', {
|
||||
isLocked: !!isLocked.value,
|
||||
publicReactions: !!publicReactions.value,
|
||||
hideOnlineStatus: !!hideOnlineStatus.value,
|
||||
noCrawle: !!noCrawle.value,
|
||||
preventAiLearning: !!preventAiLearning.value,
|
||||
});
|
||||
});
|
||||
|
||||
defineExpose<TutorialPageCommonExpose>({
|
||||
canContinue: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
</style>
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<MkInfo>{{ i18n.ts._initialAccountSetting.theseSettingsCanEditLater }}</MkInfo>
|
||||
<div style="word-break: auto-phrase; text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._profileSettings.description }}</div>
|
||||
|
||||
<FormSlot>
|
||||
<template #label>{{ i18n.ts.avatar }}</template>
|
||||
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>{{ i18n.ts._profile.description }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkInfo>{{ i18n.ts._initialAccountSetting.youCanEditMoreSettingsInSettingsPageLater }}</MkInfo>
|
||||
<MkInfo>{{ i18n.ts._initialTutorial._profileSettings.youCanChangeThemLater }}</MkInfo>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -37,9 +37,10 @@ import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { chooseFileFromPc } from '@/scripts/select-file.js';
|
||||
import { selectFile } from '@/scripts/select-file.js';
|
||||
import * as os from '@/os.js';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import { signinRequired, updateAccountPartial } from '@/account.js';
|
||||
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
@ -57,6 +58,7 @@ watch(name, () => {
|
||||
text: i18n.ts.yourNameContainsProhibitedWordsDescription,
|
||||
},
|
||||
});
|
||||
updateAccountPartial({ name: name.value });
|
||||
});
|
||||
|
||||
watch(description, () => {
|
||||
@ -65,12 +67,11 @@ watch(description, () => {
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
description: description.value || null,
|
||||
});
|
||||
updateAccountPartial({ description: description.value });
|
||||
});
|
||||
|
||||
function setAvatar(ev) {
|
||||
chooseFileFromPc(false).then(async (files) => {
|
||||
const file = files[0];
|
||||
|
||||
function setAvatar(ev: MouseEvent) {
|
||||
selectFile(ev.currentTarget ?? ev.target).then(async (file) => {
|
||||
let originalOrCropped = file;
|
||||
|
||||
const { canceled } = await os.confirm({
|
||||
@ -89,10 +90,13 @@ function setAvatar(ev) {
|
||||
const i = await os.apiWithDialog('i/update', {
|
||||
avatarId: originalOrCropped.id,
|
||||
});
|
||||
$i.avatarId = i.avatarId;
|
||||
$i.avatarUrl = i.avatarUrl;
|
||||
updateAccountPartial({ avatarId: i.avatarId, avatarUrl: i.avatarUrl });
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose<TutorialPageCommonExpose>({
|
||||
canContinue: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
@ -104,5 +108,6 @@ function setAvatar(ev) {
|
||||
.avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: var(--MI_THEME-bg);
|
||||
}
|
||||
</style>
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.description }}</div>
|
||||
<div style="word-break: auto-phrase; text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.description }}</div>
|
||||
<div>{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.tryThisFile }}</div>
|
||||
<MkInfo>{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.method }}</MkInfo>
|
||||
<MkPostForm
|
||||
@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
@fileChangeSensitive="doSucceeded"
|
||||
></MkPostForm>
|
||||
<div v-if="onceSucceeded"><b style="color: var(--MI_THEME-accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div>
|
||||
<div v-else><b :class="$style.actionWaitText">{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.doItToContinue }}</b></div>
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.previewNoteText }}</template>
|
||||
<MkNote :mock="true" :note="exampleNote" :class="$style.exampleRoot"></MkNote>
|
||||
@ -32,17 +33,13 @@ import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'succeeded'): void;
|
||||
}>();
|
||||
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||
|
||||
const onceSucceeded = ref<boolean>(false);
|
||||
|
||||
function doSucceeded(fileId: string, to: boolean) {
|
||||
if (fileId === exampleNote.fileIds?.[0] && to) {
|
||||
onceSucceeded.value = true;
|
||||
emit('succeeded');
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,6 +84,9 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
||||
renoteId: null,
|
||||
});
|
||||
|
||||
defineExpose<TutorialPageCommonExpose>({
|
||||
canContinue: onceSucceeded,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
@ -143,4 +143,8 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
||||
position: relative;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.actionWaitText {
|
||||
color: var(--MI_THEME-error);
|
||||
}
|
||||
</style>
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._timeline.description1 }}</div>
|
||||
<div style="word-break: auto-phrase; text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._timeline.description1 }}</div>
|
||||
<div class="_gaps_s">
|
||||
<div v-for="tl in basicTimelineTypes">
|
||||
<i :class="basicTimelineIconClass(tl)"></i> <b>{{ i18n.ts._timelines[tl] }}</b> … {{ i18n.ts._initialTutorial._timeline[tl] }}
|
||||
@ -27,6 +27,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script setup lang="ts">
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js';
|
||||
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||
|
||||
defineExpose<TutorialPageCommonExpose>({
|
||||
canContinue: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
439
packages/frontend/src/components/MkTutorial.vue
Normal file
439
packages/frontend/src/components/MkTutorial.vue
Normal file
@ -0,0 +1,439 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.tutorialRoot">
|
||||
<div v-if="showProgressbar" :class="$style.progressBar">
|
||||
<div :class="$style.progressBarValue" :style="{ width: `${(page / MAX_PAGE) * 100}%` }"></div>
|
||||
</div>
|
||||
<div v-if="showProgressbar && page !== 0 && page !== MAX_PAGE" :class="$style.progressText">{{ page }}/{{ MAX_PAGE }}</div>
|
||||
<div :class="$style.tutorialMain">
|
||||
<Transition
|
||||
mode="out-in"
|
||||
:enterActiveClass="$style.transition_x_enterActive"
|
||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||
:enterFromClass="$style.transition_x_enterFrom"
|
||||
:leaveToClass="$style.transition_x_leaveTo"
|
||||
|
||||
@beforeLeave="areButtonsLocked = true"
|
||||
@afterEnter="areButtonsLocked = false"
|
||||
>
|
||||
<slot v-if="page === 0" key="tutorialPage_0" name="welcome" :close="() => emit('close', true)" :next="next">
|
||||
<div :class="$style.centerPage">
|
||||
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._landing.title }}</div>
|
||||
<div>{{ i18n.ts._initialTutorial._landing.description }}</div>
|
||||
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-start @click="next">{{ i18n.ts._initialTutorial.launchTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
<MkButton v-if="skippable" style="margin: 0 auto;" transparent rounded data-cy-user-setup-close @click="emit('close', true)">{{ i18n.ts.close }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</slot>
|
||||
<slot v-else-if="page === MAX_PAGE" :key="`tutorialPage_${MAX_PAGE}`" name="finish" :close="() => emit('close')" :prev="prev">
|
||||
<div :class="$style.centerPage">
|
||||
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps">
|
||||
<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
|
||||
<div style="text-align: center; font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div>
|
||||
<I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="text-align: center; padding: 0 16px;">
|
||||
<template #link>
|
||||
<a href="https://misskey-hub.net/docs/for-users/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
|
||||
</template>
|
||||
</I18n>
|
||||
<div style="text-align: center;">{{ i18n.ts._initialTutorial._done.youCanReferTutorialBy }}</div>
|
||||
<div style="text-align: center;">{{ i18n.tsx._initialTutorial._done.haveFun({ name: instance.name ?? host }) }}</div>
|
||||
<div class="_buttonsCenter" style="margin-top: 16px;">
|
||||
<MkButton rounded @click="prev"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton rounded primary gradate @click="emit('close')">{{ i18n.ts.close }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</slot>
|
||||
<div v-else :key="`tutorialPage_${page}`" :class="$style.pageContainer">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<component
|
||||
:is="componentsDef[page - 1].component"
|
||||
ref="tutorialPageEl"
|
||||
v-bind="componentsDef[page - 1].props"
|
||||
/>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
<div :class="[$style.pageFooter, { [$style.pageFooterShown]: (page > 0 && page < MAX_PAGE) }]" :inert="(page <= 0 && page >= MAX_PAGE)">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton v-if="initialPage !== page" :disabled="areButtonsLocked" rounded @click="prev"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton primary rounded gradate :disabled="!canContinue" data-cy-user-setup-next @click="next">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Ref } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
/**
|
||||
* 【ページの足し方】
|
||||
*
|
||||
* 1. ページコンポーネントを作成
|
||||
* このとき、TutorialPageCommonExposeを実装すること
|
||||
* (canContinueを変化させることで、次へボタンが押されるのをブロックできます。ギミックがないページはtrueでOK。)
|
||||
* 2. tutorialBodyPagesDefにページのアイコン・タイトル・区分を追加
|
||||
* (区分がsetupの場合はwithSetup == falseのときにスキップされます)
|
||||
* 3. componentsDefにページのコンポーネントを追加(順番を対応させること)
|
||||
* 4. cypress/e2e/basic.cy.tsにページ分のテストを追加
|
||||
*/
|
||||
|
||||
/** チュートリアルページ用Expose */
|
||||
export type TutorialPageCommonExpose = {
|
||||
canContinue: boolean | Ref<boolean>;
|
||||
};
|
||||
|
||||
/** ページ メタデータ */
|
||||
export type TutorialPage = {
|
||||
icon?: string;
|
||||
type: 'tutorial' | 'setup';
|
||||
title: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* はじめと終わり以外のページ メタデータ
|
||||
*
|
||||
* (コンポーネントはsetup内で定義しています)
|
||||
*/
|
||||
export const tutorialBodyPagesDef = [{
|
||||
icon: 'ti ti-user',
|
||||
type: 'setup',
|
||||
title: i18n.ts._initialTutorial._profileSettings.title,
|
||||
}, {
|
||||
icon: 'ti ti-pencil',
|
||||
type: 'tutorial',
|
||||
title: i18n.ts._initialTutorial._note.title,
|
||||
}, {
|
||||
icon: 'ti ti-mood-smile',
|
||||
type: 'tutorial',
|
||||
title: i18n.ts._initialTutorial._reaction.title,
|
||||
}, {
|
||||
icon: 'ti ti-home',
|
||||
type: 'tutorial',
|
||||
title: i18n.ts._initialTutorial._timeline.title,
|
||||
}, {
|
||||
icon: 'ti ti-user',
|
||||
type: 'setup',
|
||||
title: i18n.ts.follow,
|
||||
}, {
|
||||
icon: 'ti ti-pencil-plus',
|
||||
type: 'tutorial',
|
||||
title: i18n.ts._initialTutorial._postNote.title,
|
||||
}, {
|
||||
icon: 'ti ti-eye-exclamation',
|
||||
type: 'tutorial',
|
||||
title: i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.title,
|
||||
}, {
|
||||
icon: 'ti ti-lock',
|
||||
type: 'setup',
|
||||
title: i18n.ts._initialTutorial._privacySettings.title,
|
||||
}] as const satisfies TutorialPage[];
|
||||
|
||||
export const MAX_PAGE = tutorialBodyPagesDef.length + 1; // 0始まりにするために +2 - 1 = +1
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, isRef, computed, watch } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import XProfileSettings from '@/components/MkTutorial.ProfileSettings.vue';
|
||||
import XNote from '@/components/MkTutorial.Note.vue';
|
||||
import XTimeline from '@/components/MkTutorial.Timeline.vue';
|
||||
import XFollowUsers from '@/components/MkTutorial.FollowUsers.vue';
|
||||
import XPostNote from '@/components/MkTutorial.PostNote.vue';
|
||||
import XSensitive from '@/components/MkTutorial.Sensitive.vue';
|
||||
import XPrivacySettings from '@/components/MkTutorial.PrivacySettings.vue';
|
||||
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||
import { instance } from '@/instance.js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
|
||||
import type { Component } from 'vue';
|
||||
import type { Tuple } from '@/type.js';
|
||||
|
||||
const props = defineProps<{
|
||||
initialPage?: number;
|
||||
showProgressbar?: boolean;
|
||||
skippable?: boolean;
|
||||
withSetup?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'pageChanged', to: number): void;
|
||||
(ev: 'close', withConfirm?: boolean): void;
|
||||
}>();
|
||||
|
||||
// テストの場合は全インタラクションをスキップする
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
const isTest = (import.meta.env.MODE === 'test' || window.Cypress != null);
|
||||
|
||||
type ComponentDef = {
|
||||
component: Component;
|
||||
props?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* はじめと終わり以外のページ コンポーネント
|
||||
*
|
||||
* (メタデータは上の方で定義しています)
|
||||
*/
|
||||
const componentsDef: Tuple<ComponentDef, typeof tutorialBodyPagesDef.length> = [
|
||||
{ component: XProfileSettings },
|
||||
{ component: XNote, props: { phase: 'aboutNote' } },
|
||||
{ component: XNote, props: { phase: 'howToReact' } },
|
||||
{ component: XTimeline },
|
||||
{ component: XFollowUsers },
|
||||
{ component: XPostNote },
|
||||
{ component: XSensitive },
|
||||
{ component: XPrivacySettings },
|
||||
];
|
||||
|
||||
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||
const page = ref(props.initialPage ?? 0);
|
||||
|
||||
const currentPageDef = computed(() => {
|
||||
if (page.value > 0 && page.value < MAX_PAGE) {
|
||||
return tutorialBodyPagesDef[page.value - 1] ?? null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
watch(page, (to) => {
|
||||
if (to === MAX_PAGE) {
|
||||
claimAchievement('tutorialCompleted');
|
||||
}
|
||||
});
|
||||
|
||||
// ページコンポーネントのexposeを受け取る
|
||||
const tutorialPageEl = shallowRef<TutorialPageCommonExpose | null>(null);
|
||||
|
||||
// トランジション中に連打されて進んじゃうのを防ぐ
|
||||
const areButtonsLocked = ref(false);
|
||||
|
||||
const canContinue = computed(() => {
|
||||
if (isTest) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (areButtonsLocked.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tutorialPageEl.value) {
|
||||
if (isRef(tutorialPageEl.value.canContinue)) {
|
||||
return tutorialPageEl.value.canContinue.value;
|
||||
} else {
|
||||
return tutorialPageEl.value.canContinue;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
function findNextPage(type: TutorialPage['type']) {
|
||||
const bodyPagesDefIndex = page.value - 1;
|
||||
|
||||
for (let i = bodyPagesDefIndex + 1; i < tutorialBodyPagesDef.length; i++) {
|
||||
if (tutorialBodyPagesDef[i] == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (tutorialBodyPagesDef[i].type === type) {
|
||||
return i + 1; // はじめの1ページ分足す
|
||||
}
|
||||
}
|
||||
|
||||
return MAX_PAGE;
|
||||
}
|
||||
|
||||
function findPrevPage(type: TutorialPage['type']) {
|
||||
const bodyPagesDefIndex = page.value - 1;
|
||||
|
||||
for (let i = bodyPagesDefIndex - 1; i >= 0; i--) {
|
||||
if (tutorialBodyPagesDef[i] == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (tutorialBodyPagesDef[i].type === type) {
|
||||
return i + 1; // はじめの1ページ分足す
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function next() {
|
||||
if (areButtonsLocked.value) {
|
||||
return;
|
||||
} else {
|
||||
areButtonsLocked.value = true;
|
||||
}
|
||||
|
||||
const bodyPagesDefIndex = page.value - 1;
|
||||
|
||||
if (!props.withSetup && tutorialBodyPagesDef[bodyPagesDefIndex + 1].type !== 'tutorial') {
|
||||
page.value = findNextPage('tutorial');
|
||||
} else {
|
||||
page.value = Math.min(page.value + 1, MAX_PAGE);
|
||||
}
|
||||
|
||||
emit('pageChanged', page.value);
|
||||
}
|
||||
|
||||
function prev() {
|
||||
if (areButtonsLocked.value) {
|
||||
return;
|
||||
} else {
|
||||
areButtonsLocked.value = true;
|
||||
}
|
||||
|
||||
const bodyPagesDefIndex = page.value - 1;
|
||||
|
||||
if (!props.withSetup && tutorialBodyPagesDef[bodyPagesDefIndex - 1].type !== 'tutorial') {
|
||||
page.value = findPrevPage('tutorial');
|
||||
} else {
|
||||
page.value = Math.max(page.value - 1, 0);
|
||||
}
|
||||
|
||||
emit('pageChanged', page.value);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
page,
|
||||
currentPageDef,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.tutorialRoot {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tutorialMain {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.progressBarValue {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
|
||||
transition: all 0.5s cubic-bezier(0,.5,.5,1);
|
||||
}
|
||||
|
||||
.progressText {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 1em;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.progressBarValue {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
|
||||
transition: all 0.5s cubic-bezier(0,.5,.5,1);
|
||||
}
|
||||
|
||||
.centerPage {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pageContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pageRoot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.pageMain {
|
||||
flex-grow: 1;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 56px;
|
||||
}
|
||||
|
||||
.pageFooter {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
|
||||
transition: transform 0.3s cubic-bezier(0,0,.35,1);
|
||||
transform: translateY(100%);
|
||||
visibility: hidden;
|
||||
|
||||
&.pageFooterShown {
|
||||
transform: translateY(0);
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -11,158 +11,29 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
@close="close(true)"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template v-if="page === 1" #header><i class="ti ti-pencil"></i> {{ i18n.ts._initialTutorial._note.title }}</template>
|
||||
<template v-else-if="page === 2" #header><i class="ti ti-mood-smile"></i> {{ i18n.ts._initialTutorial._reaction.title }}</template>
|
||||
<template v-else-if="page === 3" #header><i class="ti ti-home"></i> {{ i18n.ts._initialTutorial._timeline.title }}</template>
|
||||
<template v-else-if="page === 4" #header><i class="ti ti-pencil-plus"></i> {{ i18n.ts._initialTutorial._postNote.title }}</template>
|
||||
<template v-else-if="page === 5" #header><i class="ti ti-eye-exclamation"></i> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.title }}</template>
|
||||
<template v-if="tutorialEl?.currentPageDef" #header>
|
||||
<i v-if="tutorialEl.currentPageDef.icon" :class="tutorialEl.currentPageDef.icon"></i>
|
||||
{{ tutorialEl.currentPageDef.title }}
|
||||
</template>
|
||||
<template v-else #header>{{ i18n.ts._initialTutorial.title }}</template>
|
||||
|
||||
<div style="overflow-x: clip;">
|
||||
<Transition
|
||||
mode="out-in"
|
||||
:enterActiveClass="$style.transition_x_enterActive"
|
||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||
:enterFromClass="$style.transition_x_enterFrom"
|
||||
:leaveToClass="$style.transition_x_leaveTo"
|
||||
>
|
||||
<template v-if="page === 0">
|
||||
<div :class="$style.centerPage">
|
||||
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._landing.title }}</div>
|
||||
<div>{{ i18n.ts._initialTutorial._landing.description }}</div>
|
||||
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts._initialTutorial.launchTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
<MkButton style="margin: 0 auto;" transparent rounded @click="close(true)">{{ i18n.ts.close }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 1">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<XNote phase="aboutNote"/>
|
||||
</MkSpacer>
|
||||
<div :class="$style.pageFooter">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton v-if="initialPage !== 1" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 2">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<div class="_gaps">
|
||||
<XNote phase="howToReact" @reacted="isReactionTutorialPushed = true"/>
|
||||
<div v-if="!isReactionTutorialPushed">{{ i18n.ts._initialTutorial._reaction.reactToContinue }}</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<div :class="$style.pageFooter">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton primary rounded gradate :disabled="!isReactionTutorialPushed" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 3">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<XTimeline/>
|
||||
</MkSpacer>
|
||||
<div :class="$style.pageFooter">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 4">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<XPostNote/>
|
||||
</MkSpacer>
|
||||
<div :class="$style.pageFooter">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 5">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<div class="_gaps">
|
||||
<XSensitive @succeeded="isSensitiveTutorialSucceeded = true"/>
|
||||
<div v-if="!isSensitiveTutorialSucceeded">{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.doItToContinue }}</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<div :class="$style.pageFooter">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton primary rounded gradate :disabled="!isSensitiveTutorialSucceeded" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 6">
|
||||
<div :class="$style.centerPage">
|
||||
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div>
|
||||
<I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;">
|
||||
<template #link>
|
||||
<a href="https://misskey-hub.net/docs/for-users/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
|
||||
</template>
|
||||
</I18n>
|
||||
<div>{{ i18n.tsx._initialAccountSetting.haveFun({ name: instance.name ?? host }) }}</div>
|
||||
<div class="_buttonsCenter" style="margin-top: 16px;">
|
||||
<MkButton v-if="initialPage !== 4" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton rounded primary gradate @click="close(false)">{{ i18n.ts.close }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
</Transition>
|
||||
</div>
|
||||
<XTutorial
|
||||
ref="tutorialEl"
|
||||
:initialPage="initialPage"
|
||||
:skippable="true"
|
||||
@close="close"
|
||||
/>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, watch } from 'vue';
|
||||
import { shallowRef } from 'vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import XNote from '@/components/MkTutorialDialog.Note.vue';
|
||||
import XTimeline from '@/components/MkTutorialDialog.Timeline.vue';
|
||||
import XPostNote from '@/components/MkTutorialDialog.PostNote.vue';
|
||||
import XSensitive from '@/components/MkTutorialDialog.Sensitive.vue';
|
||||
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||
import XTutorial from '@/components/MkTutorial.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
initialPage?: number;
|
||||
}>();
|
||||
|
||||
@ -172,20 +43,9 @@ const emit = defineEmits<{
|
||||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
||||
const page = ref(props.initialPage ?? 0);
|
||||
const tutorialEl = shallowRef<InstanceType<typeof XTutorial>>();
|
||||
|
||||
watch(page, (to) => {
|
||||
// チュートリアルの枚数を増やしたら必ず変更すること!!
|
||||
if (to === 6) {
|
||||
claimAchievement('tutorialCompleted');
|
||||
}
|
||||
});
|
||||
|
||||
const isReactionTutorialPushed = ref<boolean>(false);
|
||||
const isSensitiveTutorialSucceeded = ref<boolean>(false);
|
||||
|
||||
async function close(skip: boolean) {
|
||||
async function close(skip?: boolean) {
|
||||
if (skip) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
|
@ -1,56 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
import MkUserSetupDialog_Follow from './MkUserSetupDialog.Follow.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkUserSetupDialog_Follow,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkUserSetupDialog_Follow v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
http.post('/api/users', () => {
|
||||
return HttpResponse.json([
|
||||
userDetailed('44'),
|
||||
userDetailed('49'),
|
||||
]);
|
||||
}),
|
||||
http.post('/api/pinned-users', () => {
|
||||
return HttpResponse.json([
|
||||
userDetailed('44'),
|
||||
userDetailed('49'),
|
||||
]);
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
} satisfies StoryObj<typeof MkUserSetupDialog_Follow>;
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import MkUserSetupDialog_Privacy from './MkUserSetupDialog.Privacy.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkUserSetupDialog_Privacy,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkUserSetupDialog_Privacy v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkUserSetupDialog_Privacy>;
|
@ -1,71 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<MkInfo>{{ i18n.ts._initialAccountSetting.theseSettingsCanEditLater }}</MkInfo>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.makeFollowManuallyApprove }}</template>
|
||||
<template #icon><i class="ti ti-lock"></i></template>
|
||||
<template #suffix>{{ isLocked ? i18n.ts.on : i18n.ts.off }}</template>
|
||||
|
||||
<MkSwitch v-model="isLocked">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.hideOnlineStatus }}</template>
|
||||
<template #icon><i class="ti ti-eye-off"></i></template>
|
||||
<template #suffix>{{ hideOnlineStatus ? i18n.ts.on : i18n.ts.off }}</template>
|
||||
|
||||
<MkSwitch v-model="hideOnlineStatus">{{ i18n.ts.hideOnlineStatus }}<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template></MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.noCrawle }}</template>
|
||||
<template #icon><i class="ti ti-world-x"></i></template>
|
||||
<template #suffix>{{ noCrawle ? i18n.ts.on : i18n.ts.off }}</template>
|
||||
|
||||
<MkSwitch v-model="noCrawle">{{ i18n.ts.noCrawle }}<template #caption>{{ i18n.ts.noCrawleDescription }}</template></MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.preventAiLearning }}</template>
|
||||
<template #icon><i class="ti ti-photo-shield"></i></template>
|
||||
<template #suffix>{{ preventAiLearning ? i18n.ts.on : i18n.ts.off }}</template>
|
||||
|
||||
<MkSwitch v-model="preventAiLearning">{{ i18n.ts.preventAiLearning }}<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template></MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkInfo>{{ i18n.ts._initialAccountSetting.youCanEditMoreSettingsInSettingsPageLater }}</MkInfo>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
|
||||
const isLocked = ref(false);
|
||||
const hideOnlineStatus = ref(false);
|
||||
const noCrawle = ref(false);
|
||||
const preventAiLearning = ref(true);
|
||||
|
||||
watch([isLocked, hideOnlineStatus, noCrawle, preventAiLearning], () => {
|
||||
misskeyApi('i/update', {
|
||||
isLocked: !!isLocked.value,
|
||||
hideOnlineStatus: !!hideOnlineStatus.value,
|
||||
noCrawle: !!noCrawle.value,
|
||||
preventAiLearning: !!preventAiLearning.value,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
</style>
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import MkUserSetupDialog_Profile from './MkUserSetupDialog.Profile.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkUserSetupDialog_Profile,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkUserSetupDialog_Profile v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkUserSetupDialog_Profile>;
|
@ -1,37 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
import MkUserSetupDialog_User from './MkUserSetupDialog.User.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkUserSetupDialog_User,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkUserSetupDialog_User v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
user: userDetailed(),
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkUserSetupDialog_User>;
|
@ -1,56 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
import MkUserSetupDialog from './MkUserSetupDialog.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkUserSetupDialog,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkUserSetupDialog v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
http.post('/api/users', () => {
|
||||
return HttpResponse.json([
|
||||
userDetailed('44'),
|
||||
userDetailed('49'),
|
||||
]);
|
||||
}),
|
||||
http.post('/api/pinned-users', () => {
|
||||
return HttpResponse.json([
|
||||
userDetailed('44'),
|
||||
userDetailed('49'),
|
||||
]);
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
} satisfies StoryObj<typeof MkUserSetupDialog>;
|
@ -1,259 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="500"
|
||||
:height="550"
|
||||
data-cy-user-setup
|
||||
@close="close(true)"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template v-if="page === 1" #header><i class="ti ti-user-edit"></i> {{ i18n.ts._initialAccountSetting.profileSetting }}</template>
|
||||
<template v-else-if="page === 2" #header><i class="ti ti-lock"></i> {{ i18n.ts._initialAccountSetting.privacySetting }}</template>
|
||||
<template v-else-if="page === 3" #header><i class="ti ti-user-plus"></i> {{ i18n.ts.follow }}</template>
|
||||
<template v-else-if="page === 4" #header><i class="ti ti-bell-plus"></i> {{ i18n.ts.pushNotification }}</template>
|
||||
<template v-else-if="page === 5" #header>{{ i18n.ts.done }}</template>
|
||||
<template v-else #header>{{ i18n.ts.initialAccountSetting }}</template>
|
||||
|
||||
<div style="overflow-x: clip;">
|
||||
<div :class="$style.progressBar">
|
||||
<div :class="$style.progressBarValue" :style="{ width: `${(page / 5) * 100}%` }"></div>
|
||||
</div>
|
||||
<Transition
|
||||
mode="out-in"
|
||||
:enterActiveClass="$style.transition_x_enterActive"
|
||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||
:enterFromClass="$style.transition_x_enterFrom"
|
||||
:leaveToClass="$style.transition_x_leaveTo"
|
||||
>
|
||||
<template v-if="page === 0">
|
||||
<div :class="$style.centerPage">
|
||||
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.accountCreated }}</div>
|
||||
<div>{{ i18n.ts._initialAccountSetting.letsStartAccountSetup }}</div>
|
||||
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts._initialAccountSetting.profileSetting }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
<MkButton style="margin: 0 auto;" transparent rounded @click="later(true)">{{ i18n.ts.later }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 1">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<XProfile/>
|
||||
</MkSpacer>
|
||||
<div :class="$style.pageFooter">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 2">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<XPrivacy/>
|
||||
</MkSpacer>
|
||||
<div :class="$style.pageFooter">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 3">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<XFollow/>
|
||||
</MkSpacer>
|
||||
<div :class="$style.pageFooter">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton primary rounded gradate style="" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 4">
|
||||
<div :class="$style.centerPage">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div>
|
||||
<div style="padding: 0 16px;">{{ i18n.tsx._initialAccountSetting.pushNotificationDescription({ name: instance.name ?? host }) }}</div>
|
||||
<MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/>
|
||||
<div class="_buttonsCenter" style="margin-top: 16px;">
|
||||
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 5">
|
||||
<div :class="$style.centerPage">
|
||||
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div>
|
||||
<div>{{ i18n.tsx._initialAccountSetting.youCanContinueTutorial({ name: instance.name ?? host }) }}</div>
|
||||
<div class="_buttonsCenter" style="margin-top: 16px;">
|
||||
<MkButton rounded primary gradate data-cy-user-setup-continue @click="launchTutorial()">{{ i18n.ts._initialAccountSetting.startTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton rounded primary data-cy-user-setup-continue @click="setupComplete()">{{ i18n.ts.close }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
</Transition>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, watch, nextTick, defineAsyncComponent } from 'vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import XProfile from '@/components/MkUserSetupDialog.Profile.vue';
|
||||
import XFollow from '@/components/MkUserSetupDialog.Follow.vue';
|
||||
import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue';
|
||||
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
||||
const page = ref(defaultStore.state.accountSetupWizard);
|
||||
|
||||
watch(page, () => {
|
||||
defaultStore.set('accountSetupWizard', page.value);
|
||||
});
|
||||
|
||||
async function close(skip: boolean) {
|
||||
if (skip) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts._initialAccountSetting.skipAreYouSure,
|
||||
});
|
||||
if (canceled) return;
|
||||
}
|
||||
|
||||
dialog.value?.close();
|
||||
defaultStore.set('accountSetupWizard', -1);
|
||||
}
|
||||
|
||||
function setupComplete() {
|
||||
defaultStore.set('accountSetupWizard', -1);
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
function launchTutorial() {
|
||||
setupComplete();
|
||||
nextTick(() => {
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {
|
||||
initialPage: 1,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function later(later: boolean) {
|
||||
if (later) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts._initialAccountSetting.laterAreYouSure,
|
||||
});
|
||||
if (canceled) return;
|
||||
}
|
||||
|
||||
dialog.value?.close();
|
||||
defaultStore.set('accountSetupWizard', 0);
|
||||
}
|
||||
</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);
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.progressBarValue {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
|
||||
transition: all 0.5s cubic-bezier(0,.5,.5,1);
|
||||
}
|
||||
|
||||
.centerPage {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100cqh;
|
||||
padding-bottom: 30px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pageRoot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.pageMain {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.pageFooter {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
flex-shrink: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
backdrop-filter: blur(15px);
|
||||
}
|
||||
</style>
|
@ -5,17 +5,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div :class="[$style.root, { [$style.inline]: inline }]">
|
||||
<a v-if="external" :class="$style.main" class="_button" :href="to" target="_blank">
|
||||
<a v-if="external" :class="[$style.main, { [$style.large]: large }]" class="_button" :href="to" target="_blank">
|
||||
<span :class="$style.icon"><slot name="icon"></slot></span>
|
||||
<span :class="$style.text"><slot></slot></span>
|
||||
<div :class="$style.headerText">
|
||||
<div>
|
||||
<MkCondensedLine :minScale="2 / 3"><slot></slot></MkCondensedLine>
|
||||
</div>
|
||||
<div v-if="$slots.caption" :class="$style.headerTextSub">
|
||||
<MkCondensedLine :minScale="2 / 3"><slot name="caption"></slot></MkCondensedLine>
|
||||
</div>
|
||||
</div>
|
||||
<span :class="$style.suffix">
|
||||
<span :class="$style.suffixText"><slot name="suffix"></slot></span>
|
||||
<i class="ti ti-external-link"></i>
|
||||
</span>
|
||||
</a>
|
||||
<MkA v-else :class="[$style.main, { [$style.active]: active }]" class="_button" :to="to" :behavior="behavior">
|
||||
<MkA v-else :class="[$style.main, { [$style.large]: large, [$style.active]: active }]" class="_button" :to="to" :behavior="behavior">
|
||||
<span :class="$style.icon"><slot name="icon"></slot></span>
|
||||
<span :class="$style.text"><slot></slot></span>
|
||||
<div :class="$style.headerText">
|
||||
<div>
|
||||
<MkCondensedLine :minScale="2 / 3"><slot></slot></MkCondensedLine>
|
||||
</div>
|
||||
<div v-if="$slots.caption" :class="$style.headerTextSub">
|
||||
<MkCondensedLine :minScale="2 / 3"><slot name="caption"></slot></MkCondensedLine>
|
||||
</div>
|
||||
</div>
|
||||
<span :class="$style.suffix">
|
||||
<span :class="$style.suffixText"><slot name="suffix"></slot></span>
|
||||
<i class="ti ti-chevron-right"></i>
|
||||
@ -33,6 +47,7 @@ const props = defineProps<{
|
||||
external?: boolean;
|
||||
behavior?: null | 'window' | 'browser';
|
||||
inline?: boolean;
|
||||
large?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@ -55,6 +70,10 @@ const props = defineProps<{
|
||||
border-radius: 6px;
|
||||
font-size: 0.9em;
|
||||
|
||||
&.large {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background: var(--MI_THEME-folderHeaderHoverBg);
|
||||
@ -81,11 +100,17 @@ const props = defineProps<{
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
flex-shrink: 1;
|
||||
white-space: normal;
|
||||
.headerText {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
text-align: start;
|
||||
overflow: hidden;
|
||||
padding-right: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.headerTextSub {
|
||||
color: var(--MI_THEME-fgTransparentWeak);
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
.suffix {
|
||||
|
@ -22,6 +22,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="prohibitSkippingInitialTutorial" @change="onChange_prohibitSkippingInitialTutorial">
|
||||
<template #label>{{ i18n.ts.prohibitSkippingInitialTutorial }}</template>
|
||||
<template #caption>{{ i18n.ts.prohibitSkippingInitialTutorialDescription }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
||||
|
||||
<MkFolder>
|
||||
@ -144,6 +149,7 @@ import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
const enableRegistration = ref<boolean>(false);
|
||||
const emailRequiredForSignup = ref<boolean>(false);
|
||||
const prohibitSkippingInitialTutorial = ref<boolean>(false);
|
||||
const sensitiveWords = ref<string>('');
|
||||
const prohibitedWords = ref<string>('');
|
||||
const prohibitedWordsForNameOfUser = ref<string>('');
|
||||
@ -157,6 +163,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');
|
||||
prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n');
|
||||
@ -193,6 +200,14 @@ function onChange_emailRequiredForSignup(value: boolean) {
|
||||
});
|
||||
}
|
||||
|
||||
function onChange_prohibitSkippingInitialTutorial(value: boolean) {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
canSkipInitialTutorial: !value,
|
||||
}).then(() => {
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
function save_preservedUsernames() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
preservedUsernames: preservedUsernames.value.split('\n'),
|
||||
|
383
packages/frontend/src/pages/onboarding.vue
Normal file
383
packages/frontend/src/pages/onboarding.vue
Normal file
@ -0,0 +1,383 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.onboardingRoot, { [$style.ready]: animationPhase >= 1 }]">
|
||||
<MkAnimBg :class="$style.onboardingBg"/>
|
||||
<div :class="[$style.onboardingContainer]">
|
||||
<div :class="[$style.tutorialTitle, { [$style.showing]: ((tutorialEl?.page ?? 0) !== 0) }]">
|
||||
<div :class="$style.text">
|
||||
<span v-if="tutorialEl?.currentPageDef">
|
||||
<i v-if="tutorialEl.currentPageDef.icon" :class="tutorialEl.currentPageDef.icon"></i> {{ tutorialEl.currentPageDef.title }}
|
||||
</span>
|
||||
<span v-else>{{ i18n.ts._initialTutorial.title }}</span>
|
||||
</div>
|
||||
<div v-if="instance.canSkipInitialTutorial" :class="$style.closeButton">
|
||||
<button class="_button" data-cy-user-setup-close @click="cancel"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<MkTutorial
|
||||
ref="tutorialEl"
|
||||
:class="$style.tutorialRoot"
|
||||
:showProgressbar="true"
|
||||
:skippable="false"
|
||||
:withSetup="true"
|
||||
>
|
||||
<template #welcome="{ next }">
|
||||
<div ref="welcomePageRootEl" :class="$style.welcomePageRoot">
|
||||
<canvas ref="confettiEl" :class="$style.welcomePageConfetti"></canvas>
|
||||
<div
|
||||
:class="[
|
||||
$style.centerPage,
|
||||
$style.welcomePageMain,
|
||||
{
|
||||
[$style.appear]: animationPhase >= 3,
|
||||
[$style.done]: animationPhase === 4,
|
||||
}
|
||||
]"
|
||||
>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps" style="word-break: auto-phrase; text-align: center;">
|
||||
<img ref="instanceIconEl" :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
|
||||
<div>
|
||||
<div style="font-size: 135%;">{{ i18n.ts._initialTutorial._onboardingLanding.accountCreated }}</div>
|
||||
<div>{{ i18n.tsx._initialTutorial._onboardingLanding.welcomeToX({ name: instance.name ?? host }) }}</div>
|
||||
</div>
|
||||
<div>{{ i18n.tsx._initialTutorial._onboardingLanding.description({ name: instance.name ?? host }) }}</div>
|
||||
<MkButton large primary rounded gradate style="margin: 16px auto 0;" data-cy-user-setup-start @click="next">{{ i18n.ts.start }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
<MkButton v-if="instance.canSkipInitialTutorial" transparent rounded style="margin: 0 auto;" data-cy-user-setup-close @click="cancel">{{ i18n.ts.later }}</MkButton>
|
||||
<MkInfo v-else warn style="width: fit-content; margin: 0 auto; text-align: start; white-space: pre-wrap;">{{ i18n.ts._initialTutorial._onboardingLanding.adminForcesToTakeTutorial }}</MkInfo>
|
||||
<MkInfo style="width: fit-content; margin: 0 auto; text-align: start; white-space: pre-wrap;">{{ i18n.tsx._initialTutorial._onboardingLanding.takesAbout({ min: 3 }) }}</MkInfo>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
$style.welcomePageAnimRoot,
|
||||
{
|
||||
[$style.appear]: animationPhase === 2,
|
||||
[$style.move]: animationPhase === 3,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #finish="{ prev }">
|
||||
<div :class="$style.centerPage">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps">
|
||||
<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
|
||||
<div style="text-align: center; font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div>
|
||||
<div style="text-align: center;">{{ i18n.tsx._initialTutorial._onboardingDone.description({ name: instance.name ?? host }) }}</div>
|
||||
<div>
|
||||
<FormLink v-if="originalPath && originalPath !== '/'" :to="originalPath" large :behavior="'browser'">
|
||||
<template #icon><i class="ti ti-directions"></i></template>
|
||||
{{ i18n.ts._initialTutorial._onboardingDone.backToOriginalPath }}
|
||||
<template #caption>{{ i18n.ts._initialTutorial._onboardingDone.backToOriginalPathDescription }}</template>
|
||||
</FormLink>
|
||||
<hr v-if="originalPath && originalPath !== '/'">
|
||||
<div class="_gaps_s">
|
||||
<FormLink to="/settings/profile" large :behavior="'browser'">
|
||||
<template #icon><i class="ti ti-user"></i></template>
|
||||
{{ i18n.ts._initialTutorial._onboardingDone.profile }}
|
||||
<template #caption>{{ i18n.ts._initialTutorial._onboardingDone.profileDescription }}</template>
|
||||
</FormLink>
|
||||
<FormLink to="/explore" large :behavior="'browser'">
|
||||
<template #icon><i class="ti ti-hash"></i></template>
|
||||
{{ i18n.ts.explore }}
|
||||
<template #caption>{{ i18n.ts._initialTutorial._onboardingDone.exploreDescription }}</template>
|
||||
</FormLink>
|
||||
<FormLink to="/" large :behavior="'browser'" data-cy-user-setup-complete>
|
||||
<template #icon><i class="ti ti-home"></i></template>
|
||||
{{ i18n.ts._initialTutorial._onboardingDone.goToTimeline }}
|
||||
<template #caption>{{ i18n.ts._initialTutorial._onboardingDone.goToTimelineDescription }}</template>
|
||||
</FormLink>
|
||||
</div>
|
||||
</div>
|
||||
<MkInfo style="border-radius: 6px;">{{ i18n.ts._initialTutorial._done.youCanReferTutorialBy }}</MkInfo>
|
||||
<div style="text-align: center;">{{ i18n.tsx._initialTutorial._done.haveFun({ name: instance.name ?? host }) }}</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
</MkTutorial>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef, computed, onMounted } from 'vue';
|
||||
import { create as createConfetti } from 'canvas-confetti';
|
||||
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import { confirm as osConfirm } from '@/os.js';
|
||||
|
||||
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkTutorial from '@/components/MkTutorial.vue';
|
||||
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
|
||||
const tutorialEl = shallowRef<InstanceType<typeof MkTutorial> | null>(null);
|
||||
|
||||
// See: @/_boot_/common.ts L123 for details
|
||||
const query = new URLSearchParams(location.search);
|
||||
const originalPath = query.get('redirected_from');
|
||||
|
||||
async function cancel() {
|
||||
const confirm = await osConfirm({
|
||||
type: 'question',
|
||||
title: i18n.ts._initialTutorial.skipAreYouSure,
|
||||
text: i18n.ts._initialTutorial._done.youCanReferTutorialBy,
|
||||
okText: i18n.ts.yes,
|
||||
cancelText: i18n.ts.no,
|
||||
});
|
||||
|
||||
if (confirm.canceled) return;
|
||||
|
||||
location.href = '/';
|
||||
}
|
||||
|
||||
// #region デフォルトオープニング画面のアニメーション
|
||||
const confettiEl = shallowRef<HTMLCanvasElement | null>(null);
|
||||
const welcomePageRootEl = shallowRef<HTMLDivElement | null>(null);
|
||||
const instanceIconEl = shallowRef<HTMLImageElement | null>(null);
|
||||
const instanceIconY = ref(0);
|
||||
// 30pxは文字が上がってくる距離、40pxは上部ヘッダの高さ
|
||||
const instanceIconYPx = computed(() => `${instanceIconY.value - 30 + 40}px`);
|
||||
|
||||
/**
|
||||
* 0 … なにもしない
|
||||
* 1 … 背景表示(mounted)
|
||||
* 2 … サーバーロゴ出現
|
||||
* 3 … サーバーロゴ移動・文字表示
|
||||
* 4 … 完了(オープニング用ロゴ消滅)
|
||||
*/
|
||||
const animationPhase = ref(0);
|
||||
|
||||
// 画面上部に表示されるアイコンの中心Y座標を取得
|
||||
function getIconY(instanceIconEl: HTMLImageElement, welcomePageRootEl: HTMLDivElement) {
|
||||
const instanceIconElRect = instanceIconEl.getBoundingClientRect();
|
||||
return instanceIconElRect.top - welcomePageRootEl.getBoundingClientRect().top;
|
||||
}
|
||||
|
||||
function instanceIconElImageLoaded() {
|
||||
return new Promise<void>((resolve) => {
|
||||
if (instanceIconEl.value!.complete) {
|
||||
resolve();
|
||||
} else {
|
||||
instanceIconEl.value!.addEventListener('load', () => resolve(), { once: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const confetti = createConfetti(confettiEl.value!, {
|
||||
resize: true,
|
||||
});
|
||||
|
||||
instanceIconY.value = getIconY(instanceIconEl.value!, welcomePageRootEl.value!);
|
||||
window.addEventListener('resize', () => {
|
||||
instanceIconY.value = getIconY(instanceIconEl.value!, welcomePageRootEl.value!);
|
||||
}, { passive: true });
|
||||
|
||||
// チュートリアル内で必須(subBootでは初期化されないので)
|
||||
Promise.all([
|
||||
reactionPicker.init(),
|
||||
instanceIconElImageLoaded(),
|
||||
]).then(() => {
|
||||
setTimeout(() => {
|
||||
// 待たないとアニメーションが正しく動かない場合がある
|
||||
animationPhase.value = 1;
|
||||
|
||||
setTimeout(() => {
|
||||
animationPhase.value = 2;
|
||||
|
||||
setTimeout(() => {
|
||||
animationPhase.value = 3;
|
||||
|
||||
setTimeout(() => {
|
||||
animationPhase.value = 4;
|
||||
confetti({
|
||||
spread: 75,
|
||||
particleCount: 100,
|
||||
origin: { y: 0.5 },
|
||||
});
|
||||
}, 1000);
|
||||
}, 1250);
|
||||
}, 500);
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
// #endregion
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.tsx._initialTutorial._onboardingLanding.welcomeToX({ name: instance.name ?? host }),
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.onboardingRoot {
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
min-height: 100svh;
|
||||
padding: 32px 32px 64px 32px;
|
||||
}
|
||||
|
||||
.onboardingBg {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
transition: opacity 2s ease;
|
||||
}
|
||||
|
||||
.onboardingContainer {
|
||||
position: relative;
|
||||
border-radius: var(--MI-radius);
|
||||
background-color: var(--MI_THEME-acrylicPanel);
|
||||
overflow: clip;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
max-width: 650px;
|
||||
max-height: 700px;
|
||||
width: 100vw;
|
||||
height: 100svh;
|
||||
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.tutorialTitle {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
height: 40px;
|
||||
padding: 0 var(--MI-margin);
|
||||
background: var(--MI_THEME-panelHighlight);
|
||||
display: flex;
|
||||
transition: transform 0.5s ease;
|
||||
transform: translateY(-100%);
|
||||
|
||||
&.showing {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.text {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
margin-left: auto;
|
||||
|
||||
>._button {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tutorialRoot {
|
||||
margin-top: 40px;
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.ready {
|
||||
& .onboardingBg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.centerPage {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.welcomePageRoot {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.welcomePageMain {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
visibility: hidden;
|
||||
|
||||
.instanceIcon {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.appear {
|
||||
transition: opacity 0.75s 0.25s ease, transform 0.75s 0.25s ease;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&.done .instanceIcon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.instanceIcon {
|
||||
height: 5em;
|
||||
width: 5em;
|
||||
margin: 0 auto;
|
||||
object-fit: contain;
|
||||
border-radius: calc(var(--MI-radius) / 2);
|
||||
}
|
||||
|
||||
.welcomePageConfetti,
|
||||
.welcomePageAnimRoot {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: clip;
|
||||
pointer-events: none;
|
||||
|
||||
.instanceIcon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.appear {
|
||||
.instanceIcon {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1.5);
|
||||
transition: opacity 1s cubic-bezier(0.22, 0.61, 0.36, 1), transform 1s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
}
|
||||
|
||||
&.move {
|
||||
.instanceIcon {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0) scale(1);
|
||||
top: v-bind(instanceIconYPx);
|
||||
transition: transform 1s ease, top 1s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -46,7 +46,7 @@ function submit() {
|
||||
misskeyApi('signup-pending', {
|
||||
code: props.code,
|
||||
}).then(res => {
|
||||
return login(res.i, '/');
|
||||
return login(res.i, '/onboarding');
|
||||
}).catch(() => {
|
||||
submitting.value = false;
|
||||
|
||||
|
@ -189,6 +189,10 @@ const routes: RouteDef[] = [{
|
||||
}, {
|
||||
path: '/signup-complete/:code',
|
||||
component: page(() => import('@/pages/signup-complete.vue')),
|
||||
}, {
|
||||
path: '/onboarding',
|
||||
component: page(() => import('@/pages/onboarding.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/announcements',
|
||||
component: page(() => import('@/pages/announcements.vue')),
|
||||
|
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i, refreshAccount } from '@/account.js';
|
||||
|
||||
export const ACHIEVEMENT_TYPES = [
|
||||
'notes1',
|
||||
@ -500,6 +500,10 @@ export async function claimAchievement(type: typeof ACHIEVEMENT_TYPES[number]) {
|
||||
await new Promise(resolve => setTimeout(resolve, (claimingQueue.size - 1) * 500));
|
||||
window.setTimeout(() => {
|
||||
claimingQueue.delete(type);
|
||||
|
||||
if (claimingQueue.size === 0) {
|
||||
refreshAccount();
|
||||
}
|
||||
}, 500);
|
||||
misskeyApi('i/claim-achievement', { name: type });
|
||||
}
|
||||
|
@ -14,12 +14,15 @@ class ReactionPicker {
|
||||
private targetNote: Ref<Misskey.entities.Note | null> = ref(null);
|
||||
private onChosen?: (reaction: string) => void;
|
||||
private onClosed?: () => void;
|
||||
public isInitialized = false;
|
||||
|
||||
constructor() {
|
||||
// nop
|
||||
}
|
||||
|
||||
public async init() {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
const reactionsRef = defaultStore.reactiveState.reactions;
|
||||
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
|
||||
src: this.src,
|
||||
@ -39,6 +42,7 @@ class ReactionPicker {
|
||||
if (this.onClosed) this.onClosed();
|
||||
},
|
||||
});
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
public show(src: HTMLElement | null, targetNote: Misskey.entities.Note | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
|
||||
|
@ -6,3 +6,6 @@
|
||||
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
||||
|
||||
export type WithNonNullable<T, K extends keyof T> = T & { [P in K]-?: NonNullable<T[P]> };
|
||||
|
||||
type TupleOf<T, N extends number, R extends unknown[]> = R['length'] extends N ? R : TupleOf<T, N, [T, ...R]>;
|
||||
export type Tuple<T, N extends number> = N extends N ? number extends N ? T[] : TupleOf<T, N, []> : never;
|
||||
|
@ -4975,6 +4975,7 @@ export type components = {
|
||||
defaultLightTheme: string | null;
|
||||
disableRegistration: boolean;
|
||||
emailRequiredForSignup: boolean;
|
||||
canSkipInitialTutorial: boolean;
|
||||
enableHcaptcha: boolean;
|
||||
hcaptchaSiteKey: string | null;
|
||||
enableMcaptcha: boolean;
|
||||
@ -5106,6 +5107,7 @@ export type operations = {
|
||||
cacheRemoteFiles: boolean;
|
||||
cacheRemoteSensitiveFiles: boolean;
|
||||
emailRequiredForSignup: boolean;
|
||||
canSkipInitialTutorial: boolean;
|
||||
enableHcaptcha: boolean;
|
||||
hcaptchaSiteKey: string | null;
|
||||
enableMcaptcha: boolean;
|
||||
@ -9508,6 +9510,7 @@ export type operations = {
|
||||
cacheRemoteFiles?: boolean;
|
||||
cacheRemoteSensitiveFiles?: boolean;
|
||||
emailRequiredForSignup?: boolean;
|
||||
canSkipInitialTutorial?: boolean;
|
||||
enableHcaptcha?: boolean;
|
||||
hcaptchaSiteKey?: string | null;
|
||||
hcaptchaSecretKey?: string | null;
|
||||
|
80
pnpm-lock.yaml
generated
80
pnpm-lock.yaml
generated
@ -142,7 +142,7 @@ importers:
|
||||
version: 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
|
||||
'@nestjs/testing':
|
||||
specifier: 10.4.7
|
||||
version: 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7))
|
||||
version: 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-express@10.4.7)
|
||||
'@peertube/http-signature':
|
||||
specifier: 1.7.0
|
||||
version: 1.7.0
|
||||
@ -1163,7 +1163,7 @@ importers:
|
||||
version: 7.17.0(eslint@9.14.0)(typescript@5.6.3)
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 1.6.0
|
||||
version: 1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.36.0))
|
||||
version: 1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0))
|
||||
'@vue/runtime-core':
|
||||
specifier: 3.5.12
|
||||
version: 3.5.12
|
||||
@ -6144,9 +6144,6 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
hasBin: true
|
||||
|
||||
electron-to-chromium@1.4.601:
|
||||
resolution: {integrity: sha512-SpwUMDWe9tQu8JX5QCO1+p/hChAi9AE9UpoC3rcHVc+gdCGlbT3SGb5I1klgb952HRIyvt9wZhSz9bNBYz9swA==}
|
||||
|
||||
electron-to-chromium@1.4.686:
|
||||
resolution: {integrity: sha512-3avY1B+vUzNxEgkBDpKOP8WarvUAEwpRaiCL0He5OKWEFxzaOFiq4WoZEZe7qh0ReS7DiWoHMnYoQCKxNZNzSg==}
|
||||
|
||||
@ -11780,7 +11777,7 @@ snapshots:
|
||||
'@babel/traverse': 7.23.5
|
||||
'@babel/types': 7.24.7
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
gensync: 1.0.0-beta.2
|
||||
json5: 2.2.3
|
||||
semver: 6.3.1
|
||||
@ -11800,7 +11797,7 @@ snapshots:
|
||||
'@babel/traverse': 7.24.7
|
||||
'@babel/types': 7.24.7
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
gensync: 1.0.0-beta.2
|
||||
json5: 2.2.3
|
||||
semver: 6.3.1
|
||||
@ -12059,7 +12056,7 @@ snapshots:
|
||||
'@babel/helper-split-export-declaration': 7.22.6
|
||||
'@babel/parser': 7.25.6
|
||||
'@babel/types': 7.24.7
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -12074,7 +12071,7 @@ snapshots:
|
||||
'@babel/helper-split-export-declaration': 7.24.7
|
||||
'@babel/parser': 7.25.6
|
||||
'@babel/types': 7.25.6
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -12465,7 +12462,7 @@ snapshots:
|
||||
'@eslint/config-array@0.18.0':
|
||||
dependencies:
|
||||
'@eslint/object-schema': 2.1.4
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
minimatch: 3.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -12475,7 +12472,7 @@ snapshots:
|
||||
'@eslint/eslintrc@3.1.0':
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
espree: 10.3.0
|
||||
globals: 14.0.0
|
||||
ignore: 5.3.1
|
||||
@ -13180,7 +13177,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@nestjs/testing@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7))':
|
||||
'@nestjs/testing@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-express@10.4.7)':
|
||||
dependencies:
|
||||
'@nestjs/common': 10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1)
|
||||
'@nestjs/core': 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
|
||||
@ -15384,7 +15381,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.36.0))':
|
||||
'@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0))':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.2.1
|
||||
'@bcoe/v8-coverage': 0.2.3
|
||||
@ -15399,7 +15396,7 @@ snapshots:
|
||||
std-env: 3.7.0
|
||||
strip-literal: 2.1.0
|
||||
test-exclude: 6.0.0
|
||||
vitest: 1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.36.0)
|
||||
vitest: 1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@ -15637,7 +15634,7 @@ snapshots:
|
||||
|
||||
agent-base@6.0.2:
|
||||
dependencies:
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
@ -16091,7 +16088,7 @@ snapshots:
|
||||
browserslist@4.22.2:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001566
|
||||
electron-to-chromium: 1.4.601
|
||||
electron-to-chromium: 1.4.686
|
||||
node-releases: 2.0.14
|
||||
update-browserslist-db: 1.0.13(browserslist@4.22.2)
|
||||
|
||||
@ -17053,8 +17050,6 @@ snapshots:
|
||||
dependencies:
|
||||
jake: 10.8.5
|
||||
|
||||
electron-to-chromium@1.4.601: {}
|
||||
|
||||
electron-to-chromium@1.4.686: {}
|
||||
|
||||
emittery@0.13.1: {}
|
||||
@ -17248,7 +17243,7 @@ snapshots:
|
||||
|
||||
esbuild-register@3.5.0(esbuild@0.24.0):
|
||||
dependencies:
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
esbuild: 0.24.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -17490,7 +17485,7 @@ snapshots:
|
||||
ajv: 6.12.6
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.3
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 8.2.0
|
||||
eslint-visitor-keys: 4.2.0
|
||||
@ -17935,7 +17930,7 @@ snapshots:
|
||||
|
||||
follow-redirects@1.15.9(debug@4.3.7):
|
||||
optionalDependencies:
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
|
||||
for-each@0.3.3:
|
||||
dependencies:
|
||||
@ -18805,7 +18800,7 @@ snapshots:
|
||||
|
||||
istanbul-lib-source-maps@4.0.1:
|
||||
dependencies:
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
source-map: 0.6.1
|
||||
transitivePeerDependencies:
|
||||
@ -19215,35 +19210,6 @@ snapshots:
|
||||
|
||||
jsdoc-type-pratt-parser@4.1.0: {}
|
||||
|
||||
jsdom@24.1.1:
|
||||
dependencies:
|
||||
cssstyle: 4.0.1
|
||||
data-urls: 5.0.0
|
||||
decimal.js: 10.4.3
|
||||
form-data: 4.0.1
|
||||
html-encoding-sniffer: 4.0.0
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.5
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
nwsapi: 2.2.12
|
||||
parse5: 7.2.1
|
||||
rrweb-cssom: 0.7.1
|
||||
saxes: 6.0.0
|
||||
symbol-tree: 3.2.4
|
||||
tough-cookie: 4.1.4
|
||||
w3c-xmlserializer: 5.0.0
|
||||
webidl-conversions: 7.0.0
|
||||
whatwg-encoding: 3.1.1
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 14.0.0
|
||||
ws: 8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
|
||||
xml-name-validator: 5.0.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
optional: true
|
||||
|
||||
jsdom@24.1.1(bufferutil@4.0.7)(utf-8-validate@6.0.3):
|
||||
dependencies:
|
||||
cssstyle: 4.0.1
|
||||
@ -19936,7 +19902,7 @@ snapshots:
|
||||
micromark@4.0.0:
|
||||
dependencies:
|
||||
'@types/debug': 4.1.12
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
decode-named-character-reference: 1.0.2
|
||||
devlop: 1.1.0
|
||||
micromark-core-commonmark: 2.0.0
|
||||
@ -21396,7 +21362,7 @@ snapshots:
|
||||
|
||||
require-in-the-middle@7.3.0:
|
||||
dependencies:
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
module-details-from-path: 1.0.3
|
||||
resolve: 1.22.8
|
||||
transitivePeerDependencies:
|
||||
@ -21821,7 +21787,7 @@ snapshots:
|
||||
socks-proxy-agent@8.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.0
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
socks: 2.7.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -21930,7 +21896,7 @@ snapshots:
|
||||
arg: 5.0.2
|
||||
bluebird: 3.7.2
|
||||
check-more-types: 2.24.0
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
execa: 5.1.1
|
||||
lazy-ass: 1.6.0
|
||||
ps-tree: 1.2.0
|
||||
@ -22777,7 +22743,7 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.36.0):
|
||||
vitest@1.6.0(@types/node@22.9.0)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.4)(terser@5.36.0):
|
||||
dependencies:
|
||||
'@vitest/expect': 1.6.0
|
||||
'@vitest/runner': 1.6.0
|
||||
@ -22802,7 +22768,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 22.9.0
|
||||
happy-dom: 10.0.3
|
||||
jsdom: 24.1.1
|
||||
jsdom: 24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
|
Loading…
Reference in New Issue
Block a user