wip
This commit is contained in:
17
locales/index.d.ts
vendored
17
locales/index.d.ts
vendored
@@ -5354,6 +5354,23 @@ export interface Locale extends ILocale {
|
||||
* チャット
|
||||
*/
|
||||
"chat": string;
|
||||
/**
|
||||
* 個人チャット
|
||||
*/
|
||||
"individualChat": string;
|
||||
/**
|
||||
* 特定ユーザーとの一対一のチャットができます。
|
||||
*/
|
||||
"individualChat_description": string;
|
||||
/**
|
||||
* ルームチャット
|
||||
*/
|
||||
"roomChat": string;
|
||||
/**
|
||||
* 複数人でのチャットができます。
|
||||
* また、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。
|
||||
*/
|
||||
"roomChat_description": string;
|
||||
"_emojiPalette": {
|
||||
/**
|
||||
* パレット
|
||||
|
@@ -1334,6 +1334,10 @@ emojiPalette: "絵文字パレット"
|
||||
postForm: "投稿フォーム"
|
||||
textCount: "文字数"
|
||||
chat: "チャット"
|
||||
individualChat: "個人チャット"
|
||||
individualChat_description: "特定ユーザーとの一対一のチャットができます。"
|
||||
roomChat: "ルームチャット"
|
||||
roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。"
|
||||
|
||||
_emojiPalette:
|
||||
palettes: "パレット"
|
||||
|
@@ -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<MiChatMessage[]> {
|
||||
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();
|
||||
|
@@ -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
|
||||
|
@@ -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<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||
|
@@ -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;
|
||||
|
@@ -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<MiChatMessage>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $chatRoomsRepository: Provider = {
|
||||
provide: DI.chatRoomsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiChatRoom).extend(miRepository as MiRepository<MiChatRoom>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $chatRoomMembershipsRepository: Provider = {
|
||||
provide: DI.chatRoomMembershipsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiChatRoomMembership).extend(miRepository as MiRepository<MiChatRoomMembership>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $bubbleGameRecordsRepository: Provider = {
|
||||
provide: DI.bubbleGameRecordsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository<MiBubbleGameRecord>),
|
||||
@@ -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,
|
||||
],
|
||||
|
@@ -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,
|
||||
|
32
packages/backend/src/models/json-schema/chat-room.ts
Normal file
32
packages/backend/src/models/json-schema/chat-room.ts
Normal file
@@ -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;
|
@@ -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,
|
||||
|
@@ -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';
|
||||
|
@@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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);
|
||||
|
@@ -94,17 +94,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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,
|
||||
})));
|
||||
}*/
|
||||
|
@@ -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
|
||||
]"
|
||||
>
|
||||
<div :class="$style.bg">
|
||||
@@ -30,10 +31,12 @@ withDefaults(defineProps<{
|
||||
tail?: 'left' | 'right' | 'none';
|
||||
negativeMargin?: boolean;
|
||||
shadow?: boolean;
|
||||
accented?: boolean;
|
||||
}>(), {
|
||||
tail: 'right',
|
||||
negativeMargin: false,
|
||||
shadow: false,
|
||||
accented: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
@@ -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
|
||||
>
|
||||
<template v-for="item in (items2 ?? [])">
|
||||
<div v-if="item.type === 'divider'" role="separator" tabindex="-1" :class="$style.divider"></div>
|
||||
|
||||
<span v-else-if="item.type === 'label'" role="menuitem" tabindex="-1" :class="[$style.label, $style.item]">
|
||||
<span style="opacity: 0.7;">{{ item.text }}</span>
|
||||
</span>
|
||||
|
||||
<span v-else-if="item.type === 'pending'" role="menuitem" tabindex="0" :class="[$style.pending, $style.item]">
|
||||
<span><MkEllipsis/></span>
|
||||
</span>
|
||||
|
||||
<div v-else-if="item.type === 'component'" role="menuitem" tabindex="-1" :class="[$style.componentItem]">
|
||||
<component :is="item.component" v-bind="item.props"/>
|
||||
</div>
|
||||
|
||||
<MkA
|
||||
v-else-if="item.type === 'link'"
|
||||
role="menuitem"
|
||||
@@ -51,10 +56,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
||||
<div :class="$style.item_content">
|
||||
<span :class="$style.item_content_text">{{ item.text }}</span>
|
||||
<div :class="$style.item_content_text">
|
||||
<div :class="$style.item_content_text_title">{{ item.text }}</div>
|
||||
<div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div>
|
||||
</div>
|
||||
<span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
|
||||
</div>
|
||||
</MkA>
|
||||
|
||||
<a
|
||||
v-else-if="item.type === 'a'"
|
||||
role="menuitem"
|
||||
@@ -70,10 +79,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
>
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||
<div :class="$style.item_content">
|
||||
<span :class="$style.item_content_text">{{ item.text }}</span>
|
||||
<div :class="$style.item_content_text">
|
||||
<div :class="$style.item_content_text_title">{{ item.text }}</div>
|
||||
<div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div>
|
||||
</div>
|
||||
<span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<button
|
||||
v-else-if="item.type === 'user'"
|
||||
role="menuitem"
|
||||
@@ -88,6 +101,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<span :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-else-if="item.type === 'switch'"
|
||||
role="menuitemcheckbox"
|
||||
@@ -101,10 +115,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||
<MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
||||
<div :class="$style.item_content">
|
||||
<span :class="[$style.item_content_text, { [$style.switchText]: !item.icon }]">{{ item.text }}</span>
|
||||
<div :class="[$style.item_content_text, { [$style.switchText]: !item.icon }]">
|
||||
<div :class="$style.item_content_text_title">{{ item.text }}</div>
|
||||
<div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div>
|
||||
</div>
|
||||
<MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-else-if="item.type === 'radio'"
|
||||
role="menuitem"
|
||||
@@ -117,10 +135,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
>
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
|
||||
<div :class="$style.item_content">
|
||||
<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
|
||||
<div :class="$style.item_content_text" style="pointer-events: none;">
|
||||
<div :class="$style.item_content_text_title">{{ item.text }}</div>
|
||||
<div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div>
|
||||
</div>
|
||||
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-else-if="item.type === 'radioOption'"
|
||||
role="menuitemradio"
|
||||
@@ -134,9 +156,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<span :class="[$style.radioIcon, { [$style.radioChecked]: unref(item.active) }]"></span>
|
||||
</div>
|
||||
<div :class="$style.item_content">
|
||||
<span :class="$style.item_content_text">{{ item.text }}</span>
|
||||
<div :class="$style.item_content_text">
|
||||
<div :class="$style.item_content_text_title">{{ item.text }}</div>
|
||||
<div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-else-if="item.type === 'parent'"
|
||||
role="menuitem"
|
||||
@@ -148,12 +174,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
>
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
|
||||
<div :class="$style.item_content">
|
||||
<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
|
||||
<div :class="$style.item_content_text" style="pointer-events: none;">
|
||||
<div :class="$style.item_content_text_title">{{ item.text }}</div>
|
||||
<div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div>
|
||||
</div>
|
||||
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-else role="menuitem"
|
||||
v-else
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
:class="['_button', $style.item, { [$style.danger]: item.danger, [$style.active]: unref(item.active) }]"
|
||||
@click.prevent="unref(item.active) ? close(false) : clicked(item.action, $event)"
|
||||
@@ -163,11 +194,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
||||
<div :class="$style.item_content">
|
||||
<span :class="$style.item_content_text">{{ item.text }}</span>
|
||||
<div :class="$style.item_content_text">
|
||||
<div :class="$style.item_content_text_title">{{ item.text }}</div>
|
||||
<div v-if="item.caption" :class="$style.item_content_text_caption">{{ item.caption }}</div>
|
||||
</div>
|
||||
<span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<span v-if="items2 == null || items2.length === 0" tabindex="-1" :class="[$style.none, $style.item]">
|
||||
<span>{{ i18n.ts.none }}</span>
|
||||
</span>
|
||||
@@ -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;
|
||||
|
@@ -28,7 +28,7 @@ export type Keys = (
|
||||
'theme' |
|
||||
'themeId' |
|
||||
'customCss' |
|
||||
'message_drafts' |
|
||||
'chatMessageDrafts' |
|
||||
'scratchpad' |
|
||||
'debug' |
|
||||
'preferences' |
|
||||
|
@@ -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',
|
||||
|
@@ -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}`"
|
||||
>
|
||||
<div>
|
||||
<MkAvatar :class="$style.avatar" :user="item.other" indicator link preview/>
|
||||
<header v-if="item.message.roomId">
|
||||
<span class="name">{{ item.message.room.name }}</span>
|
||||
<MkTime :time="item.message.createdAt" class="time"/>
|
||||
<MkAvatar v-if="item.other" :class="$style.avatar" :user="item.other" indicator link preview/>
|
||||
<header v-if="item.message.room">
|
||||
<span :class="$style.name">{{ item.message.room.name }}</span>
|
||||
<MkTime :time="item.message.createdAt" :class="$style.time"/>
|
||||
</header>
|
||||
<header v-else>
|
||||
<span class="name"><MkUserName :user="item.other"/></span>
|
||||
<span class="username">@{{ acct(item.other) }}</span>
|
||||
<MkTime :time="item.message.createdAt" class="time"/>
|
||||
<MkUserName :class="$style.name" :user="item.other!"/>
|
||||
<MkAcct :class="$style.username" :user="item.other!"/>
|
||||
<MkTime :time="item.message.createdAt" :class="$style.time"/>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p class="text"><span v-if="item.isMe" :class="$style.iSaid">{{ i18n.ts.you }}:</span>{{ item.message.text }}</p>
|
||||
</div>
|
||||
<div :class="$style.body">
|
||||
<p :class="$style.text"><span v-if="item.isMe" :class="$style.iSaid">{{ i18n.ts.you }}:</span>{{ item.message.text }}</p>
|
||||
</div>
|
||||
</MkA>
|
||||
</div>
|
||||
<div v-if="!fetching && history.length == 0" class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ i18n.ts.noHistory }}</div>
|
||||
</div>
|
||||
<MkLoading v-if="fetching"/>
|
||||
@@ -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(() => ({
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.add {
|
||||
margin: 0 auto 16px auto;
|
||||
}
|
||||
|
||||
.antenna {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
border: solid 1px var(--MI_THEME-divider);
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
border: solid 1px var(--MI_THEME-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
.start {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
354
packages/frontend/src/pages/chat/room.form.vue
Normal file
354
packages/frontend/src/pages/chat/room.form.vue
Normal file
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<div
|
||||
:class="$style.root"
|
||||
@dragover.stop="onDragover"
|
||||
@drop.stop="onDrop"
|
||||
>
|
||||
<textarea
|
||||
ref="textEl"
|
||||
v-model="text"
|
||||
:class="$style.textarea"
|
||||
class="_acrylic"
|
||||
:placeholder="i18n.ts.inputMessageHere"
|
||||
@keydown="onKeydown"
|
||||
@paste="onPaste"
|
||||
></textarea>
|
||||
<footer :class="$style.footer">
|
||||
<div v-if="file" :class="$style.file" @click="file = null">{{ file.name }}</div>
|
||||
<div :class="$style.buttons">
|
||||
<button class="_button" :class="$style.button" @click="chooseFile"><i class="ti ti-photo-plus"></i></button>
|
||||
<button class="_button" :class="$style.button" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
|
||||
<button class="_button" :class="[$style.button, $style.send]" :disabled="!canSend || sending" :title="i18n.ts.send" @click="send">
|
||||
<template v-if="!sending"><i class="ti ti-send"></i></template><template v-if="sending"><MkLoading :em="true"/></template>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
<input ref="fileEl" style="display: none;" type="file" @change="onChangeFile"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, watch, ref, shallowRef, computed } 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 { Autocomplete } from '@/utility/autocomplete.js';
|
||||
import { uploadFile } from '@/utility/upload.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = defineProps<{
|
||||
user?: Misskey.entities.UserDetailed | null;
|
||||
room?: Misskey.entities.ChatRoom | null;
|
||||
}>();
|
||||
|
||||
const textEl = shallowRef<HTMLTextAreaElement>();
|
||||
const fileEl = shallowRef<HTMLInputElement>();
|
||||
|
||||
const text = ref<string>('');
|
||||
const file = ref<Misskey.entities.DriveFile | null>(null);
|
||||
const sending = ref(false);
|
||||
|
||||
const canSend = computed(() => (text.value != null && text.value !== '') || file.value != null);
|
||||
|
||||
function getDraftKey() {
|
||||
return props.user ? 'user:' + props.user.id : 'room:' + props.room?.id;
|
||||
}
|
||||
|
||||
watch([text, file], saveDraft);
|
||||
|
||||
async function onPaste(ev: ClipboardEvent) {
|
||||
if (!ev.clipboardData) return;
|
||||
|
||||
const pastedFileName = 'yyyy-MM-dd HH-mm-ss [{{number}}]';
|
||||
|
||||
const clipboardData = ev.clipboardData;
|
||||
const items = clipboardData.items;
|
||||
|
||||
if (items.length === 1) {
|
||||
if (items[0].kind === 'file') {
|
||||
const pastedFile = items[0].getAsFile();
|
||||
if (!pastedFile) return;
|
||||
const lio = pastedFile.name.lastIndexOf('.');
|
||||
const ext = lio >= 0 ? pastedFile.name.slice(lio) : '';
|
||||
const formatted = formatTimeString(new Date(pastedFile.lastModified), pastedFileName).replace(/{{number}}/g, '1') + ext;
|
||||
if (formatted) upload(pastedFile, formatted);
|
||||
}
|
||||
} else {
|
||||
if (items[0].kind === 'file') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.onlyOneFileCanBeAttached,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onDragover(ev: DragEvent) {
|
||||
if (!ev.dataTransfer) return;
|
||||
|
||||
const isFile = ev.dataTransfer.items[0].kind === 'file';
|
||||
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
||||
if (isFile || isDriveFile) {
|
||||
ev.preventDefault();
|
||||
switch (ev.dataTransfer.effectAllowed) {
|
||||
case 'all':
|
||||
case 'uninitialized':
|
||||
case 'copy':
|
||||
case 'copyLink':
|
||||
case 'copyMove':
|
||||
ev.dataTransfer.dropEffect = 'copy';
|
||||
break;
|
||||
case 'linkMove':
|
||||
case 'move':
|
||||
ev.dataTransfer.dropEffect = 'move';
|
||||
break;
|
||||
default:
|
||||
ev.dataTransfer.dropEffect = 'none';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onDrop(ev: DragEvent): void {
|
||||
if (!ev.dataTransfer) return;
|
||||
|
||||
// ファイルだったら
|
||||
if (ev.dataTransfer.files.length === 1) {
|
||||
ev.preventDefault();
|
||||
upload(ev.dataTransfer.files[0]);
|
||||
return;
|
||||
} else if (ev.dataTransfer.files.length > 1) {
|
||||
ev.preventDefault();
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.onlyOneFileCanBeAttached,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//#region ドライブのファイル
|
||||
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||
if (driveFile != null && driveFile !== '') {
|
||||
file.value = JSON.parse(driveFile);
|
||||
ev.preventDefault();
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
function onKeydown(ev: KeyboardEvent) {
|
||||
if ((ev.key === 'Enter') && (ev.ctrlKey || ev.metaKey)) {
|
||||
send();
|
||||
}
|
||||
}
|
||||
|
||||
function chooseFile(ev: MouseEvent) {
|
||||
selectFile(ev.currentTarget ?? ev.target, i18n.ts.selectFile).then(selectedFile => {
|
||||
file.value = selectedFile;
|
||||
});
|
||||
}
|
||||
|
||||
function onChangeFile() {
|
||||
if (fileEl.value.files![0]) upload(fileEl.value.files[0]);
|
||||
}
|
||||
|
||||
function upload(fileToUpload: File, name?: string) {
|
||||
uploadFile(fileToUpload, prefer.s.uploadFolder, name).then(res => {
|
||||
file.value = res;
|
||||
});
|
||||
}
|
||||
|
||||
function send() {
|
||||
if (!canSend.value) return;
|
||||
|
||||
sending.value = true;
|
||||
misskeyApi('chat/messages/create', {
|
||||
toUserId: props.user ? props.user.id : undefined,
|
||||
toRoomId: props.room ? props.room.id : undefined,
|
||||
text: text.value ? text.value : undefined,
|
||||
fileId: file.value ? file.value.id : undefined,
|
||||
}).then(message => {
|
||||
clear();
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
}).then(() => {
|
||||
sending.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function clear() {
|
||||
text.value = '';
|
||||
file.value = null;
|
||||
deleteDraft();
|
||||
}
|
||||
|
||||
function saveDraft() {
|
||||
const drafts = JSON.parse(miLocalStorage.getItem('chatMessageDrafts') || '{}');
|
||||
|
||||
drafts[getDraftKey()] = {
|
||||
updatedAt: new Date(),
|
||||
data: {
|
||||
text: text.value,
|
||||
file: file.value,
|
||||
},
|
||||
};
|
||||
|
||||
miLocalStorage.setItem('chatMessageDrafts', JSON.stringify(drafts));
|
||||
}
|
||||
|
||||
function deleteDraft() {
|
||||
const drafts = JSON.parse(miLocalStorage.getItem('chatMessageDrafts') || '{}');
|
||||
|
||||
delete drafts[getDraftKey()];
|
||||
|
||||
miLocalStorage.setItem('chatMessageDrafts', JSON.stringify(drafts));
|
||||
}
|
||||
|
||||
async function insertEmoji(ev: MouseEvent) {
|
||||
os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textEl);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
//autosize(textEl);
|
||||
|
||||
// TODO: detach when unmount
|
||||
// TODO
|
||||
//new Autocomplete(textEl, this, { model: 'text' });
|
||||
|
||||
// 書きかけの投稿を復元
|
||||
const draft = JSON.parse(miLocalStorage.getItem('chatMessageDrafts') || '{}')[getDraftKey()];
|
||||
if (draft) {
|
||||
text.value = draft.data.text;
|
||||
file.value = draft.data.file;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
cursor: auto;
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 80px;
|
||||
margin: 0;
|
||||
padding: 16px 16px 0 16px;
|
||||
resize: none;
|
||||
font-size: 1em;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.file {
|
||||
padding: 8px;
|
||||
color: var(--fg);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
/*
|
||||
.files {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0 8px;
|
||||
list-style: none;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
> li {
|
||||
display: block;
|
||||
float: left;
|
||||
margin: 4px;
|
||||
padding: 0;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-color: #eee;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
cursor: move;
|
||||
|
||||
&:hover {
|
||||
> .remove {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-remove {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
top: -6px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
*/
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
transition: color 0.1s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--accentDarken);
|
||||
transition: color 0s ease;
|
||||
}
|
||||
}
|
||||
.send {
|
||||
margin-left: auto;
|
||||
color: var(--accent);
|
||||
|
||||
&:hover {
|
||||
color: var(--accentLighten);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--accentDarken);
|
||||
transition: color 0s ease;
|
||||
}
|
||||
}
|
||||
</style>
|
118
packages/frontend/src/pages/chat/room.message.vue
Normal file
118
packages/frontend/src/pages/chat/room.message.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div :class="[$style.root, { [$style.isMe]: isMe }]">
|
||||
<MkAvatar :class="$style.avatar" :user="user" indicator link preview/>
|
||||
<div :class="$style.body">
|
||||
<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"/>
|
||||
<div v-if="message.file" :class="$style.file">
|
||||
<a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name">
|
||||
<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
|
||||
<p v-else>{{ message.file.name }}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.content">
|
||||
<p>{{ i18n.ts.deleted }}</p>
|
||||
</div>
|
||||
</MkFukidashi>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" style="margin: 8px 0;"/>
|
||||
<div>
|
||||
<MkTime :class="$style.time" :time="message.createdAt"/>
|
||||
</div>
|
||||
</div>
|
||||
<button v-if="isMe" :class="$style.delete" :title="i18n.ts.delete" @click="del">
|
||||
<img src="/client-assets/remove.png" alt="Delete"/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkFukidashi from '@/components/MkFukidashi.vue';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const props = defineProps<{
|
||||
message: Misskey.entities.ChatMessageLite;
|
||||
user: Misskey.entities.User;
|
||||
isRoom?: boolean;
|
||||
}>();
|
||||
|
||||
const isMe = computed(() => props.message.fromUserId === $i.id);
|
||||
const urls = computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []);
|
||||
|
||||
function del(): void {
|
||||
misskeyApi('chat/messages/delete', {
|
||||
messageId: props.message.id,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
$me-balloon-color: var(--accent);
|
||||
|
||||
position: relative;
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
|
||||
&.isMe {
|
||||
flex-direction: row-reverse;
|
||||
text-align: right;
|
||||
|
||||
.content {
|
||||
color: var(--MI_THEME-fgOnAccent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: sticky;
|
||||
top: calc(var(--stickyTop, 0px) + 16px);
|
||||
display: block;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.delete {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.1s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 75%;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
292
packages/frontend/src/pages/chat/room.vue
Normal file
292
packages/frontend/src/pages/chat/room.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div style="height: 100vh; overflow:auto; display:flex; flex-direction:column-reverse;">
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<MkPageHeader/>
|
||||
</template>
|
||||
|
||||
<div ref="rootEl" :class="$style.root">
|
||||
<MkSpacer :contentMax="700">
|
||||
<MkPagination v-if="pagination" ref="pagingComponent" :key="userId || roomId" :pagination="pagination" :disableAutoLoad="true">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<div>{{ i18n.ts.noMessagesYet }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ items: messages, fetching: pFetching }">
|
||||
<MkDateSeparatedList
|
||||
v-if="messages.length > 0"
|
||||
v-slot="{ item: message }"
|
||||
:class="{ [$style['messages']]: true, 'deny-move-transition': pFetching }"
|
||||
:items="messages"
|
||||
direction="up"
|
||||
reversed
|
||||
>
|
||||
<XMessage :key="message.id" :message="message" :user="message.fromUserId === $i.id ? $i : user" :isRoom="room != null"/>
|
||||
</MkDateSeparatedList>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<MkSpacer :contentMax="700">
|
||||
<div class="_gaps">
|
||||
<Transition name="fade">
|
||||
<div v-show="showIndicator" :class="$style.new">
|
||||
<button class="_buttonPrimary" :class="$style.newButton" @click="onIndicatorClick">
|
||||
<i class="fas ti-fw fa-arrow-circle-down" :class="$style.newIcon"></i>{{ i18n.ts.newMessageExists }}
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
<XForm v-if="!fetching" :user="user" :room="room" :class="$style.form"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, computed, watch, onMounted, nextTick, onBeforeUnmount } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { isBottomVisible } from '@@/js/scroll.js';
|
||||
import XMessage from './room.message.vue';
|
||||
import XForm from './room.form.vue';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const props = defineProps<{
|
||||
userId?: string;
|
||||
roomId?: string;
|
||||
}>();
|
||||
|
||||
const rootEl = shallowRef<HTMLDivElement>();
|
||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
const fetching = ref(true);
|
||||
const user = ref<Misskey.entities.UserDetailed | null>(null);
|
||||
const room = ref<Misskey.entities.ChatRoom | null>(null);
|
||||
const connection = ref<Misskey.ChannelConnection<Misskey.Channels['chat']> | null>(null);
|
||||
const showIndicator = ref(false);
|
||||
|
||||
const pagination = ref<Paging | null>(null);
|
||||
|
||||
watch([() => props.userId, () => props.roomId], () => {
|
||||
if (connection.value) connection.value.dispose();
|
||||
fetch();
|
||||
});
|
||||
|
||||
async function fetch() {
|
||||
fetching.value = true;
|
||||
|
||||
if (props.userId) {
|
||||
user.value = await misskeyApi('users/show', { userId: props.userId });
|
||||
room.value = null;
|
||||
|
||||
pagination.value = {
|
||||
endpoint: 'chat/messages/timeline',
|
||||
limit: 20,
|
||||
params: {
|
||||
userId: user.value.id,
|
||||
},
|
||||
reversed: true,
|
||||
pageEl: rootEl.value,
|
||||
};
|
||||
connection.value = useStream().useChannel('chat', {
|
||||
other: user.value.id,
|
||||
});
|
||||
}/* else {
|
||||
user = null;
|
||||
room = await misskeyApi('users/rooms/show', { roomId: props.roomId });
|
||||
|
||||
pagination = {
|
||||
endpoint: 'chat/messages',
|
||||
limit: 20,
|
||||
params: {
|
||||
roomId: room?.id,
|
||||
},
|
||||
reversed: true,
|
||||
pageEl: $$(rootEl).value,
|
||||
};
|
||||
connection = useStream().useChannel('chat', {
|
||||
room: room?.id,
|
||||
});
|
||||
}*/
|
||||
|
||||
connection.value.on('message', onMessage);
|
||||
connection.value.on('deleted', onDeleted);
|
||||
|
||||
document.addEventListener('visibilitychange', onVisibilitychange);
|
||||
|
||||
fetching.value = false;
|
||||
}
|
||||
|
||||
function onMessage(message) {
|
||||
//sound.play('chat');
|
||||
|
||||
const _isBottom = isBottomVisible(rootEl, 64);
|
||||
|
||||
pagingComponent.value.prepend(message);
|
||||
if (message.userId !== $i.id && !document.hidden) {
|
||||
connection.value?.send('read', {
|
||||
id: message.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (_isBottom) {
|
||||
// Scroll to bottom
|
||||
nextTick(() => {
|
||||
thisScrollToBottom();
|
||||
});
|
||||
} else if (message.userId !== $i.id) {
|
||||
// Notify
|
||||
notifyNewMessage();
|
||||
}
|
||||
}
|
||||
|
||||
function onDeleted(id) {
|
||||
const msg = pagingComponent.value.items.find(m => m.id === id);
|
||||
if (msg) {
|
||||
pagingComponent.value.items = pagingComponent.value.items.filter(m => m.id !== msg.id);
|
||||
}
|
||||
}
|
||||
|
||||
function thisScrollToBottom() {
|
||||
scrollToBottom(rootEl.value, { behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function onIndicatorClick() {
|
||||
showIndicator.value = false;
|
||||
thisScrollToBottom();
|
||||
}
|
||||
|
||||
function notifyNewMessage() {
|
||||
showIndicator.value = true;
|
||||
|
||||
scrollRemove.value = onScrollBottom(rootEl, () => {
|
||||
showIndicator.value = false;
|
||||
scrollRemove.value = null;
|
||||
});
|
||||
}
|
||||
|
||||
function onVisibilitychange() {
|
||||
if (document.hidden) return;
|
||||
for (const message of pagingComponent.value.items) {
|
||||
if (message.userId !== $i.id && !message.isRead) {
|
||||
connection.value?.send('read', {
|
||||
id: message.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetch();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
connection.value?.dispose();
|
||||
document.removeEventListener('visibilitychange', onVisibilitychange);
|
||||
});
|
||||
|
||||
definePage(computed(() => !fetching.value ? user.value ? {
|
||||
userName: user,
|
||||
avatar: user,
|
||||
} : {
|
||||
title: room.value?.name,
|
||||
icon: 'ti ti-users',
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: content;
|
||||
}
|
||||
|
||||
.more {
|
||||
display: block;
|
||||
margin: 16px auto;
|
||||
padding: 0 12px;
|
||||
line-height: 24px;
|
||||
color: #fff;
|
||||
background: rgba(#000, 0.3);
|
||||
border-radius: 12px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(#000, 0.4);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(#000, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.fetching {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.messages {
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
padding-top: 8px;
|
||||
bottom: var(--minBottomSpacing);
|
||||
}
|
||||
|
||||
.new {
|
||||
width: 100%;
|
||||
padding-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.newButton {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0 12px;
|
||||
line-height: 32px;
|
||||
font-size: 12px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.newIcon {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.form {
|
||||
max-height: 12em;
|
||||
overflow-y: scroll;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
transition: opacity 0.5s;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
@@ -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')),
|
||||
|
@@ -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<boolean>, text: string, icon?: string, disabled?: boolean | Ref<boolean> };
|
||||
export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef<boolean>, avatar?: Misskey.entities.User; action: MenuAction };
|
||||
export type MenuRadio = { type: 'radio', text: string, icon?: string, ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>, options: MenuRadioOptionsDef, disabled?: boolean | Ref<boolean> };
|
||||
export type MenuRadioOption = { type: 'radioOption', text: string, action: MenuAction; active?: boolean | ComputedRef<boolean> };
|
||||
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, caption?: string, icon?: string, disabled?: boolean | Ref<boolean> };
|
||||
export type MenuButton = { type?: 'button', text: string, caption?: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef<boolean>, avatar?: Misskey.entities.User; action: MenuAction };
|
||||
export type MenuRadio = { type: 'radio', text: string, caption?: string, icon?: string, ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>, options: MenuRadioOptionsDef, disabled?: boolean | Ref<boolean> };
|
||||
export type MenuRadioOption = { type: 'radioOption', text: string, caption?: string, action: MenuAction; active?: boolean | ComputedRef<boolean> };
|
||||
export type MenuComponent<T extends Component = any> = { type: 'component', component: T, props?: ComponentProps<T> };
|
||||
export type MenuParent = { type: 'parent', text: string, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
|
||||
export type MenuParent = { type: 'parent', text: string, caption?: string, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
|
||||
|
||||
export type MenuPending = { type: 'pending' };
|
||||
|
||||
|
@@ -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<Misskey.entities.DriveFile> {
|
||||
|
@@ -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 }
|
||||
|
@@ -1537,9 +1537,9 @@ declare module '../api.js' {
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:chat*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:chat*
|
||||
*/
|
||||
request<E extends 'chat/messages/create', P extends Endpoints[E]['req']>(
|
||||
request<E extends 'chat/history', P extends Endpoints[E]['req']>(
|
||||
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<E extends 'chat/messages/history', P extends Endpoints[E]['req']>(
|
||||
request<E extends 'chat/messages/create', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
|
@@ -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 };
|
||||
|
@@ -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'];
|
||||
|
@@ -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'];
|
||||
|
@@ -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.
|
||||
|
Reference in New Issue
Block a user