diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd84f830cc..bf98a44ac4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@
   - 従来のWebsocket接続を行うモードはリアルタイムモードとして再定義されました
 	- チャットなど、一部の機能は引き続き設定に関わらずWebsocket接続が行われます
 - Enhance: メモリ使用量を軽減しました
+- Enhance: 招待されているが参加していないルームを開いたときに、招待を承認するかどうか尋ねるように
 - Enhance: リプライ元にアンケートがあることが表示されるように
 - Enhance: ノートのサーバー情報のデザインを改善・パフォーマンス向上  
   (Based on https://github.com/taiyme/misskey/pull/198, https://github.com/taiyme/misskey/pull/211, https://github.com/taiyme/misskey/pull/283)
@@ -22,6 +23,7 @@
 
 ### Server
 - Enhance: ノートのレスポンスにアンケートが添付されているかどうかを示すフラグ`hasPoll`を追加
+- Enhance: チャットルームのレスポンスに招待されているかどうかを示すフラグ`invitationExists`を追加
 - Fix: チャットルームが削除された場合・チャットルームから抜けた場合に、未読状態が残り続けることがあるのを修正
 - Fix: ユーザ除外アンテナをインポートできない問題を修正
 - Fix: アンテナのセンシティブなチャンネルのノートを含むかどうかの情報がエクスポートされない問題を修正
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 6bd5c8c3d7..a49f7b4d35 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5555,6 +5555,14 @@ export interface Locale extends ILocale {
          * チャットが使えない状態になっているか、相手がチャットを開放していません。
          */
         "cannotChatWithTheUser_description": string;
+        /**
+         * あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。
+         */
+        "youAreNotAMemberOfThisRoomButInvited": string;
+        /**
+         * 招待を承認しますか?
+         */
+        "doYouAcceptInvitation": string;
         /**
          * チャットする
          */
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 6c73285295..8b2d31f7cd 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1385,6 +1385,8 @@ _chat:
   chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。"
   cannotChatWithTheUser: "このユーザーとのチャットを開始できません"
   cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
+  youAreNotAMemberOfThisRoomButInvited: "あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。"
+  doYouAcceptInvitation: "招待を承認しますか?"
   chatWithThisUser: "チャットする"
   thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。"
   thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。"
diff --git a/packages/backend/src/core/entities/ChatEntityService.ts b/packages/backend/src/core/entities/ChatEntityService.ts
index da112d5444..6bce2413fd 100644
--- a/packages/backend/src/core/entities/ChatEntityService.ts
+++ b/packages/backend/src/core/entities/ChatEntityService.ts
@@ -238,13 +238,15 @@ export class ChatEntityService {
 		options?: {
 			_hint_?: {
 				packedOwners: Map<MiChatRoom['id'], Packed<'UserLite'>>;
-				memberships?: Map<MiChatRoom['id'], MiChatRoomMembership | null | undefined>;
+				myMemberships?: Map<MiChatRoom['id'], MiChatRoomMembership | null | undefined>;
+				myInvitations?: Map<MiChatRoom['id'], MiChatRoomInvitation | null | undefined>;
 			};
 		},
 	): Promise<Packed<'ChatRoom'>> {
 		const room = typeof src === 'object' ? src : await this.chatRoomsRepository.findOneByOrFail({ id: src });
 
-		const membership = me && me.id !== room.ownerId ? (options?._hint_?.memberships?.get(room.id) ?? await this.chatRoomMembershipsRepository.findOneBy({ roomId: room.id, userId: me.id })) : null;
+		const membership = me && me.id !== room.ownerId ? (options?._hint_?.myMemberships?.get(room.id) ?? await this.chatRoomMembershipsRepository.findOneBy({ roomId: room.id, userId: me.id })) : null;
+		const invitation = me && me.id !== room.ownerId ? (options?._hint_?.myInvitations?.get(room.id) ?? await this.chatRoomInvitationsRepository.findOneBy({ roomId: room.id, userId: me.id })) : null;
 
 		return {
 			id: room.id,
@@ -254,6 +256,7 @@ export class ChatEntityService {
 			ownerId: room.ownerId,
 			owner: options?._hint_?.packedOwners.get(room.ownerId) ?? await this.userEntityService.pack(room.owner ?? room.ownerId, me),
 			isMuted: membership != null ? membership.isMuted : false,
+			invitationExists: invitation != null,
 		};
 	}
 
@@ -278,7 +281,7 @@ export class ChatEntityService {
 
 		const owners = _rooms.map(x => x.owner ?? x.ownerId);
 
-		const [packedOwners, memberships] = await Promise.all([
+		const [packedOwners, myMemberships, myInvitations] = await Promise.all([
 			this.userEntityService.packMany(owners, me)
 				.then(users => new Map(users.map(u => [u.id, u]))),
 			this.chatRoomMembershipsRepository.find({
@@ -287,9 +290,15 @@ export class ChatEntityService {
 					userId: me.id,
 				},
 			}).then(memberships => new Map(_rooms.map(r => [r.id, memberships.find(m => m.roomId === r.id)]))),
+			this.chatRoomInvitationsRepository.find({
+				where: {
+					roomId: In(_rooms.map(x => x.id)),
+					userId: me.id,
+				},
+			}).then(invitations => new Map(_rooms.map(r => [r.id, invitations.find(i => i.roomId === r.id)]))),
 		]);
 
-		return Promise.all(_rooms.map(room => this.packRoom(room, me, { _hint_: { packedOwners, memberships } })));
+		return Promise.all(_rooms.map(room => this.packRoom(room, me, { _hint_: { packedOwners, myMemberships, myInvitations } })));
 	}
 
 	@bindThis
diff --git a/packages/backend/src/models/json-schema/chat-room.ts b/packages/backend/src/models/json-schema/chat-room.ts
index e97556e378..e628a9baa3 100644
--- a/packages/backend/src/models/json-schema/chat-room.ts
+++ b/packages/backend/src/models/json-schema/chat-room.ts
@@ -36,5 +36,9 @@ export const packedChatRoomSchema = {
 			type: 'boolean',
 			optional: true, nullable: false,
 		},
+		invitationExists: {
+			type: 'boolean',
+			optional: true, nullable: false,
+		},
 	},
 } as const;
diff --git a/packages/frontend/src/pages/chat/room.vue b/packages/frontend/src/pages/chat/room.vue
index 64d3420166..ac13c5fac6 100644
--- a/packages/frontend/src/pages/chat/room.vue
+++ b/packages/frontend/src/pages/chat/room.vue
@@ -80,7 +80,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</button>
 					</div>
 				</Transition>
-				<XForm v-if="!initializing" :user="user" :room="room" :class="$style.form"/>
+				<XForm v-if="initialized" :user="user" :room="room" :class="$style.form"/>
 			</div>
 		</div>
 	</template>
@@ -127,7 +127,8 @@ export type NormalizedChatMessage = Omit<Misskey.entities.ChatMessageLite, 'from
 	})[];
 };
 
