diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2025-04-03 15:28:10 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-04-03 15:28:10 +0900 |
| commit | e07bb1dcbce6eaa2bfe157a6c9f1361dbf9aa280 (patch) | |
| tree | 1d9f9e6d6450caa717e83ca8fc0eadf09b413129 /packages/frontend | |
| parent | perf(frontend): avoid main thread scroll repaint (diff) | |
| download | sharkey-e07bb1dcbce6eaa2bfe157a6c9f1361dbf9aa280.tar.gz sharkey-e07bb1dcbce6eaa2bfe157a6c9f1361dbf9aa280.tar.bz2 sharkey-e07bb1dcbce6eaa2bfe157a6c9f1361dbf9aa280.zip | |
fix: チャット周りの修正 (#15741)
* fix(misskey-js): チャットのChannel型定義を追加
* fix(backend); canChatで塞いでいない書き込み系のAPIを塞ぐ
* fix(frontend): チャット周りのフロントエンド型修正
* lint fix
* fix broken lockfile
* fix
* refactor
* wip
* wip
* wip
* clean up
---------
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Diffstat (limited to 'packages/frontend')
| -rw-r--r-- | packages/frontend/src/pages/chat/XMessage.vue | 62 | ||||
| -rw-r--r-- | packages/frontend/src/pages/chat/home.home.vue | 4 | ||||
| -rw-r--r-- | packages/frontend/src/pages/chat/home.invitations.vue | 9 | ||||
| -rw-r--r-- | packages/frontend/src/pages/chat/home.joiningRooms.vue | 15 | ||||
| -rw-r--r-- | packages/frontend/src/pages/chat/home.ownedRooms.vue | 10 | ||||
| -rw-r--r-- | packages/frontend/src/pages/chat/home.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/pages/chat/message.vue | 10 | ||||
| -rw-r--r-- | packages/frontend/src/pages/chat/room.form.vue | 21 | ||||
| -rw-r--r-- | packages/frontend/src/pages/chat/room.info.vue | 5 | ||||
| -rw-r--r-- | packages/frontend/src/pages/chat/room.members.vue | 5 | ||||
| -rw-r--r-- | packages/frontend/src/pages/chat/room.search.vue | 3 | ||||
| -rw-r--r-- | packages/frontend/src/pages/chat/room.vue | 97 | ||||
| -rw-r--r-- | packages/frontend/src/use/use-mutation-observer.ts | 4 |
13 files changed, 130 insertions, 117 deletions
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) => { |