diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 3fba36a754406a647826583b6d517fcbd5644ec8..3a53f470e33d876549ae0d50e5cda4f04c037423 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -13,6 +13,7 @@ fetchingAsApObject: "連合に照会中"
 ok: "OK"
 gotIt: "わかった"
 cancel: "キャンセル"
+noThankYou: "やめておく"
 enterUsername: "ユーザー名を入力"
 renotedBy: "{user}がRenote"
 noNotes: "ノートはありません"
@@ -898,6 +899,13 @@ navbar: "ナビゲーションバー"
 shuffle: "シャッフル"
 account: "アカウント"
 move: "移動"
+pushNotification: "プッシュ通知"
+subscribePushNotification: "プッシュ通知を有効化"
+unsubscribePushNotification: "プッシュ通知を停止する"
+pushNotificationAlreadySubscribed: "プッシュ通知は有効です"
+pushNotificationNotSupported: "ブラウザかインスタンスがプッシュ通知に非対応"
+sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する"
+sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。"
 
 _sensitiveMediaDetection:
   description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
@@ -1235,6 +1243,9 @@ _tutorial:
   step7_1: "これで、Misskeyの基本的な使い方の説明は終わりました。お疲れ様でした。"
   step7_2: "もっとMisskeyについて知りたいときは、{help}を見てみてください。"
   step7_3: "では、Misskeyをお楽しみください🚀"
+  step8_1: "最後に、プッシュ通知を有効化してみませんか?"
+  step8_2: "プッシュ通知を受け取ることで、Misskeyを開いていない時にもリアクションやフォロー、メンションなどに気づけます。"
+  step8_3: "通知の設定は後から変更できます。"
 
 _2fa:
   alreadyRegistered: "既に設定は完了しています。"
