From 6b5cf2e2293484a7675ec5ce1f18a49d60f3204b Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 18 Mar 2025 10:27:30 +0900 Subject: [PATCH] wip --- locales/index.d.ts | 17 + locales/ja-JP.yml | 4 + packages/backend/src/core/ChatService.ts | 13 +- packages/backend/src/di-symbols.ts | 2 + packages/backend/src/misc/json-schema.ts | 2 + .../backend/src/models/ChatRoomMembership.ts | 1 + .../backend/src/models/RepositoryModule.ts | 27 ++ .../src/models/json-schema/chat-message.ts | 15 +- .../src/models/json-schema/chat-room.ts | 32 ++ packages/backend/src/server/ServerModule.ts | 2 + .../backend/src/server/api/endpoint-list.ts | 2 +- .../endpoints/chat/{messages => }/history.ts | 0 .../api/endpoints/chat/messages/create.ts | 12 +- .../api/endpoints/chat/messages/timeline.ts | 6 +- .../frontend/src/components/MkFukidashi.vue | 7 + packages/frontend/src/components/MkMenu.vue | 66 +++- packages/frontend/src/local-storage.ts | 2 +- packages/frontend/src/navbar.ts | 7 +- packages/frontend/src/pages/chat/home.vue | 105 ++++-- .../frontend/src/pages/chat/room.form.vue | 354 ++++++++++++++++++ .../frontend/src/pages/chat/room.message.vue | 118 ++++++ packages/frontend/src/pages/chat/room.vue | 292 +++++++++++++++ packages/frontend/src/router/definition.ts | 6 + packages/frontend/src/types/menu.ts | 16 +- packages/frontend/src/utility/upload.ts | 2 +- packages/misskey-js/etc/misskey-js.api.md | 22 +- .../misskey-js/src/autogen/apiClientJSDoc.ts | 8 +- packages/misskey-js/src/autogen/endpoint.ts | 6 +- packages/misskey-js/src/autogen/entities.ts | 4 +- packages/misskey-js/src/autogen/models.ts | 1 + packages/misskey-js/src/autogen/types.ts | 144 +++---- 31 files changed, 1136 insertions(+), 159 deletions(-) create mode 100644 packages/backend/src/models/json-schema/chat-room.ts rename packages/backend/src/server/api/endpoints/chat/{messages => }/history.ts (100%) create mode 100644 packages/frontend/src/pages/chat/room.form.vue create mode 100644 packages/frontend/src/pages/chat/room.message.vue create mode 100644 packages/frontend/src/pages/chat/room.vue diff --git a/locales/index.d.ts b/locales/index.d.ts index dbfff95d15..73d1322cac 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5354,6 +5354,23 @@ export interface Locale extends ILocale { * チャット */ "chat": string; + /** + * 個人チャット + */ + "individualChat": string; + /** + * 特定ユーザーとの一対一のチャットができます。 + */ + "individualChat_description": string; + /** + * ルームチャット + */ + "roomChat": string; + /** + * 複数人でのチャットができます。 + * また、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。 + */ + "roomChat_description": string; "_emojiPalette": { /** * パレット diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 699798b552..138fa6e279 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1334,6 +1334,10 @@ emojiPalette: "絵文字パレット" postForm: "投稿フォーム" textCount: "文字数" chat: "チャット" +individualChat: "個人チャット" +individualChat_description: "特定ユーザーとの一対一のチャットができます。" +roomChat: "ルームチャット" +roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。" _emojiPalette: palettes: "パレット" diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts index 33b15795af..8289f5252a 100644 --- a/packages/backend/src/core/ChatService.ts +++ b/packages/backend/src/core/ChatService.ts @@ -186,8 +186,8 @@ export class ChatService { const activity = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), fromUser)); this.queueService.deliver(fromUser, activity, toUser.inbox); } - }/* else if (message.roomId) { - this.globalEventService.publishRoomChatStream(message.roomId, 'deleted', message.id); + }/* else if (message.toRoomId) { + this.globalEventService.publishRoomChatStream(message.toRoomId, 'deleted', message.id); }*/ } @@ -245,7 +245,7 @@ export class ChatService { } else { // そのグループにおいて未読がなければイベント発行 const unreadExist = await this.chatMessagesRepository.createQueryBuilder('message') - .where('message.roomId = :roomId', { roomId: roomId }) + .where('message.toRoomId = :roomId', { roomId: roomId }) .andWhere('message.userId != :userId', { userId: userId }) .andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) .andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // 自分が加入する前の会話については、未読扱いしない @@ -300,7 +300,7 @@ export class ChatService { .where('message.fromUserId = :meId', { meId: meId }) .orWhere('message.toUserId = :meId', { meId: meId }); })) - .andWhere('message.roomId IS NULL') + .andWhere('message.toRoomId IS NULL') .andWhere(`message.fromUserId NOT IN (${ mutingQuery.getQuery() })`) .andWhere(`message.toUserId NOT IN (${ mutingQuery.getQuery() })`); @@ -325,6 +325,7 @@ export class ChatService { @bindThis public async roomHistory(meId: MiUser['id'], limit: number): Promise { + return []; /* const rooms = await this.userRoomJoiningsRepository.findBy({ userId: meId, @@ -341,10 +342,10 @@ export class ChatService { const query = this.chatMessagesRepository.createQueryBuilder('message') .orderBy('message.id', 'DESC') - .where('message.roomId IN (:...rooms)', { rooms: rooms }); + .where('message.toRoomId IN (:...rooms)', { rooms: rooms }); if (found.length > 0) { - query.andWhere('message.roomId NOT IN (:...found)', { found: found }); + query.andWhere('message.toRoomId NOT IN (:...found)', { found: found }); } const message = await query.getOne(); diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 6f441ebb7b..d07cb23d47 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -84,6 +84,8 @@ export const DI = { flashLikesRepository: Symbol('flashLikesRepository'), userMemosRepository: Symbol('userMemosRepository'), chatMessagesRepository: Symbol('chatMessagesRepository'), + chatRoomsRepository: Symbol('chatRoomsRepository'), + chatRoomMembershipsRepository: Symbol('chatRoomMembershipsRepository'), bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'), reversiGamesRepository: Symbol('reversiGamesRepository'), //#endregion diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 3264d46db1..541badd46c 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -64,6 +64,7 @@ import { 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 { packedChatRoomSchema } from '@/models/json-schema/chat-room.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -123,6 +124,7 @@ export const refs = { AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema, ChatMessage: packedChatMessageSchema, ChatMessageLite: packedChatMessageLiteSchema, + ChatRoom: packedChatRoomSchema, }; export type Packed = SchemaType; diff --git a/packages/backend/src/models/ChatRoomMembership.ts b/packages/backend/src/models/ChatRoomMembership.ts index c527f5d795..1756dccd4b 100644 --- a/packages/backend/src/models/ChatRoomMembership.ts +++ b/packages/backend/src/models/ChatRoomMembership.ts @@ -9,6 +9,7 @@ import { MiUser } from './User.js'; import { MiChatRoom } from './ChatRoom.js'; @Entity('chat_room_membership') +@Index(['userId', 'roomId'], { unique: true }) export class MiChatRoomMembership { @PrimaryColumn(id()) public id: string; diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 04a9df6cfb..15aa620b37 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -78,6 +78,9 @@ import { MiUserPublickey, MiUserSecurityKey, MiWebhook, + MiChatMessage, + MiChatRoom, + MiChatRoomMembership, } from './_.js'; import type { Provider } from '@nestjs/common'; import type { DataSource } from 'typeorm'; @@ -490,6 +493,24 @@ const $userMemosRepository: Provider = { inject: [DI.db], }; +const $chatMessagesRepository: Provider = { + provide: DI.chatMessagesRepository, + useFactory: (db: DataSource) => db.getRepository(MiChatMessage).extend(miRepository as MiRepository), + inject: [DI.db], +}; + +const $chatRoomsRepository: Provider = { + provide: DI.chatRoomsRepository, + useFactory: (db: DataSource) => db.getRepository(MiChatRoom).extend(miRepository as MiRepository), + inject: [DI.db], +}; + +const $chatRoomMembershipsRepository: Provider = { + provide: DI.chatRoomMembershipsRepository, + useFactory: (db: DataSource) => db.getRepository(MiChatRoomMembership).extend(miRepository as MiRepository), + inject: [DI.db], +}; + const $bubbleGameRecordsRepository: Provider = { provide: DI.bubbleGameRecordsRepository, useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository), @@ -573,6 +594,9 @@ const $reversiGamesRepository: Provider = { $flashsRepository, $flashLikesRepository, $userMemosRepository, + $chatMessagesRepository, + $chatRoomsRepository, + $chatRoomMembershipsRepository, $bubbleGameRecordsRepository, $reversiGamesRepository, ], @@ -645,6 +669,9 @@ const $reversiGamesRepository: Provider = { $flashsRepository, $flashLikesRepository, $userMemosRepository, + $chatMessagesRepository, + $chatRoomsRepository, + $chatRoomMembershipsRepository, $bubbleGameRecordsRepository, $reversiGamesRepository, ], diff --git a/packages/backend/src/models/json-schema/chat-message.ts b/packages/backend/src/models/json-schema/chat-message.ts index f3907a3677..6153307c28 100644 --- a/packages/backend/src/models/json-schema/chat-message.ts +++ b/packages/backend/src/models/json-schema/chat-message.ts @@ -21,7 +21,7 @@ export const packedChatMessageSchema = { }, fromUser: { type: 'object', - optional: true, nullable: false, + optional: false, nullable: false, ref: 'UserLite', }, toUserId: { @@ -33,6 +33,15 @@ export const packedChatMessageSchema = { optional: true, nullable: true, ref: 'UserLite', }, + toRoomId: { + type: 'string', + optional: true, nullable: true, + }, + toRoom: { + type: 'object', + optional: true, nullable: true, + ref: 'ChatRoom', + }, text: { type: 'string', optional: true, nullable: true, @@ -73,6 +82,10 @@ export const packedChatMessageLiteSchema = { type: 'string', optional: true, nullable: true, }, + toRoomId: { + type: 'string', + optional: true, nullable: true, + }, text: { type: 'string', optional: true, nullable: true, diff --git a/packages/backend/src/models/json-schema/chat-room.ts b/packages/backend/src/models/json-schema/chat-room.ts new file mode 100644 index 0000000000..e17adffe6e --- /dev/null +++ b/packages/backend/src/models/json-schema/chat-room.ts @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedChatRoomSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + createdAt: { + type: 'string', + format: 'date-time', + optional: false, nullable: false, + }, + ownerId: { + type: 'string', + optional: false, nullable: false, + }, + owner: { + type: 'object', + optional: false, nullable: false, + ref: 'UserLite', + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index 3ab0b815f2..11998f8d49 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -44,6 +44,7 @@ import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js'; import { ServerStatsChannelService } from './api/stream/channels/server-stats.js'; import { UserListChannelService } from './api/stream/channels/user-list.js'; import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js'; +import { ChatChannelService } from './api/stream/channels/chat.js'; import { ReversiChannelService } from './api/stream/channels/reversi.js'; import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js'; import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js'; @@ -84,6 +85,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j GlobalTimelineChannelService, HashtagChannelService, RoleTimelineChannelService, + ChatChannelService, ReversiChannelService, ReversiGameChannelService, HomeTimelineChannelService, diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index 4d99bcc50c..34460dedb7 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -399,5 +399,5 @@ export * as 'users/show' from './endpoints/users/show.js'; export * as 'users/update-memo' from './endpoints/users/update-memo.js'; export * as 'chat/messages/create' from './endpoints/chat/messages/create.js'; export * as 'chat/messages/timeline' from './endpoints/chat/messages/timeline.js'; -export * as 'chat/messages/history' from './endpoints/chat/messages/history.js'; +export * as 'chat/history' from './endpoints/chat/history.js'; export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js'; diff --git a/packages/backend/src/server/api/endpoints/chat/messages/history.ts b/packages/backend/src/server/api/endpoints/chat/history.ts similarity index 100% rename from packages/backend/src/server/api/endpoints/chat/messages/history.ts rename to packages/backend/src/server/api/endpoints/chat/history.ts diff --git a/packages/backend/src/server/api/endpoints/chat/messages/create.ts b/packages/backend/src/server/api/endpoints/chat/messages/create.ts index f98f2991e5..7e91eb8121 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/create.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/create.ts @@ -82,7 +82,7 @@ export const paramDef = { properties: { text: { type: 'string', nullable: true, maxLength: 2000 }, fileId: { type: 'string', format: 'misskey:id' }, - userId: { type: 'string', format: 'misskey:id', nullable: true }, + toUserId: { type: 'string', format: 'misskey:id', nullable: true }, }, } as const; @@ -113,13 +113,13 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.contentRequired); } - if (ps.userId != null) { + if (ps.toUserId != null) { // Myself - if (ps.userId === me.id) { + if (ps.toUserId === me.id) { throw new ApiError(meta.errors.recipientIsYourself); } - const toUser = await this.getterService.getUser(ps.userId).catch(err => { + const toUser = await this.getterService.getUser(ps.toUserId).catch(err => { if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); throw err; }); @@ -130,9 +130,9 @@ export default class extends Endpoint { // eslint- text: ps.text, file: file, }); - }/* else if (ps.roomId != null) { + }/* else if (ps.toRoomId != null) { // Fetch recipient (room) - recipientRoom = await this.userRoomsRepository.findOneBy({ id: ps.roomId! }); + recipientRoom = await this.userRoomsRepository.findOneBy({ id: ps.toRoomId! }); if (recipientRoom == null) { throw new ApiError(meta.errors.noSuchRoom); diff --git a/packages/backend/src/server/api/endpoints/chat/messages/timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/timeline.ts index bd8f347d01..3e22025b5a 100644 --- a/packages/backend/src/server/api/endpoints/chat/messages/timeline.ts +++ b/packages/backend/src/server/api/endpoints/chat/messages/timeline.ts @@ -94,17 +94,17 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.roomAccessDenied); } - const query = this.queryService.makePaginationQuery(this.messagingMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId) + const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId) .andWhere('message.roomId = :roomId', { roomId: recipientRoom.id }); const messages = await query.take(ps.limit).getMany(); // Mark all as read if (ps.markAsRead) { - this.messagingService.readRoomMessagingMessage(me.id, recipientRoom.id, messages.map(x => x.id)); + this.chatService.readRoomMessagingMessage(me.id, recipientRoom.id, messages.map(x => x.id)); } - return await Promise.all(messages.map(message => this.messagingMessageEntityService.pack(message, me, { + return await Promise.all(messages.map(message => this.chatMessageEntityService.pack(message, me, { populateRoom: false, }))); }*/ diff --git a/packages/frontend/src/components/MkFukidashi.vue b/packages/frontend/src/components/MkFukidashi.vue index 8b1c56fca4..bf3848635b 100644 --- a/packages/frontend/src/components/MkFukidashi.vue +++ b/packages/frontend/src/components/MkFukidashi.vue @@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only tail === 'left' ? $style.left : $style.right, negativeMargin === true && $style.negativeMargin, shadow === true && $style.shadow, + accented === true && $style.accented ]" >
@@ -30,10 +31,12 @@ withDefaults(defineProps<{ tail?: 'left' | 'right' | 'none'; negativeMargin?: boolean; shadow?: boolean; + accented?: boolean; }>(), { tail: 'right', negativeMargin: false, shadow: false, + accented: false, }); @@ -47,6 +50,10 @@ withDefaults(defineProps<{ min-height: calc(var(--fukidashi-radius) * 2); padding-top: calc(var(--fukidashi-radius) * .13); + &.accented { + --fukidashi-bg: var(--MI_THEME-accent); + } + &.shadow { filter: drop-shadow(0 4px 32px var(--MI_THEME-shadow)); } diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index e3c27c5f6e..f7b46e3aab 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.center]: align === 'center', [$style.big]: big, [$style.asDrawer]: asDrawer, + [$style.widthSpecified]: width != null, }" @focusin.passive.stop="() => {}" > @@ -29,15 +30,19 @@ SPDX-License-Identifier: AGPL-3.0-only > + {{ i18n.ts.none }} @@ -438,6 +473,12 @@ onBeforeUnmount(() => { } } + &:not(.widthSpecified) { + > .menu { + max-width: 400px; + } + } + &.big:not(.asDrawer) { > .menu { min-width: 230px; @@ -607,10 +648,19 @@ onBeforeUnmount(() => { .item_content_text { max-width: calc(100vw - 4rem); +} + +.item_content_text_title { text-overflow: ellipsis; overflow: hidden; } +.item_content_text_caption { + text-wrap: auto; + font-size: 85%; + opacity: 0.7; +} + .switchButton { margin-left: -2px; --height: 1.35em; diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts index 099339fbee..f6d6bbf0fb 100644 --- a/packages/frontend/src/local-storage.ts +++ b/packages/frontend/src/local-storage.ts @@ -28,7 +28,7 @@ export type Keys = ( 'theme' | 'themeId' | 'customCss' | - 'message_drafts' | + 'chatMessageDrafts' | 'scratchpad' | 'debug' | 'preferences' | diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts index c0a6a370fc..7e82eee3b8 100644 --- a/packages/frontend/src/navbar.ts +++ b/packages/frontend/src/navbar.ts @@ -4,6 +4,7 @@ */ import { computed, reactive } from 'vue'; +import { ui } from '@@/js/config.js'; import { clearCache } from './utility/clear-cache.js'; import { $i } from '@/i.js'; import { miLocalStorage } from '@/local-storage.js'; @@ -11,7 +12,6 @@ import { openInstanceMenu, openToolsMenu } from '@/ui/_common_/common.js'; import { lookup } from '@/utility/lookup.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { ui } from '@@/js/config.js'; import { unisonReload } from '@/utility/unison-reload.js'; export const navbarItemDef = reactive({ @@ -110,6 +110,11 @@ export const navbarItemDef = reactive({ icon: 'ti ti-device-tv', to: '/channels', }, + chat: { + title: i18n.ts.chat, + icon: 'ti ti-message', + to: '/chat', + }, achievements: { title: i18n.ts.achievements, icon: 'ti ti-medal', diff --git a/packages/frontend/src/pages/chat/home.vue b/packages/frontend/src/pages/chat/home.vue index 6336470a56..95990fbbe0 100644 --- a/packages/frontend/src/pages/chat/home.vue +++ b/packages/frontend/src/pages/chat/home.vue @@ -16,27 +16,24 @@ SPDX-License-Identifier: AGPL-3.0-only :key="item.id" :class="[$style.message, { [$style.isMe]: item.isMe, [$style.isRead]: item.message.isRead }]" class="_panel" - :to="item.message.roomId ? `/chat/room/${item.message.roomId}` : `/chat/user/${item.other.id}`" + :to="item.message.toRoomId ? `/chat/room/${item.message.toRoomId}` : `/chat/user/${item.other!.id}`" > -
- -
- {{ item.message.room.name }} - -
-
- - @{{ acct(item.other) }} - -
-
-

{{ i18n.ts.you }}:{{ item.message.text }}

-
+ +
+ {{ item.message.room.name }} + +
+
+ + + +
+
+

{{ i18n.ts.you }}:{{ item.message.text }}

-
{{ i18n.ts.noHistory }}
@@ -51,12 +48,15 @@ import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; -import { infoImageUrl } from '@/instance.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { ensureSignin } from '@/i.js'; +import { useRouter } from '@/router/supplier.js'; +import * as os from '@/os.js'; const $i = ensureSignin(); +const router = useRouter(); + const fetching = ref(true); const history = ref<{ id: string; @@ -65,15 +65,58 @@ const history = ref<{ isMe: boolean; }[]>([]); +function start(ev: MouseEvent) { + os.popupMenu([{ + text: i18n.ts.individualChat, + caption: i18n.ts.individualChat_description, + icon: 'ti ti-user', + action: () => { startUser(); }, + }, { + text: i18n.ts.roomChat, + caption: i18n.ts.roomChat_description, + icon: 'ti ti-users', + action: () => { startRoom(); }, + }], ev.currentTarget ?? ev.target); +} + +async function startUser() { + os.selectUser().then(user => { + router.push(`/chat/user/${user.id}`); + }); +} + +async function startRoom() { + /* + const rooms1 = await os.api('users/rooms/owned'); + const rooms2 = await os.api('users/rooms/joined'); + if (rooms1.length === 0 && rooms2.length === 0) { + os.alert({ + type: 'warning', + title: i18n.ts.youHaveNoGroups, + text: i18n.ts.joinOrCreateGroup, + }); + return; + } + const { canceled, result: room } = await os.select({ + title: i18n.ts.room, + items: rooms1.concat(rooms2).map(room => ({ + value: room, text: room.name, + })), + }); + if (canceled) return; + router.push(`/chat/room/${room.id}`); + */ +} + async function fetchHistory() { fetching.value = true; - const [userMessages, groupMessages] = await Promise.all([ - misskeyApi('messaging/history', { group: false }), - misskeyApi('messaging/history', { group: true }), + const [userMessages, roomMessages] = await Promise.all([ + misskeyApi('chat/history', { room: false }), + misskeyApi('chat/history', { room: true }), ]); - history.value = [...userMessages, ...groupMessages] + history.value = [...userMessages, ...roomMessages] .toSorted((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) .map(m => ({ id: m.id, @@ -100,23 +143,7 @@ definePage(() => ({ diff --git a/packages/frontend/src/pages/chat/room.form.vue b/packages/frontend/src/pages/chat/room.form.vue new file mode 100644 index 0000000000..7132924125 --- /dev/null +++ b/packages/frontend/src/pages/chat/room.form.vue @@ -0,0 +1,354 @@ + + + + + diff --git a/packages/frontend/src/pages/chat/room.message.vue b/packages/frontend/src/pages/chat/room.message.vue new file mode 100644 index 0000000000..29742715d3 --- /dev/null +++ b/packages/frontend/src/pages/chat/room.message.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/packages/frontend/src/pages/chat/room.vue b/packages/frontend/src/pages/chat/room.vue new file mode 100644 index 0000000000..8baf11c950 --- /dev/null +++ b/packages/frontend/src/pages/chat/room.vue @@ -0,0 +1,292 @@ + + + + + + + diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index 9a81032cc8..b2e93c9f31 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -41,6 +41,12 @@ const routes: RouteDef[] = [{ }, { path: '/clips/:clipId', component: page(() => import('@/pages/clip.vue')), +}, { + path: '/chat', + component: page(() => import('@/pages/chat/home.vue')), +}, { + path: '/chat/user/:userId', + component: page(() => import('@/pages/chat/room.vue')), }, { path: '/instance-info/:host', component: page(() => import('@/pages/instance-info.vue')), diff --git a/packages/frontend/src/types/menu.ts b/packages/frontend/src/types/menu.ts index 5d1fc1fe72..820759ce61 100644 --- a/packages/frontend/src/types/menu.ts +++ b/packages/frontend/src/types/menu.ts @@ -15,16 +15,16 @@ export type MenuAction = (ev: MouseEvent) => void; export type MenuDivider = { type: 'divider' }; export type MenuNull = undefined; -export type MenuLabel = { type: 'label', text: string }; -export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User }; -export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean }; +export type MenuLabel = { type: 'label', text: string, caption?: string }; +export type MenuLink = { type: 'link', to: string, text: string, caption?: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User }; +export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, caption?: string, icon?: string, indicate?: boolean }; export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction }; -export type MenuSwitch = { type: 'switch', ref: Ref, text: string, icon?: string, disabled?: boolean | Ref }; -export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef, avatar?: Misskey.entities.User; action: MenuAction }; -export type MenuRadio = { type: 'radio', text: string, icon?: string, ref: Ref, options: MenuRadioOptionsDef, disabled?: boolean | Ref }; -export type MenuRadioOption = { type: 'radioOption', text: string, action: MenuAction; active?: boolean | ComputedRef }; +export type MenuSwitch = { type: 'switch', ref: Ref, text: string, caption?: string, icon?: string, disabled?: boolean | Ref }; +export type MenuButton = { type?: 'button', text: string, caption?: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef, avatar?: Misskey.entities.User; action: MenuAction }; +export type MenuRadio = { type: 'radio', text: string, caption?: string, icon?: string, ref: Ref, options: MenuRadioOptionsDef, disabled?: boolean | Ref }; +export type MenuRadioOption = { type: 'radioOption', text: string, caption?: string, action: MenuAction; active?: boolean | ComputedRef }; export type MenuComponent = { type: 'component', component: T, props?: ComponentProps }; -export type MenuParent = { type: 'parent', text: string, icon?: string, children: MenuItem[] | (() => Promise | MenuItem[]) }; +export type MenuParent = { type: 'parent', text: string, caption?: string, icon?: string, children: MenuItem[] | (() => Promise | MenuItem[]) }; export type MenuPending = { type: 'pending' }; diff --git a/packages/frontend/src/utility/upload.ts b/packages/frontend/src/utility/upload.ts index eb3cbd3dfa..e13d793ffb 100644 --- a/packages/frontend/src/utility/upload.ts +++ b/packages/frontend/src/utility/upload.ts @@ -32,7 +32,7 @@ const mimeTypeMap = { export function uploadFile( file: File, - folder?: string | Misskey.entities.DriveFolder, + folder?: string | Misskey.entities.DriveFolder | null, name?: string, keepOriginal: boolean = prefer.s.keepOriginalUploading, ): Promise { diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index eafec84952..6507eb7681 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -951,6 +951,12 @@ type ChartsUsersRequest = operations['charts___users']['requestBody']['content'] // @public (undocumented) type ChartsUsersResponse = operations['charts___users']['responses']['200']['content']['application/json']; +// @public (undocumented) +type ChatHistoryRequest = operations['chat___history']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ChatHistoryResponse = operations['chat___history']['responses']['200']['content']['application/json']; + // @public (undocumented) type ChatMessage = components['schemas']['ChatMessage']; @@ -963,18 +969,15 @@ type ChatMessagesCreateRequest = operations['chat___messages___create']['request // @public (undocumented) type ChatMessagesCreateResponse = operations['chat___messages___create']['responses']['200']['content']['application/json']; -// @public (undocumented) -type ChatMessagesHistoryRequest = operations['chat___messages___history']['requestBody']['content']['application/json']; - -// @public (undocumented) -type ChatMessagesHistoryResponse = operations['chat___messages___history']['responses']['200']['content']['application/json']; - // @public (undocumented) type ChatMessagesTimelineRequest = operations['chat___messages___timeline']['requestBody']['content']['application/json']; // @public (undocumented) type ChatMessagesTimelineResponse = operations['chat___messages___timeline']['responses']['200']['content']['application/json']; +// @public (undocumented) +type ChatRoom = components['schemas']['ChatRoom']; + // @public (undocumented) type Clip = components['schemas']['Clip']; @@ -1472,10 +1475,10 @@ declare namespace entities { ChartsUserReactionsResponse, ChartsUsersRequest, ChartsUsersResponse, + ChatHistoryRequest, + ChatHistoryResponse, ChatMessagesCreateRequest, ChatMessagesCreateResponse, - ChatMessagesHistoryRequest, - ChatMessagesHistoryResponse, ChatMessagesTimelineRequest, ChatMessagesTimelineResponse, ClipsAddNoteRequest, @@ -1912,7 +1915,8 @@ declare namespace entities { SystemWebhook, AbuseReportNotificationRecipient, ChatMessage, - ChatMessageLite + ChatMessageLite, + ChatRoom } } export { entities } diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index b9023bb4f0..80c3d11463 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -1537,9 +1537,9 @@ declare module '../api.js' { /** * No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:chat* + * **Credential required**: *Yes* / **Permission**: *read:chat* */ - request( + request( endpoint: E, params: P, credential?: string | null, @@ -1548,9 +1548,9 @@ declare module '../api.js' { /** * No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:chat* + * **Credential required**: *Yes* / **Permission**: *write:chat* */ - request( + request( endpoint: E, params: P, credential?: string | null, diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 6bc4b377b4..54db9a46bc 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -207,10 +207,10 @@ import type { ChartsUserReactionsResponse, ChartsUsersRequest, ChartsUsersResponse, + ChatHistoryRequest, + ChatHistoryResponse, ChatMessagesCreateRequest, ChatMessagesCreateResponse, - ChatMessagesHistoryRequest, - ChatMessagesHistoryResponse, ChatMessagesTimelineRequest, ChatMessagesTimelineResponse, ClipsAddNoteRequest, @@ -732,8 +732,8 @@ export type Endpoints = { 'charts/user/pv': { req: ChartsUserPvRequest; res: ChartsUserPvResponse }; 'charts/user/reactions': { req: ChartsUserReactionsRequest; res: ChartsUserReactionsResponse }; 'charts/users': { req: ChartsUsersRequest; res: ChartsUsersResponse }; + 'chat/history': { req: ChatHistoryRequest; res: ChatHistoryResponse }; 'chat/messages/create': { req: ChatMessagesCreateRequest; res: ChatMessagesCreateResponse }; - 'chat/messages/history': { req: ChatMessagesHistoryRequest; res: ChatMessagesHistoryResponse }; 'chat/messages/timeline': { req: ChatMessagesTimelineRequest; res: ChatMessagesTimelineResponse }; 'clips/add-note': { req: ClipsAddNoteRequest; res: EmptyResponse }; 'clips/create': { req: ClipsCreateRequest; res: ClipsCreateResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index bd33db8465..9f38b0689b 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -210,10 +210,10 @@ export type ChartsUserReactionsRequest = operations['charts___user___reactions'] export type ChartsUserReactionsResponse = operations['charts___user___reactions']['responses']['200']['content']['application/json']; export type ChartsUsersRequest = operations['charts___users']['requestBody']['content']['application/json']; export type ChartsUsersResponse = operations['charts___users']['responses']['200']['content']['application/json']; +export type ChatHistoryRequest = operations['chat___history']['requestBody']['content']['application/json']; +export type ChatHistoryResponse = operations['chat___history']['responses']['200']['content']['application/json']; export type ChatMessagesCreateRequest = operations['chat___messages___create']['requestBody']['content']['application/json']; export type ChatMessagesCreateResponse = operations['chat___messages___create']['responses']['200']['content']['application/json']; -export type ChatMessagesHistoryRequest = operations['chat___messages___history']['requestBody']['content']['application/json']; -export type ChatMessagesHistoryResponse = operations['chat___messages___history']['responses']['200']['content']['application/json']; export type ChatMessagesTimelineRequest = operations['chat___messages___timeline']['requestBody']['content']['application/json']; export type ChatMessagesTimelineResponse = operations['chat___messages___timeline']['responses']['200']['content']['application/json']; export type ClipsAddNoteRequest = operations['clips___add-note']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 6f5080a2ba..ca65b9e139 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -56,3 +56,4 @@ 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 ChatRoom = components['schemas']['ChatRoom']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 14eccc3c1c..741529a8fd 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -1358,6 +1358,15 @@ export type paths = { */ post: operations['charts___users']; }; + '/chat/history': { + /** + * chat/history + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:chat* + */ + post: operations['chat___history']; + }; '/chat/messages/create': { /** * chat/messages/create @@ -1367,15 +1376,6 @@ export type paths = { */ post: operations['chat___messages___create']; }; - '/chat/messages/history': { - /** - * chat/messages/history - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *read:chat* - */ - post: operations['chat___messages___history']; - }; '/chat/messages/timeline': { /** * chat/messages/timeline @@ -5178,12 +5178,15 @@ export type components = { /** Format: date-time */ createdAt: string; fromUserId: string; - fromUser?: components['schemas']['UserLite']; + fromUser: components['schemas']['UserLite']; toUserId?: string | null; toUser?: components['schemas']['UserLite'] | null; + toRoomId?: string | null; + toRoom?: components['schemas']['ChatRoom'] | null; text?: string | null; fileId?: string | null; file?: components['schemas']['DriveFile'] | null; + isRead?: boolean; }; ChatMessageLite: { id: string; @@ -5191,10 +5194,19 @@ export type components = { createdAt: string; fromUserId: string; toUserId?: string | null; + toRoomId?: string | null; text?: string | null; fileId?: string | null; file?: components['schemas']['DriveFile'] | null; }; + ChatRoom: { + id: string; + /** Format: date-time */ + createdAt: string; + ownerId: string; + owner: components['schemas']['UserLite']; + name: string; + }; }; responses: never; parameters: never; @@ -13718,6 +13730,62 @@ export type operations = { }; }; }; + /** + * chat/history + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:chat* + */ + chat___history: { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** @default false */ + room?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['ChatMessage'][]; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * chat/messages/create * @description No description provided. @@ -13781,62 +13849,6 @@ export type operations = { }; }; }; - /** - * chat/messages/history - * @description No description provided. - * - * **Credential required**: *Yes* / **Permission**: *read:chat* - */ - chat___messages___history: { - requestBody: { - content: { - 'application/json': { - /** @default 10 */ - limit?: number; - /** @default false */ - room?: boolean; - }; - }; - }; - responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': components['schemas']['ChatMessage'][]; - }; - }; - /** @description Client error */ - 400: { - content: { - 'application/json': components['schemas']['Error']; - }; - }; - /** @description Authentication error */ - 401: { - content: { - 'application/json': components['schemas']['Error']; - }; - }; - /** @description Forbidden error */ - 403: { - content: { - 'application/json': components['schemas']['Error']; - }; - }; - /** @description I'm Ai */ - 418: { - content: { - 'application/json': components['schemas']['Error']; - }; - }; - /** @description Internal server error */ - 500: { - content: { - 'application/json': components['schemas']['Error']; - }; - }; - }; - }; /** * chat/messages/timeline * @description No description provided.