-const initializing = ref(true);
+const initializing = ref(false);
+const initialized = ref(false);
 const moreFetching = ref(false);
 const messages = ref<NormalizedChatMessage[]>([]);
 const canFetchMore = ref(false);
@@ -171,7 +172,10 @@ function normalizeMessage(message: Misskey.entities.ChatMessageLite | Misskey.en
 async function initialize() {
 	const LIMIT = 20;
 
+	if (initializing.value) return;
+
 	initializing.value = true;
+	initialized.value = false;
 
 	if (props.userId) {
 		const [u, m] = await Promise.all([
@@ -194,13 +198,44 @@ async function initialize() {
 		connection.value.on('react', onReact);
 		connection.value.on('unreact', onUnreact);
 	} else {
-		const [r, m] = await Promise.all([
+		const [rResult, mResult] = await Promise.allSettled([
 			misskeyApi('chat/rooms/show', { roomId: props.roomId }),
 			misskeyApi('chat/messages/room-timeline', { roomId: props.roomId, limit: LIMIT }),
 		]);
 
-		room.value = r as Misskey.entities.ChatRoomsShowResponse;
-		messages.value = (m as Misskey.entities.ChatMessagesRoomTimelineResponse).map(x => normalizeMessage(x));
+		if (rResult.status === 'rejected') {
+			os.alert({
+				type: 'error',
+				text: i18n.ts.somethingHappened,
+			});
+			initializing.value = false;
+			return;
+		}
+
+		const r = rResult.value as Misskey.entities.ChatRoomsShowResponse;
+
+		if (r.invitationExists) {
+			const confirm = await os.confirm({
+				type: 'question',
+				title: r.name,
+				text: i18n.ts._chat.youAreNotAMemberOfThisRoomButInvited + '\n' + i18n.ts._chat.doYouAcceptInvitation,
+			});
+			if (confirm.canceled) {
+				initializing.value = false;
+				router.push('/chat');
+				return;
+			} else {
+				await os.apiWithDialog('chat/rooms/join', { roomId: r.id });
+				initializing.value = false;
+				initialize();
+				return;
+			}
+		}
+
+		const m = mResult.status === 'fulfilled' ? mResult.value as Misskey.entities.ChatMessagesRoomTimelineResponse : [];
+
+		room.value = r;
+		messages.value = m.map(x => normalizeMessage(x));
 
 		if (messages.value.length === LIMIT) {
 			canFetchMore.value = true;
@@ -217,6 +252,7 @@ async function initialize() {
 
 	window.document.addEventListener('visibilitychange', onVisibilitychange);
 
+	initialized.value = true;
 	initializing.value = false;
 }
 
@@ -319,6 +355,12 @@ onMounted(() => {
 	initialize();
 });
 
+onActivated(() => {
+	if (!initialized.value) {
+		initialize();
+	}
+});
+
 onBeforeUnmount(() => {
 	connection.value?.dispose();
 	window.document.removeEventListener('visibilitychange', onVisibilitychange);
@@ -410,7 +452,7 @@ const headerActions = computed<PageHeaderItem[]>(() => [{
 }]);
 
 definePage(computed(() => {
-	if (!initializing.value) {
+	if (initialized.value) {
 		if (user.value) {
 			return {
 				userName: user.value,
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 91359cffda..1e38446882 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -5534,6 +5534,7 @@ export type components = {
       name: string;
       description: string;
       isMuted?: boolean;
+      invitationExists?: boolean;
     };
     ChatRoomInvitation: {
       id: string;