diff --git a/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js b/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js
new file mode 100644
index 0000000000000000000000000000000000000000..2265b006170deabafe129b9f5a69d66d8acb3f5e
--- /dev/null
+++ b/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js
@@ -0,0 +1,11 @@
+export class whetherPushNotifyToSendReadMessage1669138716634 {
+    name = 'whetherPushNotifyToSendReadMessage1669138716634'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "sw_subscription" ADD "sendReadMessage" boolean NOT NULL DEFAULT false`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "sw_subscription" DROP COLUMN "sendReadMessage"`);
+    }
+}
diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts
index df5284de4be4ed2111ba7ae130dda0861cf1f8d6..842cd1a9f8e8a765e307526cde7c7785aa7b2464 100644
--- a/packages/backend/src/core/PushNotificationService.ts
+++ b/packages/backend/src/core/PushNotificationService.ts
@@ -69,6 +69,14 @@ export class PushNotificationService {
 		});
 	
 		for (const subscription of subscriptions) {
+			// Continue if sendReadMessage is false
+			if ([
+				'readNotifications',
+				'readAllNotifications',
+				'readAllMessagingMessages',
+				'readAllMessagingMessagesOfARoom',
+			].includes(type) && !subscription.sendReadMessage) continue;
+
 			const pushSubscription = {
 				endpoint: subscription.endpoint,
 				keys: {
diff --git a/packages/backend/src/models/entities/SwSubscription.ts b/packages/backend/src/models/entities/SwSubscription.ts
index 51b9786e96f8512c31545e9cdd84be660e1d767f..0658294983eeb8a4d5a6234bbfe008009ea5cefe 100644
--- a/packages/backend/src/models/entities/SwSubscription.ts
+++ b/packages/backend/src/models/entities/SwSubscription.ts
@@ -34,4 +34,9 @@ export class SwSubscription {
 		length: 128,
 	})
 	public publickey: string;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public sendReadMessage: boolean;
 }
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index e41ed388b43a82bbbdfa127dea4874bf0a8e36cd..647f60317a38c46c7d65e94290ed6cd87ec0971f 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -272,6 +272,8 @@ import * as ep___resetDb from './endpoints/reset-db.js';
 import * as ep___resetPassword from './endpoints/reset-password.js';
 import * as ep___serverInfo from './endpoints/server-info.js';
 import * as ep___stats from './endpoints/stats.js';
+import * as ep___sw_show_registration from './endpoints/sw/show-registration.js';
+import * as ep___sw_update_registration from './endpoints/sw/update-registration.js';
 import * as ep___sw_register from './endpoints/sw/register.js';
 import * as ep___sw_unregister from './endpoints/sw/unregister.js';
 import * as ep___test from './endpoints/test.js';
@@ -588,6 +590,8 @@ const $resetDb: Provider = { provide: 'ep:reset-db', useClass: ep___resetDb.defa
 const $resetPassword: Provider = { provide: 'ep:reset-password', useClass: ep___resetPassword.default };
 const $serverInfo: Provider = { provide: 'ep:server-info', useClass: ep___serverInfo.default };
 const $stats: Provider = { provide: 'ep:stats', useClass: ep___stats.default };
+const $sw_show_registration: Provider = { provide: 'ep:sw/show-registration', useClass: ep___sw_show_registration.default };
+const $sw_update_registration: Provider = { provide: 'ep:sw/update-registration', useClass: ep___sw_update_registration.default };
 const $sw_register: Provider = { provide: 'ep:sw/register', useClass: ep___sw_register.default };
 const $sw_unregister: Provider = { provide: 'ep:sw/unregister', useClass: ep___sw_unregister.default };
 const $test: Provider = { provide: 'ep:test', useClass: ep___test.default };
@@ -908,6 +912,8 @@ const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.d
 		$resetPassword,
 		$serverInfo,
 		$stats,
+		$sw_show_registration,
+		$sw_update_registration,
 		$sw_register,
 		$sw_unregister,
 		$test,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index b2ab36e070b1a1741d80a36eec4c8a49e281ac63..6d10cb8f35ade54c034b07a99edbc892150c28b1 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -271,6 +271,8 @@ import * as ep___resetDb from './endpoints/reset-db.js';
 import * as ep___resetPassword from './endpoints/reset-password.js';
 import * as ep___serverInfo from './endpoints/server-info.js';
 import * as ep___stats from './endpoints/stats.js';
+import * as ep___sw_show_registration from './endpoints/sw/show-registration.js';
+import * as ep___sw_update_registration from './endpoints/sw/update-registration.js';
 import * as ep___sw_register from './endpoints/sw/register.js';
 import * as ep___sw_unregister from './endpoints/sw/unregister.js';
 import * as ep___test from './endpoints/test.js';
@@ -585,6 +587,8 @@ const eps = [
 	['reset-password', ep___resetPassword],
 	['server-info', ep___serverInfo],
 	['stats', ep___stats],
+	['sw/show-registration', ep___sw_show_registration],
+	['sw/update-registration', ep___sw_update_registration],
 	['sw/register', ep___sw_register],
 	['sw/unregister', ep___sw_unregister],
 	['test', ep___test],
diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts
index ddec877dd4246274660224bc22a36815abfebdf9..bfd5de7b007f102cbd96e370820bc9066c16c9a4 100644
--- a/packages/backend/src/server/api/endpoints/sw/register.ts
+++ b/packages/backend/src/server/api/endpoints/sw/register.ts
@@ -25,6 +25,18 @@ export const meta = {
 				type: 'string',
 				optional: false, nullable: true,
 			},
+			userId: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+			endpoint: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+			sendReadMessage: {
+				type: 'boolean',
+				optional: false, nullable: false,
+			},
 		},
 	},
 } as const;
@@ -35,6 +47,7 @@ export const paramDef = {
 		endpoint: { type: 'string' },
 		auth: { type: 'string' },
 		publickey: { type: 'string' },
+		sendReadMessage: { type: 'boolean', default: false },
 	},
 	required: ['endpoint', 'auth', 'publickey'],
 } as const;
@@ -64,6 +77,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				return {
 					state: 'already-subscribed' as const,
 					key: instance.swPublicKey,
+					userId: me.id,
+					endpoint: exist.endpoint,
+					sendReadMessage: exist.sendReadMessage,
 				};
 			}
 
@@ -74,11 +90,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				endpoint: ps.endpoint,
 				auth: ps.auth,
 				publickey: ps.publickey,
+				sendReadMessage: ps.sendReadMessage,
 			});
 
 			return {
 				state: 'subscribed' as const,
 				key: instance.swPublicKey,
+				userId: me.id,
+				endpoint: ps.endpoint,
+				sendReadMessage: ps.sendReadMessage,
 			};
 		});
 	}
