1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-27 09:34:39 +02:00

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>
This commit is contained in:
かっこかり
2025-04-03 15:28:10 +09:00
committed by GitHub
parent 7cecaa5c54
commit e07bb1dcbc
29 changed files with 453 additions and 153 deletions

View File

@@ -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>