From 24e629ca5c50789ff0aba31532ae66b51148d70f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 3 Nov 2023 15:35:07 +0900
Subject: [PATCH] =?UTF-8?q?enhance:=20=E5=88=9D=E6=9C=9F=E8=A8=AD=E5=AE=9A?=
 =?UTF-8?q?=E3=81=A8=E3=83=81=E3=83=A5=E3=83=BC=E3=83=88=E3=83=AA=E3=82=A2?=
 =?UTF-8?q?=E3=83=AB=E3=82=92=E7=B5=B1=E5=90=88=20(#12141)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* better onboarding experience

* enhance: iroiro

* (add) title

* (enhance) 戻る・次へボタンを全ページでstickyに

* fix merging

* (add) iroiro

* remove unnecessary file

* Update CHANGELOG.md

* tweak texts

* (fix) reactionViewer mock

* change strings

* Update MkTutorialDialog.Note.vue

* Update ja-JP.yml

* (fix) reactionViewer error

* (fix) path

* refactor

* fix

* Update MkPostForm.vue

* Update ja-JP.yml

* Update ja-JP.yml

* tweak text

* Update ja-JP.yml

* Update ja-JP.yml

* Update ja-JP.yml

* (add) achivement

* (add) もう一度見れますよメッセージを追加

* Revert "feat: レジストリAPIをサードパーティから利用可能に (#12229)"

This reverts commit 79346272f8792d35955efd3aaaa1e42e0cd2a6e3.

* Revert "(add) もう一度見れますよメッセージを追加"

This reverts commit 6123b35215133f0d5e5db356bb43f4acbafab8fa.

* Revert "Revert "feat: レジストリAPIをサードパーティから利用可能に (#12229)""

This reverts commit bae684e484ef99308d7ac816a822047117efe1c6.

* tweak

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
---
 CHANGELOG.md                                  |   1 +
 locales/index.d.ts                            |  98 ++++++-
 locales/ja-JP.yml                             |  88 +++++-
 .../backend/src/core/AchievementService.ts    |   1 +
 packages/frontend/assets/tutorial/ai.webp     | Bin 0 -> 12238 bytes
 .../assets/tutorial/natto_failed.webp         | Bin 0 -> 13196 bytes
 .../frontend/assets/tutorial/timeline_tab.png | Bin 0 -> 2860 bytes
 packages/frontend/src/components/MkInfo.vue   |  20 +-
 packages/frontend/src/components/MkNote.vue   | 150 +++++++---
 .../frontend/src/components/MkNoteHeader.vue  |  14 +-
 .../frontend/src/components/MkPostForm.vue    |  28 +-
 .../src/components/MkPostFormAttaches.vue     |  17 +-
 .../components/MkReactionsViewer.reaction.vue |  52 ++--
 .../src/components/MkReactionsViewer.vue      |  19 +-
 .../src/components/MkTutorialDialog.Note.vue  | 117 ++++++++
 .../components/MkTutorialDialog.PostNote.vue  | 135 +++++++++
 .../components/MkTutorialDialog.Sensitive.vue | 144 ++++++++++
 .../components/MkTutorialDialog.Timeline.vue  |  87 ++++++
 .../src/components/MkTutorialDialog.vue       | 260 ++++++++++++++++++
 .../src/components/MkUserSetupDialog.vue      |  77 ++++--
 .../frontend/src/pages/timeline.tutorial.vue  | 123 ---------
 packages/frontend/src/pages/timeline.vue      |  16 +-
 packages/frontend/src/scripts/achievements.ts |   6 +
 packages/frontend/src/store.ts                |   9 +-
 packages/frontend/src/ui/_common_/common.ts   |   9 +-
 25 files changed, 1223 insertions(+), 248 deletions(-)
 create mode 100644 packages/frontend/assets/tutorial/ai.webp
 create mode 100644 packages/frontend/assets/tutorial/natto_failed.webp
 create mode 100644 packages/frontend/assets/tutorial/timeline_tab.png
 create mode 100644 packages/frontend/src/components/MkTutorialDialog.Note.vue
 create mode 100644 packages/frontend/src/components/MkTutorialDialog.PostNote.vue
 create mode 100644 packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
 create mode 100644 packages/frontend/src/components/MkTutorialDialog.Timeline.vue
 create mode 100644 packages/frontend/src/components/MkTutorialDialog.vue
 delete mode 100644 packages/frontend/src/pages/timeline.tutorial.vue

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6837cf6e1b..4fdf9687ec 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,7 @@
 	- 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください
 	  https://misskey-hub.net/docs/advanced/publish-on-your-website.html
 - Feat: 通知をグルーピングして表示するオプション(オプトアウト)
+- Feat: Misskeyの基本的なチュートリアルを実装
 - Feat: スワイプしてタイムラインを再読込できるように
 	- PCの場合は右上のボタンからでも再読込できます
 - Enhance: タイムラインの自動更新を無効にできるように
diff --git a/locales/index.d.ts b/locales/index.d.ts
index f6db40e944..b45559eea2 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -1182,10 +1182,91 @@ export interface Locale {
         "pushNotificationDescription": string;
         "initialAccountSettingCompleted": string;
         "haveFun": string;
-        "ifYouNeedLearnMore": string;
+        "youCanContinueTutorial": string;
+        "startTutorial": string;
         "skipAreYouSure": string;
         "laterAreYouSure": string;
     };
+    "_initialTutorial": {
+        "launchTutorial": string;
+        "title": string;
+        "wellDone": string;
+        "skipAreYouSure": string;
+        "_landing": {
+            "title": string;
+            "description": string;
+        };
+        "_note": {
+            "title": string;
+            "description": string;
+            "reply": string;
+            "renote": string;
+            "reaction": string;
+            "menu": string;
+        };
+        "_reaction": {
+            "title": string;
+            "description": string;
+            "letsTryReacting": string;
+            "reactToContinue": string;
+            "reactNotification": string;
+            "reactDone": string;
+        };
+        "_timeline": {
+            "title": string;
+            "description1": string;
+            "home": string;
+            "local": string;
+            "social": string;
+            "global": string;
+            "description2": string;
+            "description3": string;
+        };
+        "_postNote": {
+            "title": string;
+            "description1": string;
+            "_visibility": {
+                "description": string;
+                "public": string;
+                "home": string;
+                "followers": string;
+                "direct": string;
+                "doNotSendConfidencialOnDirect1": string;
+                "doNotSendConfidencialOnDirect2": string;
+                "localOnly": string;
+            };
+            "_cw": {
+                "title": string;
+                "description": string;
+                "_exampleNote": {
+                    "cw": string;
+                    "note": string;
+                };
+                "useCases": string;
+            };
+        };
+        "_howToMakeAttachmentsSensitive": {
+            "title": string;
+            "description": string;
+            "tryThisFile": string;
+            "_exampleNote": {
+                "note": string;
+            };
+            "method": string;
+            "sensitiveSucceeded": string;
+            "doItToContinue": string;
+        };
+        "_done": {
+            "title": string;
+            "description": string;
+        };
+    };
+    "_timelineDescription": {
+        "home": string;
+        "local": string;
+        "social": string;
+        "global": string;
+    };
     "_serverRules": {
         "description": string;
     };
@@ -1533,6 +1614,10 @@ export interface Locale {
                 "title": string;
                 "description": string;
             };
+            "_tutorialCompleted": {
+                "title": string;
+                "description": string;
+            };
         };
     };
     "_role": {
@@ -1861,17 +1946,6 @@ export interface Locale {
         "hour": string;
         "day": string;
     };
-    "_timelineTutorial": {
-        "title": string;
-        "step1_1": string;
-        "step1_2": string;
-        "step2_1": string;
-        "step2_2": string;
-        "step3_1": string;
-        "step3_2": string;
-        "step4_1": string;
-        "step4_2": string;
-    };
     "_2fa": {
         "alreadyRegistered": string;
         "registerTOTP": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 1b79c399e7..8fd77afd92 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1170,7 +1170,7 @@ _announcement:
 
 _initialAccountSetting:
   accountCreated: "アカウントの作成が完了しました!"
-  letsStartAccountSetup: "アカウントの初期設定を行いましょう。"
+  letsStartAccountSetup: "さっそくアカウントの初期設定を行いましょう。"
   letsFillYourProfile: "まずはあなたのプロフィールを設定しましょう。"
   profileSetting: "プロフィール設定"
   privacySetting: "プライバシー設定"
@@ -1180,10 +1180,80 @@ _initialAccountSetting:
   pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をお使いのデバイスで受け取ることができます。"
   initialAccountSettingCompleted: "初期設定が完了しました!"
   haveFun: "{name}をお楽しみください!"
-  ifYouNeedLearnMore: "{name}(Misskey)の使い方などを詳しく知るには{link}をご覧ください。"
+  youCanContinueTutorial: "このまま{name}(Misskey)の使い方についてのチュートリアルに進むこともできますが、ここで中断してすぐに使い始めることもできます。"
+  startTutorial: "チュートリアルを開始"
   skipAreYouSure: "初期設定をスキップしますか?"
   laterAreYouSure: "初期設定をあとでやり直しますか?"
 
+_initialTutorial:
+  launchTutorial: "チュートリアルを見る"
+  title: "チュートリアル"
+  wellDone: "よくできました"
+  skipAreYouSure: "チュートリアルを終了しますか?"
+  _landing:
+    title: "チュートリアルへようこそ"
+    description: "ここでは、Misskeyの基本的な使い方や機能を確認できます。"
+  _note:
+    title: "ノートって何?"
+    description: "Misskeyでの投稿は「ノート」と呼びます。ノートはタイムラインに時系列で並んでいて、リアルタイムで更新されていきます。"
+    reply: "返信することができます。返信に対しての返信も可能で、スレッドのように会話を続けることもできます。"
+    renote: "そのノートを自分のタイムラインに流して共有することができます。テキストを追加して引用することも可能です。"
+    reaction: "リアクションをつけることができます。詳しくは次のページで解説します。"
+    menu: "ノートの詳細を表示したり、リンクをコピーしたりなどの様々な操作が行えます。"
+  _reaction:
+    title: "リアクションって何?"
+    description: "ノートには「リアクション」をつけることができます。「いいね」では伝わらないニュアンスも、リアクションで簡単・気軽に表現できます。"
+    letsTryReacting: "リアクションは、ノートの「+」ボタンをクリックするとつけられます。試しにこのサンプルのノートにリアクションをつけてみてください!"
+    reactToContinue: "リアクションをつけると先に進めるようになります。"
+    reactNotification: "あなたのノートが誰かにリアクションされると、リアルタイムで通知を受け取ります。"
+    reactDone: "「ー」ボタンを押すとリアクションを取り消すことができます。"
+  _timeline:
+    title: "タイムラインのしくみ"
+    description1: "Misskeyには、使い方に応じて複数のタイムラインが用意されています(サーバーによってはいずれかが無効になっていることがあります)。"
+    home: "あなたがフォローしているアカウントの投稿を見られます。"
+    local: "このサーバーにいるユーザー全員の投稿を見られます。"
+    social: "ホームタイムラインとローカルタイムラインの投稿が両方表示されます。"
+    global: "接続している他のすべてのサーバーからの投稿を見られます。"
+    description2: "それぞれのタイムラインは、画面上部でいつでも切り替えられます。"
+    description3: "その他にも、リストタイムラインやチャンネルタイムラインなどがあります。詳しくは{link}をご覧ください。"
+  _postNote:
+    title: "ノートの投稿設定"
+    description1: "Misskeyにノートを投稿する際には、様々なオプションの設定が可能です。投稿フォームはこのようになっています。"
+    _visibility:
+      description: "ノートを表示できる相手を制限できます。"
+      public: "すべてのユーザーに公開。"
+      home: "ホームタイムラインのみに公開。フォロワー・プロフィールを見に来た人・リノートから、他のユーザーも見ることができます。"
+      followers: "フォロワーにのみ公開。本人以外がリノートすることはできず、またフォロワー以外は閲覧できません。"
+      direct: "指定したユーザーにのみ公開され、また相手に通知が入ります。ダイレクトメッセージのかわりにお使いいただけます。"
+      doNotSendConfidencialOnDirect1: "機密情報は送信する際は注意してください。"
+      doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーにダイレクト投稿を送信する場合は、機密情報の扱いに注意が必要です。"
+      localOnly: "他のサーバーに投稿を連合しません。上記の公開範囲に関わらず、他のサーバーのユーザーは、この設定がついたノートを直接閲覧することができなくなります。"
+    _cw:
+      title: "内容を隠す(CW)"
+      description: "本文のかわりに「注釈」に書いた内容が表示されます。「もっと見る」を押すと本文が表示されます。"
+      _exampleNote:
+        cw: "飯テロ注意"
+        note: "チョコのかかったドーナツを食べました🍩😋"
+      useCases: "サーバーのガイドラインにより必要とされるノートに指定したり、ネタバレ投稿やセンシティブな文章を自主規制したりするときに使います。"
+  _howToMakeAttachmentsSensitive:
+    title: "添付ファイルをセンシティブにするには?"
+    description: "サーバーのガイドラインにより必要とされる際や、そのまま見れる状態にしておくべきではない添付ファイルには、「センシティブ」設定を付けます。"
+    tryThisFile: "試しに、このフォームに添付された画像をセンシティブにしてみてください!"
+    _exampleNote:
+      note: "納豆のフタ開けるのミスったわね…"
+    method: "添付ファイルをセンシティブにする際は、そのファイルをクリックしてメニューを開き、「センシティブとして設定」をクリックします。"
+    sensitiveSucceeded: "ファイルを添付する際は、サーバーのガイドラインに従ってセンシティブを適切に設定してください。"
+    doItToContinue: "画像をセンシティブに設定すると先に進めるようになります。"
+  _done:
+    title: "チュートリアルは終了です🎉"
+    description: "ここで紹介した機能はほんの一部にすぎません。Misskeyの使い方をより詳しく知るには、{link}をご覧ください。"
+
+_timelineDescription:
+  home: "ホームタイムラインでは、あなたがフォローしているアカウントの投稿を見られます。"
+  local: "ローカルタイムラインでは、このサーバーにいるユーザー全員の投稿を見られます。"
+  social: "ソーシャルタイムラインには、ホームタイムラインとローカルタイムラインの投稿が両方表示されます。"
+  global: "グローバルタイムラインでは、接続している他のすべてのサーバーからの投稿を見られます。"
+
 _serverRules:
   description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
 
@@ -1456,6 +1526,9 @@ _achievements:
     _smashTestNotificationButton:
       title: "テスト過剰"
       description: "通知のテストをごく短時間のうちに連続して行った"
+    _tutorialCompleted:
+      title: "Misskey初心者講座 修了証"
+      description: "チュートリアルを完了した"
 
 _role:
   new: "ロールの作成"
@@ -1778,17 +1851,6 @@ _time:
   hour: "時間"
   day: "日"
 
-_timelineTutorial:
-  title: "Misskeyの使い方"
-  step1_1: "この画面は「タイムライン」です。{name}に投稿された「ノート」が時系列で表示されます。"
-  step1_2: "タイムラインにはいくつか種類があり、例えば「ホームタイムライン」にはあなたがフォローしている人のノートが流れ、「ローカルタイムライン」には{name}全体のノートが流れます。"
-  step2_1: "試しに、何かノートを投稿してみましょう。画面上にある鉛筆マークのボタンを押すとフォームが開きます。"
-  step2_2: "初めてのノートの内容は、あなたの自己紹介や「{name}始めました」などがおすすめです。"
-  step3_1: "投稿できましたか?"
-  step3_2: "あなたのノートがタイムラインに表示されていれば成功です。"
-  step4_1: "ノートには、「リアクション」を付けることができます。"
-  step4_2: "リアクションを付けるには、ノートの「+」マークをクリックして、好きな絵文字を選択します。"
-
 _2fa:
   alreadyRegistered: "既に設定は完了しています。"
   registerTOTP: "認証アプリの設定を開始"
diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts
index 1b8718335b..88fc033859 100644
--- a/packages/backend/src/core/AchievementService.ts
+++ b/packages/backend/src/core/AchievementService.ts
@@ -86,6 +86,7 @@ export const ACHIEVEMENT_TYPES = [
 	'cookieClicked',
 	'brainDiver',
 	'smashTestNotificationButton',
+	'tutorialCompleted',
 ] as const;
 
 @Injectable()
diff --git a/packages/frontend/assets/tutorial/ai.webp b/packages/frontend/assets/tutorial/ai.webp
new file mode 100644
index 0000000000000000000000000000000000000000..d9d456494272913452dba78c1b13192db0e7d49d
GIT binary patch
literal 12238
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

literal 0
HcmV?d00001

diff --git a/packages/frontend/assets/tutorial/natto_failed.webp b/packages/frontend/assets/tutorial/natto_failed.webp
new file mode 100644
index 0000000000000000000000000000000000000000..87db5f7732de56cf1f9074e38de6dd80c6cdfcba
GIT binary patch
literal 13196
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

literal 0
HcmV?d00001

diff --git a/packages/frontend/assets/tutorial/timeline_tab.png b/packages/frontend/assets/tutorial/timeline_tab.png
new file mode 100644
index 0000000000000000000000000000000000000000..b52ad5fb519119efa418f47204c1131db0a9d1c5
GIT binary patch
literal 2860
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

literal 0
HcmV?d00001

diff --git a/packages/frontend/src/components/MkInfo.vue b/packages/frontend/src/components/MkInfo.vue
index 37490887e1..19402a44ce 100644
--- a/packages/frontend/src/components/MkInfo.vue
+++ b/packages/frontend/src/components/MkInfo.vue
@@ -7,7 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div :class="[$style.root, { [$style.warn]: warn }]">
 	<i v-if="warn" class="ti ti-alert-triangle" :class="$style.i"></i>
 	<i v-else class="ti ti-info-circle" :class="$style.i"></i>
-	<slot></slot>
+	<div><slot></slot></div>
+	<button v-if="closable" :class="$style.button" class="_button" @click="close()"><i class="ti ti-x"></i></button>
 </div>
 </template>
 
@@ -16,11 +17,23 @@ import { } from 'vue';
 
 const props = defineProps<{
 	warn?: boolean;
+	closable?: boolean;
 }>();
+
+const emit = defineEmits<{
+	(ev: 'close'): void;
+}>();
+
+function close() {
+	// こいつの中では非表示動作は行わない
+	emit('close');
+}
 </script>
 
 <style lang="scss" module>
 .root {
+	display: flex;
+  align-items: center;
 	padding: 12px 14px;
 	font-size: 90%;
 	background: var(--infoBg);
@@ -37,4 +50,9 @@ const props = defineProps<{
 .i {
 	margin-right: 4px;
 }
+
+.button {
+	margin-left: auto;
+	padding: 4px;
+}
 </style>
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index b31ee78532..d71b07c51b 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 	<article v-else :class="$style.article" @contextmenu.stop="onContextmenu">
 		<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
-		<MkAvatar :class="$style.avatar" :user="appearNote.user" link preview/>
+		<MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/>
 		<div :class="$style.main">
 			<MkNoteHeader :note="appearNote" :mini="true"/>
 			<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
@@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 				<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
 			</div>
-			<MkReactionsViewer :note="appearNote" :maxNumber="16">
+			<MkReactionsViewer :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction">
 				<template #more>
 					<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
 				</template>
@@ -136,7 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent } from 'vue';
+import { computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent, watch, provide } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
 import MkNoteSub from '@/components/MkNoteSub.vue';
@@ -170,9 +170,19 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 import { shouldCollapsed } from '@/scripts/collapsed.js';
 
-const props = defineProps<{
+const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
 	pinned?: boolean;
+	mock?: boolean;
+}>(), {
+	mock: false,
+});
+
+provide('mock', props.mock);
+
+const emit = defineEmits<{
+	(ev: 'reaction', emoji: string): void;
+	(ev: 'removeReaction', emoji: string): void;
 }>();
 
 const inChannel = inject('inChannel', null);
@@ -232,30 +242,38 @@ const keymap = {
 	's': () => showContent.value !== showContent.value,
 };
 
-useNoteCapture({
-	rootEl: el,
-	note: $$(appearNote),
-	pureNote: $$(note),
-	isDeletedRef: isDeleted,
-});
-
-useTooltip(renoteButton, async (showing) => {
-	const renotes = await os.api('notes/renotes', {
-		noteId: appearNote.id,
-		limit: 11,
+if (props.mock) {
+	watch(() => props.note, (to) => {
+		note = deepClone(to);
+	}, { deep: true });
+} else {
+	useNoteCapture({
+		rootEl: el,
+		note: $$(appearNote),
+		pureNote: $$(note),
+		isDeletedRef: isDeleted,
 	});
+}
 
-	const users = renotes.map(x => x.user);
+if (!props.mock) {
+	useTooltip(renoteButton, async (showing) => {
+		const renotes = await os.api('notes/renotes', {
+			noteId: appearNote.id,
+			limit: 11,
+		});
 
-	if (users.length < 1) return;
+		const users = renotes.map(x => x.user);
 
-	os.popup(MkUsersTooltip, {
-		showing,
-		users,
-		count: appearNote.renoteCount,
-		targetElement: renoteButton.value,
-	}, {}, 'closed');
-});
+		if (users.length < 1) return;
+
+		os.popup(MkUsersTooltip, {
+			showing,
+			users,
+			count: appearNote.renoteCount,
+			targetElement: renoteButton.value,
+		}, {}, 'closed');
+	});
+}
 
 type Visibility = 'public' | 'home' | 'followers' | 'specified';
 
@@ -287,21 +305,25 @@ function renote(viaKeyboard = false) {
 					os.popup(MkRippleEffect, { x, y }, {}, 'end');
 				}
 
-				os.api('notes/create', {
-					renoteId: appearNote.id,
-					channelId: appearNote.channelId,
-				}).then(() => {
-					os.toast(i18n.ts.renoted);
-				});
+				if (!props.mock) {
+					os.api('notes/create', {
+						renoteId: appearNote.id,
+						channelId: appearNote.channelId,
+					}).then(() => {
+						os.toast(i18n.ts.renoted);
+					});
+				}
 			},
 		}, {
 			text: i18n.ts.inChannelQuote,
 			icon: 'ti ti-quote',
 			action: () => {
-				os.post({
-					renote: appearNote,
-					channel: appearNote.channel,
-				});
+				if (!props.mock) {
+					os.post({
+						renote: appearNote,
+						channel: appearNote.channel,
+					});
+				}
 			},
 		}, null]);
 	}