diff --git a/packages/backend/src/server/api/endpoints/sw/show-registration.ts b/packages/backend/src/server/api/endpoints/sw/show-registration.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bede10be5cf3be4f35c964eec04b5eb2ccec9114
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/sw/show-registration.ts
@@ -0,0 +1,66 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { SwSubscriptionsRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+	tags: ['account'],
+
+	requireCredential: true,
+
+	description: 'Check push notification registration exists.',
+
+	res: {
+		type: 'object',
+		optional: false, nullable: true,
+		properties: {
+			userId: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+			endpoint: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+			sendReadMessage: {
+				type: 'boolean',
+				optional: false, nullable: false,
+			},
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		endpoint: { type: 'string' },
+	},
+	required: ['endpoint'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.swSubscriptionsRepository)
+		private swSubscriptionsRepository: SwSubscriptionsRepository,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			// if already subscribed
+			const exist = await this.swSubscriptionsRepository.findOneBy({
+				userId: me.id,
+				endpoint: ps.endpoint,
+			});
+
+			if (exist != null) {
+				return {
+					userId: exist.userId,
+					endpoint: exist.endpoint,
+					sendReadMessage: exist.sendReadMessage,
+				};
+			}
+
+			return null;
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts
index 5772eeee266a8b490fb668917817593e388040df..f12b98617d33c03ae96d4c3824d90807d726ad1d 100644
--- a/packages/backend/src/server/api/endpoints/sw/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/sw/unregister.ts
@@ -6,7 +6,7 @@ import { DI } from '@/di-symbols.js';
 export const meta = {
 	tags: ['account'],
 
-	requireCredential: true,
+	requireCredential: false,
 
 	description: 'Unregister from receiving push notifications.',
 } as const;
@@ -28,7 +28,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			await this.swSubscriptionsRepository.delete({
-				userId: me.id,
+				...(me ? { userId: me.id } : {}),
 				endpoint: ps.endpoint,
 			});
 		});
