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;
|
"chat": string;
|
||||||
|
/**
|
||||||
|
* 個人チャット
|
||||||
|
*/
|
||||||
|
"individualChat": string;
|
||||||
|
/**
|
||||||
|
* 特定ユーザーとの一対一のチャットができます。
|
||||||
|
*/
|
||||||
|
"individualChat_description": string;
|
||||||
|
/**
|
||||||
|
* ルームチャット
|
||||||
|
*/
|
||||||
|
"roomChat": string;
|
||||||
|
/**
|
||||||
|
* 複数人でのチャットができます。
|
||||||
|
* また、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。
|
||||||
|
*/
|
||||||
|
"roomChat_description": string;
|
||||||
"_emojiPalette": {
|
"_emojiPalette": {
|
||||||
/**
|
/**
|
||||||
* パレット
|
* パレット
|
||||||
|
@@ -1334,6 +1334,10 @@ emojiPalette: "絵文字パレット"
|
|||||||
postForm: "投稿フォーム"
|
postForm: "投稿フォーム"
|
||||||
textCount: "文字数"
|
textCount: "文字数"
|
||||||
chat: "チャット"
|
chat: "チャット"
|
||||||
|
individualChat: "個人チャット"
|
||||||
|
individualChat_description: "特定ユーザーとの一対一のチャットができます。"
|
||||||
|
roomChat: "ルームチャット"
|
||||||
|
roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。"
|
||||||
|
|
||||||
_emojiPalette:
|
_emojiPalette:
|
||||||
palettes: "パレット"
|
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));
|
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);
|
this.queueService.deliver(fromUser, activity, toUser.inbox);
|
||||||
}
|
}
|
||||||
}/* else if (message.roomId) {
|
}/* else if (message.toRoomId) {
|
||||||
this.globalEventService.publishRoomChatStream(message.roomId, 'deleted', message.id);
|
this.globalEventService.publishRoomChatStream(message.toRoomId, 'deleted', message.id);
|
||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ export class ChatService {
|
|||||||
} else {
|
} else {
|
||||||
// そのグループにおいて未読がなければイベント発行
|
// そのグループにおいて未読がなければイベント発行
|
||||||
const unreadExist = await this.chatMessagesRepository.createQueryBuilder('message')
|
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('message.userId != :userId', { userId: userId })
|
||||||
.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
|
.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
|
||||||
.andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // 自分が加入する前の会話については、未読扱いしない
|
.andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // 自分が加入する前の会話については、未読扱いしない
|
||||||
@@ -300,7 +300,7 @@ export class ChatService {
|
|||||||
.where('message.fromUserId = :meId', { meId: meId })
|
.where('message.fromUserId = :meId', { meId: meId })
|
||||||
.orWhere('message.toUserId = :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.fromUserId NOT IN (${ mutingQuery.getQuery() })`)
|
||||||
.andWhere(`message.toUserId NOT IN (${ mutingQuery.getQuery() })`);
|
.andWhere(`message.toUserId NOT IN (${ mutingQuery.getQuery() })`);
|
||||||
|
|
||||||
@@ -325,6 +325,7 @@ export class ChatService {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async roomHistory(meId: MiUser['id'], limit: number): Promise<MiChatMessage[]> {
|
public async roomHistory(meId: MiUser['id'], limit: number): Promise<MiChatMessage[]> {
|
||||||
|
return [];
|
||||||
/*
|
/*
|
||||||
const rooms = await this.userRoomJoiningsRepository.findBy({
|
const rooms = await this.userRoomJoiningsRepository.findBy({
|
||||||
userId: meId,
|
userId: meId,
|
||||||
@@ -341,10 +342,10 @@ export class ChatService {
|
|||||||
|
|
||||||
const query = this.chatMessagesRepository.createQueryBuilder('message')
|
const query = this.chatMessagesRepository.createQueryBuilder('message')
|
||||||
.orderBy('message.id', 'DESC')
|
.orderBy('message.id', 'DESC')
|
||||||
.where('message.roomId IN (:...rooms)', { rooms: rooms });
|
.where('message.toRoomId IN (:...rooms)', { rooms: rooms });
|
||||||
|
|
||||||
if (found.length > 0) {
|
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();
|
const message = await query.getOne();
|
||||||
|
@@ -84,6 +84,8 @@ export const DI = {
|
|||||||
flashLikesRepository: Symbol('flashLikesRepository'),
|
flashLikesRepository: Symbol('flashLikesRepository'),
|
||||||
userMemosRepository: Symbol('userMemosRepository'),
|
userMemosRepository: Symbol('userMemosRepository'),
|
||||||
chatMessagesRepository: Symbol('chatMessagesRepository'),
|
chatMessagesRepository: Symbol('chatMessagesRepository'),
|
||||||
|
chatRoomsRepository: Symbol('chatRoomsRepository'),
|
||||||
|
chatRoomMembershipsRepository: Symbol('chatRoomMembershipsRepository'),
|
||||||
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
||||||
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@@ -64,6 +64,7 @@ import {
|
|||||||
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
|
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
|
||||||
import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
|
import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
|
||||||
import { packedChatMessageSchema, packedChatMessageLiteSchema } from '@/models/json-schema/chat-message.js';
|
import { packedChatMessageSchema, packedChatMessageLiteSchema } from '@/models/json-schema/chat-message.js';
|
||||||
|
import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js';
|
||||||
|
|
||||||
export const refs = {
|
export const refs = {
|
||||||
UserLite: packedUserLiteSchema,
|
UserLite: packedUserLiteSchema,
|
||||||
@@ -123,6 +124,7 @@ export const refs = {
|
|||||||
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
|
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
|
||||||
ChatMessage: packedChatMessageSchema,
|
ChatMessage: packedChatMessageSchema,
|
||||||
ChatMessageLite: packedChatMessageLiteSchema,
|
ChatMessageLite: packedChatMessageLiteSchema,
|
||||||
|
ChatRoom: packedChatRoomSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
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';
|
import { MiChatRoom } from './ChatRoom.js';
|
||||||
|
|
||||||
@Entity('chat_room_membership')
|
@Entity('chat_room_membership')
|
||||||
|
@Index(['userId', 'roomId'], { unique: true })
|
||||||
export class MiChatRoomMembership {
|
export class MiChatRoomMembership {
|
||||||
@PrimaryColumn(id())
|
@PrimaryColumn(id())
|
||||||
public id: string;
|
public id: string;
|
||||||
|
@@ -78,6 +78,9 @@ import {
|
|||||||
MiUserPublickey,
|
MiUserPublickey,
|
||||||
MiUserSecurityKey,
|
MiUserSecurityKey,
|
||||||
MiWebhook,
|
MiWebhook,
|
||||||
|
MiChatMessage,
|
||||||
|
MiChatRoom,
|
||||||
|
MiChatRoomMembership,
|
||||||
} from './_.js';
|
} from './_.js';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
import type { DataSource } from 'typeorm';
|
import type { DataSource } from 'typeorm';
|
||||||
@@ -490,6 +493,24 @@ const $userMemosRepository: Provider = {
|
|||||||
inject: [DI.db],
|
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 = {
|
const $bubbleGameRecordsRepository: Provider = {
|
||||||
provide: DI.bubbleGameRecordsRepository,
|
provide: DI.bubbleGameRecordsRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository<MiBubbleGameRecord>),
|
useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository<MiBubbleGameRecord>),
|
||||||
@@ -573,6 +594,9 @@ const $reversiGamesRepository: Provider = {
|
|||||||
$flashsRepository,
|
$flashsRepository,
|
||||||
$flashLikesRepository,
|
$flashLikesRepository,
|
||||||
$userMemosRepository,
|
$userMemosRepository,
|
||||||
|
$chatMessagesRepository,
|
||||||
|
$chatRoomsRepository,
|
||||||
|
$chatRoomMembershipsRepository,
|
||||||
$bubbleGameRecordsRepository,
|
$bubbleGameRecordsRepository,
|
||||||
$reversiGamesRepository,
|
$reversiGamesRepository,
|
||||||
],
|
],
|
||||||
@@ -645,6 +669,9 @@ const $reversiGamesRepository: Provider = {
|
|||||||
$flashsRepository,
|
$flashsRepository,
|
||||||
$flashLikesRepository,
|
$flashLikesRepository,
|
||||||
$userMemosRepository,
|
$userMemosRepository,
|
||||||
|
$chatMessagesRepository,
|
||||||
|
$chatRoomsRepository,
|
||||||
|
$chatRoomMembershipsRepository,
|
||||||
$bubbleGameRecordsRepository,
|
$bubbleGameRecordsRepository,
|
||||||
$reversiGamesRepository,
|
$reversiGamesRepository,
|
||||||
],
|
],
|
||||||
|
@@ -21,7 +21,7 @@ export const packedChatMessageSchema = {
|
|||||||
},
|
},
|
||||||
fromUser: {
|
fromUser: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
ref: 'UserLite',
|
ref: 'UserLite',
|
||||||
},
|
},
|
||||||
toUserId: {
|
toUserId: {
|
||||||
@@ -33,6 +33,15 @@ export const packedChatMessageSchema = {
|
|||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
ref: 'UserLite',
|
ref: 'UserLite',
|
||||||
},
|
},
|
||||||
|
toRoomId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
},
|
||||||
|
toRoom: {
|
||||||
|
type: 'object',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
ref: 'ChatRoom',
|
||||||
|
},
|
||||||
text: {
|
text: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
@@ -73,6 +82,10 @@ export const packedChatMessageLiteSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
},
|
},
|
||||||
|
toRoomId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
},
|
||||||
text: {
|
text: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
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 { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
|
||||||
import { UserListChannelService } from './api/stream/channels/user-list.js';
|
import { UserListChannelService } from './api/stream/channels/user-list.js';
|
||||||
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.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 { ReversiChannelService } from './api/stream/channels/reversi.js';
|
||||||
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
|
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
|
||||||
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
|
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
|
||||||
@@ -84,6 +85,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
|||||||
GlobalTimelineChannelService,
|
GlobalTimelineChannelService,
|
||||||
HashtagChannelService,
|
HashtagChannelService,
|
||||||
RoleTimelineChannelService,
|
RoleTimelineChannelService,
|
||||||
|
ChatChannelService,
|
||||||
ReversiChannelService,
|
ReversiChannelService,
|
||||||
ReversiGameChannelService,
|
ReversiGameChannelService,
|
||||||
HomeTimelineChannelService,
|
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 'users/update-memo' from './endpoints/users/update-memo.js';
|
||||||
export * as 'chat/messages/create' from './endpoints/chat/messages/create.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/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';
|
export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';
|
||||||
|
@@ -82,7 +82,7 @@ export const paramDef = {
|
|||||||
properties: {
|
properties: {
|
||||||
text: { type: 'string', nullable: true, maxLength: 2000 },
|
text: { type: 'string', nullable: true, maxLength: 2000 },
|
||||||
fileId: { type: 'string', format: 'misskey:id' },
|
fileId: { type: 'string', format: 'misskey:id' },
|
||||||
userId: { type: 'string', format: 'misskey:id', nullable: true },
|
toUserId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -113,13 +113,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
throw new ApiError(meta.errors.contentRequired);
|
throw new ApiError(meta.errors.contentRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.userId != null) {
|
if (ps.toUserId != null) {
|
||||||
// Myself
|
// Myself
|
||||||
if (ps.userId === me.id) {
|
if (ps.toUserId === me.id) {
|
||||||
throw new ApiError(meta.errors.recipientIsYourself);
|
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);
|
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
@@ -130,9 +130,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
text: ps.text,
|
text: ps.text,
|
||||||
file: file,
|
file: file,
|
||||||
});
|
});
|
||||||
}/* else if (ps.roomId != null) {
|
}/* else if (ps.toRoomId != null) {
|
||||||
// Fetch recipient (room)
|
// Fetch recipient (room)
|
||||||
recipientRoom = await this.userRoomsRepository.findOneBy({ id: ps.roomId! });
|
recipientRoom = await this.userRoomsRepository.findOneBy({ id: ps.toRoomId! });
|
||||||
|
|
||||||
if (recipientRoom == null) {
|
if (recipientRoom == null) {
|
||||||
throw new ApiError(meta.errors.noSuchRoom);
|
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);
|
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 });
|
.andWhere('message.roomId = :roomId', { roomId: recipientRoom.id });
|
||||||
|
|
||||||
const messages = await query.take(ps.limit).getMany();
|
const messages = await query.take(ps.limit).getMany();
|
||||||
|
|
||||||
// Mark all as read
|
// Mark all as read
|
||||||
if (ps.markAsRead) {
|
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,
|
populateRoom: false,
|
||||||
})));
|
})));
|
||||||
}*/
|
}*/
|
||||||
|
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
tail === 'left' ? $style.left : $style.right,
|
tail === 'left' ? $style.left : $style.right,
|
||||||
negativeMargin === true && $style.negativeMargin,
|
negativeMargin === true && $style.negativeMargin,
|
||||||
shadow === true && $style.shadow,
|
shadow === true && $style.shadow,
|
||||||
|
accented === true && $style.accented
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<div :class="$style.bg">
|
<div :class="$style.bg">
|
||||||
@@ -30,10 +31,12 @@ withDefaults(defineProps<{
|
|||||||
tail?: 'left' | 'right' | 'none';
|
tail?: 'left' | 'right' | 'none';
|
||||||
negativeMargin?: boolean;
|
negativeMargin?: boolean;
|
||||||
shadow?: boolean;
|
shadow?: boolean;
|
||||||
|
accented?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
tail: 'right',
|
tail: 'right',
|
||||||
negativeMargin: false,
|
negativeMargin: false,
|
||||||
shadow: false,
|
shadow: false,
|
||||||
|
accented: false,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -47,6 +50,10 @@ withDefaults(defineProps<{
|
|||||||
min-height: calc(var(--fukidashi-radius) * 2);
|
min-height: calc(var(--fukidashi-radius) * 2);
|
||||||
padding-top: calc(var(--fukidashi-radius) * .13);
|
padding-top: calc(var(--fukidashi-radius) * .13);
|
||||||
|
|
||||||
|
&.accented {
|
||||||
|
--fukidashi-bg: var(--MI_THEME-accent);
|
||||||
|
}
|
||||||
|
|
||||||
&.shadow {
|
&.shadow {
|
||||||
filter: drop-shadow(0 4px 32px var(--MI_THEME-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.center]: align === 'center',
|
||||||
[$style.big]: big,
|
[$style.big]: big,
|
||||||
[$style.asDrawer]: asDrawer,
|
[$style.asDrawer]: asDrawer,
|
||||||
|
[$style.widthSpecified]: width != null,
|
||||||
}"
|
}"
|
||||||
@focusin.passive.stop="() => {}"
|
@focusin.passive.stop="() => {}"
|
||||||
>
|
>
|
||||||
@@ -29,15 +30,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
>
|
>
|
||||||
<template v-for="item in (items2 ?? [])">
|
<template v-for="item in (items2 ?? [])">
|
||||||
<div v-if="item.type === 'divider'" role="separator" tabindex="-1" :class="$style.divider"></div>
|
<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 v-else-if="item.type === 'label'" role="menuitem" tabindex="-1" :class="[$style.label, $style.item]">
|
||||||
<span style="opacity: 0.7;">{{ item.text }}</span>
|
<span style="opacity: 0.7;">{{ item.text }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span v-else-if="item.type === 'pending'" role="menuitem" tabindex="0" :class="[$style.pending, $style.item]">
|
<span v-else-if="item.type === 'pending'" role="menuitem" tabindex="0" :class="[$style.pending, $style.item]">
|
||||||
<span><MkEllipsis/></span>
|
<span><MkEllipsis/></span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div v-else-if="item.type === 'component'" role="menuitem" tabindex="-1" :class="[$style.componentItem]">
|
<div v-else-if="item.type === 'component'" role="menuitem" tabindex="-1" :class="[$style.componentItem]">
|
||||||
<component :is="item.component" v-bind="item.props"/>
|
<component :is="item.component" v-bind="item.props"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkA
|
<MkA
|
||||||
v-else-if="item.type === 'link'"
|
v-else-if="item.type === 'link'"
|
||||||
role="menuitem"
|
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>
|
<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"/>
|
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
||||||
<div :class="$style.item_content">
|
<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>
|
<span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</MkA>
|
</MkA>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
v-else-if="item.type === 'a'"
|
v-else-if="item.type === 'a'"
|
||||||
role="menuitem"
|
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>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||||
<div :class="$style.item_content">
|
<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>
|
<span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-else-if="item.type === 'user'"
|
v-else-if="item.type === 'user'"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
@@ -88,6 +101,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<span :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
|
<span :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-else-if="item.type === 'switch'"
|
v-else-if="item.type === 'switch'"
|
||||||
role="menuitemcheckbox"
|
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>
|
<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)"/>
|
<MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
||||||
<div :class="$style.item_content">
|
<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)"/>
|
<MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-else-if="item.type === 'radio'"
|
v-else-if="item.type === 'radio'"
|
||||||
role="menuitem"
|
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>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
|
||||||
<div :class="$style.item_content">
|
<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>
|
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-else-if="item.type === 'radioOption'"
|
v-else-if="item.type === 'radioOption'"
|
||||||
role="menuitemradio"
|
role="menuitemradio"
|
||||||
@@ -134,9 +156,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<span :class="[$style.radioIcon, { [$style.radioChecked]: unref(item.active) }]"></span>
|
<span :class="[$style.radioIcon, { [$style.radioChecked]: unref(item.active) }]"></span>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.item_content">
|
<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>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-else-if="item.type === 'parent'"
|
v-else-if="item.type === 'parent'"
|
||||||
role="menuitem"
|
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>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
|
||||||
<div :class="$style.item_content">
|
<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>
|
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-else role="menuitem"
|
v-else
|
||||||
|
role="menuitem"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
:class="['_button', $style.item, { [$style.danger]: item.danger, [$style.active]: unref(item.active) }]"
|
:class="['_button', $style.item, { [$style.danger]: item.danger, [$style.active]: unref(item.active) }]"
|
||||||
@click.prevent="unref(item.active) ? close(false) : clicked(item.action, $event)"
|
@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>
|
<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"/>
|
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
||||||
<div :class="$style.item_content">
|
<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>
|
<span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<span v-if="items2 == null || items2.length === 0" tabindex="-1" :class="[$style.none, $style.item]">
|
<span v-if="items2 == null || items2.length === 0" tabindex="-1" :class="[$style.none, $style.item]">
|
||||||
<span>{{ i18n.ts.none }}</span>
|
<span>{{ i18n.ts.none }}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -438,6 +473,12 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not(.widthSpecified) {
|
||||||
|
> .menu {
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.big:not(.asDrawer) {
|
&.big:not(.asDrawer) {
|
||||||
> .menu {
|
> .menu {
|
||||||
min-width: 230px;
|
min-width: 230px;
|
||||||
@@ -607,10 +648,19 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.item_content_text {
|
.item_content_text {
|
||||||
max-width: calc(100vw - 4rem);
|
max-width: calc(100vw - 4rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item_content_text_title {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item_content_text_caption {
|
||||||
|
text-wrap: auto;
|
||||||
|
font-size: 85%;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
.switchButton {
|
.switchButton {
|
||||||
margin-left: -2px;
|
margin-left: -2px;
|
||||||
--height: 1.35em;
|
--height: 1.35em;
|
||||||
|
@@ -28,7 +28,7 @@ export type Keys = (
|
|||||||
'theme' |
|
'theme' |
|
||||||
'themeId' |
|
'themeId' |
|
||||||
'customCss' |
|
'customCss' |
|
||||||
'message_drafts' |
|
'chatMessageDrafts' |
|
||||||
'scratchpad' |
|
'scratchpad' |
|
||||||
'debug' |
|
'debug' |
|
||||||
'preferences' |
|
'preferences' |
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { computed, reactive } from 'vue';
|
import { computed, reactive } from 'vue';
|
||||||
|
import { ui } from '@@/js/config.js';
|
||||||
import { clearCache } from './utility/clear-cache.js';
|
import { clearCache } from './utility/clear-cache.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import { miLocalStorage } from '@/local-storage.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 { lookup } from '@/utility/lookup.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { ui } from '@@/js/config.js';
|
|
||||||
import { unisonReload } from '@/utility/unison-reload.js';
|
import { unisonReload } from '@/utility/unison-reload.js';
|
||||||
|
|
||||||
export const navbarItemDef = reactive({
|
export const navbarItemDef = reactive({
|
||||||
@@ -110,6 +110,11 @@ export const navbarItemDef = reactive({
|
|||||||
icon: 'ti ti-device-tv',
|
icon: 'ti ti-device-tv',
|
||||||
to: '/channels',
|
to: '/channels',
|
||||||
},
|
},
|
||||||
|
chat: {
|
||||||
|
title: i18n.ts.chat,
|
||||||
|
icon: 'ti ti-message',
|
||||||
|
to: '/chat',
|
||||||
|
},
|
||||||
achievements: {
|
achievements: {
|
||||||
title: i18n.ts.achievements,
|
title: i18n.ts.achievements,
|
||||||
icon: 'ti ti-medal',
|
icon: 'ti ti-medal',
|
||||||
|
@@ -16,27 +16,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
:key="item.id"
|
:key="item.id"
|
||||||
:class="[$style.message, { [$style.isMe]: item.isMe, [$style.isRead]: item.message.isRead }]"
|
:class="[$style.message, { [$style.isMe]: item.isMe, [$style.isRead]: item.message.isRead }]"
|
||||||
class="_panel"
|
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 v-if="item.other" :class="$style.avatar" :user="item.other" indicator link preview/>
|
||||||
<MkAvatar :class="$style.avatar" :user="item.other" indicator link preview/>
|
<header v-if="item.message.room">
|
||||||
<header v-if="item.message.roomId">
|
<span :class="$style.name">{{ item.message.room.name }}</span>
|
||||||
<span class="name">{{ item.message.room.name }}</span>
|
<MkTime :time="item.message.createdAt" :class="$style.time"/>
|
||||||
<MkTime :time="item.message.createdAt" class="time"/>
|
</header>
|
||||||
</header>
|
<header v-else>
|
||||||
<header v-else>
|
<MkUserName :class="$style.name" :user="item.other!"/>
|
||||||
<span class="name"><MkUserName :user="item.other"/></span>
|
<MkAcct :class="$style.username" :user="item.other!"/>
|
||||||
<span class="username">@{{ acct(item.other) }}</span>
|
<MkTime :time="item.message.createdAt" :class="$style.time"/>
|
||||||
<MkTime :time="item.message.createdAt" class="time"/>
|
</header>
|
||||||
</header>
|
<div :class="$style.body">
|
||||||
<div class="body">
|
<p :class="$style.text"><span v-if="item.isMe" :class="$style.iSaid">{{ i18n.ts.you }}:</span>{{ item.message.text }}</p>
|
||||||
<p class="text"><span v-if="item.isMe" :class="$style.iSaid">{{ i18n.ts.you }}:</span>{{ item.message.text }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</MkA>
|
</MkA>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!fetching && history.length == 0" class="_fullinfo">
|
<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>{{ i18n.ts.noHistory }}</div>
|
||||||
</div>
|
</div>
|
||||||
<MkLoading v-if="fetching"/>
|
<MkLoading v-if="fetching"/>
|
||||||
@@ -51,12 +48,15 @@ import * as Misskey from 'misskey-js';
|
|||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
import { infoImageUrl } from '@/instance.js';
|
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { ensureSignin } from '@/i.js';
|
import { ensureSignin } from '@/i.js';
|
||||||
|
import { useRouter } from '@/router/supplier.js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
const history = ref<{
|
const history = ref<{
|
||||||
id: string;
|
id: string;
|
||||||
@@ -65,15 +65,58 @@ const history = ref<{
|
|||||||
isMe: boolean;
|
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() {
|
async function fetchHistory() {
|
||||||
fetching.value = true;
|
fetching.value = true;
|
||||||
|
|
||||||
const [userMessages, groupMessages] = await Promise.all([
|
const [userMessages, roomMessages] = await Promise.all([
|
||||||
misskeyApi('messaging/history', { group: false }),
|
misskeyApi('chat/history', { room: false }),
|
||||||
misskeyApi('messaging/history', { group: true }),
|
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())
|
.toSorted((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||||
.map(m => ({
|
.map(m => ({
|
||||||
id: m.id,
|
id: m.id,
|
||||||
@@ -100,23 +143,7 @@ definePage(() => ({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.add {
|
.start {
|
||||||
margin: 0 auto 16px auto;
|
margin: 0 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;
|
|
||||||
}
|
}
|
||||||
</style>
|
</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',
|
path: '/clips/:clipId',
|
||||||
component: page(() => import('@/pages/clip.vue')),
|
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',
|
path: '/instance-info/:host',
|
||||||
component: page(() => import('@/pages/instance-info.vue')),
|
component: page(() => import('@/pages/instance-info.vue')),
|
||||||
|
@@ -15,16 +15,16 @@ export type MenuAction = (ev: MouseEvent) => void;
|
|||||||
|
|
||||||
export type MenuDivider = { type: 'divider' };
|
export type MenuDivider = { type: 'divider' };
|
||||||
export type MenuNull = undefined;
|
export type MenuNull = undefined;
|
||||||
export type MenuLabel = { type: 'label', text: string };
|
export type MenuLabel = { type: 'label', text: string, caption?: string };
|
||||||
export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
|
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, icon?: string, indicate?: boolean };
|
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 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 MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, caption?: 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 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, icon?: string, ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>, options: MenuRadioOptionsDef, disabled?: boolean | Ref<boolean> };
|
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, action: MenuAction; active?: boolean | ComputedRef<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 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' };
|
export type MenuPending = { type: 'pending' };
|
||||||
|
|
||||||
|
@@ -32,7 +32,7 @@ const mimeTypeMap = {
|
|||||||
|
|
||||||
export function uploadFile(
|
export function uploadFile(
|
||||||
file: File,
|
file: File,
|
||||||
folder?: string | Misskey.entities.DriveFolder,
|
folder?: string | Misskey.entities.DriveFolder | null,
|
||||||
name?: string,
|
name?: string,
|
||||||
keepOriginal: boolean = prefer.s.keepOriginalUploading,
|
keepOriginal: boolean = prefer.s.keepOriginalUploading,
|
||||||
): Promise<Misskey.entities.DriveFile> {
|
): Promise<Misskey.entities.DriveFile> {
|
||||||
|
@@ -951,6 +951,12 @@ type ChartsUsersRequest = operations['charts___users']['requestBody']['content']
|
|||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type ChartsUsersResponse = operations['charts___users']['responses']['200']['content']['application/json'];
|
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)
|
// @public (undocumented)
|
||||||
type ChatMessage = components['schemas']['ChatMessage'];
|
type ChatMessage = components['schemas']['ChatMessage'];
|
||||||
|
|
||||||
@@ -963,18 +969,15 @@ type ChatMessagesCreateRequest = operations['chat___messages___create']['request
|
|||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type ChatMessagesCreateResponse = operations['chat___messages___create']['responses']['200']['content']['application/json'];
|
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)
|
// @public (undocumented)
|
||||||
type ChatMessagesTimelineRequest = operations['chat___messages___timeline']['requestBody']['content']['application/json'];
|
type ChatMessagesTimelineRequest = operations['chat___messages___timeline']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type ChatMessagesTimelineResponse = operations['chat___messages___timeline']['responses']['200']['content']['application/json'];
|
type ChatMessagesTimelineResponse = operations['chat___messages___timeline']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type ChatRoom = components['schemas']['ChatRoom'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type Clip = components['schemas']['Clip'];
|
type Clip = components['schemas']['Clip'];
|
||||||
|
|
||||||
@@ -1472,10 +1475,10 @@ declare namespace entities {
|
|||||||
ChartsUserReactionsResponse,
|
ChartsUserReactionsResponse,
|
||||||
ChartsUsersRequest,
|
ChartsUsersRequest,
|
||||||
ChartsUsersResponse,
|
ChartsUsersResponse,
|
||||||
|
ChatHistoryRequest,
|
||||||
|
ChatHistoryResponse,
|
||||||
ChatMessagesCreateRequest,
|
ChatMessagesCreateRequest,
|
||||||
ChatMessagesCreateResponse,
|
ChatMessagesCreateResponse,
|
||||||
ChatMessagesHistoryRequest,
|
|
||||||
ChatMessagesHistoryResponse,
|
|
||||||
ChatMessagesTimelineRequest,
|
ChatMessagesTimelineRequest,
|
||||||
ChatMessagesTimelineResponse,
|
ChatMessagesTimelineResponse,
|
||||||
ClipsAddNoteRequest,
|
ClipsAddNoteRequest,
|
||||||
@@ -1912,7 +1915,8 @@ declare namespace entities {
|
|||||||
SystemWebhook,
|
SystemWebhook,
|
||||||
AbuseReportNotificationRecipient,
|
AbuseReportNotificationRecipient,
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
ChatMessageLite
|
ChatMessageLite,
|
||||||
|
ChatRoom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export { entities }
|
export { entities }
|
||||||
|
@@ -1537,9 +1537,9 @@ declare module '../api.js' {
|
|||||||
/**
|
/**
|
||||||
* No description provided.
|
* 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,
|
endpoint: E,
|
||||||
params: P,
|
params: P,
|
||||||
credential?: string | null,
|
credential?: string | null,
|
||||||
@@ -1548,9 +1548,9 @@ declare module '../api.js' {
|
|||||||
/**
|
/**
|
||||||
* No description provided.
|
* 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,
|
endpoint: E,
|
||||||
params: P,
|
params: P,
|
||||||
credential?: string | null,
|
credential?: string | null,
|
||||||
|
@@ -207,10 +207,10 @@ import type {
|
|||||||
ChartsUserReactionsResponse,
|
ChartsUserReactionsResponse,
|
||||||
ChartsUsersRequest,
|
ChartsUsersRequest,
|
||||||
ChartsUsersResponse,
|
ChartsUsersResponse,
|
||||||
|
ChatHistoryRequest,
|
||||||
|
ChatHistoryResponse,
|
||||||
ChatMessagesCreateRequest,
|
ChatMessagesCreateRequest,
|
||||||
ChatMessagesCreateResponse,
|
ChatMessagesCreateResponse,
|
||||||
ChatMessagesHistoryRequest,
|
|
||||||
ChatMessagesHistoryResponse,
|
|
||||||
ChatMessagesTimelineRequest,
|
ChatMessagesTimelineRequest,
|
||||||
ChatMessagesTimelineResponse,
|
ChatMessagesTimelineResponse,
|
||||||
ClipsAddNoteRequest,
|
ClipsAddNoteRequest,
|
||||||
@@ -732,8 +732,8 @@ export type Endpoints = {
|
|||||||
'charts/user/pv': { req: ChartsUserPvRequest; res: ChartsUserPvResponse };
|
'charts/user/pv': { req: ChartsUserPvRequest; res: ChartsUserPvResponse };
|
||||||
'charts/user/reactions': { req: ChartsUserReactionsRequest; res: ChartsUserReactionsResponse };
|
'charts/user/reactions': { req: ChartsUserReactionsRequest; res: ChartsUserReactionsResponse };
|
||||||
'charts/users': { req: ChartsUsersRequest; res: ChartsUsersResponse };
|
'charts/users': { req: ChartsUsersRequest; res: ChartsUsersResponse };
|
||||||
|
'chat/history': { req: ChatHistoryRequest; res: ChatHistoryResponse };
|
||||||
'chat/messages/create': { req: ChatMessagesCreateRequest; res: ChatMessagesCreateResponse };
|
'chat/messages/create': { req: ChatMessagesCreateRequest; res: ChatMessagesCreateResponse };
|
||||||
'chat/messages/history': { req: ChatMessagesHistoryRequest; res: ChatMessagesHistoryResponse };
|
|
||||||
'chat/messages/timeline': { req: ChatMessagesTimelineRequest; res: ChatMessagesTimelineResponse };
|
'chat/messages/timeline': { req: ChatMessagesTimelineRequest; res: ChatMessagesTimelineResponse };
|
||||||
'clips/add-note': { req: ClipsAddNoteRequest; res: EmptyResponse };
|
'clips/add-note': { req: ClipsAddNoteRequest; res: EmptyResponse };
|
||||||
'clips/create': { req: ClipsCreateRequest; res: ClipsCreateResponse };
|
'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 ChartsUserReactionsResponse = operations['charts___user___reactions']['responses']['200']['content']['application/json'];
|
||||||
export type ChartsUsersRequest = operations['charts___users']['requestBody']['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 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 ChatMessagesCreateRequest = operations['chat___messages___create']['requestBody']['content']['application/json'];
|
||||||
export type ChatMessagesCreateResponse = operations['chat___messages___create']['responses']['200']['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 ChatMessagesTimelineRequest = operations['chat___messages___timeline']['requestBody']['content']['application/json'];
|
||||||
export type ChatMessagesTimelineResponse = operations['chat___messages___timeline']['responses']['200']['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'];
|
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 AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient'];
|
||||||
export type ChatMessage = components['schemas']['ChatMessage'];
|
export type ChatMessage = components['schemas']['ChatMessage'];
|
||||||
export type ChatMessageLite = components['schemas']['ChatMessageLite'];
|
export type ChatMessageLite = components['schemas']['ChatMessageLite'];
|
||||||
|
export type ChatRoom = components['schemas']['ChatRoom'];
|
||||||
|
@@ -1358,6 +1358,15 @@ export type paths = {
|
|||||||
*/
|
*/
|
||||||
post: operations['charts___users'];
|
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': {
|
||||||
/**
|
/**
|
||||||
* chat/messages/create
|
* chat/messages/create
|
||||||
@@ -1367,15 +1376,6 @@ export type paths = {
|
|||||||
*/
|
*/
|
||||||
post: operations['chat___messages___create'];
|
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': {
|
||||||
/**
|
/**
|
||||||
* chat/messages/timeline
|
* chat/messages/timeline
|
||||||
@@ -5178,12 +5178,15 @@ export type components = {
|
|||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
fromUserId: string;
|
fromUserId: string;
|
||||||
fromUser?: components['schemas']['UserLite'];
|
fromUser: components['schemas']['UserLite'];
|
||||||
toUserId?: string | null;
|
toUserId?: string | null;
|
||||||
toUser?: components['schemas']['UserLite'] | null;
|
toUser?: components['schemas']['UserLite'] | null;
|
||||||
|
toRoomId?: string | null;
|
||||||
|
toRoom?: components['schemas']['ChatRoom'] | null;
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
fileId?: string | null;
|
fileId?: string | null;
|
||||||
file?: components['schemas']['DriveFile'] | null;
|
file?: components['schemas']['DriveFile'] | null;
|
||||||
|
isRead?: boolean;
|
||||||
};
|
};
|
||||||
ChatMessageLite: {
|
ChatMessageLite: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -5191,10 +5194,19 @@ export type components = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
fromUserId: string;
|
fromUserId: string;
|
||||||
toUserId?: string | null;
|
toUserId?: string | null;
|
||||||
|
toRoomId?: string | null;
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
fileId?: string | null;
|
fileId?: string | null;
|
||||||
file?: components['schemas']['DriveFile'] | null;
|
file?: components['schemas']['DriveFile'] | null;
|
||||||
};
|
};
|
||||||
|
ChatRoom: {
|
||||||
|
id: string;
|
||||||
|
/** Format: date-time */
|
||||||
|
createdAt: string;
|
||||||
|
ownerId: string;
|
||||||
|
owner: components['schemas']['UserLite'];
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
responses: never;
|
responses: never;
|
||||||
parameters: 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
|
* chat/messages/create
|
||||||
* @description No description provided.
|
* @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
|
* chat/messages/timeline
|
||||||
* @description No description provided.
|
* @description No description provided.
|
||||||
|
Reference in New Issue
Block a user