diff options
29 files changed, 453 insertions, 153 deletions
diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts index 6194f624b1..3984cefc80 100644 --- a/packages/backend/src/core/ChatService.ts +++ b/packages/backend/src/core/ChatService.ts @@ -99,7 +99,7 @@ export class ChatService { text?: string | null; file?: MiDriveFile | null; uri?: string | null; - }): Promise<Packed<'ChatMessageLite'>> { + }): Promise<Packed<'ChatMessageLiteFor1on1'>> { if (fromUser.id === toUser.id) { throw new Error('yourself'); } @@ -210,7 +210,7 @@ export class ChatService { text?: string | null; file?: MiDriveFile | null; uri?: string | null; - }): Promise<Packed<'ChatMessageLite'>> { + }): Promise<Packed<'ChatMessageLiteForRoom'>> { const memberships = (await this.chatRoomMembershipsRepository.findBy({ roomId: toRoom.id })).map(m => ({ userId: m.userId, isMuted: m.isMuted, diff --git a/packages/backend/src/core/entities/ChatEntityService.ts b/packages/backend/src/core/entities/ChatEntityService.ts index 099a9e3ad2..da112d5444 100644 --- a/packages/backend/src/core/entities/ChatEntityService.ts +++ b/packages/backend/src/core/entities/ChatEntityService.ts @@ -128,7 +128,7 @@ export class ChatEntityService { packedFiles: Map<MiChatMessage['fileId'], Packed<'DriveFile'> | null>; }; }, - ): Promise<Packed<'ChatMessageLite'>> { + ): Promise<Packed<'ChatMessageLiteFor1on1'>> { const packedFiles = options?._hint_?.packedFiles; const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src }); @@ -147,7 +147,7 @@ export class ChatEntityService { createdAt: this.idService.parse(message.id).date.toISOString(), text: message.text, fromUserId: message.fromUserId, - toUserId: message.toUserId, + toUserId: message.toUserId!, fileId: message.fileId, file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null, reactions, @@ -177,7 +177,7 @@ export class ChatEntityService { packedUsers: Map<MiUser['id'], Packed<'UserLite'>>; }; }, - ): Promise<Packed<'ChatMessageLite'>> { + ): Promise<Packed<'ChatMessageLiteForRoom'>> { const packedFiles = options?._hint_?.packedFiles; const packedUsers = options?._hint_?.packedUsers; @@ -199,7 +199,7 @@ export class ChatEntityService { text: message.text, fromUserId: message.fromUserId, fromUser: packedUsers?.get(message.fromUserId) ?? await this.userEntityService.pack(message.fromUser ?? message.fromUserId), - toRoomId: message.toRoomId, + toRoomId: message.toRoomId!, fileId: message.fileId, file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null, reactions, diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index bc9308ca9b..27aa3d89de 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -63,7 +63,7 @@ import { } from '@/models/json-schema/meta.js'; import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js'; import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js'; -import { packedChatMessageSchema, packedChatMessageLiteSchema } from '@/models/json-schema/chat-message.js'; +import { packedChatMessageSchema, packedChatMessageLiteSchema, packedChatMessageLiteForRoomSchema, packedChatMessageLiteFor1on1Schema } from '@/models/json-schema/chat-message.js'; import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js'; import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js'; import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js'; @@ -126,6 +126,8 @@ export const refs = { AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema, ChatMessage: packedChatMessageSchema, ChatMessageLite: packedChatMessageLiteSchema, + ChatMessageLiteFor1on1: packedChatMessageLiteFor1on1Schema, + ChatMessageLiteForRoom: packedChatMessageLiteForRoomSchema, ChatRoom: packedChatRoomSchema, ChatRoomInvitation: packedChatRoomInvitationSchema, ChatRoomMembership: packedChatRoomMembershipSchema, diff --git a/packages/backend/src/models/json-schema/chat-message.ts b/packages/backend/src/models/json-schema/chat-message.ts index 44b7298702..3b5e85ab69 100644 --- a/packages/backend/src/models/json-schema/chat-message.ts +++ b/packages/backend/src/models/json-schema/chat-message.ts @@ -72,7 +72,7 @@ export const packedChatMessageSchema = { }, user: { type: 'object', - optional: true, nullable: true, + optional: false, nullable: false, ref: 'UserLite', }, }, @@ -144,3 +144,113 @@ export const packedChatMessageLiteSchema = { }, }, } as const; + +export const packedChatMessageLiteFor1on1Schema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + createdAt: { + type: 'string', + format: 'date-time', + optional: false, nullable: false, + }, + fromUserId: { + type: 'string', + optional: false, nullable: false, + }, + toUserId: { + type: 'string', + optional: false, nullable: false, + }, + text: { + type: 'string', + optional: true, nullable: true, + }, + fileId: { + type: 'string', + optional: true, nullable: true, + }, + file: { + type: 'object', + optional: true, nullable: true, + ref: 'DriveFile', + }, + reactions: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + reaction: { + type: 'string', + optional: false, nullable: false, + }, + }, + }, + }, + }, +} as const; + +export const packedChatMessageLiteForRoomSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + createdAt: { + type: 'string', + format: 'date-time', + optional: false, nullable: false, + }, + fromUserId: { + type: 'string', + optional: false, nullable: false, + }, + fromUser: { + type: 'object', + optional: false, nullable: false, + ref: 'UserLite', + }, + toRoomId: { + type: 'string', + optional: false, nullable: false, + }, + text: { + type: 'string', + optional: true, nullable: true, + }, + fileId: { + type: 'string', + optional: true, nullable: true, + }, + file: { + type: 'object', + optional: true, nullable: true, + ref: 'DriveFile', + }, + reactions: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + reaction: { + type: 'string', + optional: false, nullable: false, + }, + user: { + type: 'object', + optional: false, nullable: false, + ref: 'UserLite', + }, + }, + }, + }, + }, +} as const; diff --git a/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts b/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts index 1f334d5750..a988dc60b9 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts @@ -30,7 +30,7 @@ export const meta = { res: { type: 'object', optional: false, nullable: false, - ref: 'ChatMessageLite', + ref: 'ChatMessageLiteForRoom', }, errors: { diff --git a/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts b/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts index 6b77a026fb..bbaab8a6c3 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts @@ -30,7 +30,7 @@ export const meta = { res: { type: 'object', optional: false, nullable: false, - ref: 'ChatMessageLite', + ref: 'ChatMessageLiteFor1on1', }, errors: { diff --git a/packages/backend/src/server/api/endpoints/chat/messages/delete.ts b/packages/backend/src/server/api/endpoints/chat/messages/delete.ts index 959599ddcf..25fc774d4f 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/delete.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/delete.ts @@ -13,6 +13,7 @@ export const meta = { tags: ['chat'], requireCredential: true, + requiredRolePolicy: 'canChat', kind: 'write:chat', diff --git a/packages/backend/src/server/api/endpoints/chat/messages/react.ts b/packages/backend/src/server/api/endpoints/chat/messages/react.ts index 561e36ed19..0145e380be 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/react.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/react.ts @@ -13,6 +13,7 @@ export const meta = { tags: ['chat'], requireCredential: true, + requiredRolePolicy: 'canChat', kind: 'write:chat', diff --git a/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts index 7aef35db04..b6d3356196 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts @@ -23,7 +23,7 @@ export const meta = { items: { type: 'object', optional: false, nullable: false, - ref: 'ChatMessageLite', + ref: 'ChatMessageLiteForRoom', }, }, diff --git a/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts index 4eb25259fb..b97bad8a9c 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/unreact.ts @@ -13,6 +13,7 @@ export const meta = { tags: ['chat'], requireCredential: true, + requiredRolePolicy: 'canChat', kind: 'write:chat', diff --git a/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts index 9d308d79b0..a35f121bb1 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts @@ -24,7 +24,7 @@ export const meta = { items: { type: 'object', optional: false, nullable: false, - ref: 'ChatMessageLite', + ref: 'ChatMessageLiteFor1on1', }, }, diff --git a/packages/frontend/src/pages/chat/XMessage.vue b/packages/frontend/src/pages/chat/XMessage.vue index 33741b1845..eb8b0d79ee 100644 --- a/packages/frontend/src/pages/chat/XMessage.vue +++ b/packages/frontend/src/pages/chat/XMessage.vue @@ -5,33 +5,28 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="[$style.root, { [$style.isMe]: isMe }]"> - <MkAvatar :class="$style.avatar" :user="message.fromUser" :link="!isMe" :preview="false"/> + <MkAvatar :class="$style.avatar" :user="message.fromUser!" :link="!isMe" :preview="false"/> <div :class="$style.body" @contextmenu.stop="onContextmenu"> - <div :class="$style.header"><MkUserName v-if="!isMe && prefer.s['chat.showSenderName']" :user="message.fromUser"/></div> + <div :class="$style.header"><MkUserName v-if="!isMe && prefer.s['chat.showSenderName'] && message.fromUser != null" :user="message.fromUser"/></div> <MkFukidashi :class="$style.fukidashi" :tail="isMe ? 'right' : 'left'" :accented="isMe"> - <div v-if="!message.isDeleted" :class="$style.content"> - <Mfm - v-if="message.text" - ref="text" - class="_selectable" - :text="message.text" - :i="$i" - :nyaize="'respect'" - :enableEmojiMenu="true" - :enableEmojiMenuReaction="true" - /> - <MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/> - </div> - <div v-else :class="$style.content"> - <p>{{ i18n.ts.deleted }}</p> - </div> + <Mfm + v-if="message.text" + ref="text" + class="_selectable" + :text="message.text" + :i="$i" + :nyaize="'respect'" + :enableEmojiMenu="true" + :enableEmojiMenuReaction="true" + /> + <MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/> </MkFukidashi> <MkUrlPreview v-for="url in urls" :key="url" :url="url" style="margin: 8px 0;"/> <div :class="$style.footer"> <button class="_textButton" style="color: currentColor;" @click="showMenu"><i class="ti ti-dots-circle-horizontal"></i></button> <MkTime :class="$style.time" :time="message.createdAt"/> - <MkA v-if="isSearchResult && message.toRoomId" :to="`/chat/room/${message.toRoomId}`">{{ message.toRoom.name }}</MkA> - <MkA v-if="isSearchResult && message.toUserId && isMe" :to="`/chat/user/${message.toUserId}`">@{{ message.toUser.username }}</MkA> + <MkA v-if="isSearchResult && 'toRoom' in message && message.toRoom != null" :to="`/chat/room/${message.toRoomId}`">{{ message.toRoom.name }}</MkA> + <MkA v-if="isSearchResult && 'toUser' in message && message.toUser != null && isMe" :to="`/chat/user/${message.toUserId}`">@{{ message.toUser.username }}</MkA> </div> <TransitionGroup :enterActiveClass="prefer.s.animation ? $style.transition_reaction_enterActive : ''" @@ -62,6 +57,7 @@ import * as Misskey from 'misskey-js'; import { url } from '@@/js/config.js'; import { isLink } from '@@/js/is-link.js'; import type { MenuItem } from '@/types/menu.js'; +import type { NormalizedChatMessage } from './room.vue'; import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import { ensureSignin } from '@/i.js'; @@ -76,11 +72,12 @@ import * as sound from '@/utility/sound.js'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import { prefer } from '@/preferences.js'; import { DI } from '@/di.js'; +import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js'; const $i = ensureSignin(); const props = defineProps<{ - message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage; + message: NormalizedChatMessage | Misskey.entities.ChatMessage; isSearchResult?: boolean; }>(); @@ -88,6 +85,8 @@ const isMe = computed(() => props.message.fromUserId === $i.id); const urls = computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []); provide(DI.mfmEmojiReactCallback, (reaction) => { + if (!$i.policies.canChat) return; + sound.playMisskeySfx('reaction'); misskeyApi('chat/messages/react', { messageId: props.message.id, @@ -96,7 +95,12 @@ provide(DI.mfmEmojiReactCallback, (reaction) => { }); function react(ev: MouseEvent) { - reactionPicker.show(ev.currentTarget ?? ev.target, null, async (reaction) => { + if (!$i.policies.canChat) return; + + const targetEl = getHTMLElementOrNull(ev.currentTarget ?? ev.target); + if (!targetEl) return; + + reactionPicker.show(targetEl, null, async (reaction) => { sound.playMisskeySfx('reaction'); misskeyApi('chat/messages/react', { messageId: props.message.id, @@ -106,6 +110,8 @@ function react(ev: MouseEvent) { } function onReactionClick(record: Misskey.entities.ChatMessage['reactions'][0]) { + if (!$i.policies.canChat) return; + if (record.user.id === $i.id) { misskeyApi('chat/messages/unreact', { messageId: props.message.id, @@ -132,7 +138,7 @@ function onContextmenu(ev: MouseEvent) { function showMenu(ev: MouseEvent, contextmenu = false) { const menu: MenuItem[] = []; - if (!isMe.value) { + if (!isMe.value && $i.policies.canChat) { menu.push({ text: i18n.ts.reaction, icon: 'ti ti-mood-plus', @@ -150,7 +156,7 @@ function showMenu(ev: MouseEvent, contextmenu = false) { text: i18n.ts.copyContent, icon: 'ti ti-copy', action: () => { - copyToClipboard(props.message.text); + copyToClipboard(props.message.text ?? ''); }, }); @@ -158,7 +164,7 @@ function showMenu(ev: MouseEvent, contextmenu = false) { type: 'divider', }); - if (isMe.value) { + if (isMe.value && $i.policies.canChat) { menu.push({ text: i18n.ts.delete, icon: 'ti ti-trash', @@ -169,14 +175,16 @@ function showMenu(ev: MouseEvent, contextmenu = false) { }); }, }); - } else { + } + + if (!isMe.value && props.message.fromUser != null) { menu.push({ text: i18n.ts.reportAbuse, icon: 'ti ti-exclamation-circle', action: () => { const localUrl = `${url}/chat/messages/${props.message.id}`; const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { - user: props.message.fromUser, + user: props.message.fromUser!, initialComment: `${localUrl}\n-----\n`, }, { closed: () => dispose(), diff --git a/packages/frontend/src/pages/chat/home.home.vue b/packages/frontend/src/pages/chat/home.home.vue index 105f5f7989..17f0e0fbcd 100644 --- a/packages/frontend/src/pages/chat/home.home.vue +++ b/packages/frontend/src/pages/chat/home.home.vue @@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onActivated, onDeactivated, onMounted, ref } from 'vue'; +import { onActivated, onDeactivated, onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { useInterval } from '@@/js/use-interval.js'; import XMessage from './XMessage.vue'; @@ -163,7 +163,7 @@ async function fetchHistory() { .map(m => ({ id: m.id, message: m, - other: m.room == null ? (m.fromUserId === $i.id ? m.toUser : m.fromUser) : null, + other: (!('room' in m) || m.room == null) ? (m.fromUserId === $i.id ? m.toUser : m.fromUser) : null, isMe: m.fromUserId === $i.id, })); diff --git a/packages/frontend/src/pages/chat/home.invitations.vue b/packages/frontend/src/pages/chat/home.invitations.vue index 4c3c0b282e..82b22ea9dd 100644 --- a/packages/frontend/src/pages/chat/home.invitations.vue +++ b/packages/frontend/src/pages/chat/home.invitations.vue @@ -35,18 +35,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onMounted, ref } from 'vue'; +import { onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import { misskeyApi } from '@/utility/misskey-api.js'; -import { ensureSignin } from '@/i.js'; import { useRouter } from '@/router.js'; -import * as os from '@/os.js'; import MkFolder from '@/components/MkFolder.vue'; -const $i = ensureSignin(); - const router = useRouter(); const fetching = ref(true); @@ -55,8 +51,7 @@ const invitations = ref<Misskey.entities.ChatRoomInvitation[]>([]); async function fetchInvitations() { fetching.value = true; - const res = await misskeyApi('chat/rooms/invitations/inbox', { - }); + const res = await misskeyApi('chat/rooms/invitations/inbox'); invitations.value = res; diff --git a/packages/frontend/src/pages/chat/home.joiningRooms.vue b/packages/frontend/src/pages/chat/home.joiningRooms.vue index 63e4d2adf8..f9fd6bfd55 100644 --- a/packages/frontend/src/pages/chat/home.joiningRooms.vue +++ b/packages/frontend/src/pages/chat/home.joiningRooms.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps"> <div v-if="memberships.length > 0" class="_gaps_s"> - <XRoom v-for="membership in memberships" :key="membership.id" :room="membership.room"/> + <XRoom v-for="membership in memberships" :key="membership.id" :room="membership.room!"/> </div> <div v-if="!fetching && memberships.length == 0" class="_fullinfo"> <div>{{ i18n.ts._chat.noRooms }}</div> @@ -16,19 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onMounted, ref } from 'vue'; +import { onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; import XRoom from './XRoom.vue'; -import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import { misskeyApi } from '@/utility/misskey-api.js'; -import { ensureSignin } from '@/i.js'; -import { useRouter } from '@/router.js'; -import * as os from '@/os.js'; - -const $i = ensureSignin(); - -const router = useRouter(); const fetching = ref(true); const memberships = ref<Misskey.entities.ChatRoomMembership[]>([]); @@ -36,8 +28,7 @@ const memberships = ref<Misskey.entities.ChatRoomMembership[]>([]); async function fetchRooms() { fetching.value = true; - const res = await misskeyApi('chat/rooms/joining', { - }); + const res = await misskeyApi('chat/rooms/joining'); memberships.value = res; diff --git a/packages/frontend/src/pages/chat/home.ownedRooms.vue b/packages/frontend/src/pages/chat/home.ownedRooms.vue index b0449fb373..ce7da15563 100644 --- a/packages/frontend/src/pages/chat/home.ownedRooms.vue +++ b/packages/frontend/src/pages/chat/home.ownedRooms.vue @@ -16,19 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onMounted, ref } from 'vue'; +import { onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; import XRoom from './XRoom.vue'; -import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import { misskeyApi } from '@/utility/misskey-api.js'; -import { ensureSignin } from '@/i.js'; -import { useRouter } from '@/router.js'; -import * as os from '@/os.js'; - -const $i = ensureSignin(); - -const router = useRouter(); const fetching = ref(true); const rooms = ref<Misskey.entities.ChatRoom[]>([]); diff --git a/packages/frontend/src/pages/chat/home.vue b/packages/frontend/src/pages/chat/home.vue index 9bb7235a64..e29ab28f2d 100644 --- a/packages/frontend/src/pages/chat/home.vue +++ b/packages/frontend/src/pages/chat/home.vue @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onMounted, ref } from 'vue'; +import { computed, ref } from 'vue'; import XHome from './home.home.vue'; import XInvitations from './home.invitations.vue'; import XJoiningRooms from './home.joiningRooms.vue'; diff --git a/packages/frontend/src/pages/chat/message.vue b/packages/frontend/src/pages/chat/message.vue index 975d1a2be9..3ac90a93fd 100644 --- a/packages/frontend/src/pages/chat/message.vue +++ b/packages/frontend/src/pages/chat/message.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <PageWithHeader> <MkSpacer :contentMax="700"> - <div v-if="initializing"> + <div v-if="initializing || message == null"> <MkLoading/> </div> <div v-else> @@ -17,23 +17,19 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, useTemplateRef, computed, watch, onMounted, nextTick, onBeforeUnmount, onDeactivated, onActivated } from 'vue'; +import { ref, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; import XMessage from './XMessage.vue'; -import * as os from '@/os.js'; -import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; -import { ensureSignin } from '@/i.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { definePage } from '@/page.js'; -import MkButton from '@/components/MkButton.vue'; const props = defineProps<{ messageId?: string; }>(); const initializing = ref(true); -const message = ref<Misskey.entities.ChatMessage>(); +const message = ref<Misskey.entities.ChatMessage | null>(); async function initialize() { initializing.value = true; diff --git a/packages/frontend/src/pages/chat/room.form.vue b/packages/frontend/src/pages/chat/room.form.vue index 27ddbeb565..9389b16ce7 100644 --- a/packages/frontend/src/pages/chat/room.form.vue +++ b/packages/frontend/src/pages/chat/room.form.vue @@ -34,14 +34,12 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, watch, ref, shallowRef, computed, nextTick, readonly } from 'vue'; +import { onMounted, watch, ref, shallowRef, computed, nextTick, readonly, onBeforeUnmount } from 'vue'; import * as Misskey from 'misskey-js'; //import insertTextAtCursor from 'insert-text-at-cursor'; -import { throttle } from 'throttle-debounce'; import { formatTimeString } from '@/utility/format-time-string.js'; import { selectFile } from '@/utility/select-file.js'; import * as os from '@/os.js'; -import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { uploadFile } from '@/utility/upload.js'; import { miLocalStorage } from '@/local-storage.js'; @@ -62,6 +60,7 @@ const text = ref<string>(''); const file = ref<Misskey.entities.DriveFile | null>(null); const sending = ref(false); const textareaReadOnly = ref(false); +let autocompleteInstance: Autocomplete | null = null; const canSend = computed(() => (text.value != null && text.value !== '') || file.value != null); @@ -171,7 +170,9 @@ function chooseFile(ev: MouseEvent) { } function onChangeFile() { - if (fileEl.value.files![0]) upload(fileEl.value.files[0]); + if (fileEl.value == null || fileEl.value.files == null) return; + + if (fileEl.value.files[0]) upload(fileEl.value.files[0]); } function upload(fileToUpload: File, name?: string) { @@ -270,8 +271,9 @@ async function insertEmoji(ev: MouseEvent) { } onMounted(() => { - // TODO: detach when unmount - new Autocomplete(textareaEl.value, text); + if (textareaEl.value != null) { + autocompleteInstance = new Autocomplete(textareaEl.value, text); + } // 書きかけの投稿を復元 const draft = JSON.parse(miLocalStorage.getItem('chatMessageDrafts') || '{}')[getDraftKey()]; @@ -280,6 +282,13 @@ onMounted(() => { file.value = draft.data.file; } }); + +onBeforeUnmount(() => { + if (autocompleteInstance) { + autocompleteInstance.detach(); + autocompleteInstance = null; + } +}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/chat/room.info.vue b/packages/frontend/src/pages/chat/room.info.vue index 8439e5f772..2f091388a0 100644 --- a/packages/frontend/src/pages/chat/room.info.vue +++ b/packages/frontend/src/pages/chat/room.info.vue @@ -26,11 +26,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onMounted, ref, watch } from 'vue'; +import { computed, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; -import { misskeyApi } from '@/utility/misskey-api.js'; import * as os from '@/os.js'; import { ensureSignin } from '@/i.js'; import MkInput from '@/components/MkInput.vue'; @@ -73,7 +72,7 @@ async function del() { router.push('/chat'); } -const isMuted = ref(props.room.isMuted); +const isMuted = ref(props.room.isMuted ?? false); watch(isMuted, async () => { await os.apiWithDialog('chat/rooms/mute', { diff --git a/packages/frontend/src/pages/chat/room.members.vue b/packages/frontend/src/pages/chat/room.members.vue index bff038570f..5a574068cb 100644 --- a/packages/frontend/src/pages/chat/room.members.vue +++ b/packages/frontend/src/pages/chat/room.members.vue @@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only <hr v-if="memberships.length > 0"> <div v-for="membership in memberships" :key="membership.id" :class="$style.membership"> - <MkA :class="$style.membershipBody" :to="`${userPage(membership.user)}`"> - <MkUserCardMini :user="membership.user"/> + <MkA :class="$style.membershipBody" :to="`${userPage(membership.user!)}`"> + <MkUserCardMini :user="membership.user!"/> </MkA> </div> @@ -39,7 +39,6 @@ import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import { misskeyApi } from '@/utility/misskey-api.js'; -import * as os from '@/os.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import { userPage } from '@/filters/user.js'; import { ensureSignin } from '@/i.js'; diff --git a/packages/frontend/src/pages/chat/room.search.vue b/packages/frontend/src/pages/chat/room.search.vue index e382834578..20b6e22a46 100644 --- a/packages/frontend/src/pages/chat/room.search.vue +++ b/packages/frontend/src/pages/chat/room.search.vue @@ -33,14 +33,13 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onMounted, ref } from 'vue'; +import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import XMessage from './XMessage.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import { infoImageUrl } from '@/instance.js'; import { misskeyApi } from '@/utility/misskey-api.js'; -import * as os from '@/os.js'; import MkInput from '@/components/MkInput.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; diff --git a/packages/frontend/src/pages/chat/room.vue b/packages/frontend/src/pages/chat/room.vue index ce823968f7..dcce70ae89 100644 --- a/packages/frontend/src/pages/chat/room.vue +++ b/packages/frontend/src/pages/chat/room.vue @@ -79,15 +79,16 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, useTemplateRef, computed, watch, onMounted, nextTick, onBeforeUnmount, onDeactivated, onActivated } from 'vue'; +import { ref, useTemplateRef, computed, onMounted, onBeforeUnmount, onDeactivated, onActivated } from 'vue'; import * as Misskey from 'misskey-js'; -import { getScrollContainer, isTailVisible } from '@@/js/scroll.js'; +import { getScrollContainer } from '@@/js/scroll.js'; import XMessage from './XMessage.vue'; import XForm from './room.form.vue'; import XSearch from './room.search.vue'; import XMembers from './room.members.vue'; import XInfo from './room.info.vue'; import type { MenuItem } from '@/types/menu.js'; +import type { PageHeaderItem } from '@/types/page-header.js'; import * as os from '@/os.js'; import { useStream } from '@/stream.js'; import * as sound from '@/utility/sound.js'; @@ -109,13 +110,20 @@ const props = defineProps<{ roomId?: string; }>(); +export type NormalizedChatMessage = Omit<Misskey.entities.ChatMessageLite, 'fromUser' | 'reactions'> & { + fromUser: Misskey.entities.UserLite; + reactions: (Misskey.entities.ChatMessageLite['reactions'][number] & { + user: Misskey.entities.UserLite; + })[]; +}; + const initializing = ref(true); const moreFetching = ref(false); -const messages = ref<Misskey.entities.ChatMessage[]>([]); +const messages = ref<NormalizedChatMessage[]>([]); const canFetchMore = ref(false); const user = ref<Misskey.entities.UserDetailed | null>(null); const room = ref<Misskey.entities.ChatRoom | null>(null); -const connection = ref<Misskey.ChannelConnection<Misskey.Channels['chatUser'] | Misskey.Channels['chatRoom']> | null>(null); +const connection = ref<Misskey.IChannelConnection<Misskey.Channels['chatUser']> | Misskey.IChannelConnection<Misskey.Channels['chatRoom']> | null>(null); const showIndicator = ref(false); const timelineEl = useTemplateRef('timelineEl'); @@ -138,18 +146,14 @@ useMutationObserver(timelineEl, { } }); -function normalizeMessage(message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage) { - const reactions = [...message.reactions]; - for (const record of reactions) { - if (room.value == null && record.user == null) { // 1on1の時はuserは省略される - record.user = message.fromUserId === $i.id ? user.value : $i; - } - } - +function normalizeMessage(message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage): NormalizedChatMessage { return { ...message, - fromUser: message.fromUser ?? (message.fromUserId === $i.id ? $i : user), - reactions, + fromUser: message.fromUser ?? (message.fromUserId === $i.id ? $i : user.value!), + reactions: message.reactions.map(record => ({ + ...record, + user: record.user ?? (message.fromUserId === $i.id ? user.value! : $i), + })), }; } @@ -184,8 +188,8 @@ async function initialize() { misskeyApi('chat/messages/room-timeline', { roomId: props.roomId, limit: LIMIT }), ]); - room.value = r; - messages.value = m.map(x => normalizeMessage(x)); + room.value = r as Misskey.entities.ChatRoomsShowResponse; + messages.value = (m as Misskey.entities.ChatMessagesRoomTimelineResponse).map(x => normalizeMessage(x)); if (messages.value.length === LIMIT) { canFetchMore.value = true; @@ -221,11 +225,11 @@ async function fetchMore() { moreFetching.value = true; const newMessages = props.userId ? await misskeyApi('chat/messages/user-timeline', { - userId: user.value.id, + userId: user.value!.id, limit: LIMIT, untilId: messages.value[messages.value.length - 1].id, }) : await misskeyApi('chat/messages/room-timeline', { - roomId: room.value.id, + roomId: room.value!.id, limit: LIMIT, untilId: messages.value[messages.value.length - 1].id, }); @@ -236,7 +240,7 @@ async function fetchMore() { moreFetching.value = false; } -function onMessage(message: Misskey.entities.ChatMessage) { +function onMessage(message: Misskey.entities.ChatMessageLite) { sound.playMisskeySfx('chatMessage'); messages.value.unshift(normalizeMessage(message)); @@ -253,34 +257,34 @@ function onMessage(message: Misskey.entities.ChatMessage) { } } -function onDeleted(id) { +function onDeleted(id: string) { const index = messages.value.findIndex(m => m.id === id); if (index !== -1) { messages.value.splice(index, 1); } } -function onReact(ctx) { +function onReact(ctx: Parameters<Misskey.Channels['chatUser']['events']['react']>[0] | Parameters<Misskey.Channels['chatRoom']['events']['react']>[0]) { const message = messages.value.find(m => m.id === ctx.messageId); if (message) { if (room.value == null) { // 1on1の時はuserは省略される message.reactions.push({ reaction: ctx.reaction, - user: message.fromUserId === $i.id ? user : $i, + user: message.fromUserId === $i.id ? user.value! : $i, }); } else { message.reactions.push({ reaction: ctx.reaction, - user: ctx.user, + user: ctx.user!, }); } } } -function onUnreact(ctx) { +function onUnreact(ctx: Parameters<Misskey.Channels['chatUser']['events']['unreact']>[0] | Parameters<Misskey.Channels['chatRoom']['events']['unreact']>[0]) { const message = messages.value.find(m => m.id === ctx.messageId); if (message) { - const index = message.reactions.findIndex(r => r.reaction === ctx.reaction && r.user.id === ctx.user.id); + const index = message.reactions.findIndex(r => r.reaction === ctx.reaction && r.user.id === ctx.user!.id); if (index !== -1) { message.reactions.splice(index, 1); } @@ -310,14 +314,18 @@ onBeforeUnmount(() => { }); async function inviteUser() { + if (room.value == null) return; + const invitee = await os.selectUser({ includeSelf: false, localOnly: true }); os.apiWithDialog('chat/rooms/invitations/create', { - roomId: room.value?.id, + roomId: room.value.id, userId: invitee.id, }); } async function leaveRoom() { + if (room.value == null) return; + const { canceled } = await os.confirm({ type: 'warning', text: i18n.ts.areYouSure, @@ -325,7 +333,7 @@ async function leaveRoom() { if (canceled) return; misskeyApi('chat/rooms/leave', { - roomId: room.value?.id, + roomId: room.value.id, }); router.push('/chat'); } @@ -384,19 +392,36 @@ const headerTabs = computed(() => room.value ? [{ icon: 'ti ti-search', }]); -const headerActions = computed(() => [{ +const headerActions = computed<PageHeaderItem[]>(() => [{ icon: 'ti ti-dots', + text: '', handler: showMenu, }]); -definePage(computed(() => !initializing.value ? user.value ? { - userName: user, - title: user.value.name ?? user.value.username, - avatar: user, -} : { - title: room.value?.name, - icon: 'ti ti-users', -} : null)); +definePage(computed(() => { + if (!initializing.value) { + if (user.value) { + return { + userName: user.value, + title: user.value.name ?? user.value.username, + avatar: user.value, + }; + } else if (room.value) { + return { + title: room.value.name, + icon: 'ti ti-users', + }; + } else { + return { + title: i18n.ts.chat, + }; + } + } else { + return { + title: i18n.ts.chat, + }; + } +})); </script> <style lang="scss" module> diff --git a/packages/frontend/src/use/use-mutation-observer.ts b/packages/frontend/src/use/use-mutation-observer.ts index b35dbcd7a8..7b774022dc 100644 --- a/packages/frontend/src/use/use-mutation-observer.ts +++ b/packages/frontend/src/use/use-mutation-observer.ts @@ -4,9 +4,9 @@ */ import { onUnmounted, watch } from 'vue'; -import type { Ref, ShallowRef } from 'vue'; +import type { Ref } from 'vue'; -export function useMutationObserver(targetNodeRef: Ref<HTMLElement | undefined>, options: MutationObserverInit, callback: MutationCallback): void { +export function useMutationObserver(targetNodeRef: Ref<HTMLElement | null | undefined>, options: MutationObserverInit, callback: MutationCallback): void { const observer = new MutationObserver(callback); watch(targetNodeRef, (targetNode) => { diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index cc397e2270..f54fc9de1e 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -813,6 +813,54 @@ export type Channels = { claimTimeIsUp: null | Record<string, never>; }; }; + chatUser: { + params: { + otherId: string; + }; + events: { + message: (payload: ChatMessageLite) => void; + deleted: (payload: ChatMessageLite['id']) => void; + react: (payload: { + reaction: string; + user?: UserLite; + messageId: ChatMessageLite['id']; + }) => void; + unreact: (payload: { + reaction: string; + user?: UserLite; + messageId: ChatMessageLite['id']; + }) => void; + }; + receives: { + read: { + id: ChatMessageLite['id']; + }; + }; + }; + chatRoom: { + params: { + roomId: string; + }; + events: { + message: (payload: ChatMessageLite) => void; + deleted: (payload: ChatMessageLite['id']) => void; + react: (payload: { + reaction: string; + user?: UserLite; + messageId: ChatMessageLite['id']; + }) => void; + unreact: (payload: { + reaction: string; + user?: UserLite; + messageId: ChatMessageLite['id']; + }) => void; + }; + receives: { + read: { + id: ChatMessageLite['id']; + }; + }; + }; }; // @public (undocumented) @@ -960,6 +1008,12 @@ type ChatMessage = components['schemas']['ChatMessage']; type ChatMessageLite = components['schemas']['ChatMessageLite']; // @public (undocumented) +type ChatMessageLiteFor1on1 = components['schemas']['ChatMessageLiteFor1on1']; + +// @public (undocumented) +type ChatMessageLiteForRoom = components['schemas']['ChatMessageLiteForRoom']; + +// @public (undocumented) type ChatMessagesCreateToRoomRequest = operations['chat___messages___create-to-room']['requestBody']['content']['application/json']; // @public (undocumented) @@ -2086,6 +2140,8 @@ declare namespace entities { AbuseReportNotificationRecipient, ChatMessage, ChatMessageLite, + ChatMessageLiteFor1on1, + ChatMessageLiteForRoom, ChatRoom, ChatRoomInvitation, ChatRoomMembership @@ -3655,8 +3711,8 @@ type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['respons // // src/entities.ts:50:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts -// src/streaming.types.ts:217:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts -// src/streaming.types.ts:227:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts +// src/streaming.types.ts:218:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts +// src/streaming.types.ts:228:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 0ff9749602..15c3ee7e55 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -56,6 +56,8 @@ export type SystemWebhook = components['schemas']['SystemWebhook']; export type AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient']; export type ChatMessage = components['schemas']['ChatMessage']; export type ChatMessageLite = components['schemas']['ChatMessageLite']; +export type ChatMessageLiteFor1on1 = components['schemas']['ChatMessageLiteFor1on1']; +export type ChatMessageLiteForRoom = components['schemas']['ChatMessageLiteForRoom']; export type ChatRoom = components['schemas']['ChatRoom']; export type ChatRoomInvitation = components['schemas']['ChatRoomInvitation']; export type ChatRoomMembership = components['schemas']['ChatRoomMembership']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 037b09660c..0d54c2d4ab 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -5406,10 +5406,10 @@ export type components = { fileId?: string | null; file?: components['schemas']['DriveFile'] | null; isRead?: boolean; - reactions: ({ + reactions: { reaction: string; - user?: components['schemas']['UserLite'] | null; - })[]; + user: components['schemas']['UserLite']; + }[]; }; ChatMessageLite: { id: string; @@ -5427,6 +5427,34 @@ export type components = { user?: components['schemas']['UserLite'] | null; })[]; }; + ChatMessageLiteFor1on1: { + id: string; + /** Format: date-time */ + createdAt: string; + fromUserId: string; + toUserId: string; + text?: string | null; + fileId?: string | null; + file?: components['schemas']['DriveFile'] | null; + reactions: { + reaction: string; + }[]; + }; + ChatMessageLiteForRoom: { + id: string; + /** Format: date-time */ + createdAt: string; + fromUserId: string; + fromUser: components['schemas']['UserLite']; + toRoomId: string; + text?: string | null; + fileId?: string | null; + file?: components['schemas']['DriveFile'] | null; + reactions: { + reaction: string; + user: components['schemas']['UserLite']; + }[]; + }; ChatRoom: { id: string; /** Format: date-time */ @@ -14067,7 +14095,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['ChatMessageLite']; + 'application/json': components['schemas']['ChatMessageLiteForRoom']; }; }; /** @description Client error */ @@ -14130,7 +14158,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['ChatMessageLite']; + 'application/json': components['schemas']['ChatMessageLiteFor1on1']; }; }; /** @description Client error */ @@ -14305,7 +14333,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['ChatMessageLite'][]; + 'application/json': components['schemas']['ChatMessageLiteForRoom'][]; }; }; /** @description Client error */ @@ -14533,7 +14561,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['ChatMessageLite'][]; + 'application/json': components['schemas']['ChatMessageLiteFor1on1'][]; }; }; /** @description Client error */ diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts index 8e5438eeb6..10204fb2c9 100644 --- a/packages/misskey-js/src/streaming.types.ts +++ b/packages/misskey-js/src/streaming.types.ts @@ -1,6 +1,7 @@ import { Antenna, ChatMessage, + ChatMessageLite, DriveFile, DriveFolder, Note, @@ -227,7 +228,55 @@ export type Channels = { updateSettings: ReversiUpdateSettings<ReversiUpdateKey>; claimTimeIsUp: null | Record<string, never>; } - } + }; + chatUser: { + params: { + otherId: string; + }; + events: { + message: (payload: ChatMessageLite) => void; + deleted: (payload: ChatMessageLite['id']) => void; + react: (payload: { + reaction: string; + user?: UserLite; + messageId: ChatMessageLite['id']; + }) => void; + unreact: (payload: { + reaction: string; + user?: UserLite; + messageId: ChatMessageLite['id']; + }) => void; + }; + receives: { + read: { + id: ChatMessageLite['id']; + }; + }; + }; + chatRoom: { + params: { + roomId: string; + }; + events: { + message: (payload: ChatMessageLite) => void; + deleted: (payload: ChatMessageLite['id']) => void; + react: (payload: { + reaction: string; + user?: UserLite; + messageId: ChatMessageLite['id']; + }) => void; + unreact: (payload: { + reaction: string; + user?: UserLite; + messageId: ChatMessageLite['id']; + }) => void; + }; + receives: { + read: { + id: ChatMessageLite['id']; + }; + }; + }; }; export type NoteUpdatedEvent = { id: Note['id'] } & ({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46416c0332..e1d60e0f05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4564,8 +4564,8 @@ packages: '@types/pg@8.6.1': resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} - '@types/prop-types@15.7.5': - resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + '@types/prop-types@15.7.14': + resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} '@types/pug@2.0.10': resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} @@ -4603,8 +4603,8 @@ packages: '@types/sanitize-html@2.13.0': resolution: {integrity: sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==} - '@types/scheduler@0.16.2': - resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} + '@types/scheduler@0.23.0': + resolution: {integrity: sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==} '@types/seedrandom@2.4.34': resolution: {integrity: sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==} @@ -8479,6 +8479,10 @@ packages: resolution: {integrity: sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==} engines: {node: '>=10'} + node-abi@3.74.0: + resolution: {integrity: sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==} + engines: {node: '>=10'} + node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} @@ -9350,6 +9354,9 @@ packages: pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} @@ -9473,6 +9480,10 @@ packages: resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} engines: {node: '>= 6'} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + readable-stream@4.3.0: resolution: {integrity: sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -9738,6 +9749,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + send@0.19.0: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} @@ -14515,7 +14531,7 @@ snapshots: '@stylistic/eslint-plugin@2.13.0(eslint@9.22.0)(typescript@5.8.2)': dependencies: - '@typescript-eslint/utils': 8.27.0(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/utils': 8.29.0(eslint@9.22.0)(typescript@5.8.2) eslint: 9.22.0 eslint-visitor-keys: 4.2.0 espree: 10.3.0 @@ -14991,7 +15007,7 @@ snapshots: pg-protocol: 1.7.1 pg-types: 2.2.0 - '@types/prop-types@15.7.5': {} + '@types/prop-types@15.7.14': {} '@types/pug@2.0.10': {} @@ -15011,8 +15027,8 @@ snapshots: '@types/react@18.0.28': dependencies: - '@types/prop-types': 15.7.5 - '@types/scheduler': 0.16.2 + '@types/prop-types': 15.7.14 + '@types/scheduler': 0.23.0 csstype: 3.1.3 '@types/readdir-glob@1.1.1': @@ -15027,7 +15043,7 @@ snapshots: dependencies: htmlparser2: 8.0.1 - '@types/scheduler@0.16.2': {} + '@types/scheduler@0.23.0': {} '@types/seedrandom@2.4.34': {} @@ -16071,7 +16087,7 @@ snapshots: dependencies: buffer: 5.7.1 inherits: 2.0.4 - readable-stream: 3.6.0 + readable-stream: 3.6.2 optional: true blob-util@2.0.2: {} @@ -20143,6 +20159,11 @@ snapshots: dependencies: semver: 7.6.3 + node-abi@3.74.0: + dependencies: + semver: 7.7.1 + optional: true + node-abort-controller@3.1.1: {} node-addon-api@3.2.1: @@ -20841,8 +20862,8 @@ snapshots: minimist: 1.2.8 mkdirp-classic: 0.5.3 napi-build-utils: 2.0.0 - node-abi: 3.62.0 - pump: 3.0.0 + node-abi: 3.74.0 + pump: 3.0.2 rc: 1.2.8 simple-get: 4.0.1 tar-fs: 2.1.2 @@ -21020,6 +21041,12 @@ snapshots: end-of-stream: 1.4.4 once: 1.4.0 + pump@3.0.2: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + optional: true + punycode.js@2.3.1: {} punycode@2.3.1: {} @@ -21161,6 +21188,13 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + optional: true + readable-stream@4.3.0: dependencies: abort-controller: 3.0.0 @@ -21460,6 +21494,9 @@ snapshots: semver@7.6.3: {} + semver@7.7.1: + optional: true + send@0.19.0: dependencies: debug: 2.6.9 @@ -22036,7 +22073,7 @@ snapshots: dependencies: chownr: 1.1.4 mkdirp-classic: 0.5.3 - pump: 3.0.0 + pump: 3.0.2 tar-stream: 2.2.0 optional: true @@ -22046,7 +22083,7 @@ snapshots: end-of-stream: 1.4.4 fs-constants: 1.0.0 inherits: 2.0.4 - readable-stream: 3.6.0 + readable-stream: 3.6.2 optional: true tar-stream@3.1.6: |