diff --git a/packages/backend/src/server/api/endpoints/sw/update-registration.ts b/packages/backend/src/server/api/endpoints/sw/update-registration.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9f08c8148d3e0c25e8a48664c7289e1b10ced845
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/sw/update-registration.ts
@@ -0,0 +1,82 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { SwSubscriptionsRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+	tags: ['account'],
+
+	requireCredential: true,
+
+	description: 'Update push notification registration.',
+
+	res: {
+		type: 'object',
+		optional: false, nullable: false,
+		properties: {
+			userId: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+			endpoint: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+			sendReadMessage: {
+				type: 'boolean',
+				optional: false, nullable: false,
+			},
+		},
+	},
+	errors: {
+		noSuchRegistration: {
+			message: 'No such registration.',
+			code: 'NO_SUCH_REGISTRATION',
+			id: ' b09d8066-8064-5613-efb6-0e963b21d012',
+		},
+	}
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		endpoint: { type: 'string' },
+		sendReadMessage: { type: 'boolean' },
+	},
+	required: ['endpoint'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.swSubscriptionsRepository)
+		private swSubscriptionsRepository: SwSubscriptionsRepository,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const swSubscription = await this.swSubscriptionsRepository.findOneBy({
+				userId: me.id,
+				endpoint: ps.endpoint,
+			});
+
+			if (swSubscription === null) {
+				throw new ApiError(meta.errors.noSuchRegistration);
+			}
+
+			if (ps.sendReadMessage !== undefined) {
+				swSubscription.sendReadMessage = ps.sendReadMessage;
+			}
+
+			await this.swSubscriptionsRepository.update(swSubscription.id, {
+				sendReadMessage: swSubscription.sendReadMessage,
+			});
+
+			return {
+				userId: swSubscription.userId,
+				endpoint: swSubscription.endpoint,
+				sendReadMessage: swSubscription.sendReadMessage,
+			};
+		});
+	}
+}
diff --git a/packages/client/src/components/MkPushNotificationAllowButton.vue b/packages/client/src/components/MkPushNotificationAllowButton.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a762914e64e4b611efbad13b9493b7b8ffd02d4b
--- /dev/null
+++ b/packages/client/src/components/MkPushNotificationAllowButton.vue
@@ -0,0 +1,167 @@
+<template>
+<MkButton
+	v-if="supported && !pushRegistrationInServer"
+	type="button"
+	primary
+	:gradate="gradate"
+	:rounded="rounded"
+	:inline="inline"
+	:autofocus="autofocus"
+	:wait="wait"
+	:full="full"
+	@click="subscribe"
+>
+	{{ i18n.ts.subscribePushNotification }}
+</MkButton>
+<MkButton
+	v-else-if="!showOnlyToRegister && ($i ? pushRegistrationInServer : pushSubscription)"
+	type="button"
+	:primary="false"
+	:gradate="gradate"
+	:rounded="rounded"
+	:inline="inline"
+	:autofocus="autofocus"
+	:wait="wait"
+	:full="full"
+	@click="unsubscribe"
+>
+	{{ i18n.ts.unsubscribePushNotification }}
+</MkButton>
+<MkButton v-else-if="$i && pushRegistrationInServer" disabled :rounded="rounded" :inline="inline" :wait="wait" :full="full">
+	{{ i18n.ts.pushNotificationAlreadySubscribed }}
+</MkButton>
+<MkButton v-else-if="!supported" disabled :rounded="rounded" :inline="inline" :wait="wait" :full="full">
+	{{ i18n.ts.pushNotificationNotSupported }}
+</MkButton>
+</template>
+
+<script setup lang="ts">
+import { $i, getAccounts } from '@/account';
+import MkButton from '@/components/MkButton.vue';
+import { instance } from '@/instance';
+import { api, apiWithDialog, promiseDialog } from '@/os';
+import { i18n } from '@/i18n';
+
+defineProps<{
+	primary?: boolean;
+	gradate?: boolean;
+	rounded?: boolean;
+	inline?: boolean;
+	link?: boolean;
+	to?: string;
+	autofocus?: boolean;
+	wait?: boolean;
+	danger?: boolean;
+	full?: boolean;
+	showOnlyToRegister?: boolean;
+}>();
+
+// ServiceWorker registration
+let registration = $ref<ServiceWorkerRegistration | undefined>();
+// If this browser supports push notification
+let supported = $ref(false);
+// If this browser has already subscribed to push notification
+let pushSubscription = $ref<PushSubscription | null>(null);
+let pushRegistrationInServer = $ref<{ state?: string; key?: string; userId: string; endpoint: string; sendReadMessage: boolean; } | undefined>();
+
+function subscribe() {
+	if (!registration || !supported || !instance.swPublickey) return;
+
+	// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
+	return promiseDialog(registration.pushManager.subscribe({
+		userVisibleOnly: true,
+		applicationServerKey: urlBase64ToUint8Array(instance.swPublickey)
+	})
+	.then(async subscription => {
+		pushSubscription = subscription;
+
+		// Register
+		pushRegistrationInServer = await api('sw/register', {
+			endpoint: subscription.endpoint,
+			auth: encode(subscription.getKey('auth')),
+			publickey: encode(subscription.getKey('p256dh'))
+		});
+	}, async err => { // When subscribe failed
+		// 通知が許可されていなかったとき
+		if (err?.name === 'NotAllowedError') {
+			console.info('User denied the notification permission request.');
+			return;
+		}
+
+		// 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
+		// 既に存在していることが原因でエラーになった可能性があるので、
+		// そのサブスクリプションを解除しておく
+		// (これは実行されなさそうだけど、おまじない的に古い実装から残してある)
+		await unsubscribe();
+	}), null, null);
+}
+
+async function unsubscribe() {
+	if (!pushSubscription) return;
+
+	const endpoint = pushSubscription.endpoint;
+	const accounts = await getAccounts();
+
+	pushRegistrationInServer = undefined;
+
+	if ($i && accounts.length >= 2) {
+		apiWithDialog('sw/unregister', {
+			i: $i.token,
+			endpoint,
+		});
+	} else {
+		pushSubscription.unsubscribe();
+		apiWithDialog('sw/unregister', {
+			endpoint,
+		});
+		pushSubscription = null;
+	}
+}
+
+function encode(buffer: ArrayBuffer | null) {
+	return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
+}
+
+/**
+ * Convert the URL safe base64 string to a Uint8Array
+ * @param base64String base64 string
+ */
+ function urlBase64ToUint8Array(base64String: string): Uint8Array {
+	const padding = '='.repeat((4 - base64String.length % 4) % 4);
+	const base64 = (base64String + padding)
+		.replace(/-/g, '+')
+		.replace(/_/g, '/');
+
+	const rawData = window.atob(base64);
+	const outputArray = new Uint8Array(rawData.length);
+
+	for (let i = 0; i < rawData.length; ++i) {
+		outputArray[i] = rawData.charCodeAt(i);
+	}
+	return outputArray;
+}
+
+navigator.serviceWorker.ready.then(async swr => {
+	registration = swr;
+
+	pushSubscription = await registration.pushManager.getSubscription();
+
+	if (instance.swPublickey && ('PushManager' in window) && $i && $i.token) {
+		supported = true;
+
+		if (pushSubscription) {
+			const res = await api('sw/show-registration', {
+				endpoint: pushSubscription.endpoint,
+			});
+
+			if (res) {
+				pushRegistrationInServer = res;
+			}
+		}
+	}
+});
+
+defineExpose({
+	pushRegistrationInServer: $$(pushRegistrationInServer),
+});
+</script>
diff --git a/packages/client/src/pages/settings/notifications.vue b/packages/client/src/pages/settings/notifications.vue
index 5703e0c6b6df88e7960766b0854bc9bf11125bd9..77ec567da41cc34d50e6d09000ae7b28ff2a6b59 100644
--- a/packages/client/src/pages/settings/notifications.vue
+++ b/packages/client/src/pages/settings/notifications.vue
@@ -6,6 +6,18 @@
 		<FormLink class="_formBlock" @click="readAllUnreadNotes">{{ i18n.ts.markAsReadAllUnreadNotes }}</FormLink>
 		<FormLink class="_formBlock" @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormLink>
 	</FormSection>
