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;