@@ -327,15 +349,17 @@ function renote(viaKeyboard = false) {
 				visibility = smallerVisibility(visibility, 'home');
 			}
 
-			os.api('notes/create', {
-				localOnly,
-				visibility,
-				renoteId: appearNote.id,
-			}).then(() => {
-				os.toast(i18n.ts.renoted);
-			});
+			if (!props.mock) {
+				os.api('notes/create', {
+					localOnly,
+					visibility,
+					renoteId: appearNote.id,
+				}).then(() => {
+					os.toast(i18n.ts.renoted);
+				});
+			}
 		},
-	}, {
+	}, (props.mock) ? undefined : {
 		text: i18n.ts.quote,
 		icon: 'ti ti-quote',
 		action: () => {
@@ -352,6 +376,9 @@ function renote(viaKeyboard = false) {
 
 function reply(viaKeyboard = false): void {
 	pleaseLogin();
+	if (props.mock) {
+		return;
+	}
 	os.post({
 		reply: appearNote,
 		channel: appearNote.channel,
@@ -365,6 +392,10 @@ function react(viaKeyboard = false): void {
 	pleaseLogin();
 	showMovedDialog();
 	if (appearNote.reactionAcceptance === 'likeOnly') {
+		if (props.mock) {
+			return;
+		}
+
 		os.api('notes/reactions/create', {
 			noteId: appearNote.id,
 			reaction: '❤️',
@@ -379,6 +410,11 @@ function react(viaKeyboard = false): void {
 	} else {
 		blur();
 		reactionPicker.show(reactButton.value, reaction => {
+			if (props.mock) {
+				emit('reaction', reaction);
+				return;
+			}
+
 			os.api('notes/reactions/create', {
 				noteId: appearNote.id,
 				reaction: reaction,
@@ -395,12 +431,22 @@ function react(viaKeyboard = false): void {
 function undoReact(note): void {
 	const oldReaction = note.myReaction;
 	if (!oldReaction) return;
+
+	if (props.mock) {
+		emit('removeReaction', oldReaction);
+		return;
+	}
+
 	os.api('notes/reactions/delete', {
 		noteId: note.id,
 	});
 }
 
 function onContextmenu(ev: MouseEvent): void {
+	if (props.mock) {
+		return;
+	}
+
 	const isLink = (el: HTMLElement) => {
 		if (el.tagName === 'A') return true;
 		// 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。
@@ -422,6 +468,10 @@ function onContextmenu(ev: MouseEvent): void {
 }
 
 function menu(viaKeyboard = false): void {
+	if (props.mock) {
+		return;
+	}
+
 	const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
 	os.popupMenu(menu, menuButton.value, {
 		viaKeyboard,
@@ -429,10 +479,18 @@ function menu(viaKeyboard = false): void {
 }
 
 async function clip() {
+	if (props.mock) {
+		return;
+	}
+
 	os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
 }
 
 function showRenoteMenu(viaKeyboard = false): void {
+	if (props.mock) {
+		return;
+	}
+
 	function getUnrenote(): MenuItem {
 		return {
 			text: i18n.ts.unrenote,
@@ -490,6 +548,14 @@ function readPromo() {
 	});
 	isDeleted.value = true;
 }
+
+function emitUpdReaction(emoji: string, delta: number) {
+	if (delta < 0) {
+		emit('removeReaction', emoji);
+	} else if (delta > 0) {
+		emit('reaction', emoji);
+	}
+}
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue
index 52d5b03685..b2236b99c2 100644
--- a/packages/frontend/src/components/MkNoteHeader.vue
+++ b/packages/frontend/src/components/MkNoteHeader.vue
@@ -5,7 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <header :class="$style.root">
-	<MkA v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
+	<div v-if="mock" :class="$style.name">
+		<MkUserName :user="note.user"/>
+	</div>
+	<MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
 		<MkUserName :user="note.user"/>
 	</MkA>
 	<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
@@ -14,7 +17,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
 	</div>
 	<div :class="$style.info">
-		<MkA :to="notePage(note)">
+		<div v-if="mock">
+			<MkTime :time="note.createdAt" colored/>
+		</div>
+		<MkA v-else :to="notePage(note)">
 			<MkTime :time="note.createdAt" colored/>
 		</MkA>
 		<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
@@ -29,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { } from 'vue';
+import { inject } from 'vue';
 import * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
 import { notePage } from '@/filters/note.js';
@@ -38,6 +44,8 @@ import { userPage } from '@/filters/user.js';
 defineProps<{
 	note: Misskey.entities.Note;
 }>();
+
+const mock = inject<boolean>('mock', false);
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 1fa5685861..46faae9523 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -98,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { inject, watch, nextTick, onMounted, defineAsyncComponent } from 'vue';
+import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
 import insertTextAtCursor from 'insert-text-at-cursor';
@@ -143,15 +143,22 @@ const props = withDefaults(defineProps<{
 	fixed?: boolean;
 	autofocus?: boolean;
 	freezeAfterPosted?: boolean;
+	mock?: boolean;
 }>(), {
 	initialVisibleUsers: () => [],
 	autofocus: true,
+	mock: false,
 });
 
+provide('mock', props.mock);
+
 const emit = defineEmits<{
 	(ev: 'posted'): void;
 	(ev: 'cancel'): void;
 	(ev: 'esc'): void;
+
+	// Mock用
+	(ev: 'fileChangeSensitive', fileId: string, to: boolean): void;
 }>();
 
 const textareaEl = $shallowRef<HTMLTextAreaElement | null>(null);
@@ -239,7 +246,7 @@ const maxTextLength = $computed((): number => {
 });
 
 const canPost = $computed((): boolean => {
-	return !posting && !posted &&
+	return !props.mock && !posting && !posted &&
 		(1 <= textLength || 1 <= files.length || !!poll || !!props.renote) &&
 		(textLength <= maxTextLength) &&
 		(!poll || poll.choices.length >= 2);
@@ -396,6 +403,8 @@ function focus() {
 }
 
 function chooseFileFrom(ev) {
+	if (props.mock) return;
+
 	selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => {
 		for (const file of files_) {
 			files.push(file);
@@ -408,6 +417,9 @@ function detachFile(id) {
 }
 
 function updateFileSensitive(file, sensitive) {
+	if (props.mock) {
+		emit('fileChangeSensitive', file.id, sensitive);
+	}
 	files[files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
 }
 
@@ -420,6 +432,8 @@ function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities
 }
 
 function upload(file: File, name?: string): void {
+	if (props.mock) return;
+
 	uploadFile(file, defaultStore.state.uploadFolder, name).then(res => {
 		files.push(res);
 	});
@@ -545,6 +559,8 @@ function onCompositionEnd(ev: CompositionEvent) {
 }
 
 async function onPaste(ev: ClipboardEvent) {
+	if (props.mock) return;
+
 	for (const { item, i } of Array.from(ev.clipboardData.items, (item, i) => ({ item, i }))) {
 		if (item.kind === 'file') {
 			const file = item.getAsFile();
@@ -629,7 +645,7 @@ function onDrop(ev): void {
 }
 
 function saveDraft() {
-	if (props.instant) return;
+	if (props.instant || props.mock) return;
 
 	const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
 
@@ -674,6 +690,8 @@ async function post(ev?: MouseEvent) {
 		os.popup(MkRippleEffect, { x, y }, {}, 'end');
 	}
 
+	if (props.mock) return;
+
 	const annoying =
 		text.includes('$[x2') ||
 		text.includes('$[x3') ||
@@ -839,6 +857,8 @@ function showActions(ev) {
 let postAccount = $ref<Misskey.entities.UserDetailed | null>(null);
 
 function openAccountMenu(ev: MouseEvent) {
+	if (props.mock) return;
+
 	openAccountMenu_({
 		withExtraOperation: false,
 		includeCurrentAccount: true,
@@ -869,7 +889,7 @@ onMounted(() => {
 
 	nextTick(() => {
 		// 書きかけの投稿を復元
-		if (!props.instant && !props.mention && !props.specified) {
+		if (!props.instant && !props.mention && !props.specified && !props.mock) {
 			const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey];
 			if (draft) {
 				text = draft.data.text;
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index d499a22ed6..28a09c571f 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent } from 'vue';
+import { defineAsyncComponent, inject } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 import * as os from '@/os.js';
@@ -33,6 +33,8 @@ const props = defineProps<{
 	detachMediaFn?: (id: string) => void;
 }>();
 
+const mock = inject<boolean>('mock', false);
+
 const emit = defineEmits<{
 	(ev: 'update:modelValue', value: any[]): void;
 	(ev: 'detach', id: string): void;
@@ -44,6 +46,8 @@ const emit = defineEmits<{
 let menuShowing = false;
 
 function detachMedia(id: string) {
+	if (mock) return;
+
 	if (props.detachMediaFn) {
 		props.detachMediaFn(id);
 	} else {
@@ -52,6 +56,11 @@ function detachMedia(id: string) {
 }
 
 function toggleSensitive(file) {
+	if (mock) {
+		emit('changeSensitive', file, !file.isSensitive);
+		return;
+	}
+
 	os.api('drive/files/update', {
 		fileId: file.id,
 		isSensitive: !file.isSensitive,
@@ -61,6 +70,8 @@ function toggleSensitive(file) {
 }
 
 async function rename(file) {
+	if (mock) return;
+
 	const { canceled, result } = await os.inputText({
 		title: i18n.ts.enterFileName,
 		default: file.name,
@@ -77,6 +88,8 @@ async function rename(file) {
 }
 
 async function describe(file) {
+	if (mock) return;
+
 	os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
 		default: file.comment !== null ? file.comment : '',
 		file: file,
@@ -94,6 +107,8 @@ async function describe(file) {
 }
 
 async function crop(file: Misskey.entities.DriveFile): Promise<void> {
+	if (mock) return;
+
 	const newFile = await os.cropImage(file, { aspectRatio: NaN });
 	emit('replaceFile', file, newFile);
 }
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index d0db515219..d532ef9b66 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, onMounted, shallowRef, watch } from 'vue';
+import { computed, inject, onMounted, shallowRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import XDetails from '@/components/MkReactionsViewer.details.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
@@ -36,6 +36,12 @@ const props = defineProps<{
 	note: Misskey.entities.Note;
 }>();
 
+const mock = inject<boolean>('mock', false);
+
+const emit = defineEmits<{
+	(ev: 'reactionToggled', emoji: string, newCount: number): void;
+}>();
+
 const buttonEl = shallowRef<HTMLElement>();
 
 const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
@@ -53,6 +59,11 @@ async function toggleReaction() {
 		});
 		if (confirm.canceled) return;
 
+		if (mock) {
+			emit('reactionToggled', props.reaction, (props.count - 1));
+			return;
+		}
+
 		os.api('notes/reactions/delete', {
 			noteId: props.note.id,
 		}).then(() => {
@@ -64,6 +75,11 @@ async function toggleReaction() {
 			}
 		});
 	} else {
+		if (mock) {
+			emit('reactionToggled', props.reaction, (props.count + 1));
+			return;
+		}
+
 		os.api('notes/reactions/create', {
 			noteId: props.note.id,
 			reaction: props.reaction,
@@ -92,24 +108,26 @@ onMounted(() => {
 	if (!props.isInitial) anime();
 });
 
-useTooltip(buttonEl, async (showing) => {
-	const reactions = await os.apiGet('notes/reactions', {
-		noteId: props.note.id,
-		type: props.reaction,
-		limit: 11,
-		_cacheKey_: props.count,
-	});
+if (!mock) {
+	useTooltip(buttonEl, async (showing) => {
+		const reactions = await os.apiGet('notes/reactions', {
+			noteId: props.note.id,
+			type: props.reaction,
+			limit: 11,
+			_cacheKey_: props.count,
+		});
 
-	const users = reactions.map(x => x.user);
+		const users = reactions.map(x => x.user);
 
-	os.popup(XDetails, {
-		showing,
-		reaction: props.reaction,
-		users,
-		count: props.count,
-		targetElement: buttonEl.value,
-	}, {}, 'closed');
-}, 100);
+		os.popup(XDetails, {
+			showing,
+			reaction: props.reaction,
+			users,
+			count: props.count,
+			targetElement: buttonEl.value,
+		}, {}, 'closed');
+	}, 100);
+}
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue
index 52ead19a4b..eaa7faa4f9 100644
--- a/packages/frontend/src/components/MkReactionsViewer.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.vue
@@ -12,14 +12,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:moveClass="defaultStore.state.animation ? $style.transition_x_move : ''"
 	tag="div" :class="$style.root"
 >
-	<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note"/>
+	<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/>
 	<slot v-if="hasMoreReactions" name="more"/>
 </TransitionGroup>
 </template>
 
 <script lang="ts" setup>
 import * as Misskey from 'misskey-js';
-import { watch } from 'vue';
+import { inject, watch } from 'vue';
 import XReaction from '@/components/MkReactionsViewer.reaction.vue';
 import { defaultStore } from '@/store.js';
 
@@ -30,6 +30,12 @@ const props = withDefaults(defineProps<{
 	maxNumber: Infinity,
 });
 
+const mock = inject<boolean>('mock', false);
+
+const emit = defineEmits<{
+	(ev: 'mockUpdateMyReaction', emoji: string, delta: number): void;
+}>();
+
 const initialReactions = new Set(Object.keys(props.note.reactions));
 
 let reactions = $ref<[string, number][]>([]);
@@ -39,6 +45,15 @@ if (props.note.myReaction && !Object.keys(reactions).includes(props.note.myReact
 	reactions[props.note.myReaction] = props.note.reactions[props.note.myReaction];
 }
 
+function onMockToggleReaction(emoji: string, count: number) {
+	if (!mock) return;
+
+	const i = reactions.findIndex((item) => item[0] === emoji);
+	if (i < 0) return;
+
+	emit('mockUpdateMyReaction', emoji, (count - reactions[i][1]));
+}
+
 watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
 	let newReactions: [string, number][] = [];
 	hasMoreReactions = Object.keys(newSource).length > maxNumber;
diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue
new file mode 100644
index 0000000000..c7df1a576e
--- /dev/null
+++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue
@@ -0,0 +1,117 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+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 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>
+		<div><i class="ti ti-plus"></i> <b>{{ i18n.ts.reaction }}</b> … {{ i18n.ts._initialTutorial._note.reaction }}</div>
+		<div><i class="ti ti-dots"></i> <b>{{ i18n.ts.menu }}</b> … {{ i18n.ts._initialTutorial._note.menu }}</div>
+	</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>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div>
+	<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction" @updateReaction="updateReaction"/>
+	<div v-if="onceReacted"><b style="color: var(--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>
+</template>
+
+<script setup lang="ts">
+import * as Misskey from 'misskey-js';
+import { ref, reactive } from 'vue';
+import { i18n } from '@/i18n.js';
+import { globalEvents } from '@/events.js';
+import { $i } from '@/account.js';
+import MkNote from '@/components/MkNote.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',
+	userId: '0000000001',
+	user: {
+		id: '0000000001',
+		name: '藍',
+		username: 'ai',
+		host: null,
+		avatarDecorations: [],
+		avatarUrl: '/client-assets/tutorial/ai.webp',
+		avatarBlurhash: 'eiKmhHIByXxZ~qWXs:-pR*NbR*s:xuRjoL-oR*WCt6WWf6WVf6oeWB',
+		isBot: false,
+		isCat: true,
+		emojis: {},
+		onlineStatus: null,
+		badgeRoles: [],
+	},
+	text: 'just setting up my msky',
+	cw: null,
+	visibility: 'public',
+	localOnly: false,
+	reactionAcceptance: null,
+	renoteCount: 0,
+	repliesCount: 1,
+	reactions: {},
+	reactionEmojis: {},
+	fileIds: [],
+	files: [],
+	replyId: null,
+	renoteId: null,
+});
+const onceReacted = ref<boolean>(false);
+
+function addReaction(emoji) {
+	onceReacted.value = true;
+	emit('reacted');
+	exampleNote.reactions[emoji] = 1;
+	exampleNote.myReaction = emoji;
+	doNotification(emoji);
+}
+
+function doNotification(emoji: string): void {
+	if (!$i || !emoji) return;
+
+	const notification: Misskey.entities.Notification = {
+		id: Math.random().toString(),
+		createdAt: new Date().toUTCString(),
+		isRead: false,
+		type: 'reaction',
+		reaction: emoji,
+		user: $i,
+		userId: $i.id,
+		note: exampleNote,
+	};
+
+	globalEvents.emit('clientNotification', notification);
+}
+
+function removeReaction(emoji) {
+	delete exampleNote.reactions[emoji];
+	exampleNote.myReaction = undefined;
+}
+</script>
+
+<style lang="scss" module>
+.exampleNoteRoot {
+	border-radius: var(--radius);
+	border: var(--panelBorder);
+	background: var(--panel);
+}
+
+.divider {
+	height: 1px;
+	background: var(--divider);
+}
+</style>
diff --git a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
new file mode 100644
index 0000000000..9b55a1dca7
--- /dev/null
+++ b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
@@ -0,0 +1,135 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+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>
+	<MkPostForm :class="$style.exampleRoot" :mock="true"/>
+	<MkFormSection>
+		<template #label>{{ i18n.ts.visibility }}</template>
+		<div class="_gaps">
+			<div>{{ i18n.ts._initialTutorial._postNote._visibility.description }}</div>
+			<div><i class="ti ti-world"></i> <b>{{ i18n.ts._visibility.public }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.public }}</div>
+			<div><i class="ti ti-home"></i> <b>{{ i18n.ts._visibility.home }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.home }}</div>
+			<div><i class="ti ti-lock"></i> <b>{{ i18n.ts._visibility.followers }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.followers }}</div>
+			<div class="_gaps_s">
+				<div><i class="ti ti-mail"></i> <b>{{ i18n.ts._visibility.specified }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.direct }}</div>
+				<MkInfo :warn="true">
+					<b>{{ i18n.ts._initialTutorial._postNote._visibility.doNotSendConfidencialOnDirect1 }}</b> {{ i18n.ts._initialTutorial._postNote._visibility.doNotSendConfidencialOnDirect2 }}
+				</MkInfo>
+			</div>
+			<div><i class="ti ti-rocket-off"></i> <b>{{ i18n.ts._visibility.disableFederation }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.localOnly }}</div>
+		</div>
+	</MkFormSection>
+	<MkFormSection>
+		<template #label>{{ i18n.ts._initialTutorial._postNote._cw.title }}</template>
+		<div class="_gaps">
+			<div>{{ i18n.ts._initialTutorial._postNote._cw.description }}</div>
+			<MkNote :class="$style.exampleRoot" :note="exampleCWNote" :mock="true"/>
+			<div>{{ i18n.ts._initialTutorial._postNote._cw.useCases }}</div>
+		</div>
+	</MkFormSection>
+</div>
+</template>
+
+<script setup lang="ts">
+import * as Misskey from 'misskey-js';
+import { reactive } from 'vue';
+import { i18n } from '@/i18n.js';
+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';
+
+const exampleCWNote = reactive<Misskey.entities.Note>({
+	id: '0000000000',
+	createdAt: '2019-04-14T17:30:49.181Z',
+	userId: '0000000001',
+	user: {
+		id: '0000000001',
+		name: '藍',
+		username: 'ai',
+		host: null,
+		avatarDecorations: [],
+		avatarUrl: '/client-assets/tutorial/ai.webp',
+		avatarBlurhash: 'eiKmhHIByXxZ~qWXs:-pR*NbR*s:xuRjoL-oR*WCt6WWf6WVf6oeWB',
+		isBot: false,
+		isCat: true,
+		emojis: {},
+		onlineStatus: null,
+		badgeRoles: [],
+	},
+	text: i18n.ts._initialTutorial._postNote._cw._exampleNote.note,
+	cw: i18n.ts._initialTutorial._postNote._cw._exampleNote.cw,
+	visibility: 'public',
+	localOnly: false,
+	reactionAcceptance: null,
+	renoteCount: 0,
+	repliesCount: 1,
+	reactions: {},
+	reactionEmojis: {},
+	fileIds: [],
+	files: [],
+	replyId: null,
+	renoteId: null,
+});
+</script>
+
+<style lang="scss" module>
+.exampleRoot {
+	max-width: none!important;
+	border-radius: var(--radius);
+	border: var(--panelBorder);
+	background: var(--panel);
+}
+
+.divider {
+	height: 1px;
+	background: var(--divider);
+}
+
+.image {
+	max-width: 300px;
+	margin: 0 auto;
+}
+
+.post {
+	position: relative;
+	display: block;
+	width: 100%;
+	height: 40px;
+	color: var(--fgOnAccent);
+	font-weight: bold;
+	text-align: left;
+
+	&:before {
+		content: "";
+		display: block;
+		width: calc(100% - 38px);
+		height: 100%;
+		margin: auto;
+		position: absolute;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		border-radius: 999px;
+		background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+	}
+
+}
+
+.postIcon {
+	position: relative;
+	margin-left: 30px;
+	margin-right: 8px;
+	width: 32px;
+}
+
+.postText {
+	position: relative;
+	line-height: 40px;
+}
+</style>
diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
new file mode 100644
index 0000000000..768d00bb07
--- /dev/null
+++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
@@ -0,0 +1,144 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+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>{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.tryThisFile }}</div>
+	<MkInfo>{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.method }}</MkInfo>
+	<MkPostForm
+		:class="$style.exampleRoot"
+		:mock="true"
+		:initialNote="exampleNote"
+		@fileChangeSensitive="doSucceeded"
+	></MkPostForm>
+	<div v-if="onceSucceeded"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div>
+	<MkFolder>
+		<template #label>{{ i18n.ts.previewNoteText }}</template>
+		<MkNote :mock="true" :note="exampleNote" :class="$style.exampleRoot"></MkNote>
+	</MkFolder>
+</div>
+</template>
+
+<script setup lang="ts">
+import * as Misskey from 'misskey-js';
+import { ref, reactive } from 'vue';
+import { i18n } from '@/i18n.js';
+import MkPostForm from '@/components/MkPostForm.vue';
+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;
+}>();
+
+const onceSucceeded = ref<boolean>(false);
+
+function doSucceeded(fileId: string, to: boolean) {
+	if (fileId === exampleNote.fileIds[0] && to) {
+		onceSucceeded.value = true;
+		emit('succeeded');
+	}
+}
+
+const exampleNote = reactive<Misskey.entities.Note>({
+	id: '0000000000',
+	createdAt: '2019-04-14T17:30:49.181Z',
+	userId: '0000000001',
+	user: $i!,
+	text: i18n.ts._initialTutorial._howToMakeAttachmentsSensitive._exampleNote.note,
+	cw: null,
+	visibility: 'public',
+	localOnly: false,
+	reactionAcceptance: null,
+	renoteCount: 0,
+	repliesCount: 1,
+	reactions: {},
+	reactionEmojis: {},
+	fileIds: ['0000000002'],
+	files: [{
+		id: '0000000002',
+		createdAt: '2019-04-14T17:30:49.181Z',
+		name: 'natto_failed.webp',
+		type: 'image/webp',
+		md5: 'c44286cf152d0740be0ce5ad45ea85c3',
+		size: 827532,
+		isSensitive: false,
+		blurhash: 'LXNA3TD*XAIA%1%M%gt7.TofRioz',
+		properties: {
+			width: 256,
+			height: 256,
+		},
+		url: '/client-assets/tutorial/natto_failed.webp',
+		thumbnailUrl: '/client-assets/tutorial/natto_failed.webp',
+		comment: null,
+		folderId: null,
+		folder: null,
+		userId: null,
+		user: null,
+	}],
+	replyId: null,
+	renoteId: null,
+});
+
+</script>
+
+<style lang="scss" module>
+.exampleRoot {
+	border-radius: var(--radius);
+	border: var(--panelBorder);
+	background: var(--panel);
+}
+
+.divider {
+	height: 1px;
+	background: var(--divider);
+}
+
+.image {
+	max-width: 300px;
+	margin: 0 auto;
+}
+
+.post {
+	position: relative;
+	display: block;
+	width: 100%;
+	height: 40px;
+	color: var(--fgOnAccent);
+	font-weight: bold;
+	text-align: left;
+
+	&:before {
+		content: "";
+		display: block;
+		width: calc(100% - 38px);
+		height: 100%;
+		margin: auto;
+		position: absolute;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		border-radius: 999px;
+		background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+	}
+
+}
+
+.postIcon {
+	position: relative;
+	margin-left: 30px;
+	margin-right: 8px;
+	width: 32px;
+}
+
+.postText {
+	position: relative;
+	line-height: 40px;
+}
+</style>
diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
new file mode 100644
index 0000000000..75b917f33c
--- /dev/null
+++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
@@ -0,0 +1,87 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+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 class="_gaps_s">
+		<div><i class="ti ti-home"></i> <b>{{ i18n.ts._timelines.home }}</b> … {{ i18n.ts._initialTutorial._timeline.home }}</div>
+		<div><i class="ti ti-planet"></i> <b>{{ i18n.ts._timelines.local }}</b> … {{ i18n.ts._initialTutorial._timeline.local }}</div>
+		<div><i class="ti ti-universe"></i> <b>{{ i18n.ts._timelines.social }}</b> … {{ i18n.ts._initialTutorial._timeline.social }}</div>
+		<div><i class="ti ti-whirl"></i> <b>{{ i18n.ts._timelines.global }}</b> … {{ i18n.ts._initialTutorial._timeline.global }}</div>
+	</div>
+	<div class="_gaps_s">
+		<div>{{ i18n.ts._initialTutorial._timeline.description2 }}</div>
+		<img :class="$style.image" src="/client-assets/tutorial/timeline_tab.png"/>
+	</div>
+	<div :class="$style.divider"></div>
+	<I18n :src="i18n.ts._initialTutorial._timeline.description3" tag="div" style="padding: 0 16px;">
+		<template #link>
+			<a href="https://misskey-hub.net/docs/features/timeline.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
+		</template>
+	</I18n>
+
+</div>
+</template>
+
+<script setup lang="ts">
+import { i18n } from '@/i18n.js';
+</script>
+
+<style lang="scss" module>
+.exampleNoteRoot {
+	border-radius: var(--radius);
+	border: var(--panelBorder);
+	background: var(--panel);
+}
+
+.divider {
+	height: 1px;
+	background: var(--divider);
+}
+
+.image {
+	max-width: 300px;
+	margin: 0 auto;
+}
+
+.post {
+	position: relative;
+	display: block;
+	width: 100%;
+	height: 40px;
+	color: var(--fgOnAccent);
+	font-weight: bold;
+	text-align: left;
+
+	&:before {
+		content: "";
+		display: block;
+		width: calc(100% - 38px);
+		height: 100%;
+		margin: auto;
+		position: absolute;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		border-radius: 999px;
+		background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+	}
+
+}
+
+.postIcon {
+	position: relative;
+	margin-left: 30px;
+	margin-right: 8px;
+	width: 32px;
+}
+
+.postText {
+	position: relative;
+	line-height: 40px;
+}
+</style>
diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue
new file mode 100644
index 0000000000..e28838425f
--- /dev/null
+++ b/packages/frontend/src/components/MkTutorialDialog.vue
@@ -0,0 +1,260 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkModalWindow
+	ref="dialog"
+	:width="600"
+	:height="650"
+	@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-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(--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(--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/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
+								</template>
+							</I18n>
+							<div>{{ i18n.t('_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>
+</MkModalWindow>
+</template>
+
+<script lang="ts" setup>
+import { ref, shallowRef, watch } 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 { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
+import { host } from '@/config.js';
+import { claimAchievement } from '@/scripts/achievements.js';
+import * as os from '@/os.js';
+
+const props = defineProps<{
+	initialPage?: number;
+}>();
+
+const emit = defineEmits<{
+	(ev: 'closed'): void;
+}>();
+
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+
+// eslint-disable-next-line vue/no-setup-props-destructure
+const page = ref(props.initialPage ?? 0);
+
+watch(page, (to) => {
+	// チュートリアルの枚数を増やしたら必ず変更すること!!
+	if (to === 6) {
+		claimAchievement('tutorialCompleted');
+	}
+});
+
+const isReactionTutorialPushed = ref<boolean>(false);
+const isSensitiveTutorialSucceeded = ref<boolean>(false);
+
+async function close(skip: boolean) {
+	if (skip) {
+		const { canceled } = await os.confirm({
+			type: 'warning',
+			text: i18n.ts._initialTutorial.skipAreYouSure,
+		});
+		if (canceled) return;
+	}
+
+	dialog.value?.close();
+}
+</script>
+
+<style lang="scss" module>
+.transition_x_enterActive,
+.transition_x_leaveActive {
+	transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
+}
+.transition_x_enterFrom {
+	opacity: 0;
+	transform: translateX(50px);
+}
+.transition_x_leaveTo {
+	opacity: 0;
+	transform: translateX(-50px);
+}
+
+.progressBar {
+	position: absolute;
+	top: 0;
+	left: 0;
+	z-index: 10;
+	width: 100%;
+	height: 4px;
+}
+
+.progressBarValue {
+	height: 100%;
+	background: linear-gradient(90deg, var(--buttonGradateA), var(--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;
+	line-height: 1.5;
+}
+
+.pageFooter {
+	position: sticky;
+	bottom: 0;
+	left: 0;
+	flex-shrink: 0;
+	padding: 12px;
+	border-top: solid 0.5px var(--divider);
+	-webkit-backdrop-filter: blur(15px);
+	backdrop-filter: blur(15px);
+}
+</style>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue
index d60e01c44d..05b55f77a7 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.vue
@@ -46,24 +46,32 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</template>
 			<template v-else-if="page === 1">
 				<div style="height: 100cqh; overflow: auto;">
-					<MkSpacer :marginMin="20" :marginMax="28">
-						<XProfile/>
-						<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 :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>
-					</MkSpacer>
+					</div>
 				</div>
 			</template>
 			<template v-else-if="page === 2">
 				<div style="height: 100cqh; overflow: auto;">
-					<MkSpacer :marginMin="20" :marginMax="28">
-						<XPrivacy/>
-						<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 :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>
-					</MkSpacer>
+					</div>
 				</div>
 			</template>
 			<template v-else-if="page === 3">
@@ -102,16 +110,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<div class="_gaps" style="text-align: center;">
 							<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
 							<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div>
-							<I18n :src="i18n.ts._initialAccountSetting.ifYouNeedLearnMore" tag="div" style="padding: 0 16px;">
-								<template #name>{{ instance.name ?? host }}</template>
-								<template #link>
-									<a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
-								</template>
-							</I18n>
-							<div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div>
+							<div>{{ i18n.t('_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 primary rounded gradate data-cy-user-setup-continue @click="close(false)">{{ i18n.ts.close }}</MkButton>
+								<MkButton rounded primary data-cy-user-setup-continue @click="setupComplete()">{{ i18n.ts.close }}</MkButton>
 							</div>
 						</div>
 					</MkSpacer>
@@ -123,7 +128,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, shallowRef, watch } from 'vue';
+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';
@@ -143,6 +148,7 @@ const emit = defineEmits<{
 
 const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 
+// eslint-disable-next-line vue/no-setup-props-destructure
 const page = ref(defaultStore.state.accountSetupWizard);
 
 watch(page, () => {
@@ -158,10 +164,24 @@ async function close(skip: boolean) {
 		if (canceled) return;
 	}
 
-	dialog.value.close();
+	dialog.value?.close();
 	defaultStore.set('accountSetupWizard', -1);
 }
 
+function setupComplete() {
+	defaultStore.set('accountSetupWizard', -1);
+	dialog.value?.close();
+}
+
+function launchTutorial() {
+	setupComplete();
+	nextTick(() => {
+		os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {
+			initialPage: 1,
+		}, {}, 'closed');
+	});
+}
+
 async function later(later: boolean) {
 	if (later) {
 		const { canceled } = await os.confirm({
@@ -171,7 +191,7 @@ async function later(later: boolean) {
 		if (canceled) return;
 	}
 
-	dialog.value.close();
+	dialog.value?.close();
 	defaultStore.set('accountSetupWizard', 0);
 }
 </script>
@@ -214,10 +234,21 @@ async function later(later: boolean) {
 	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(--divider);
 	-webkit-backdrop-filter: blur(15px);
diff --git a/packages/frontend/src/pages/timeline.tutorial.vue b/packages/frontend/src/pages/timeline.tutorial.vue
deleted file mode 100644
index 66b8e796e5..0000000000
--- a/packages/frontend/src/pages/timeline.tutorial.vue
+++ /dev/null
@@ -1,123 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<div :class="$style.container">
-	<div :class="$style.title">
-		<div :class="$style.titleText"><i class="ti ti-info-circle"></i> {{ i18n.ts._timelineTutorial.title }}</div>
-		<div :class="$style.step">
-			<button class="_button" :class="$style.stepArrow" :disabled="tutorial === 0" @click="tutorial--">
-				<i class="ti ti-chevron-left"></i>
-			</button>
-			<span :class="$style.stepNumber">{{ tutorial + 1 }} / {{ tutorialsNumber }}</span>
-			<button class="_button" :class="$style.stepArrow" :disabled="tutorial === tutorialsNumber - 1" @click="tutorial++">
-				<i class="ti ti-chevron-right"></i>
-			</button>
-		</div>
-	</div>
-
-	<div v-if="tutorial === 0" :class="$style.body">
-		<div>{{ i18n.t('_timelineTutorial.step1_1', { name: instance.name ?? host }) }}</div>
-		<div>{{ i18n.t('_timelineTutorial.step1_2', { name: instance.name ?? host }) }}</div>
-	</div>
-	<div v-else-if="tutorial === 1" :class="$style.body">
-		<div>{{ i18n.ts._timelineTutorial.step2_1 }}</div>
-		<div>{{ i18n.t('_timelineTutorial.step2_2', { name: instance.name ?? host }) }}</div>
-	</div>
-	<div v-else-if="tutorial === 2" :class="$style.body">
-		<div>{{ i18n.ts._timelineTutorial.step3_1 }}</div>
-		<div>{{ i18n.ts._timelineTutorial.step3_2 }}</div>
-	</div>
-	<div v-else-if="tutorial === 3" :class="$style.body">
-		<div>{{ i18n.ts._timelineTutorial.step4_1 }}</div>
-		<div>{{ i18n.ts._timelineTutorial.step4_2 }}</div>
-	</div>
-
-	<div :class="$style.footer">
-		<template v-if="tutorial === tutorialsNumber - 1">
-			<MkButton :class="$style.footerItem" primary rounded gradate @click="tutorial = -1">{{ i18n.ts.done }} <i class="ti ti-check"></i></MkButton>
-		</template>
-		<template v-else>
-			<MkButton :class="$style.footerItem" primary rounded gradate @click="tutorial++">{{ i18n.ts.next }} <i class="ti ti-arrow-right"></i></MkButton>
-		</template>
-	</div>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { computed } from 'vue';
-import MkButton from '@/components/MkButton.vue';
-import { defaultStore } from '@/store.js';
-import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
-import { host } from '@/config.js';
-
-const tutorialsNumber = 4;
-
-const tutorial = computed({
-	get() { return defaultStore.reactiveState.timelineTutorial.value || 0; },
-	set(value) { defaultStore.set('timelineTutorial', value); },
-});
-</script>
-
-<style lang="scss" module>
-.small {
-	opacity: 0.7;
-}
-
-.container {
-	border: solid 2px var(--accent);
-}
-
-.title {
-	display: flex;
-	flex-wrap: wrap;
-	padding: 22px 32px;
-	font-weight: bold;
-
-	&Text {
-		margin: 4px 0;
-		padding-right: 4px;
-	}
-}
-
-.step {
-	margin-left: auto;
-
-	&Arrow {
-		padding: 4px;
-		&:disabled {
-			opacity: 0.5;
-		}
-		&:first-child {
-			padding-right: 8px;
-		}
-		&:last-child {
-			padding-left: 8px;
-		}
-	}
-
-	&Number {
-		font-weight: normal;
-		margin: 4px;
-	}
-}
-
-.body {
-	padding: 0 32px;
-}
-
-.footer {
-	display: flex;
-	flex-wrap: wrap;
-	flex-direction: row;
-	justify-content: right;
-	padding: 22px 32px;
-
-	&Item {
-		margin: 4px;
-	}
-}
-</style>
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 5b97385ead..cfe270aefb 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -8,7 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template>
 	<MkSpacer :contentMax="800">
 		<div ref="rootEl" v-hotkey.global="keymap">
-			<XTutorial v-if="$i && defaultStore.reactiveState.timelineTutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/>
+			<MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()">
+				{{ i18n.ts._timelineDescription[src] }}
+			</MkInfo>
 			<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
 
 			<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
@@ -31,9 +33,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, computed, watch, provide } from 'vue';
+import { computed, watch, provide } from 'vue';
 import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
 import MkTimeline from '@/components/MkTimeline.vue';
+import MkInfo from '@/components/MkInfo.vue';
 import MkPostForm from '@/components/MkPostForm.vue';
 import { scroll } from '@/scripts/scroll.js';
 import * as os from '@/os.js';
@@ -48,8 +51,6 @@ import { deviceKind } from '@/scripts/device-kind.js';
 
 provide('shouldOmitHeaderTitle', true);
 
-const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue'));
-
 const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
 const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
 const keymap = {
@@ -140,6 +141,13 @@ function focus(): void {
 	tlComponent.focus();
 }
 
+function closeTutorial(): void {
+	if (!['home', 'local', 'social', 'global'].includes(src)) return;
+	const before = defaultStore.state.timelineTutorials;
+	before[src] = true;
+	defaultStore.set('timelineTutorials', before);
+}
+
 const headerActions = $computed(() => {
 	const tmp = [
 		{
diff --git a/packages/frontend/src/scripts/achievements.ts b/packages/frontend/src/scripts/achievements.ts
index af7c6b060c..e7585fcf81 100644
--- a/packages/frontend/src/scripts/achievements.ts
+++ b/packages/frontend/src/scripts/achievements.ts
@@ -82,6 +82,7 @@ export const ACHIEVEMENT_TYPES = [
 	'cookieClicked',
 	'brainDiver',
 	'smashTestNotificationButton',
+	'tutorialCompleted',
 ] as const;
 
 export const ACHIEVEMENT_BADGES = {
@@ -460,6 +461,11 @@ export const ACHIEVEMENT_BADGES = {
 		bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
 		frame: 'bronze',
 	},
+	'tutorialCompleted': {
+		img: '/fluent-emoji/1f393.png',
+		bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
+		frame: 'bronze',
+	},
 /* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
 } as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
 	img: string;
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 0f2e642b7b..7f916656de 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -49,9 +49,14 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'account',
 		default: 0,
 	},
-	timelineTutorial: {
+	timelineTutorials: {
 		where: 'account',
-		default: 0,
+		default: {
+			home: false,
+			local: false,
+			social: false,
+			global: false,
+		},
 	},
 	keepCw: {
 		where: 'account',
diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts
index ff6157f5f8..64008c5748 100644
--- a/packages/frontend/src/ui/_common_/common.ts
+++ b/packages/frontend/src/ui/_common_/common.ts
@@ -3,6 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
+import { defineAsyncComponent } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
 import * as os from '@/os.js';
 import { instance } from '@/instance.js';
@@ -102,7 +103,13 @@ export function openInstanceMenu(ev: MouseEvent) {
 		action: () => {
 			window.open('https://misskey-hub.net/help.html', '_blank');
 		},
-	}, {
+	}, ($i) ? {
+		text: i18n.ts._initialTutorial.launchTutorial,
+		icon: 'ti ti-presentation',
+		action: () => {
+			os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {}, 'closed');
+		},
+	} : undefined, {
 		type: 'link',
 		text: i18n.ts.aboutMisskey,
 		to: '/about-misskey',