+	<FormSection>
+		<template #label>{{ i18n.ts.pushNotification }}</template>
+		<MkPushNotificationAllowButton ref="allowButton" />
+		<FormSwitch class="_formBlock" :disabled="!pushRegistrationInServer" :model-value="sendReadMessage" @update:modelValue="onChangeSendReadMessage">
+			<template #label>{{ i18n.ts.sendPushNotificationReadMessage }}</template>
+			<template #caption>
+				<I18n :src="i18n.ts.sendPushNotificationReadMessageCaption">
+					<template #emptyPushNotificationMessage>{{ i18n.ts._notification.emptyPushNotificationMessage }}</template>
+				</I18n>
+			</template>
+		</FormSwitch>
+	</FormSection>
 </div>
 </template>
 
@@ -15,10 +27,16 @@ import { notificationTypes } from 'misskey-js';
 import FormButton from '@/components/MkButton.vue';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
+import FormSwitch from '@/components/form/switch.vue';
 import * as os from '@/os';
 import { $i } from '@/account';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
+import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
+
+let allowButton = $ref<InstanceType<typeof MkPushNotificationAllowButton>>();
+let pushRegistrationInServer = $computed(() => allowButton?.pushRegistrationInServer);
+let sendReadMessage = $computed(() => pushRegistrationInServer?.sendReadMessage || false);
 
 async function readAllUnreadNotes() {
 	await os.api('i/read-all-unread-notes');
@@ -49,6 +67,18 @@ function configure() {
 	}, 'closed');
 }
 
+function onChangeSendReadMessage(v: boolean) {
+	if (!pushRegistrationInServer) return;
+
+	os.apiWithDialog('sw/update-registration', {
+		endpoint: pushRegistrationInServer.endpoint,
+		sendReadMessage: v,
+	}).then(res => {
+		if (!allowButton)	return;
+		allowButton.pushRegistrationInServer = res;
+	});
+}
+
 const headerActions = $computed(() => []);
 
 const headerTabs = $computed(() => []);
diff --git a/packages/client/src/pages/timeline.tutorial.vue b/packages/client/src/pages/timeline.tutorial.vue
index 7f08ccc2a1aea96a96cab49cbc75909a360c001d..9683cc22a5b4502f73614a822c444aa9ba326097 100644
--- a/packages/client/src/pages/timeline.tutorial.vue
+++ b/packages/client/src/pages/timeline.tutorial.vue
@@ -1,6 +1,17 @@
 <template>
-<div class="_card tbkwesmv">
-	<div class="_title"><i class="fas fa-info-circle"></i> {{ i18n.ts._tutorial.title }}</div>
+<div class="_card">
+	<div :class="$style.title" class="_title">
+		<div :class="$style.titleText"><i class="fas fa-info-circle"></i> {{ i18n.ts._tutorial.title }}</div>
+		<div :class="$style.step">
+			<button class="_button" :class="$style.stepArrow" :disabled="tutorial === 0" @click="tutorial--">
+				<i class="fas fa-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="fas fa-chevron-right"></i>
+			</button>
+		</div>
+	</div>
 	<div v-if="tutorial === 0" class="_content">
 		<div>{{ i18n.ts._tutorial.step1_1 }}</div>
 		<div>{{ i18n.ts._tutorial.step1_2 }}</div>
@@ -15,7 +26,7 @@
 		<div>{{ i18n.ts._tutorial.step3_1 }}</div>
 		<div>{{ i18n.ts._tutorial.step3_2 }}</div>
 		<div>{{ i18n.ts._tutorial.step3_3 }}</div>
-		<small>{{ i18n.ts._tutorial.step3_4 }}</small>
+		<small :class="$style.small">{{ i18n.ts._tutorial.step3_4 }}</small>
 	</div>
 	<div v-else-if="tutorial === 3" class="_content">
 		<div>{{ i18n.ts._tutorial.step4_1 }}</div>
@@ -32,7 +43,7 @@
 			</template>
 		</I18n>
 		<div>{{ i18n.ts._tutorial.step5_3 }}</div>
-		<small>{{ i18n.ts._tutorial.step5_4 }}</small>
+		<small :class="$style.small">{{ i18n.ts._tutorial.step5_4 }}</small>
 	</div>
 	<div v-else-if="tutorial === 5" class="_content">
 		<div>{{ i18n.ts._tutorial.step6_1 }}</div>
@@ -48,19 +59,20 @@
 		</I18n>
 		<div>{{ i18n.ts._tutorial.step7_3 }}</div>
 	</div>
+	<div v-else-if="tutorial === 7" class="_content">
+		<div>{{ i18n.ts._tutorial.step8_1 }}</div>
+		<div>{{ i18n.ts._tutorial.step8_2 }}</div>
+		<small :class="$style.small">{{ i18n.ts._tutorial.step8_3 }}</small>
+	</div>
 
-	<div class="_footer navigation">
-		<div class="step">
-			<button class="arrow _button" :disabled="tutorial === 0" @click="tutorial--">
-				<i class="fas fa-chevron-left"></i>
-			</button>
-			<span>{{ tutorial + 1 }} / 7</span>
-			<button class="arrow _button" :disabled="tutorial === 6" @click="tutorial++">
-				<i class="fas fa-chevron-right"></i>
-			</button>
-		</div>
-		<MkButton v-if="tutorial === 6" class="ok" primary @click="tutorial = -1"><i class="fas fa-check"></i> {{ i18n.ts.gotIt }}</MkButton>
-		<MkButton v-else class="ok" primary @click="tutorial++"><i class="fas fa-check"></i> {{ i18n.ts.next }}</MkButton>
+	<div class="_footer" :class="$style.footer">
+		<template v-if="tutorial === tutorialsNumber - 1">
+			<MkPushNotificationAllowButton :class="$style.footerItem" primary show-only-to-register @click="tutorial = -1" />
+			<MkButton :class="$style.footerItem" :primary="false" @click="tutorial = -1">{{ i18n.ts.noThankYou }}</MkButton>
+		</template>
+		<template v-else>
+			<MkButton :class="$style.footerItem" primary @click="tutorial++"><i class="fas fa-check"></i> {{ i18n.ts.next }}</MkButton>
+		</template>
 	</div>
 </div>
 </template>
@@ -68,53 +80,63 @@
 <script lang="ts" setup>
 import { computed } from 'vue';
 import MkButton from '@/components/MkButton.vue';
+import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
 import { defaultStore } from '@/store';
 import { i18n } from '@/i18n';
 
+const tutorialsNumber = 8;
+
 const tutorial = computed({
 	get() { return defaultStore.reactiveState.tutorial.value || 0; },
 	set(value) { defaultStore.set('tutorial', value); },
 });
 </script>
 
-<style lang="scss" scoped>
-.tbkwesmv {
-	> ._content {
-		> small {
-			opacity: 0.7;
-		}
-	}
+<style lang="scss" module>
+.small {
+	opacity: 0.7;
+}
 
-	> .navigation {
-		display: flex;
-		flex-direction: row;
-		align-items: baseline;
+.title {
+	display: flex;
+	flex-wrap: wrap;
 
-		> .step {
-			> .arrow {
-				padding: 4px;
+	&Text {
+		margin: 4px 0;
+		padding-right: 4px;
+	}
+}
 
-				&:disabled {
-					opacity: 0.5;
-				}
+.step {
+	margin-left: auto;
 
-				&:first-child {
-					padding-right: 8px;
-				}
+	&Arrow {
+		padding: 4px;
+		&:disabled {
+			opacity: 0.5;
+		}
+		&:first-child {
+			padding-right: 8px;
+		}
+		&:last-child {
+			padding-left: 8px;
+		}
+	}
 
-				&:last-child {
-					padding-left: 8px;
-				}
-			}
+	&Number {
+		font-weight: normal;
+		margin: 4px;
+	}
+}
 
-			> span {
-				margin: 0 4px;
-			}
-		}
+.footer {
+	display: flex;
+	flex-wrap: wrap;
+	flex-direction: row;
+	justify-content: right;
 
-		> .ok {
-			margin-left: auto;
-		}
+	&Item {
+		margin: 4px;
 	}
 }
 </style>
diff --git a/packages/client/src/scripts/initialize-sw.ts b/packages/client/src/scripts/initialize-sw.ts
index 7bacfbdf00ccccfcc8e05d0a0f836e28bd745938..de52f30523aedccb510e12cca573b090aa9a1294 100644
--- a/packages/client/src/scripts/initialize-sw.ts
+++ b/packages/client/src/scripts/initialize-sw.ts
@@ -1,6 +1,3 @@
-import { instance } from '@/instance';
-import { $i } from '@/account';
-import { api } from '@/os';
 import { lang } from '@/config';
 
 export async function initializeSw() {
@@ -12,57 +9,5 @@ export async function initializeSw() {
 			msg: 'initialize',
 			lang,
 		});
-
-		if (instance.swPublickey && ('PushManager' in window) && $i && $i.token) {
-			// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
-			registration.pushManager.subscribe({
-				userVisibleOnly: true,
-				applicationServerKey: urlBase64ToUint8Array(instance.swPublickey)
-			})
-			.then(subscription => {
-				function encode(buffer: ArrayBuffer | null) {
-					return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
-				}
-		
-				// Register
-				api('sw/register', {
-					endpoint: subscription.endpoint,
-					auth: encode(subscription.getKey('auth')),
-					publickey: encode(subscription.getKey('p256dh'))
-				});
-			})
-			// When subscribe failed
-			.catch(async (err: Error) => {
-				// 通知が許可されていなかったとき
-				if (err.name === 'NotAllowedError') {
-					return;
-				}
-		
-				// 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
-				// 既に存在していることが原因でエラーになった可能性があるので、
-				// そのサブスクリプションを解除しておく
-				const subscription = await registration.pushManager.getSubscription();
-				if (subscription) subscription.unsubscribe();
-			});
-		}
 	});
 }
-
-/**
- * Convert the URL safe base64 string to a Uint8Array
- * @param base64String base64 string
- */
-function urlBase64ToUint8Array(base64String: string): Uint8Array {
-	const padding = '='.repeat((4 - base64String.length % 4) % 4);
-	const base64 = (base64String + padding)
-		.replace(/-/g, '+')
-		.replace(/_/g, '/');
-
-	const rawData = window.atob(base64);
-	const outputArray = new Uint8Array(rawData.length);
-
-	for (let i = 0; i < rawData.length; ++i) {
-		outputArray[i] = rawData.charCodeAt(i);
-	}
-	return outputArray;
-}